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)]
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] {

View file

@ -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()))

View file

@ -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))

View file

@ -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::<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();
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()

View file

@ -11,15 +11,16 @@ pub struct StoreDetail {
carousel: Option<adw::Carousel>,
#[tracker::do_not_track]
icon: Option<gtk::Image>,
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!();

View file

@ -15,18 +15,20 @@ pub struct StoreRowModel {
#[tracker::do_not_track]
pub input_sender: relm4::Sender<StoreRowModelMsg>,
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,
}
}