diff --git a/src/config.rs b/src/config.rs index f0c1aa2..5a2e29e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -20,8 +20,7 @@ use std::{ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PluginConfig { - pub appid: String, - pub version: String, + pub plugin: Plugin, pub enabled: bool, pub exec_path: PathBuf, } @@ -29,14 +28,19 @@ pub struct PluginConfig { impl From<&Plugin> for PluginConfig { fn from(p: &Plugin) -> Self { Self { - appid: p.appid.clone(), - version: p.version.clone(), + plugin: p.clone(), enabled: true, exec_path: p.exec_path(), } } } +impl From<&PluginConfig> for Plugin { + fn from(cp: &PluginConfig) -> Self { + cp.plugin.clone() + } +} + const DEFAULT_WIN_SIZE: [i32; 2] = [360, 400]; const fn default_win_size() -> [i32; 2] { diff --git a/src/ui/app.rs b/src/ui/app.rs index ec33efd..5185554 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -268,7 +268,10 @@ impl App { .filter_map(|cp| { if cp.enabled { if let Err(e) = mark_as_executable(&cp.exec_path) { - eprintln!("Failed to mark plugin {} as executable: {e}", cp.appid); + eprintln!( + "Failed to mark plugin {} as executable: {e}", + cp.plugin.appid + ); None } else { Some(format!("'{}'", cp.exec_path.to_string_lossy())) diff --git a/src/ui/plugins/mod.rs b/src/ui/plugins/mod.rs index ea5d935..3a88848 100644 --- a/src/ui/plugins/mod.rs +++ b/src/ui/plugins/mod.rs @@ -6,7 +6,7 @@ use crate::{paths::get_plugins_dir, util::file_utils::mark_as_executable}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub struct Plugin { pub appid: String, pub name: String, @@ -19,20 +19,6 @@ pub struct Plugin { pub exec_url: String, } -impl PartialEq for Plugin { - fn eq(&self, other: &Self) -> bool { - self.appid == other.appid - && self.name == other.name - && self.version == other.version - && self.icon_url == other.icon_url - && self.short_description == other.short_description - && self.description == other.description - && self.hompage_url == other.hompage_url - && self.screenshots == other.screenshots - && self.exec_url == other.exec_url - } -} - impl Plugin { pub fn exec_path(&self) -> PathBuf { get_plugins_dir().join(format!("{}.AppImage", self.appid)) diff --git a/src/ui/plugins/store.rs b/src/ui/plugins/store.rs index 36bb3d3..c8221c8 100644 --- a/src/ui/plugins/store.rs +++ b/src/ui/plugins/store.rs @@ -172,7 +172,7 @@ impl AsyncComponent for PluginStore { Self::Input::Refresh => { self.set_refreshing(true); // TODO: populate from web - self.set_plugins(vec![ + let mut plugins = vec![ Plugin { appid: "com.github.galiser.wlx-overlay-s".into(), name: "WLX Overlay S".into(), @@ -186,7 +186,22 @@ impl AsyncComponent for PluginStore { short_description: Some("Access your Wayland/X11 desktop".into()), exec_url: "https://github.com/galister/wlx-overlay-s/releases/download/v0.4.4/WlxOverlay-S-v0.4.4-x86_64.AppImage".into() }, - ]); + ]; + { + let appids_from_web = plugins + .iter() + .map(|p| p.appid.clone()) + .collect::>(); + // add all plugins that are in config but not retrieved + plugins.extend(self.config_plugins.values().filter_map(|cp| { + if appids_from_web.contains(&cp.plugin.appid) { + None + } else { + Some(Plugin::from(cp)) + } + })); + } + self.set_plugins(plugins); { let mut guard = self.plugin_rows.as_mut().unwrap().guard(); guard.clear(); @@ -197,6 +212,10 @@ impl AsyncComponent for PluginStore { .config_plugins .get(&plugin.appid) .is_some_and(|cp| cp.enabled), + needs_update: self + .config_plugins + .get(&plugin.appid) + .is_some_and(|cp| cp.plugin.version != plugin.version), }); }); } @@ -256,8 +275,8 @@ impl AsyncComponent for PluginStore { )) .expect(SENDER_IO_ERR_MSG); } - row_sender.emit(StoreRowModelMsg::Refresh(true)); - self.details.emit(StoreDetailMsg::Refresh(true)); + row_sender.emit(StoreRowModelMsg::Refresh(true, false)); + self.details.emit(StoreDetailMsg::Refresh(true, false)); self.set_locked(false); } Self::Input::Remove(plugin, row_sender) => { @@ -282,8 +301,8 @@ impl AsyncComponent for PluginStore { .expect(SENDER_IO_ERR_MSG); } } - row_sender.emit(StoreRowModelMsg::Refresh(false)); - self.details.emit(StoreDetailMsg::Refresh(false)); + row_sender.emit(StoreRowModelMsg::Refresh(false, false)); + self.details.emit(StoreDetailMsg::Refresh(false, false)); self.set_locked(false); } Self::Input::SetEnabled(plugin, enabled) => { @@ -298,11 +317,17 @@ impl AsyncComponent for PluginStore { .find(|row| row.is_some_and(|row| row.plugin.appid == plugin.appid)) .flatten() { - row.input_sender.emit(StoreRowModelMsg::Refresh(enabled)); + row.input_sender.emit(StoreRowModelMsg::Refresh( + enabled, + cp.plugin.version != plugin.version, + )); } else { eprintln!("could not find corresponding listbox row!") } - self.details.emit(StoreDetailMsg::Refresh(enabled)); + self.details.emit(StoreDetailMsg::Refresh( + enabled, + cp.plugin.version != plugin.version, + )); } else { eprintln!( "failed to set plugin {} enabled: could not find in hashmap", @@ -324,6 +349,9 @@ impl AsyncComponent for PluginStore { self.config_plugins .get(&plugin.appid) .is_some_and(|cp| cp.enabled), + self.config_plugins + .get(&plugin.appid) + .is_some_and(|cp| cp.plugin.version != plugin.version), )); self.main_stack .as_ref() diff --git a/src/ui/plugins/store_detail.rs b/src/ui/plugins/store_detail.rs index 077423b..1897a22 100644 --- a/src/ui/plugins/store_detail.rs +++ b/src/ui/plugins/store_detail.rs @@ -11,15 +11,16 @@ pub struct StoreDetail { carousel: Option, #[tracker::do_not_track] icon: Option, + needs_update: bool, } #[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum StoreDetailMsg { - SetPlugin(Plugin, bool), + SetPlugin(Plugin, bool, bool), SetIcon, SetScreenshots, - Refresh(bool), + Refresh(bool, bool), Install, Remove, SetEnabled(bool), @@ -130,6 +131,20 @@ impl AsyncComponent for StoreDetail { sender.input(Self::Input::Remove); } }, + gtk::Button { + #[track = "model.changed(Self::plugin()) || model.changed(Self::needs_update())"] + set_visible: model + .plugin + .as_ref() + .is_some_and(|p| p.is_installed()) && model.needs_update, + add_css_class: "suggested-action", + set_label: "Update", + set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, + connect_clicked[sender] => move |_| { + sender.input(Self::Input::Install); + } + }, gtk::Switch { #[track = "model.changed(Self::plugin())"] set_visible: model.plugin.as_ref() @@ -194,9 +209,10 @@ impl AsyncComponent for StoreDetail { self.reset(); match message { - Self::Input::SetPlugin(p, enabled) => { + Self::Input::SetPlugin(p, enabled, needs_update) => { self.set_plugin(Some(p)); self.set_enabled(enabled); + self.set_needs_update(needs_update); sender.input(Self::Input::SetIcon); sender.input(Self::Input::SetScreenshots); } @@ -244,9 +260,10 @@ impl AsyncComponent for StoreDetail { } } } - Self::Input::Refresh(enabled) => { + Self::Input::Refresh(enabled, needs_update) => { self.mark_all_changed(); self.set_enabled(enabled); + self.set_needs_update(needs_update); } Self::Input::Install => { if let Some(plugin) = self.plugin.as_ref() { @@ -284,6 +301,7 @@ impl AsyncComponent for StoreDetail { enabled: false, carousel: None, icon: None, + needs_update: false, }; let widgets = view_output!(); diff --git a/src/ui/plugins/store_row_factory.rs b/src/ui/plugins/store_row_factory.rs index 38fc4ac..84e1e2b 100644 --- a/src/ui/plugins/store_row_factory.rs +++ b/src/ui/plugins/store_row_factory.rs @@ -15,18 +15,20 @@ pub struct StoreRowModel { #[tracker::do_not_track] pub input_sender: relm4::Sender, pub enabled: bool, + pub needs_update: bool, } #[derive(Debug)] pub struct StoreRowModelInit { pub plugin: Plugin, pub enabled: bool, + pub needs_update: bool, } #[derive(Debug)] pub enum StoreRowModelMsg { LoadIcon, - Refresh(bool), + Refresh(bool, bool), SetEnabled(bool), } @@ -105,22 +107,45 @@ impl AsyncFactoryComponent for StoreRowModel { .expect(SENDER_IO_ERR_MSG); } }, - gtk::Button { - #[track = "self.changed(StoreRowModel::plugin())"] - set_visible: self.plugin.is_installed(), - set_icon_name: "app-remove-symbolic", - add_css_class: "destructive-action", - set_tooltip_text: Some("Remove"), + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 6, set_valign: gtk::Align::Center, set_halign: gtk::Align::Center, - connect_clicked[sender, plugin] => move |_| { - sender - .output(Self::Output::Remove( - plugin.clone(), - sender.input_sender().clone() - )) - .expect(SENDER_IO_ERR_MSG); - } + gtk::Button { + #[track = "self.changed(StoreRowModel::plugin())"] + set_visible: self.plugin.is_installed(), + set_icon_name: "app-remove-symbolic", + add_css_class: "destructive-action", + set_tooltip_text: Some("Remove"), + set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, + connect_clicked[sender, plugin] => move |_| { + sender + .output(Self::Output::Remove( + plugin.clone(), + sender.input_sender().clone() + )) + .expect(SENDER_IO_ERR_MSG); + } + }, + gtk::Button { + #[track = "self.changed(StoreRowModel::plugin()) || self.changed(StoreRowModel::needs_update())"] + set_visible: self.plugin.is_installed() && self.needs_update, + set_icon_name: "view-refresh-symbolic", + add_css_class: "suggested-action", + set_tooltip_text: Some("Update"), + set_valign: gtk::Align::Center, + set_halign: gtk::Align::Center, + connect_clicked[sender, plugin] => move |_| { + sender + .output(Self::Output::Install( + plugin.clone(), + sender.input_sender().clone() + )) + .expect(SENDER_IO_ERR_MSG); + } + }, }, gtk::Switch { #[track = "self.changed(StoreRowModel::plugin())"] @@ -162,9 +187,10 @@ impl AsyncFactoryComponent for StoreRowModel { .output(Self::Output::SetEnabled(self.plugin.clone(), state)) .expect(SENDER_IO_ERR_MSG); } - Self::Input::Refresh(enabled) => { + Self::Input::Refresh(enabled, needs_update) => { self.mark_all_changed(); - self.set_enabled(enabled) + self.set_enabled(enabled); + self.set_needs_update(needs_update); } } } @@ -180,6 +206,7 @@ impl AsyncFactoryComponent for StoreRowModel { enabled: init.enabled, icon: None, input_sender: sender.input_sender().clone(), + needs_update: init.needs_update, } }