mirror of
https://gitlab.com/gabmus/envision.git
synced 2025-04-19 19:14:53 +00:00
feat: add custom plugins
This commit is contained in:
parent
4a7ba9c0c2
commit
b6d8b0e6c8
9 changed files with 491 additions and 146 deletions
|
@ -22,7 +22,6 @@ use std::{
|
|||
pub struct PluginConfig {
|
||||
pub plugin: Plugin,
|
||||
pub enabled: bool,
|
||||
pub exec_path: PathBuf,
|
||||
}
|
||||
|
||||
impl From<&Plugin> for PluginConfig {
|
||||
|
@ -30,7 +29,6 @@ impl From<&Plugin> for PluginConfig {
|
|||
Self {
|
||||
plugin: p.clone(),
|
||||
enabled: true,
|
||||
exec_path: p.exec_path(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -266,15 +266,20 @@ impl App {
|
|||
.plugins
|
||||
.values()
|
||||
.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.plugin.appid
|
||||
);
|
||||
None
|
||||
if cp.enabled && cp.plugin.validate() {
|
||||
if let Some(exec) = cp.plugin.executable() {
|
||||
if let Err(e) = mark_as_executable(&exec) {
|
||||
error!(
|
||||
"failed to mark plugin {} as executable: {e}",
|
||||
cp.plugin.appid
|
||||
);
|
||||
None
|
||||
} else {
|
||||
Some(format!("'{}'", exec.to_string_lossy()))
|
||||
}
|
||||
} else {
|
||||
Some(format!("'{}'", cp.exec_path.to_string_lossy()))
|
||||
error!("no executable for plugin {}", cp.plugin.appid);
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
|
|
@ -148,7 +148,7 @@ impl AsyncComponent for MainView {
|
|||
menu! {
|
||||
app_menu: {
|
||||
section! {
|
||||
"Plugin _store" => PluginStoreAction,
|
||||
"Plugin_s" => PluginStoreAction,
|
||||
// value inside action is ignored
|
||||
"_Debug View" => DebugViewToggleAction,
|
||||
"_Build Profile" => BuildProfileAction,
|
||||
|
|
169
src/ui/plugins/add_custom_plugin_win.rs
Normal file
169
src/ui/plugins/add_custom_plugin_win.rs
Normal file
|
@ -0,0 +1,169 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
constants::APP_ID,
|
||||
ui::{
|
||||
preference_rows::{entry_row, file_row},
|
||||
SENDER_IO_ERR_MSG,
|
||||
},
|
||||
};
|
||||
|
||||
use super::Plugin;
|
||||
use adw::prelude::*;
|
||||
use gtk::glib::clone;
|
||||
use relm4::prelude::*;
|
||||
|
||||
#[tracker::track]
|
||||
pub struct AddCustomPluginWin {
|
||||
#[tracker::do_not_track]
|
||||
parent: gtk::Window,
|
||||
#[tracker::do_not_track]
|
||||
win: Option<adw::Dialog>,
|
||||
/// this is true when enough fields are populated, allowing the creation
|
||||
/// of the plugin object to add
|
||||
can_add: bool,
|
||||
#[tracker::do_not_track]
|
||||
plugin: Plugin,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AddCustomPluginWinMsg {
|
||||
Present,
|
||||
Close,
|
||||
OnNameChange(String),
|
||||
OnExecPathChange(Option<String>),
|
||||
Add,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AddCustomPluginWinOutMsg {
|
||||
Add(Plugin),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AddCustomPluginWinInit {
|
||||
pub parent: gtk::Window,
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for AddCustomPluginWin {
|
||||
type Init = AddCustomPluginWinInit;
|
||||
type Input = AddCustomPluginWinMsg;
|
||||
type Output = AddCustomPluginWinOutMsg;
|
||||
|
||||
view! {
|
||||
#[name(win)]
|
||||
adw::Dialog {
|
||||
set_can_close: true,
|
||||
#[wrap(Some)]
|
||||
set_child: inner = &adw::ToolbarView {
|
||||
set_top_bar_style: adw::ToolbarStyle::Flat,
|
||||
set_bottom_bar_style: adw::ToolbarStyle::Flat,
|
||||
set_vexpand: true,
|
||||
set_hexpand: true,
|
||||
add_top_bar: top_bar = &adw::HeaderBar {
|
||||
set_show_end_title_buttons: false,
|
||||
set_show_start_title_buttons: false,
|
||||
pack_start: cancel_btn = >k::Button {
|
||||
set_label: "Cancel",
|
||||
add_css_class: "destructive-action",
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::Close)
|
||||
},
|
||||
},
|
||||
pack_end: add_btn = >k::Button {
|
||||
set_label: "Add",
|
||||
add_css_class: "suggested-action",
|
||||
#[track = "model.changed(AddCustomPluginWin::can_add())"]
|
||||
set_sensitive: model.can_add,
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::Add)
|
||||
},
|
||||
},
|
||||
#[wrap(Some)]
|
||||
set_title_widget: title_label = &adw::WindowTitle {
|
||||
set_title: "Add Custom Plugin",
|
||||
},
|
||||
},
|
||||
#[wrap(Some)]
|
||||
set_content: content = &adw::PreferencesPage {
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
add: grp = &adw::PreferencesGroup {
|
||||
add: &entry_row(
|
||||
"Plugin Name",
|
||||
"",
|
||||
clone!(
|
||||
#[strong] sender,
|
||||
move |row| sender.input(Self::Input::OnNameChange(row.text().to_string()))
|
||||
)
|
||||
),
|
||||
add: &file_row(
|
||||
"Plugin Executable",
|
||||
None,
|
||||
None,
|
||||
Some(model.parent.clone()),
|
||||
clone!(
|
||||
#[strong] sender,
|
||||
move |path_s| sender.input(Self::Input::OnExecPathChange(path_s))
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
|
||||
self.reset();
|
||||
|
||||
match message {
|
||||
Self::Input::Present => self.win.as_ref().unwrap().present(Some(&self.parent)),
|
||||
Self::Input::Close => {
|
||||
self.win.as_ref().unwrap().close();
|
||||
}
|
||||
Self::Input::Add => {
|
||||
if self.plugin.validate() {
|
||||
sender
|
||||
.output(Self::Output::Add(self.plugin.clone()))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
self.win.as_ref().unwrap().close();
|
||||
}
|
||||
}
|
||||
Self::Input::OnNameChange(name) => {
|
||||
self.plugin.appid = if !name.is_empty() {
|
||||
format!("{APP_ID}.customPlugin.{name}")
|
||||
} else {
|
||||
String::default()
|
||||
};
|
||||
self.plugin.name = name;
|
||||
self.set_can_add(self.plugin.validate());
|
||||
}
|
||||
Self::Input::OnExecPathChange(ep) => {
|
||||
self.plugin.exec_path = ep.map(PathBuf::from);
|
||||
self.set_can_add(self.plugin.validate());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
init: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let mut model = Self {
|
||||
tracker: 0,
|
||||
win: None,
|
||||
parent: init.parent,
|
||||
can_add: false,
|
||||
plugin: Plugin {
|
||||
short_description: Some("Custom Plugin".into()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
let widgets = view_output!();
|
||||
model.win = Some(widgets.win.clone());
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
}
|
|
@ -1,34 +1,63 @@
|
|||
pub mod add_custom_plugin_win;
|
||||
pub mod store;
|
||||
mod store_detail;
|
||||
mod store_row_factory;
|
||||
|
||||
use crate::{paths::get_plugins_dir, util::file_utils::mark_as_executable};
|
||||
use anyhow::bail;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)]
|
||||
pub struct Plugin {
|
||||
pub appid: String,
|
||||
pub name: String,
|
||||
pub icon_url: Option<String>,
|
||||
pub version: String,
|
||||
pub version: Option<String>,
|
||||
pub short_description: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub hompage_url: String,
|
||||
pub hompage_url: Option<String>,
|
||||
pub screenshots: Vec<String>,
|
||||
pub exec_url: String,
|
||||
/// either one of exec_url or exec_path must be provided
|
||||
pub exec_url: Option<String>,
|
||||
/// either one of exec_url or exec_path must be provided
|
||||
pub exec_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
pub fn exec_path(&self) -> PathBuf {
|
||||
get_plugins_dir().join(format!("{}.AppImage", self.appid))
|
||||
pub fn executable(&self) -> Option<PathBuf> {
|
||||
if self.exec_path.is_some() {
|
||||
self.exec_path.clone()
|
||||
} else {
|
||||
let canonical = self.canonical_exec_path();
|
||||
if canonical.is_file() {
|
||||
Some(canonical)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn canonical_exec_path(&self) -> PathBuf {
|
||||
get_plugins_dir().join(&self.appid)
|
||||
}
|
||||
|
||||
pub fn is_installed(&self) -> bool {
|
||||
self.exec_path().exists()
|
||||
self.executable().as_ref().is_some_and(|p| p.is_file())
|
||||
}
|
||||
|
||||
pub fn mark_as_executable(&self) -> anyhow::Result<()> {
|
||||
mark_as_executable(&self.exec_path())
|
||||
if let Some(p) = self.executable().as_ref() {
|
||||
mark_as_executable(p)
|
||||
} else {
|
||||
bail!("no exec_path for plugin")
|
||||
}
|
||||
}
|
||||
|
||||
/// validate if the plugin can be displayed correctly and run
|
||||
pub fn validate(&self) -> bool {
|
||||
!self.appid.is_empty()
|
||||
&& !self.name.is_empty()
|
||||
&& self.executable().as_ref().is_some_and(|p| p.is_file())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use super::{
|
||||
add_custom_plugin_win::{
|
||||
AddCustomPluginWin, AddCustomPluginWinInit, AddCustomPluginWinMsg, AddCustomPluginWinOutMsg,
|
||||
},
|
||||
store_detail::{StoreDetail, StoreDetailMsg, StoreDetailOutMsg},
|
||||
store_row_factory::{StoreRowModel, StoreRowModelInit, StoreRowModelMsg, StoreRowModelOutMsg},
|
||||
Plugin,
|
||||
|
@ -11,6 +14,7 @@ use crate::{
|
|||
use adw::prelude::*;
|
||||
use relm4::{factory::AsyncFactoryVecDeque, prelude::*};
|
||||
use std::{collections::HashMap, fs::remove_file};
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[tracker::track]
|
||||
pub struct PluginStore {
|
||||
|
@ -27,6 +31,14 @@ pub struct PluginStore {
|
|||
refreshing: bool,
|
||||
locked: bool,
|
||||
plugins: Vec<Plugin>,
|
||||
#[tracker::do_not_track]
|
||||
add_custom_plugin_win: Option<Controller<AddCustomPluginWin>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum PluginStoreSignalSource {
|
||||
Row,
|
||||
Detail,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -36,11 +48,13 @@ pub enum PluginStoreMsg {
|
|||
Install(Plugin, relm4::Sender<StoreRowModelMsg>),
|
||||
InstallFromDetails(Plugin),
|
||||
InstallDownload(Plugin, relm4::Sender<StoreRowModelMsg>),
|
||||
RemoveFromDetails(Plugin),
|
||||
Remove(Plugin, relm4::Sender<StoreRowModelMsg>),
|
||||
SetEnabled(Plugin, bool),
|
||||
Remove(Plugin),
|
||||
SetEnabled(PluginStoreSignalSource, Plugin, bool),
|
||||
ShowDetails(usize),
|
||||
ShowPluginList,
|
||||
PresentAddCustomPluginWin,
|
||||
AddPluginToConfig(Plugin),
|
||||
AddCustomPlugin(Plugin),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -53,6 +67,26 @@ pub enum PluginStoreOutMsg {
|
|||
UpdateConfigPlugins(HashMap<String, PluginConfig>),
|
||||
}
|
||||
|
||||
impl PluginStore {
|
||||
fn refresh_plugin_rows(&mut self) {
|
||||
let mut guard = self.plugin_rows.as_mut().unwrap().guard();
|
||||
guard.clear();
|
||||
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),
|
||||
needs_update: self
|
||||
.config_plugins
|
||||
.get(&plugin.appid)
|
||||
.is_some_and(|cp| cp.plugin.version != plugin.version),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[relm4::component(pub async)]
|
||||
impl AsyncComponent for PluginStore {
|
||||
type Init = PluginStoreInit;
|
||||
|
@ -63,12 +97,21 @@ impl AsyncComponent for PluginStore {
|
|||
view! {
|
||||
#[name(win)]
|
||||
adw::Window {
|
||||
set_title: Some("Plugin Store"),
|
||||
set_title: Some("Plugins"),
|
||||
#[name(main_stack)]
|
||||
gtk::Stack {
|
||||
add_child = &adw::ToolbarView {
|
||||
set_top_bar_style: adw::ToolbarStyle::Flat,
|
||||
add_top_bar: headerbar = &adw::HeaderBar {
|
||||
pack_start: add_custom_plugin_btn = >k::Button {
|
||||
set_icon_name: "list-add-symbolic",
|
||||
set_tooltip_text: Some("Add custom plugin"),
|
||||
#[track = "model.changed(PluginStore::refreshing()) || model.changed(PluginStore::locked())"]
|
||||
set_sensitive: !(model.refreshing || model.locked),
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::PresentAddCustomPluginWin)
|
||||
},
|
||||
},
|
||||
pack_end: refreshbtn = >k::Button {
|
||||
set_icon_name: "view-refresh-symbolic",
|
||||
set_tooltip_text: Some("Refresh"),
|
||||
|
@ -76,8 +119,8 @@ impl AsyncComponent for PluginStore {
|
|||
set_sensitive: !(model.refreshing || model.locked),
|
||||
connect_clicked[sender] => move |_| {
|
||||
sender.input(Self::Input::Refresh);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
#[wrap(Some)]
|
||||
set_content: inner = >k::Box {
|
||||
|
@ -169,6 +212,40 @@ impl AsyncComponent for PluginStore {
|
|||
self.win.as_ref().unwrap().present();
|
||||
sender.input(Self::Input::Refresh);
|
||||
}
|
||||
Self::Input::AddCustomPlugin(plugin) => {
|
||||
if self.config_plugins.contains_key(&plugin.appid) {
|
||||
alert(
|
||||
"Failed to add custom plugin",
|
||||
Some("A plugin with the same name already exists"),
|
||||
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
return;
|
||||
}
|
||||
sender.input(Self::Input::AddPluginToConfig(plugin));
|
||||
sender.input(Self::Input::Refresh);
|
||||
}
|
||||
Self::Input::AddPluginToConfig(plugin) => {
|
||||
self.config_plugins
|
||||
.insert(plugin.appid.clone(), PluginConfig::from(&plugin));
|
||||
sender
|
||||
.output(Self::Output::UpdateConfigPlugins(
|
||||
self.config_plugins.clone(),
|
||||
))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
Self::Input::PresentAddCustomPluginWin => {
|
||||
let add_win = AddCustomPluginWin::builder()
|
||||
.launch(AddCustomPluginWinInit {
|
||||
parent: self.win.as_ref().unwrap().clone().upcast(),
|
||||
})
|
||||
.forward(sender.input_sender(), |msg| match msg {
|
||||
AddCustomPluginWinOutMsg::Add(plugin) => {
|
||||
Self::Input::AddCustomPlugin(plugin)
|
||||
}
|
||||
});
|
||||
add_win.sender().emit(AddCustomPluginWinMsg::Present);
|
||||
self.add_custom_plugin_win = Some(add_win);
|
||||
}
|
||||
Self::Input::Refresh => {
|
||||
self.set_refreshing(true);
|
||||
// TODO: populate from web
|
||||
|
@ -176,15 +253,16 @@ impl AsyncComponent for PluginStore {
|
|||
Plugin {
|
||||
appid: "com.github.galiser.wlx-overlay-s".into(),
|
||||
name: "WLX Overlay S".into(),
|
||||
version: "0.4.4".into(),
|
||||
hompage_url: "https://github.com/galister/wlx-overlay-s".into(),
|
||||
version: Some("0.6.0".into()),
|
||||
hompage_url: Some("https://github.com/galister/wlx-overlay-s".into()),
|
||||
icon_url: Some("https://github.com/galister/wlx-overlay-s/raw/main/wlx-overlay-s.svg".into()),
|
||||
screenshots: vec![
|
||||
"https://github.com/galister/wlx-overlay-s/raw/guide/wlx-s.png".into(),
|
||||
],
|
||||
description: Some("A lightweight OpenXR/OpenVR overlay for Wayland and X11 desktops, inspired by XSOverlay.\n\nWlxOverlay-S allows you to access your desktop screens while in VR.\n\nIn comparison to similar overlays, WlxOverlay-S aims to run alongside VR games and experiences while having as little performance impact as possible. The UI appearance and rendering techniques are kept as simple and efficient as possible, while still allowing a high degree of customizability.".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: Some("https://github.com/galister/wlx-overlay-s/releases/download/v0.6/WlxOverlay-S-v0.6-x86_64.AppImage".into()),
|
||||
exec_path: None,
|
||||
},
|
||||
];
|
||||
{
|
||||
|
@ -202,23 +280,7 @@ impl AsyncComponent for PluginStore {
|
|||
}));
|
||||
}
|
||||
self.set_plugins(plugins);
|
||||
{
|
||||
let mut guard = self.plugin_rows.as_mut().unwrap().guard();
|
||||
guard.clear();
|
||||
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),
|
||||
needs_update: self
|
||||
.config_plugins
|
||||
.get(&plugin.appid)
|
||||
.is_some_and(|cp| cp.plugin.version != plugin.version),
|
||||
});
|
||||
});
|
||||
}
|
||||
self.refresh_plugin_rows();
|
||||
self.set_refreshing(false);
|
||||
}
|
||||
Self::Input::InstallFromDetails(plugin) => {
|
||||
|
@ -233,23 +295,7 @@ impl AsyncComponent for PluginStore {
|
|||
{
|
||||
sender.input(Self::Input::Install(plugin, row.input_sender.clone()))
|
||||
} else {
|
||||
eprintln!("could not find corresponding listbox row!")
|
||||
}
|
||||
}
|
||||
// TODO: merge implementation with install
|
||||
Self::Input::RemoveFromDetails(plugin) => {
|
||||
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()
|
||||
{
|
||||
sender.input(Self::Input::Remove(plugin, row.input_sender.clone()))
|
||||
} else {
|
||||
eprintln!("could not find corresponding listbox row!")
|
||||
error!("could not find corresponding listbox row")
|
||||
}
|
||||
}
|
||||
Self::Input::Install(plugin, row_sender) => {
|
||||
|
@ -257,79 +303,110 @@ impl AsyncComponent for PluginStore {
|
|||
sender.input(Self::Input::InstallDownload(plugin, row_sender))
|
||||
}
|
||||
Self::Input::InstallDownload(plugin, row_sender) => {
|
||||
if let Err(e) = download_file_async(&plugin.exec_url, &plugin.exec_path()).await {
|
||||
alert(
|
||||
"Download failed",
|
||||
Some(&format!(
|
||||
"Downloading {} {} failed:\n\n{e}",
|
||||
plugin.name, plugin.version
|
||||
)),
|
||||
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);
|
||||
}
|
||||
let mut plugin = plugin.clone();
|
||||
match plugin.exec_url.as_ref() {
|
||||
Some(url) => {
|
||||
let exec_path = plugin.canonical_exec_path();
|
||||
if let Err(e) = download_file_async(url, &exec_path).await {
|
||||
alert(
|
||||
"Download failed",
|
||||
Some(&format!(
|
||||
"Downloading {} {} failed:\n\n{e}",
|
||||
plugin.name,
|
||||
plugin
|
||||
.version
|
||||
.as_ref()
|
||||
.unwrap_or(&"(no version)".to_string())
|
||||
)),
|
||||
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
} else {
|
||||
plugin.exec_path = Some(exec_path);
|
||||
sender.input(Self::Input::AddPluginToConfig(plugin.clone()));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
alert(
|
||||
"Download failed",
|
||||
Some(&format!(
|
||||
"Downloading {} {} failed:\n\nNo executable url provided for this plugin, this is likely a bug!",
|
||||
plugin.name,
|
||||
plugin.version.as_ref().unwrap_or(&"(no version)".to_string()))
|
||||
),
|
||||
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>())
|
||||
);
|
||||
}
|
||||
};
|
||||
row_sender.emit(StoreRowModelMsg::Refresh(true, false));
|
||||
self.details.emit(StoreDetailMsg::Refresh(true, false));
|
||||
self.details
|
||||
.emit(StoreDetailMsg::Refresh(plugin.appid, true, false));
|
||||
self.set_locked(false);
|
||||
}
|
||||
Self::Input::Remove(plugin, row_sender) => {
|
||||
Self::Input::Remove(plugin) => {
|
||||
self.set_locked(true);
|
||||
let exec = plugin.exec_path();
|
||||
if exec.is_file() {
|
||||
if let Err(e) = remove_file(&exec) {
|
||||
alert(
|
||||
"Failed removing plugin",
|
||||
Some(&format!(
|
||||
"Could not remove plugin executable {}:\n\n{e}",
|
||||
exec.to_string_lossy()
|
||||
)),
|
||||
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);
|
||||
if let Some(exec) = plugin.executable() {
|
||||
// delete executable only if it's not a custom plugin
|
||||
if exec.is_file() && plugin.exec_url.is_some() {
|
||||
if let Err(e) = remove_file(&exec) {
|
||||
alert(
|
||||
"Failed removing plugin",
|
||||
Some(&format!(
|
||||
"Could not remove plugin executable {}:\n\n{e}",
|
||||
exec.to_string_lossy()
|
||||
)),
|
||||
Some(&self.win.as_ref().unwrap().clone().upcast::<gtk::Window>()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
row_sender.emit(StoreRowModelMsg::Refresh(false, false));
|
||||
self.details.emit(StoreDetailMsg::Refresh(false, false));
|
||||
self.config_plugins.remove(&plugin.appid);
|
||||
self.set_plugins(
|
||||
self.plugins
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|p| !(p.appid == plugin.appid && p.exec_url.is_none()))
|
||||
.collect(),
|
||||
);
|
||||
sender
|
||||
.output(Self::Output::UpdateConfigPlugins(
|
||||
self.config_plugins.clone(),
|
||||
))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
self.refresh_plugin_rows();
|
||||
self.details
|
||||
.emit(StoreDetailMsg::Refresh(plugin.appid, false, false));
|
||||
self.set_locked(false);
|
||||
}
|
||||
Self::Input::SetEnabled(plugin, enabled) => {
|
||||
Self::Input::SetEnabled(signal_sender, 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(
|
||||
if signal_sender == PluginStoreSignalSource::Detail {
|
||||
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,
|
||||
cp.plugin.version != plugin.version,
|
||||
));
|
||||
} else {
|
||||
error!("could not find corresponding listbox row")
|
||||
}
|
||||
}
|
||||
if signal_sender == PluginStoreSignalSource::Row {
|
||||
self.details.emit(StoreDetailMsg::Refresh(
|
||||
plugin.appid,
|
||||
enabled,
|
||||
cp.plugin.version != plugin.version,
|
||||
));
|
||||
} else {
|
||||
eprintln!("could not find corresponding listbox row!")
|
||||
}
|
||||
self.details.emit(StoreDetailMsg::Refresh(
|
||||
enabled,
|
||||
cp.plugin.version != plugin.version,
|
||||
));
|
||||
} else {
|
||||
eprintln!(
|
||||
debug!(
|
||||
"failed to set plugin {} enabled: could not find in hashmap",
|
||||
plugin.appid
|
||||
)
|
||||
|
@ -358,7 +435,7 @@ impl AsyncComponent for PluginStore {
|
|||
.unwrap()
|
||||
.set_visible_child_name("detailsview");
|
||||
} else {
|
||||
eprintln!("plugins list index out of range!")
|
||||
error!("plugins list index out of range!")
|
||||
}
|
||||
}
|
||||
Self::Input::ShowPluginList => {
|
||||
|
@ -387,13 +464,14 @@ impl AsyncComponent for PluginStore {
|
|||
.forward(sender.input_sender(), move |msg| match msg {
|
||||
StoreDetailOutMsg::GoBack => Self::Input::ShowPluginList,
|
||||
StoreDetailOutMsg::Install(plugin) => Self::Input::InstallFromDetails(plugin),
|
||||
StoreDetailOutMsg::Remove(plugin) => Self::Input::RemoveFromDetails(plugin),
|
||||
StoreDetailOutMsg::Remove(plugin) => Self::Input::Remove(plugin),
|
||||
StoreDetailOutMsg::SetEnabled(plugin, enabled) => {
|
||||
Self::Input::SetEnabled(plugin, enabled)
|
||||
Self::Input::SetEnabled(PluginStoreSignalSource::Detail, plugin, enabled)
|
||||
}
|
||||
}),
|
||||
config_plugins: init.config_plugins,
|
||||
main_stack: None,
|
||||
add_custom_plugin_win: None,
|
||||
};
|
||||
|
||||
let details_view = model.details.widget();
|
||||
|
@ -408,11 +486,9 @@ impl AsyncComponent for PluginStore {
|
|||
StoreRowModelOutMsg::Install(appid, row_sender) => {
|
||||
Self::Input::Install(appid, row_sender)
|
||||
}
|
||||
StoreRowModelOutMsg::Remove(appid, row_sender) => {
|
||||
Self::Input::Remove(appid, row_sender)
|
||||
}
|
||||
StoreRowModelOutMsg::Remove(appid) => Self::Input::Remove(appid),
|
||||
StoreRowModelOutMsg::SetEnabled(plugin, enabled) => {
|
||||
Self::Input::SetEnabled(plugin, enabled)
|
||||
Self::Input::SetEnabled(PluginStoreSignalSource::Row, plugin, enabled)
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ use super::Plugin;
|
|||
use crate::{downloader::cache_file, ui::SENDER_IO_ERR_MSG};
|
||||
use adw::prelude::*;
|
||||
use relm4::prelude::*;
|
||||
use tracing::warn;
|
||||
|
||||
#[tracker::track]
|
||||
pub struct StoreDetail {
|
||||
|
@ -20,7 +21,7 @@ pub enum StoreDetailMsg {
|
|||
SetPlugin(Plugin, bool, bool),
|
||||
SetIcon,
|
||||
SetScreenshots,
|
||||
Refresh(bool, bool),
|
||||
Refresh(String, bool, bool),
|
||||
Install,
|
||||
Remove,
|
||||
SetEnabled(bool),
|
||||
|
@ -86,7 +87,8 @@ impl AsyncComponent for StoreDetail {
|
|||
set_hexpand: true,
|
||||
#[name(icon)]
|
||||
gtk::Image {
|
||||
set_icon_name: Some("image-missing-symbolic"),
|
||||
set_icon_name: Some("application-x-addon-symbolic"),
|
||||
set_margin_end: 12,
|
||||
set_pixel_size: 96,
|
||||
},
|
||||
gtk::Label {
|
||||
|
@ -224,9 +226,14 @@ impl AsyncComponent for StoreDetail {
|
|||
self.icon.as_ref().unwrap().set_from_file(Some(dest));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed downloading icon '{url}': {e}");
|
||||
warn!("Failed downloading icon '{url}': {e}");
|
||||
}
|
||||
};
|
||||
} else {
|
||||
self.icon
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.set_icon_name(Some("application-x-addon-symbolic"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -254,16 +261,18 @@ impl AsyncComponent for StoreDetail {
|
|||
carousel.append(&clamp);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed downloading screenshot '{url}': {e}");
|
||||
warn!("failed downloading screenshot '{url}': {e}");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::Input::Refresh(enabled, needs_update) => {
|
||||
self.mark_all_changed();
|
||||
self.set_enabled(enabled);
|
||||
self.set_needs_update(needs_update);
|
||||
Self::Input::Refresh(appid, enabled, needs_update) => {
|
||||
if self.plugin.as_ref().is_some_and(|p| p.appid == appid) {
|
||||
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() {
|
||||
|
@ -277,6 +286,11 @@ impl AsyncComponent for StoreDetail {
|
|||
sender
|
||||
.output(Self::Output::Remove(plugin.clone()))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
if plugin.exec_url.is_none() {
|
||||
sender
|
||||
.output(Self::Output::GoBack)
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::Input::SetEnabled(enabled) => {
|
||||
|
|
|
@ -4,6 +4,7 @@ use gtk::prelude::*;
|
|||
use relm4::{
|
||||
factory::AsyncFactoryComponent, prelude::DynamicIndex, AsyncFactorySender, RelmWidgetExt,
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[tracker::track]
|
||||
|
@ -28,6 +29,7 @@ pub struct StoreRowModelInit {
|
|||
#[derive(Debug)]
|
||||
pub enum StoreRowModelMsg {
|
||||
LoadIcon,
|
||||
/// params: enabled, needs_update
|
||||
Refresh(bool, bool),
|
||||
SetEnabled(bool),
|
||||
}
|
||||
|
@ -35,7 +37,7 @@ pub enum StoreRowModelMsg {
|
|||
#[derive(Debug)]
|
||||
pub enum StoreRowModelOutMsg {
|
||||
Install(Plugin, relm4::Sender<StoreRowModelMsg>),
|
||||
Remove(Plugin, relm4::Sender<StoreRowModelMsg>),
|
||||
Remove(Plugin),
|
||||
SetEnabled(Plugin, bool),
|
||||
}
|
||||
|
||||
|
@ -57,7 +59,7 @@ impl AsyncFactoryComponent for StoreRowModel {
|
|||
set_margin_all: 12,
|
||||
#[name(icon)]
|
||||
gtk::Image {
|
||||
set_icon_name: Some("image-missing-symbolic"),
|
||||
set_icon_name: Some("application-x-addon-symbolic"),
|
||||
set_icon_size: gtk::IconSize::Large,
|
||||
},
|
||||
gtk::Box {
|
||||
|
@ -124,7 +126,6 @@ impl AsyncFactoryComponent for StoreRowModel {
|
|||
sender
|
||||
.output(Self::Output::Remove(
|
||||
plugin.clone(),
|
||||
sender.input_sender().clone()
|
||||
))
|
||||
.expect(SENDER_IO_ERR_MSG);
|
||||
}
|
||||
|
@ -176,7 +177,7 @@ impl AsyncFactoryComponent for StoreRowModel {
|
|||
self.icon.as_ref().unwrap().set_from_file(Some(dest));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed downloading icon '{url}': {e}");
|
||||
error!("failed downloading icon '{url}': {e}");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -156,13 +156,12 @@ pub fn spin_row<F: Fn(>k::Adjustment) + 'static>(
|
|||
row
|
||||
}
|
||||
|
||||
pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
||||
fn filedialog_row_base<F: Fn(Option<String>) + 'static + Clone>(
|
||||
title: &str,
|
||||
description: Option<&str>,
|
||||
value: Option<String>,
|
||||
root_win: Option<gtk::Window>,
|
||||
cb: F,
|
||||
) -> adw::ActionRow {
|
||||
) -> (adw::ActionRow, gtk::Label) {
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(title)
|
||||
.subtitle_lines(0)
|
||||
|
@ -174,14 +173,14 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
|||
row.set_subtitle(d);
|
||||
}
|
||||
|
||||
let path_label = >k::Label::builder()
|
||||
let path_label = gtk::Label::builder()
|
||||
.label(match value.as_ref() {
|
||||
None => "(None)",
|
||||
Some(p) => p.as_str(),
|
||||
})
|
||||
.wrap(true)
|
||||
.build();
|
||||
row.add_suffix(path_label);
|
||||
row.add_suffix(&path_label);
|
||||
|
||||
let clear_btn = gtk::Button::builder()
|
||||
.icon_name("edit-clear-symbolic")
|
||||
|
@ -200,6 +199,60 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
|||
cb(None)
|
||||
}
|
||||
));
|
||||
(row, path_label)
|
||||
}
|
||||
|
||||
pub fn file_row<F: Fn(Option<String>) + 'static + Clone>(
|
||||
title: &str,
|
||||
description: Option<&str>,
|
||||
value: Option<String>,
|
||||
root_win: Option<gtk::Window>,
|
||||
cb: F,
|
||||
) -> adw::ActionRow {
|
||||
let (row, path_label) = filedialog_row_base(title, description, value, cb.clone());
|
||||
|
||||
let filedialog = gtk::FileDialog::builder()
|
||||
.modal(true)
|
||||
.title(format!("Select {}", title))
|
||||
.build();
|
||||
|
||||
row.connect_activated(clone!(
|
||||
#[weak]
|
||||
path_label,
|
||||
move |_| {
|
||||
filedialog.open(
|
||||
root_win.as_ref(),
|
||||
gio::Cancellable::NONE,
|
||||
clone!(
|
||||
#[weak]
|
||||
path_label,
|
||||
#[strong]
|
||||
cb,
|
||||
move |res| {
|
||||
if let Ok(file) = res {
|
||||
if let Some(path) = file.path() {
|
||||
let path_s = path.to_string_lossy().to_string();
|
||||
path_label.set_text(&path_s);
|
||||
cb(Some(path_s))
|
||||
}
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
));
|
||||
|
||||
row
|
||||
}
|
||||
|
||||
pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
||||
title: &str,
|
||||
description: Option<&str>,
|
||||
value: Option<String>,
|
||||
root_win: Option<gtk::Window>,
|
||||
cb: F,
|
||||
) -> adw::ActionRow {
|
||||
let (row, path_label) = filedialog_row_base(title, description, value, cb.clone());
|
||||
let filedialog = gtk::FileDialog::builder()
|
||||
.modal(true)
|
||||
.title(format!("Select Path for {}", title))
|
||||
|
@ -220,8 +273,8 @@ pub fn path_row<F: Fn(Option<String>) + 'static + Clone>(
|
|||
move |res| {
|
||||
if let Ok(file) = res {
|
||||
if let Some(path) = file.path() {
|
||||
let path_s = path.to_str().unwrap().to_string();
|
||||
path_label.set_text(path_s.as_str());
|
||||
let path_s = path.to_string_lossy().to_string();
|
||||
path_label.set_text(&path_s);
|
||||
cb(Some(path_s))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue