feat: can update plugins; show plugins that are installed but no longer available

This commit is contained in:
Gabriele Musco 2024-08-21 07:53:59 +02:00
parent 01a61bc842
commit e8922203e8
6 changed files with 115 additions and 49 deletions

View file

@ -20,8 +20,7 @@ use std::{
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PluginConfig { pub struct PluginConfig {
pub appid: String, pub plugin: Plugin,
pub version: String,
pub enabled: bool, pub enabled: bool,
pub exec_path: PathBuf, pub exec_path: PathBuf,
} }
@ -29,14 +28,19 @@ pub struct PluginConfig {
impl From<&Plugin> for PluginConfig { impl From<&Plugin> for PluginConfig {
fn from(p: &Plugin) -> Self { fn from(p: &Plugin) -> Self {
Self { Self {
appid: p.appid.clone(), plugin: p.clone(),
version: p.version.clone(),
enabled: true, enabled: true,
exec_path: p.exec_path(), 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 DEFAULT_WIN_SIZE: [i32; 2] = [360, 400];
const fn default_win_size() -> [i32; 2] { const fn default_win_size() -> [i32; 2] {

View file

@ -268,7 +268,10 @@ impl App {
.filter_map(|cp| { .filter_map(|cp| {
if cp.enabled { if cp.enabled {
if let Err(e) = mark_as_executable(&cp.exec_path) { 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 None
} else { } else {
Some(format!("'{}'", cp.exec_path.to_string_lossy())) Some(format!("'{}'", cp.exec_path.to_string_lossy()))

View file

@ -6,7 +6,7 @@ use crate::{paths::get_plugins_dir, util::file_utils::mark_as_executable};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Debug, Clone, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct Plugin { pub struct Plugin {
pub appid: String, pub appid: String,
pub name: String, pub name: String,
@ -19,20 +19,6 @@ pub struct Plugin {
pub exec_url: String, 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 { impl Plugin {
pub fn exec_path(&self) -> PathBuf { pub fn exec_path(&self) -> PathBuf {
get_plugins_dir().join(format!("{}.AppImage", self.appid)) get_plugins_dir().join(format!("{}.AppImage", self.appid))

View file

@ -172,7 +172,7 @@ impl AsyncComponent for PluginStore {
Self::Input::Refresh => { Self::Input::Refresh => {
self.set_refreshing(true); self.set_refreshing(true);
// TODO: populate from web // TODO: populate from web
self.set_plugins(vec![ let mut plugins = vec![
Plugin { Plugin {
appid: "com.github.galiser.wlx-overlay-s".into(), appid: "com.github.galiser.wlx-overlay-s".into(),
name: "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()), 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() 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::<Vec<String>>();
// 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(); let mut guard = self.plugin_rows.as_mut().unwrap().guard();
guard.clear(); guard.clear();
@ -197,6 +212,10 @@ impl AsyncComponent for PluginStore {
.config_plugins .config_plugins
.get(&plugin.appid) .get(&plugin.appid)
.is_some_and(|cp| cp.enabled), .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); .expect(SENDER_IO_ERR_MSG);
} }
row_sender.emit(StoreRowModelMsg::Refresh(true)); row_sender.emit(StoreRowModelMsg::Refresh(true, false));
self.details.emit(StoreDetailMsg::Refresh(true)); self.details.emit(StoreDetailMsg::Refresh(true, false));
self.set_locked(false); self.set_locked(false);
} }
Self::Input::Remove(plugin, row_sender) => { Self::Input::Remove(plugin, row_sender) => {
@ -282,8 +301,8 @@ impl AsyncComponent for PluginStore {
.expect(SENDER_IO_ERR_MSG); .expect(SENDER_IO_ERR_MSG);
} }
} }
row_sender.emit(StoreRowModelMsg::Refresh(false)); row_sender.emit(StoreRowModelMsg::Refresh(false, false));
self.details.emit(StoreDetailMsg::Refresh(false)); self.details.emit(StoreDetailMsg::Refresh(false, false));
self.set_locked(false); self.set_locked(false);
} }
Self::Input::SetEnabled(plugin, enabled) => { 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)) .find(|row| row.is_some_and(|row| row.plugin.appid == plugin.appid))
.flatten() .flatten()
{ {
row.input_sender.emit(StoreRowModelMsg::Refresh(enabled)); row.input_sender.emit(StoreRowModelMsg::Refresh(
enabled,
cp.plugin.version != plugin.version,
));
} else { } else {
eprintln!("could not find corresponding listbox row!") 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 { } else {
eprintln!( eprintln!(
"failed to set plugin {} enabled: could not find in hashmap", "failed to set plugin {} enabled: could not find in hashmap",
@ -324,6 +349,9 @@ impl AsyncComponent for PluginStore {
self.config_plugins self.config_plugins
.get(&plugin.appid) .get(&plugin.appid)
.is_some_and(|cp| cp.enabled), .is_some_and(|cp| cp.enabled),
self.config_plugins
.get(&plugin.appid)
.is_some_and(|cp| cp.plugin.version != plugin.version),
)); ));
self.main_stack self.main_stack
.as_ref() .as_ref()

View file

@ -11,15 +11,16 @@ pub struct StoreDetail {
carousel: Option<adw::Carousel>, carousel: Option<adw::Carousel>,
#[tracker::do_not_track] #[tracker::do_not_track]
icon: Option<gtk::Image>, icon: Option<gtk::Image>,
needs_update: bool,
} }
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Debug)] #[derive(Debug)]
pub enum StoreDetailMsg { pub enum StoreDetailMsg {
SetPlugin(Plugin, bool), SetPlugin(Plugin, bool, bool),
SetIcon, SetIcon,
SetScreenshots, SetScreenshots,
Refresh(bool), Refresh(bool, bool),
Install, Install,
Remove, Remove,
SetEnabled(bool), SetEnabled(bool),
@ -130,6 +131,20 @@ impl AsyncComponent for StoreDetail {
sender.input(Self::Input::Remove); 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 { gtk::Switch {
#[track = "model.changed(Self::plugin())"] #[track = "model.changed(Self::plugin())"]
set_visible: model.plugin.as_ref() set_visible: model.plugin.as_ref()
@ -194,9 +209,10 @@ impl AsyncComponent for StoreDetail {
self.reset(); self.reset();
match message { match message {
Self::Input::SetPlugin(p, enabled) => { Self::Input::SetPlugin(p, enabled, needs_update) => {
self.set_plugin(Some(p)); self.set_plugin(Some(p));
self.set_enabled(enabled); self.set_enabled(enabled);
self.set_needs_update(needs_update);
sender.input(Self::Input::SetIcon); sender.input(Self::Input::SetIcon);
sender.input(Self::Input::SetScreenshots); 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.mark_all_changed();
self.set_enabled(enabled); self.set_enabled(enabled);
self.set_needs_update(needs_update);
} }
Self::Input::Install => { Self::Input::Install => {
if let Some(plugin) = self.plugin.as_ref() { if let Some(plugin) = self.plugin.as_ref() {
@ -284,6 +301,7 @@ impl AsyncComponent for StoreDetail {
enabled: false, enabled: false,
carousel: None, carousel: None,
icon: None, icon: None,
needs_update: false,
}; };
let widgets = view_output!(); let widgets = view_output!();

View file

@ -15,18 +15,20 @@ pub struct StoreRowModel {
#[tracker::do_not_track] #[tracker::do_not_track]
pub input_sender: relm4::Sender<StoreRowModelMsg>, pub input_sender: relm4::Sender<StoreRowModelMsg>,
pub enabled: bool, pub enabled: bool,
pub needs_update: bool,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct StoreRowModelInit { pub struct StoreRowModelInit {
pub plugin: Plugin, pub plugin: Plugin,
pub enabled: bool, pub enabled: bool,
pub needs_update: bool,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum StoreRowModelMsg { pub enum StoreRowModelMsg {
LoadIcon, LoadIcon,
Refresh(bool), Refresh(bool, bool),
SetEnabled(bool), SetEnabled(bool),
} }
@ -105,6 +107,11 @@ impl AsyncFactoryComponent for StoreRowModel {
.expect(SENDER_IO_ERR_MSG); .expect(SENDER_IO_ERR_MSG);
} }
}, },
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 6,
set_valign: gtk::Align::Center,
set_halign: gtk::Align::Center,
gtk::Button { gtk::Button {
#[track = "self.changed(StoreRowModel::plugin())"] #[track = "self.changed(StoreRowModel::plugin())"]
set_visible: self.plugin.is_installed(), set_visible: self.plugin.is_installed(),
@ -122,6 +129,24 @@ impl AsyncFactoryComponent for StoreRowModel {
.expect(SENDER_IO_ERR_MSG); .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 { gtk::Switch {
#[track = "self.changed(StoreRowModel::plugin())"] #[track = "self.changed(StoreRowModel::plugin())"]
set_visible: self.plugin.is_installed(), set_visible: self.plugin.is_installed(),
@ -162,9 +187,10 @@ impl AsyncFactoryComponent for StoreRowModel {
.output(Self::Output::SetEnabled(self.plugin.clone(), state)) .output(Self::Output::SetEnabled(self.plugin.clone(), state))
.expect(SENDER_IO_ERR_MSG); .expect(SENDER_IO_ERR_MSG);
} }
Self::Input::Refresh(enabled) => { Self::Input::Refresh(enabled, needs_update) => {
self.mark_all_changed(); 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, enabled: init.enabled,
icon: None, icon: None,
input_sender: sender.input_sender().clone(), input_sender: sender.input_sender().clone(),
needs_update: init.needs_update,
} }
} }