fix(plugins): harden marketplace loading
Use public GitHub metadata to avoid unauthenticated API limits. Read bundled manifests before enabling installs and surface startup load failures.
This commit is contained in:
parent
5116369dec
commit
64d109a614
7 changed files with 203 additions and 41 deletions
|
|
@ -523,6 +523,9 @@ pub(super) struct OpenCADStudio {
|
||||||
/// `external_plugins` — compatible, with a library, dlopen'd at startup).
|
/// `external_plugins` — compatible, with a library, dlopen'd at startup).
|
||||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||||
loaded_plugin_ids: rustc_hash::FxHashSet<String>,
|
loaded_plugin_ids: rustc_hash::FxHashSet<String>,
|
||||||
|
/// Startup load failures keyed by plugin id. The Plugin Manager uses these
|
||||||
|
/// to distinguish an installed-but-failed package from one awaiting restart.
|
||||||
|
plugin_load_errors: rustc_hash::FxHashMap<String, String>,
|
||||||
/// Curated plugin registry fetched from the OpenCADStudio repo.
|
/// Curated plugin registry fetched from the OpenCADStudio repo.
|
||||||
plugin_registry: Vec<crate::plugin::external::RegistryEntry>,
|
plugin_registry: Vec<crate::plugin::external::RegistryEntry>,
|
||||||
/// True while the curated registry request is in flight.
|
/// True while the curated registry request is in flight.
|
||||||
|
|
@ -540,7 +543,8 @@ pub(super) struct OpenCADStudio {
|
||||||
/// Live filter for installed and available plugin cards.
|
/// Live filter for installed and available plugin cards.
|
||||||
plugin_search_input: String,
|
plugin_search_input: String,
|
||||||
/// Installable release tags fetched per linked repo (for the dropdown).
|
/// Installable release tags fetched per linked repo (for the dropdown).
|
||||||
repo_release_tags: rustc_hash::FxHashMap<String, Vec<String>>,
|
repo_release_tags:
|
||||||
|
rustc_hash::FxHashMap<String, Vec<crate::plugin::external::ReleaseInfo>>,
|
||||||
/// The release tag currently selected per linked repo.
|
/// The release tag currently selected per linked repo.
|
||||||
repo_selected_tag: rustc_hash::FxHashMap<String, String>,
|
repo_selected_tag: rustc_hash::FxHashMap<String, String>,
|
||||||
/// Repository currently shown in the Plugin Manager detail pane.
|
/// Repository currently shown in the Plugin Manager detail pane.
|
||||||
|
|
@ -2174,8 +2178,11 @@ pub enum Message {
|
||||||
RecentThumbsLoaded(
|
RecentThumbsLoaded(
|
||||||
Vec<(std::path::PathBuf, Option<iced::widget::image::Handle>)>,
|
Vec<(std::path::PathBuf, Option<iced::widget::image::Handle>)>,
|
||||||
),
|
),
|
||||||
/// Installable release tags fetched for `owner/repo`.
|
/// Installable releases and manifest API versions fetched for `owner/repo`.
|
||||||
PluginReleasesFetched(String, Result<Vec<String>, String>),
|
PluginReleasesFetched(
|
||||||
|
String,
|
||||||
|
Result<Vec<crate::plugin::external::ReleaseInfo>, String>,
|
||||||
|
),
|
||||||
/// Choose a release tag for a repo (`repo`, `tag`).
|
/// Choose a release tag for a repo (`repo`, `tag`).
|
||||||
PluginReleaseSelect(String, String),
|
PluginReleaseSelect(String, String),
|
||||||
/// Select a plugin source and show its GitHub README in the detail pane.
|
/// Select a plugin source and show its GitHub README in the detail pane.
|
||||||
|
|
@ -2696,6 +2703,7 @@ impl OpenCADStudio {
|
||||||
disabled_plugins: rustc_hash::FxHashSet::default(),
|
disabled_plugins: rustc_hash::FxHashSet::default(),
|
||||||
external_plugins: Vec::new(),
|
external_plugins: Vec::new(),
|
||||||
loaded_plugin_ids: rustc_hash::FxHashSet::default(),
|
loaded_plugin_ids: rustc_hash::FxHashSet::default(),
|
||||||
|
plugin_load_errors: rustc_hash::FxHashMap::default(),
|
||||||
plugin_registry: Vec::new(),
|
plugin_registry: Vec::new(),
|
||||||
plugin_registry_loading: false,
|
plugin_registry_loading: false,
|
||||||
plugin_registry_error: None,
|
plugin_registry_error: None,
|
||||||
|
|
@ -2934,6 +2942,7 @@ impl OpenCADStudio {
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
app.command_line
|
app.command_line
|
||||||
.push_error(&format!("Plugin '{id}' failed to load: {e}"));
|
.push_error(&format!("Plugin '{id}' failed to load: {e}"));
|
||||||
|
app.plugin_load_errors.insert(id, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
app.loaded_plugin_ids = crate::plugin::external::loaded_ids().into_iter().collect();
|
app.loaded_plugin_ids = crate::plugin::external::loaded_ids().into_iter().collect();
|
||||||
|
|
|
||||||
|
|
@ -273,19 +273,13 @@ impl OpenCADStudio {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Background task: fetch `owner/repo`'s installable release tags.
|
/// Background task: fetch `owner/repo`'s installable releases and their
|
||||||
|
/// manifest API versions.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(in crate::app) fn fetch_releases_task(&self, repo: String) -> Task<Message> {
|
pub(in crate::app) fn fetch_releases_task(&self, repo: String) -> Task<Message> {
|
||||||
let label = repo.clone();
|
let label = repo.clone();
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async move {
|
async move { crate::plugin::marketplace::fetch_release_info(&repo) },
|
||||||
crate::plugin::marketplace::fetch_releases(&repo).map(|rs| {
|
|
||||||
rs.into_iter()
|
|
||||||
.filter(|r| r.installable())
|
|
||||||
.map(|r| r.tag)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
move |res| Message::PluginReleasesFetched(label, res),
|
move |res| Message::PluginReleasesFetched(label, res),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4699,17 +4699,20 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::PluginReleasesFetched(repo, Ok(tags)) => {
|
Message::PluginReleasesFetched(repo, Ok(releases)) => {
|
||||||
if let Some(first) = tags.first() {
|
if let Some(first) = releases.first() {
|
||||||
self.repo_selected_tag
|
self.repo_selected_tag
|
||||||
.entry(repo.clone())
|
.entry(repo.clone())
|
||||||
.or_insert_with(|| first.clone());
|
.or_insert_with(|| first.tag.clone());
|
||||||
}
|
}
|
||||||
if self.marketplace_status == format!("Fetching releases for {repo}…") {
|
if self.marketplace_status == format!("Fetching releases for {repo}…") {
|
||||||
self.marketplace_status =
|
self.marketplace_status =
|
||||||
format!("Repository added. {} installable release(s) found.", tags.len());
|
format!(
|
||||||
|
"Repository added. {} installable release(s) found.",
|
||||||
|
releases.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
self.repo_release_tags.insert(repo, tags);
|
self.repo_release_tags.insert(repo, releases);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::PluginReleasesFetched(repo, Err(e)) => {
|
Message::PluginReleasesFetched(repo, Err(e)) => {
|
||||||
|
|
@ -4754,6 +4757,7 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
Message::PluginInstalled(Ok(id)) => {
|
Message::PluginInstalled(Ok(id)) => {
|
||||||
self.marketplace_status = format!("Installed '{id}'. Restart to load it.");
|
self.marketplace_status = format!("Installed '{id}'. Restart to load it.");
|
||||||
|
self.plugin_load_errors.remove(&id);
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
self.external_plugins = crate::plugin::external::discover();
|
self.external_plugins = crate::plugin::external::discover();
|
||||||
|
|
@ -4771,6 +4775,7 @@ impl OpenCADStudio {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.marketplace_status =
|
self.marketplace_status =
|
||||||
format!("Uninstalled '{id}'. Restart to unload it.");
|
format!("Uninstalled '{id}'. Restart to unload it.");
|
||||||
|
self.plugin_load_errors.remove(&id);
|
||||||
self.external_plugins = crate::plugin::external::discover();
|
self.external_plugins = crate::plugin::external::discover();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ impl OpenCADStudio {
|
||||||
&self.disabled_plugins,
|
&self.disabled_plugins,
|
||||||
&self.external_plugins,
|
&self.external_plugins,
|
||||||
&self.loaded_plugin_ids,
|
&self.loaded_plugin_ids,
|
||||||
|
&self.plugin_load_errors,
|
||||||
crate::ui::window::plugin_manager::MarketView {
|
crate::ui::window::plugin_manager::MarketView {
|
||||||
registry: &self.plugin_registry,
|
registry: &self.plugin_registry,
|
||||||
registry_loading: self.plugin_registry_loading,
|
registry_loading: self.plugin_registry_loading,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,13 @@ pub struct RegistryEntry {
|
||||||
pub description: String,
|
pub description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Installable release metadata shown by the marketplace before download.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ReleaseInfo {
|
||||||
|
pub tag: String,
|
||||||
|
pub api_version: u32,
|
||||||
|
}
|
||||||
|
|
||||||
/// An add-on package found on disk (not necessarily loaded or compatible).
|
/// An add-on package found on disk (not necessarily loaded or compatible).
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ExternalPlugin {
|
pub struct ExternalPlugin {
|
||||||
|
|
@ -48,7 +55,7 @@ pub struct ExternalPlugin {
|
||||||
impl ExternalPlugin {
|
impl ExternalPlugin {
|
||||||
/// True when the package's API version is supported by this host.
|
/// True when the package's API version is supported by this host.
|
||||||
pub fn api_compatible(&self) -> bool {
|
pub fn api_compatible(&self) -> bool {
|
||||||
ocs_plugin_api::host_accepts_plugin_version(self.api_version)
|
self.api_version == ocs_plugin_api::API_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
/// True when the package can be loaded today: compatible API *and* a native
|
/// True when the package can be loaded today: compatible API *and* a native
|
||||||
|
|
@ -348,7 +355,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn api_v2_plugin_from_template_is_compatible() {
|
fn api_v2_plugin_from_template_is_incompatible() {
|
||||||
let toml = r#"
|
let toml = r#"
|
||||||
[plugin]
|
[plugin]
|
||||||
id = "opencad.my_plugin"
|
id = "opencad.my_plugin"
|
||||||
|
|
@ -367,7 +374,7 @@ xdata_apps = ["MYPLUGIN_RECORD"]
|
||||||
assert_eq!(p.api_version, 2);
|
assert_eq!(p.api_version, 2);
|
||||||
assert_eq!(p.repository.as_deref(), Some("example/opencad-my-plugin"));
|
assert_eq!(p.repository.as_deref(), Some("example/opencad-my-plugin"));
|
||||||
assert!(p.command_prefixes.contains(&"MP_".to_string()));
|
assert!(p.command_prefixes.contains(&"MP_".to_string()));
|
||||||
assert!(p.api_compatible(), "API v2 plugins must run on API v3 host");
|
assert!(!p.api_compatible(), "old API plugins must be rebuilt");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use std::io::Read as _;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use super::external;
|
use super::external;
|
||||||
use super::external::RegistryEntry;
|
use super::external::{RegistryEntry, ReleaseInfo};
|
||||||
|
|
||||||
/// The curated registry, read from the OpenCADStudio repo's `main` branch.
|
/// The curated registry, read from the OpenCADStudio repo's `main` branch.
|
||||||
pub(crate) const REGISTRY_URL: &str =
|
pub(crate) const REGISTRY_URL: &str =
|
||||||
|
|
@ -109,8 +109,68 @@ fn agent() -> ureq::Agent {
|
||||||
|
|
||||||
const UA: &str = concat!("OpenCADStudio/", env!("CARGO_PKG_VERSION"));
|
const UA: &str = concat!("OpenCADStudio/", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
/// Fetch the releases of `owner/repo` from the GitHub API.
|
/// Fetch releases without consuming GitHub API quota. The public Atom feed
|
||||||
|
/// supplies tags and the expanded-assets endpoint supplies download links.
|
||||||
|
/// Fall back to the API only if GitHub changes either public representation.
|
||||||
pub fn fetch_releases(repo: &str) -> Result<Vec<Release>, String> {
|
pub fn fetch_releases(repo: &str) -> Result<Vec<Release>, String> {
|
||||||
|
match fetch_releases_public(repo) {
|
||||||
|
Ok(releases) => Ok(releases),
|
||||||
|
Err(public_error) => fetch_releases_api(repo).map_err(|api_error| {
|
||||||
|
format!(
|
||||||
|
"public release metadata failed: {public_error}; \
|
||||||
|
GitHub API fallback failed: {api_error}"
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_releases_public(repo: &str) -> Result<Vec<Release>, String> {
|
||||||
|
let atom_url = format!("https://github.com/{repo}/releases.atom");
|
||||||
|
let atom = download_string(&atom_url)?;
|
||||||
|
let tag_marker = format!("https://github.com/{repo}/releases/tag/");
|
||||||
|
let mut tags = atom
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
let start = line.find(&tag_marker)? + tag_marker.len();
|
||||||
|
let rest = &line[start..];
|
||||||
|
let end = rest.find('"')?;
|
||||||
|
let tag = &rest[..end];
|
||||||
|
(!tag.is_empty()).then(|| tag.to_string())
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
tags.dedup();
|
||||||
|
if tags.is_empty() {
|
||||||
|
return Err("release feed contains no tags".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut releases = Vec::new();
|
||||||
|
for tag in tags {
|
||||||
|
let assets_url = format!("https://github.com/{repo}/releases/expanded_assets/{tag}");
|
||||||
|
let html = download_string(&assets_url)?;
|
||||||
|
let asset_prefix = format!("/{repo}/releases/download/{tag}/");
|
||||||
|
let mut assets = html
|
||||||
|
.split("href=\"")
|
||||||
|
.skip(1)
|
||||||
|
.filter_map(|tail| {
|
||||||
|
let end = tail.find('"')?;
|
||||||
|
let href = &tail[..end];
|
||||||
|
if !href.starts_with(&asset_prefix) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let name = href.rsplit('/').next()?.to_string();
|
||||||
|
Some(Asset {
|
||||||
|
name,
|
||||||
|
url: format!("https://github.com{href}"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assets.dedup_by(|left, right| left.url == right.url);
|
||||||
|
releases.push(Release { tag, assets });
|
||||||
|
}
|
||||||
|
Ok(releases)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_releases_api(repo: &str) -> Result<Vec<Release>, String> {
|
||||||
let url = format!("https://api.github.com/repos/{repo}/releases");
|
let url = format!("https://api.github.com/repos/{repo}/releases");
|
||||||
let body = agent()
|
let body = agent()
|
||||||
.get(&url)
|
.get(&url)
|
||||||
|
|
@ -145,8 +205,46 @@ pub fn fetch_releases(repo: &str) -> Result<Vec<Release>, String> {
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch installable releases and read each bundled manifest so compatibility
|
||||||
|
/// is known before the Plugin Manager offers an Install action.
|
||||||
|
pub fn fetch_release_info(repo: &str) -> Result<Vec<ReleaseInfo>, String> {
|
||||||
|
let releases = fetch_releases(repo)?;
|
||||||
|
let mut info = Vec::new();
|
||||||
|
let mut last_error = None;
|
||||||
|
for release in releases.into_iter().filter(Release::installable) {
|
||||||
|
let result = (|| {
|
||||||
|
let manifest_asset = release
|
||||||
|
.toml_asset()
|
||||||
|
.ok_or_else(|| format!("release {} has no plugin.toml", release.tag))?;
|
||||||
|
let manifest_text = download_string(&manifest_asset.url)?;
|
||||||
|
let manifest = external::parse_plugin_toml(&manifest_text)
|
||||||
|
.ok_or_else(|| format!("release {} plugin.toml is missing an id", release.tag))?;
|
||||||
|
Ok::<_, String>(ReleaseInfo {
|
||||||
|
tag: release.tag,
|
||||||
|
api_version: manifest.api_version,
|
||||||
|
})
|
||||||
|
})();
|
||||||
|
match result {
|
||||||
|
Ok(release) => info.push(release),
|
||||||
|
Err(error) => last_error = Some(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if info.is_empty() {
|
||||||
|
Err(last_error.unwrap_or_else(|| "no installable releases found".to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch the repository README from its default branch as raw Markdown.
|
/// Fetch the repository README from its default branch as raw Markdown.
|
||||||
pub fn fetch_readme(repo: &str) -> Result<String, String> {
|
pub fn fetch_readme(repo: &str) -> Result<String, String> {
|
||||||
|
// GitHub's raw host is not subject to the unauthenticated REST API's
|
||||||
|
// 60-request/hour limit. `HEAD` resolves the repository's default branch.
|
||||||
|
let raw_url = format!("https://raw.githubusercontent.com/{repo}/HEAD/README.md");
|
||||||
|
if let Ok(readme) = download_string(&raw_url) {
|
||||||
|
return Ok(readme);
|
||||||
|
}
|
||||||
|
|
||||||
let url = format!("https://api.github.com/repos/{repo}/readme");
|
let url = format!("https://api.github.com/repos/{repo}/readme");
|
||||||
agent()
|
agent()
|
||||||
.get(&url)
|
.get(&url)
|
||||||
|
|
@ -193,11 +291,10 @@ pub fn install(release: &Release, repository: &str) -> Result<String, String> {
|
||||||
|
|
||||||
let toml_text = download_string(&toml.url)?;
|
let toml_text = download_string(&toml.url)?;
|
||||||
let manifest = external::parse_plugin_toml(&toml_text).ok_or("plugin.toml is missing an id")?;
|
let manifest = external::parse_plugin_toml(&toml_text).ok_or("plugin.toml is missing an id")?;
|
||||||
if !ocs_plugin_api::host_accepts_plugin_version(manifest.api_version) {
|
if manifest.api_version != ocs_plugin_api::API_VERSION {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"API version {} is incompatible (host supports {}-{})",
|
"API version {} is incompatible (host requires {})",
|
||||||
manifest.api_version,
|
manifest.api_version,
|
||||||
ocs_plugin_api::API_VERSION_MIN_SUPPORTED,
|
|
||||||
ocs_plugin_api::API_VERSION
|
ocs_plugin_api::API_VERSION
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
#![cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
#![cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use crate::plugin::external::{ExternalPlugin, RegistryEntry};
|
use crate::plugin::external::{ExternalPlugin, RegistryEntry, ReleaseInfo};
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
button, column, container, markdown, row, rule, scrollable, text, text_input, Space,
|
button, column, container, markdown, row, rule, scrollable, text, text_input, Space,
|
||||||
};
|
};
|
||||||
|
|
@ -27,7 +27,7 @@ pub struct MarketView<'a> {
|
||||||
pub input: &'a str,
|
pub input: &'a str,
|
||||||
pub search: &'a str,
|
pub search: &'a str,
|
||||||
pub repos: &'a [String],
|
pub repos: &'a [String],
|
||||||
pub release_tags: &'a FxHashMap<String, Vec<String>>,
|
pub release_tags: &'a FxHashMap<String, Vec<ReleaseInfo>>,
|
||||||
pub selected_tag: &'a FxHashMap<String, String>,
|
pub selected_tag: &'a FxHashMap<String, String>,
|
||||||
pub selected_repo: Option<&'a str>,
|
pub selected_repo: Option<&'a str>,
|
||||||
pub readmes: &'a FxHashMap<String, Result<markdown::Content, String>>,
|
pub readmes: &'a FxHashMap<String, Result<markdown::Content, String>>,
|
||||||
|
|
@ -212,15 +212,20 @@ fn external_card<'a>(
|
||||||
repository: Option<String>,
|
repository: Option<String>,
|
||||||
update_tag: Option<String>,
|
update_tag: Option<String>,
|
||||||
loaded: bool,
|
loaded: bool,
|
||||||
|
load_error: Option<&str>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
|
let failed_old_api =
|
||||||
|
load_error.is_some() && p.api_version != ocs_plugin_api::API_VERSION;
|
||||||
let (status, kind) = if loaded && disabled {
|
let (status, kind) = if loaded && disabled {
|
||||||
("Disabled", StatusKind::Muted)
|
("Disabled", StatusKind::Muted)
|
||||||
} else if loaded {
|
} else if loaded {
|
||||||
("Loaded", StatusKind::Success)
|
("Loaded", StatusKind::Success)
|
||||||
} else if !p.api_compatible() {
|
} else if !p.api_compatible() || failed_old_api {
|
||||||
("API incompatible", StatusKind::Danger)
|
("API incompatible", StatusKind::Danger)
|
||||||
|
} else if load_error.is_some() {
|
||||||
|
("Load failed", StatusKind::Danger)
|
||||||
} else if !p.lib_present {
|
} else if !p.lib_present {
|
||||||
("No library", StatusKind::Warning)
|
("No library", StatusKind::Warning)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -249,6 +254,26 @@ fn external_card<'a>(
|
||||||
.style(muted_style),
|
.style(muted_style),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let Some(error) = load_error {
|
||||||
|
let detail = if failed_old_api {
|
||||||
|
format!(
|
||||||
|
"Plugin API {} is incompatible with this build's API {} ribbon ABI. \
|
||||||
|
Update or rebuild the plugin.",
|
||||||
|
p.api_version,
|
||||||
|
ocs_plugin_api::API_VERSION,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("Load failed: {error}")
|
||||||
|
};
|
||||||
|
info_body = info_body.push(
|
||||||
|
text(detail)
|
||||||
|
.size(11)
|
||||||
|
.width(Fill)
|
||||||
|
.style(|theme: &Theme| iced::widget::text::Style {
|
||||||
|
color: Some(theme.palette().danger.base.color),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let info: Element<'a, Message> = if let Some(repo) = repository.clone() {
|
let info: Element<'a, Message> = if let Some(repo) = repository.clone() {
|
||||||
button(info_body.width(Fill))
|
button(info_body.width(Fill))
|
||||||
|
|
@ -316,11 +341,21 @@ fn pill_icon_button<'a>(
|
||||||
/// Release dropdown + Install (+ optional unlink) for one repo.
|
/// Release dropdown + Install (+ optional unlink) for one repo.
|
||||||
fn install_controls<'a>(
|
fn install_controls<'a>(
|
||||||
repo: &str,
|
repo: &str,
|
||||||
tags: Vec<String>,
|
releases: Vec<ReleaseInfo>,
|
||||||
selected: Option<String>,
|
selected: Option<String>,
|
||||||
removable: bool,
|
removable: bool,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
let repo_s = repo.to_string();
|
let repo_s = repo.to_string();
|
||||||
|
let tags = releases
|
||||||
|
.iter()
|
||||||
|
.map(|release| release.tag.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let selected_api = selected.as_ref().and_then(|selected| {
|
||||||
|
releases
|
||||||
|
.iter()
|
||||||
|
.find(|release| release.tag == *selected)
|
||||||
|
.map(|release| release.api_version)
|
||||||
|
});
|
||||||
let picker: Element<'_, Message> = if tags.is_empty() {
|
let picker: Element<'_, Message> = if tags.is_empty() {
|
||||||
text("no releases").size(11).style(muted_style).into()
|
text("no releases").size(11).style(muted_style).into()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -330,15 +365,18 @@ fn install_controls<'a>(
|
||||||
.text_size(12)
|
.text_size(12)
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
let mut controls = row![
|
let action = match selected_api {
|
||||||
picker,
|
Some(api_version) if api_version == ocs_plugin_api::API_VERSION => {
|
||||||
Space::new().width(8),
|
|
||||||
pill_button(
|
pill_button(
|
||||||
"Install",
|
"Install",
|
||||||
Message::PluginInstall(repo_s.clone()),
|
Message::PluginInstall(repo_s.clone()),
|
||||||
button::success,
|
button::success,
|
||||||
),
|
)
|
||||||
]
|
}
|
||||||
|
Some(_) => status_badge("Incompatible", StatusKind::Danger),
|
||||||
|
None => status_badge("Unavailable", StatusKind::Muted),
|
||||||
|
};
|
||||||
|
let mut controls = row![picker, Space::new().width(8), action]
|
||||||
.align_y(iced::Center)
|
.align_y(iced::Center)
|
||||||
.spacing(4);
|
.spacing(4);
|
||||||
if removable {
|
if removable {
|
||||||
|
|
@ -766,6 +804,7 @@ pub fn view_window<'a>(
|
||||||
disabled: &FxHashSet<String>,
|
disabled: &FxHashSet<String>,
|
||||||
externals: &[ExternalPlugin],
|
externals: &[ExternalPlugin],
|
||||||
loaded: &FxHashSet<String>,
|
loaded: &FxHashSet<String>,
|
||||||
|
load_errors: &FxHashMap<String, String>,
|
||||||
market: MarketView<'a>,
|
market: MarketView<'a>,
|
||||||
theme: &'a Theme,
|
theme: &'a Theme,
|
||||||
sizing: crate::ui::modal::ModalSizing,
|
sizing: crate::ui::modal::ModalSizing,
|
||||||
|
|
@ -796,12 +835,22 @@ pub fn view_window<'a>(
|
||||||
let update_tag = repository
|
let update_tag = repository
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|repo| market.release_tags.get(repo))
|
.and_then(|repo| market.release_tags.get(repo))
|
||||||
.and_then(|tags| newest_update(&p.version, tags));
|
.and_then(|releases| {
|
||||||
|
let compatible_tags = releases
|
||||||
|
.iter()
|
||||||
|
.filter(|release| {
|
||||||
|
release.api_version == ocs_plugin_api::API_VERSION
|
||||||
|
})
|
||||||
|
.map(|release| release.tag.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
newest_update(&p.version, &compatible_tags)
|
||||||
|
});
|
||||||
list = list.push(external_card(
|
list = list.push(external_card(
|
||||||
p,
|
p,
|
||||||
repository,
|
repository,
|
||||||
update_tag,
|
update_tag,
|
||||||
loaded.contains(&p.id),
|
loaded.contains(&p.id),
|
||||||
|
load_errors.get(&p.id).map(String::as_str),
|
||||||
disabled.contains(&p.id),
|
disabled.contains(&p.id),
|
||||||
selected,
|
selected,
|
||||||
));
|
));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue