feat: can enable and disable plugins

This commit is contained in:
Gabriele Musco 2024-08-19 19:22:29 +02:00
parent 2f8caafac2
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,
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<Profile>,
#[serde(default = "default_win_size")]
pub win_size: [i32; 2],
#[serde(default)]
pub plugins: HashMap<String, PluginConfig>,
}
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(),
}
}
}

View file

@ -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<String, PluginConfig>),
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();
}
}
}

View file

@ -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<StoreDetail>,
#[tracker::do_not_track]
main_stack: Option<gtk::Stack>,
#[tracker::do_not_track]
config_plugins: HashMap<String, PluginConfig>,
refreshing: bool,
locked: bool,
plugins: Vec<Plugin>,
@ -32,18 +38,24 @@ pub enum PluginStoreMsg {
InstallDownload(Plugin, relm4::Sender<StoreRowModelMsg>),
RemoveFromDetails(Plugin),
Remove(Plugin, relm4::Sender<StoreRowModelMsg>),
Enable(String),
Disable(String),
SetEnabled(Plugin, bool),
ShowDetails(usize),
ShowPluginList,
}
#[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)]
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::<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);
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::<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);
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<Self>,
) -> AsyncComponentParts<Self> {
@ -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());

View file

@ -6,6 +6,7 @@ use relm4::prelude::*;
#[tracker::track]
pub struct StoreDetail {
plugin: Option<Plugin>,
enabled: bool,
#[tracker::do_not_track]
carousel: Option<adw::Carousel>,
#[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,
};

View file

@ -14,23 +14,27 @@ pub struct StoreRowModel {
icon: Option<gtk::Image>,
#[tracker::do_not_track]
pub input_sender: relm4::Sender<StoreRowModelMsg>,
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<StoreRowModelMsg>),
Remove(Plugin, relm4::Sender<StoreRowModelMsg>),
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<Self>) {
async fn update(&mut self, message: Self::Input, sender: AsyncFactorySender<Self>) {
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(),
}