feat: can enable and disable plugins

This commit is contained in:
Gabriele Musco 2024-08-19 19:22:29 +02:00
commit 781e2f4fd2
5 changed files with 178 additions and 29 deletions

View file

@ -7,15 +7,36 @@ use crate::{
lighthouse::lighthouse_profile, openhmd::openhmd_profile, simulated::simulated_profile, lighthouse::lighthouse_profile, openhmd::openhmd_profile, simulated::simulated_profile,
survive::survive_profile, wivrn::wivrn_profile, wmr::wmr_profile, survive::survive_profile, wivrn::wivrn_profile, wmr::wmr_profile,
}, },
ui::plugins::Plugin,
util::file_utils::get_writer, util::file_utils::get_writer,
}; };
use serde::{de::Error, Deserialize, Serialize}; use serde::{de::Error, Deserialize, Serialize};
use std::{ use std::{
collections::HashMap,
fs::File, fs::File,
io::BufReader, io::BufReader,
path::{Path, PathBuf}, 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 DEFAULT_WIN_SIZE: [i32; 2] = [360, 400];
const fn default_win_size() -> [i32; 2] { const fn default_win_size() -> [i32; 2] {
@ -29,6 +50,8 @@ pub struct Config {
pub user_profiles: Vec<Profile>, pub user_profiles: Vec<Profile>,
#[serde(default = "default_win_size")] #[serde(default = "default_win_size")]
pub win_size: [i32; 2], pub win_size: [i32; 2],
#[serde(default)]
pub plugins: HashMap<String, PluginConfig>,
} }
impl Default for Config { impl Default for Config {
@ -37,8 +60,9 @@ impl Default for Config {
// TODO: using an empty string here is ugly // TODO: using an empty string here is ugly
selected_profile_uuid: "".to_string(), selected_profile_uuid: "".to_string(),
debug_view_enabled: false, debug_view_enabled: false,
user_profiles: vec![], user_profiles: Vec::default(),
win_size: DEFAULT_WIN_SIZE, win_size: DEFAULT_WIN_SIZE,
plugins: HashMap::default(),
} }
} }
} }

View file

@ -11,7 +11,7 @@ use super::{
}, },
libsurvive_setup_window::{LibsurviveSetupMsg, LibsurviveSetupWindow}, libsurvive_setup_window::{LibsurviveSetupMsg, LibsurviveSetupWindow},
main_view::{MainView, MainViewInit, MainViewMsg, MainViewOutMsg}, 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}, util::{copiable_code_snippet, copy_text, open_with_default_handler},
wivrn_conf_editor::{WivrnConfEditor, WivrnConfEditorInit, WivrnConfEditorMsg}, 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_opencomposite::get_build_opencomposite_jobs, build_openhmd::get_build_openhmd_jobs,
build_wivrn::get_build_wivrn_jobs, build_wivrn::get_build_wivrn_jobs,
}, },
config::Config, config::{Config, PluginConfig},
constants::APP_NAME, constants::APP_NAME,
depcheck::common::dep_pkexec, depcheck::common::dep_pkexec,
file_builders::{ file_builders::{
@ -57,7 +57,11 @@ use relm4::{
new_action_group, new_stateful_action, new_stateless_action, new_action_group, new_stateful_action, new_stateless_action,
prelude::*, prelude::*,
}; };
use std::{collections::VecDeque, fs::remove_file, time::Duration}; use std::{
collections::{HashMap, VecDeque},
fs::remove_file,
time::Duration,
};
use tracing::error; use tracing::error;
pub struct App { pub struct App {
@ -123,6 +127,7 @@ pub enum Msg {
OnProberExit(bool), OnProberExit(bool),
WivrnCheckPairMode, WivrnCheckPairMode,
OpenPluginStore, OpenPluginStore,
UpdateConfigPlugins(HashMap<String, PluginConfig>),
NoOp, NoOp,
} }
@ -795,10 +800,20 @@ impl AsyncComponent for App {
} }
} }
Msg::OpenPluginStore => { 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); pluginstore.sender().emit(PluginStoreMsg::Present);
self.pluginstore = Some(pluginstore); self.pluginstore = Some(pluginstore);
} }
Msg::UpdateConfigPlugins(cp) => {
self.config.plugins = cp;
self.config.save();
}
} }
} }

View file

@ -3,10 +3,14 @@ use super::{
store_row_factory::{StoreRowModel, StoreRowModelInit, StoreRowModelMsg, StoreRowModelOutMsg}, store_row_factory::{StoreRowModel, StoreRowModelInit, StoreRowModelMsg, StoreRowModelOutMsg},
Plugin, 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 adw::prelude::*;
use relm4::{factory::AsyncFactoryVecDeque, prelude::*}; use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
use std::fs::remove_file; use std::{collections::HashMap, fs::remove_file};
#[tracker::track] #[tracker::track]
pub struct PluginStore { pub struct PluginStore {
@ -18,6 +22,8 @@ pub struct PluginStore {
details: AsyncController<StoreDetail>, details: AsyncController<StoreDetail>,
#[tracker::do_not_track] #[tracker::do_not_track]
main_stack: Option<gtk::Stack>, main_stack: Option<gtk::Stack>,
#[tracker::do_not_track]
config_plugins: HashMap<String, PluginConfig>,
refreshing: bool, refreshing: bool,
locked: bool, locked: bool,
plugins: Vec<Plugin>, plugins: Vec<Plugin>,
@ -32,18 +38,24 @@ pub enum PluginStoreMsg {
InstallDownload(Plugin, relm4::Sender<StoreRowModelMsg>), InstallDownload(Plugin, relm4::Sender<StoreRowModelMsg>),
RemoveFromDetails(Plugin), RemoveFromDetails(Plugin),
Remove(Plugin, relm4::Sender<StoreRowModelMsg>), Remove(Plugin, relm4::Sender<StoreRowModelMsg>),
Enable(String), SetEnabled(Plugin, bool),
Disable(String),
ShowDetails(usize), ShowDetails(usize),
ShowPluginList, ShowPluginList,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum PluginStoreOutMsg {} pub struct PluginStoreInit {
pub config_plugins: HashMap<String, PluginConfig>,
}
#[derive(Debug)]
pub enum PluginStoreOutMsg {
UpdateConfigPlugins(HashMap<String, PluginConfig>),
}
#[relm4::component(pub async)] #[relm4::component(pub async)]
impl AsyncComponent for PluginStore { impl AsyncComponent for PluginStore {
type Init = (); type Init = PluginStoreInit;
type Input = PluginStoreMsg; type Input = PluginStoreMsg;
type Output = PluginStoreOutMsg; type Output = PluginStoreOutMsg;
type CommandOutput = (); type CommandOutput = ();
@ -181,6 +193,10 @@ impl AsyncComponent for PluginStore {
self.plugins.iter().for_each(|plugin| { self.plugins.iter().for_each(|plugin| {
guard.push_back(StoreRowModelInit { guard.push_back(StoreRowModelInit {
plugin: plugin.clone(), 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::<gtk::Window>()), Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>()),
); );
} 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); row_sender.emit(StoreRowModelMsg::Refresh(true));
self.details.emit(StoreDetailMsg::Refresh); self.details.emit(StoreDetailMsg::Refresh(true));
self.set_locked(false); self.set_locked(false);
} }
Self::Input::Remove(plugin, row_sender) => { Self::Input::Remove(plugin, row_sender) => {
@ -249,21 +273,58 @@ impl AsyncComponent for PluginStore {
)), )),
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>()), Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>()),
); );
} 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); row_sender.emit(StoreRowModelMsg::Refresh(false));
self.details.emit(StoreDetailMsg::Refresh); self.details.emit(StoreDetailMsg::Refresh(false));
self.set_locked(false); self.set_locked(false);
} }
Self::Input::Enable(_) => todo!(), Self::Input::SetEnabled(plugin, enabled) => {
Self::Input::Disable(_) => todo!(), 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 // 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 // send this signal, so I don't directly have the plugin object
Self::Input::ShowDetails(index) => { Self::Input::ShowDetails(index) => {
if let Some(plugin) = self.plugins.get(index) { if let Some(plugin) = self.plugins.get(index) {
self.details self.details.sender().emit(StoreDetailMsg::SetPlugin(
.sender() plugin.clone(),
.emit(StoreDetailMsg::SetPlugin(plugin.clone())); self.config_plugins
.get(&plugin.appid)
.is_some_and(|cp| cp.enabled),
));
self.main_stack self.main_stack
.as_ref() .as_ref()
.unwrap() .unwrap()
@ -282,7 +343,7 @@ impl AsyncComponent for PluginStore {
} }
async fn init( async fn init(
_init: Self::Init, init: Self::Init,
root: Self::Root, root: Self::Root,
sender: AsyncComponentSender<Self>, sender: AsyncComponentSender<Self>,
) -> AsyncComponentParts<Self> { ) -> AsyncComponentParts<Self> {
@ -299,7 +360,11 @@ impl AsyncComponent for PluginStore {
StoreDetailOutMsg::GoBack => Self::Input::ShowPluginList, StoreDetailOutMsg::GoBack => Self::Input::ShowPluginList,
StoreDetailOutMsg::Install(plugin) => Self::Input::InstallFromDetails(plugin), StoreDetailOutMsg::Install(plugin) => Self::Input::InstallFromDetails(plugin),
StoreDetailOutMsg::Remove(plugin) => Self::Input::RemoveFromDetails(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, main_stack: None,
}; };
@ -318,6 +383,9 @@ impl AsyncComponent for PluginStore {
StoreRowModelOutMsg::Remove(appid, row_sender) => { StoreRowModelOutMsg::Remove(appid, row_sender) => {
Self::Input::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()); model.main_stack = Some(widgets.main_stack.clone());

View file

@ -6,6 +6,7 @@ use relm4::prelude::*;
#[tracker::track] #[tracker::track]
pub struct StoreDetail { pub struct StoreDetail {
plugin: Option<Plugin>, plugin: Option<Plugin>,
enabled: bool,
#[tracker::do_not_track] #[tracker::do_not_track]
carousel: Option<adw::Carousel>, carousel: Option<adw::Carousel>,
#[tracker::do_not_track] #[tracker::do_not_track]
@ -15,12 +16,13 @@ pub struct StoreDetail {
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Debug)] #[derive(Debug)]
pub enum StoreDetailMsg { pub enum StoreDetailMsg {
SetPlugin(Plugin), SetPlugin(Plugin, bool),
SetIcon, SetIcon,
SetScreenshots, SetScreenshots,
Refresh, Refresh(bool),
Install, Install,
Remove, Remove,
SetEnabled(bool),
} }
#[derive(Debug)] #[derive(Debug)]
@ -28,6 +30,7 @@ pub enum StoreDetailOutMsg {
Install(Plugin), Install(Plugin),
Remove(Plugin), Remove(Plugin),
GoBack, GoBack,
SetEnabled(Plugin, bool),
} }
#[relm4::component(pub async)] #[relm4::component(pub async)]
@ -128,11 +131,18 @@ impl AsyncComponent for StoreDetail {
} }
}, },
gtk::Switch { gtk::Switch {
#[track = "self.changed(Self::plugin())"] #[track = "model.changed(Self::plugin())"]
set_visible: model.plugin.as_ref() set_visible: model.plugin.as_ref()
.is_some_and(|p| p.is_installed()), .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_valign: gtk::Align::Center,
set_halign: 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(); self.reset();
match message { match message {
Self::Input::SetPlugin(p) => { Self::Input::SetPlugin(p, enabled) => {
self.set_plugin(Some(p)); self.set_plugin(Some(p));
self.set_enabled(enabled);
sender.input(Self::Input::SetIcon); sender.input(Self::Input::SetIcon);
sender.input(Self::Input::SetScreenshots); 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.mark_all_changed();
self.set_enabled(enabled);
} }
Self::Input::Install => { Self::Input::Install => {
if let Some(plugin) = self.plugin.as_ref() { if let Some(plugin) = self.plugin.as_ref() {
@ -250,6 +262,14 @@ impl AsyncComponent for StoreDetail {
.expect(SENDER_IO_ERR_MSG); .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 { let mut model = Self {
tracker: 0, tracker: 0,
plugin: None, plugin: None,
enabled: false,
carousel: None, carousel: None,
icon: None, icon: None,
}; };

View file

@ -14,23 +14,27 @@ pub struct StoreRowModel {
icon: Option<gtk::Image>, icon: Option<gtk::Image>,
#[tracker::do_not_track] #[tracker::do_not_track]
pub input_sender: relm4::Sender<StoreRowModelMsg>, pub input_sender: relm4::Sender<StoreRowModelMsg>,
pub enabled: bool,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StoreRowModelInit { pub struct StoreRowModelInit {
pub plugin: Plugin, pub plugin: Plugin,
pub enabled: bool,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum StoreRowModelMsg { pub enum StoreRowModelMsg {
LoadIcon, LoadIcon,
Refresh, Refresh(bool),
SetEnabled(bool),
} }
#[derive(Debug)] #[derive(Debug)]
pub enum StoreRowModelOutMsg { pub enum StoreRowModelOutMsg {
Install(Plugin, relm4::Sender<StoreRowModelMsg>), Install(Plugin, relm4::Sender<StoreRowModelMsg>),
Remove(Plugin, relm4::Sender<StoreRowModelMsg>), Remove(Plugin, relm4::Sender<StoreRowModelMsg>),
SetEnabled(Plugin, bool),
} }
#[relm4::factory(async pub)] #[relm4::factory(async pub)]
@ -121,15 +125,22 @@ impl AsyncFactoryComponent for StoreRowModel {
gtk::Switch { gtk::Switch {
#[track = "self.changed(StoreRowModel::plugin())"] #[track = "self.changed(StoreRowModel::plugin())"]
set_visible: self.plugin.is_installed(), set_visible: self.plugin.is_installed(),
#[track = "self.changed(StoreRowModel::enabled())"]
set_active: self.enabled,
set_valign: gtk::Align::Center, set_valign: gtk::Align::Center,
set_halign: 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<Self>) { async fn update(&mut self, message: Self::Input, sender: AsyncFactorySender<Self>) {
self.reset(); self.reset();
match message { 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 { Self {
tracker: 0, tracker: 0,
plugin: init.plugin, plugin: init.plugin,
enabled: init.enabled,
icon: None, icon: None,
input_sender: sender.input_sender().clone(), input_sender: sender.input_sender().clone(),
} }