diff --git a/src/config.rs b/src/config.rs index a35dd93..f0c1aa2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,15 +7,36 @@ use crate::{ lighthouse::lighthouse_profile, openhmd::openhmd_profile, simulated::simulated_profile, survive::survive_profile, wivrn::wivrn_profile, wmr::wmr_profile, }, + ui::plugins::Plugin, util::file_utils::get_writer, }; use serde::{de::Error, Deserialize, Serialize}; use std::{ + collections::HashMap, fs::File, io::BufReader, path::{Path, PathBuf}, }; +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluginConfig { + pub appid: String, + pub version: String, + pub enabled: bool, + pub exec_path: PathBuf, +} + +impl From<&Plugin> for PluginConfig { + fn from(p: &Plugin) -> Self { + Self { + appid: p.appid.clone(), + version: p.version.clone(), + enabled: true, + exec_path: p.exec_path(), + } + } +} + const DEFAULT_WIN_SIZE: [i32; 2] = [360, 400]; const fn default_win_size() -> [i32; 2] { @@ -29,6 +50,8 @@ pub struct Config { pub user_profiles: Vec, #[serde(default = "default_win_size")] pub win_size: [i32; 2], + #[serde(default)] + pub plugins: HashMap, } impl Default for Config { @@ -37,8 +60,9 @@ impl Default for Config { // TODO: using an empty string here is ugly selected_profile_uuid: "".to_string(), debug_view_enabled: false, - user_profiles: vec![], + user_profiles: Vec::default(), win_size: DEFAULT_WIN_SIZE, + plugins: HashMap::default(), } } } diff --git a/src/ui/app.rs b/src/ui/app.rs index 486bb0e..afe2869 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -11,7 +11,7 @@ use super::{ }, libsurvive_setup_window::{LibsurviveSetupMsg, LibsurviveSetupWindow}, main_view::{MainView, MainViewInit, MainViewMsg, MainViewOutMsg}, - plugins::store::{PluginStore, PluginStoreMsg}, + plugins::store::{PluginStore, PluginStoreInit, PluginStoreMsg, PluginStoreOutMsg}, util::{copiable_code_snippet, copy_text, open_with_default_handler}, wivrn_conf_editor::{WivrnConfEditor, WivrnConfEditorInit, WivrnConfEditorMsg}, }; @@ -22,7 +22,7 @@ use crate::{ build_opencomposite::get_build_opencomposite_jobs, build_openhmd::get_build_openhmd_jobs, build_wivrn::get_build_wivrn_jobs, }, - config::Config, + config::{Config, PluginConfig}, constants::APP_NAME, depcheck::common::dep_pkexec, file_builders::{ @@ -57,7 +57,11 @@ use relm4::{ new_action_group, new_stateful_action, new_stateless_action, prelude::*, }; -use std::{collections::VecDeque, fs::remove_file, time::Duration}; +use std::{ + collections::{HashMap, VecDeque}, + fs::remove_file, + time::Duration, +}; use tracing::error; pub struct App { @@ -123,6 +127,7 @@ pub enum Msg { OnProberExit(bool), WivrnCheckPairMode, OpenPluginStore, + UpdateConfigPlugins(HashMap), NoOp, } @@ -795,10 +800,20 @@ impl AsyncComponent for App { } } Msg::OpenPluginStore => { - let pluginstore = PluginStore::builder().launch(()).detach(); + let pluginstore = PluginStore::builder() + .launch(PluginStoreInit { + config_plugins: self.config.plugins.clone(), + }) + .forward(sender.input_sender(), move |msg| match msg { + PluginStoreOutMsg::UpdateConfigPlugins(cp) => Msg::UpdateConfigPlugins(cp), + }); pluginstore.sender().emit(PluginStoreMsg::Present); self.pluginstore = Some(pluginstore); } + Msg::UpdateConfigPlugins(cp) => { + self.config.plugins = cp; + self.config.save(); + } } } diff --git a/src/ui/plugins/store.rs b/src/ui/plugins/store.rs index 5433e65..36bb3d3 100644 --- a/src/ui/plugins/store.rs +++ b/src/ui/plugins/store.rs @@ -3,10 +3,14 @@ use super::{ store_row_factory::{StoreRowModel, StoreRowModelInit, StoreRowModelMsg, StoreRowModelOutMsg}, Plugin, }; -use crate::{downloader::download_file_async, ui::alert::alert}; +use crate::{ + config::PluginConfig, + downloader::download_file_async, + ui::{alert::alert, SENDER_IO_ERR_MSG}, +}; use adw::prelude::*; use relm4::{factory::AsyncFactoryVecDeque, prelude::*}; -use std::fs::remove_file; +use std::{collections::HashMap, fs::remove_file}; #[tracker::track] pub struct PluginStore { @@ -18,6 +22,8 @@ pub struct PluginStore { details: AsyncController, #[tracker::do_not_track] main_stack: Option, + #[tracker::do_not_track] + config_plugins: HashMap, refreshing: bool, locked: bool, plugins: Vec, @@ -32,18 +38,24 @@ pub enum PluginStoreMsg { InstallDownload(Plugin, relm4::Sender), RemoveFromDetails(Plugin), Remove(Plugin, relm4::Sender), - Enable(String), - Disable(String), + SetEnabled(Plugin, bool), ShowDetails(usize), ShowPluginList, } #[derive(Debug)] -pub enum PluginStoreOutMsg {} +pub struct PluginStoreInit { + pub config_plugins: HashMap, +} + +#[derive(Debug)] +pub enum PluginStoreOutMsg { + UpdateConfigPlugins(HashMap), +} #[relm4::component(pub async)] impl AsyncComponent for PluginStore { - type Init = (); + type Init = PluginStoreInit; type Input = PluginStoreMsg; type Output = PluginStoreOutMsg; type CommandOutput = (); @@ -181,6 +193,10 @@ impl AsyncComponent for PluginStore { self.plugins.iter().for_each(|plugin| { guard.push_back(StoreRowModelInit { plugin: plugin.clone(), + enabled: self + .config_plugins + .get(&plugin.appid) + .is_some_and(|cp| cp.enabled), }); }); } @@ -231,9 +247,17 @@ impl AsyncComponent for PluginStore { )), Some(&self.win.as_ref().unwrap().clone().upcast::()), ); + } else { + self.config_plugins + .insert(plugin.appid.clone(), PluginConfig::from(&plugin)); + sender + .output(Self::Output::UpdateConfigPlugins( + self.config_plugins.clone(), + )) + .expect(SENDER_IO_ERR_MSG); } - row_sender.emit(StoreRowModelMsg::Refresh); - self.details.emit(StoreDetailMsg::Refresh); + row_sender.emit(StoreRowModelMsg::Refresh(true)); + self.details.emit(StoreDetailMsg::Refresh(true)); self.set_locked(false); } Self::Input::Remove(plugin, row_sender) => { @@ -249,21 +273,58 @@ impl AsyncComponent for PluginStore { )), Some(&self.win.as_ref().unwrap().clone().upcast::()), ); + } else { + self.config_plugins.remove(&plugin.appid); + sender + .output(Self::Output::UpdateConfigPlugins( + self.config_plugins.clone(), + )) + .expect(SENDER_IO_ERR_MSG); } } - row_sender.emit(StoreRowModelMsg::Refresh); - self.details.emit(StoreDetailMsg::Refresh); + row_sender.emit(StoreRowModelMsg::Refresh(false)); + self.details.emit(StoreDetailMsg::Refresh(false)); self.set_locked(false); } - Self::Input::Enable(_) => todo!(), - Self::Input::Disable(_) => todo!(), + Self::Input::SetEnabled(plugin, enabled) => { + if let Some(cp) = self.config_plugins.get_mut(&plugin.appid) { + cp.enabled = enabled; + if let Some(row) = self + .plugin_rows + .as_mut() + .unwrap() + .guard() + .iter() + .find(|row| row.is_some_and(|row| row.plugin.appid == plugin.appid)) + .flatten() + { + row.input_sender.emit(StoreRowModelMsg::Refresh(enabled)); + } else { + eprintln!("could not find corresponding listbox row!") + } + self.details.emit(StoreDetailMsg::Refresh(enabled)); + } else { + eprintln!( + "failed to set plugin {} enabled: could not find in hashmap", + plugin.appid + ) + } + sender + .output(Self::Output::UpdateConfigPlugins( + self.config_plugins.clone(), + )) + .expect(SENDER_IO_ERR_MSG); + } // we use index here because it's the listbox not the row that can // send this signal, so I don't directly have the plugin object Self::Input::ShowDetails(index) => { if let Some(plugin) = self.plugins.get(index) { - self.details - .sender() - .emit(StoreDetailMsg::SetPlugin(plugin.clone())); + self.details.sender().emit(StoreDetailMsg::SetPlugin( + plugin.clone(), + self.config_plugins + .get(&plugin.appid) + .is_some_and(|cp| cp.enabled), + )); self.main_stack .as_ref() .unwrap() @@ -282,7 +343,7 @@ impl AsyncComponent for PluginStore { } async fn init( - _init: Self::Init, + init: Self::Init, root: Self::Root, sender: AsyncComponentSender, ) -> AsyncComponentParts { @@ -299,7 +360,11 @@ impl AsyncComponent for PluginStore { StoreDetailOutMsg::GoBack => Self::Input::ShowPluginList, StoreDetailOutMsg::Install(plugin) => Self::Input::InstallFromDetails(plugin), StoreDetailOutMsg::Remove(plugin) => Self::Input::RemoveFromDetails(plugin), + StoreDetailOutMsg::SetEnabled(plugin, enabled) => { + Self::Input::SetEnabled(plugin, enabled) + } }), + config_plugins: init.config_plugins, main_stack: None, }; @@ -318,6 +383,9 @@ impl AsyncComponent for PluginStore { StoreRowModelOutMsg::Remove(appid, row_sender) => { Self::Input::Remove(appid, row_sender) } + StoreRowModelOutMsg::SetEnabled(plugin, enabled) => { + Self::Input::SetEnabled(plugin, enabled) + } }), ); model.main_stack = Some(widgets.main_stack.clone()); diff --git a/src/ui/plugins/store_detail.rs b/src/ui/plugins/store_detail.rs index b5fd8e5..077423b 100644 --- a/src/ui/plugins/store_detail.rs +++ b/src/ui/plugins/store_detail.rs @@ -6,6 +6,7 @@ use relm4::prelude::*; #[tracker::track] pub struct StoreDetail { plugin: Option, + enabled: bool, #[tracker::do_not_track] carousel: Option, #[tracker::do_not_track] @@ -15,12 +16,13 @@ pub struct StoreDetail { #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum StoreDetailMsg { - SetPlugin(Plugin), + SetPlugin(Plugin, bool), SetIcon, SetScreenshots, - Refresh, + Refresh(bool), Install, Remove, + SetEnabled(bool), } #[derive(Debug)] @@ -28,6 +30,7 @@ pub enum StoreDetailOutMsg { Install(Plugin), Remove(Plugin), GoBack, + SetEnabled(Plugin, bool), } #[relm4::component(pub async)] @@ -128,11 +131,18 @@ impl AsyncComponent for StoreDetail { } }, gtk::Switch { - #[track = "self.changed(Self::plugin())"] + #[track = "model.changed(Self::plugin())"] set_visible: model.plugin.as_ref() .is_some_and(|p| p.is_installed()), + #[track = "model.changed(Self::enabled())"] + set_active: model.enabled, + set_tooltip_text: Some("Plugin enabled"), set_valign: gtk::Align::Center, set_halign: gtk::Align::Center, + connect_state_set[sender] => move |_, state| { + sender.input(Self::Input::SetEnabled(state)); + gtk::glib::Propagation::Proceed + } }, } } @@ -184,8 +194,9 @@ impl AsyncComponent for StoreDetail { self.reset(); match message { - Self::Input::SetPlugin(p) => { + Self::Input::SetPlugin(p, enabled) => { self.set_plugin(Some(p)); + self.set_enabled(enabled); sender.input(Self::Input::SetIcon); sender.input(Self::Input::SetScreenshots); } @@ -233,8 +244,9 @@ impl AsyncComponent for StoreDetail { } } } - Self::Input::Refresh => { + Self::Input::Refresh(enabled) => { self.mark_all_changed(); + self.set_enabled(enabled); } Self::Input::Install => { if let Some(plugin) = self.plugin.as_ref() { @@ -250,6 +262,14 @@ impl AsyncComponent for StoreDetail { .expect(SENDER_IO_ERR_MSG); } } + Self::Input::SetEnabled(enabled) => { + self.set_enabled(enabled); + if let Some(plugin) = self.plugin.as_ref() { + sender + .output(Self::Output::SetEnabled(plugin.clone(), enabled)) + .expect(SENDER_IO_ERR_MSG); + } + } } } @@ -261,6 +281,7 @@ impl AsyncComponent for StoreDetail { let mut model = Self { tracker: 0, plugin: None, + enabled: false, carousel: None, icon: None, }; diff --git a/src/ui/plugins/store_row_factory.rs b/src/ui/plugins/store_row_factory.rs index fdf6e66..38fc4ac 100644 --- a/src/ui/plugins/store_row_factory.rs +++ b/src/ui/plugins/store_row_factory.rs @@ -14,23 +14,27 @@ pub struct StoreRowModel { icon: Option, #[tracker::do_not_track] pub input_sender: relm4::Sender, + pub enabled: bool, } #[derive(Debug)] pub struct StoreRowModelInit { pub plugin: Plugin, + pub enabled: bool, } #[derive(Debug)] pub enum StoreRowModelMsg { LoadIcon, - Refresh, + Refresh(bool), + SetEnabled(bool), } #[derive(Debug)] pub enum StoreRowModelOutMsg { Install(Plugin, relm4::Sender), Remove(Plugin, relm4::Sender), + SetEnabled(Plugin, bool), } #[relm4::factory(async pub)] @@ -121,15 +125,22 @@ impl AsyncFactoryComponent for StoreRowModel { gtk::Switch { #[track = "self.changed(StoreRowModel::plugin())"] set_visible: self.plugin.is_installed(), + #[track = "self.changed(StoreRowModel::enabled())"] + set_active: self.enabled, set_valign: gtk::Align::Center, set_halign: gtk::Align::Center, + set_tooltip_text: Some("Plugin enabled"), + connect_state_set[sender] => move |_, state| { + sender.input(Self::Input::SetEnabled(state)); + gtk::glib::Propagation::Proceed + } }, }, } } } - async fn update(&mut self, message: Self::Input, _sender: AsyncFactorySender) { + async fn update(&mut self, message: Self::Input, sender: AsyncFactorySender) { self.reset(); match message { @@ -145,7 +156,16 @@ impl AsyncFactoryComponent for StoreRowModel { }; } } - Self::Input::Refresh => self.mark_all_changed(), + Self::Input::SetEnabled(state) => { + self.set_enabled(state); + sender + .output(Self::Output::SetEnabled(self.plugin.clone(), state)) + .expect(SENDER_IO_ERR_MSG); + } + Self::Input::Refresh(enabled) => { + self.mark_all_changed(); + self.set_enabled(enabled) + } } } @@ -157,6 +177,7 @@ impl AsyncFactoryComponent for StoreRowModel { Self { tracker: 0, plugin: init.plugin, + enabled: init.enabled, icon: None, input_sender: sender.input_sender().clone(), }