feat(plugins): redesign plugin manager

This commit is contained in:
Hakan Seven committed 2026-07-29 14:27:32 +03:00
1 parent d80637cda7
commit 4e25e88c31
11 files changed
+741 -74

No files matched your search

+21
View File
@@ -528,10 +528,19 @@ pub(super) struct OpenCADStudio {
plugin_repos: Vec<String>,
/// Add-repository text field in the Plugin Manager.
plugin_repo_input: String,
/// Live filter for installed and available plugin cards.
plugin_search_input: String,
/// Installable release tags fetched per linked repo (for the dropdown).
repo_release_tags: rustc_hash::FxHashMap<String, Vec<String>>,
/// The release tag currently selected per linked repo.
repo_selected_tag: rustc_hash::FxHashMap<String, String>,
/// Repository currently shown in the Plugin Manager detail pane.
selected_plugin_repo: Option<String>,
/// Parsed GitHub README content or the last fetch error, cached per repo.
plugin_readmes:
rustc_hash::FxHashMap<String, Result<iced::widget::markdown::Content, String>>,
/// README requests in flight, used to render a deterministic loading state.
plugin_readme_loading: rustc_hash::FxHashSet<String>,
/// Last marketplace status / error line shown in the Plugin Manager.
marketplace_status: String,
/// PDSIZE text buffer for the Point Style (DDPTYPE) dialog.
@@ -2059,6 +2068,8 @@ pub enum Message {
// ── Plugin marketplace (install from a linked repo's releases) ─────────
/// Edit the add-repository text field.
PluginRepoInput(String),
/// Filter installed and available plugin cards.
PluginSearchInput(String),
/// Link the repository currently in the text field.
PluginRepoAdd,
/// Unlink a repository.
@@ -2079,8 +2090,14 @@ pub enum Message {
PluginReleasesFetched(String, Result<Vec<String>, String>),
/// Choose a release tag for a repo (`repo`, `tag`).
PluginReleaseSelect(String, String),
/// Select a plugin source and show its GitHub README in the detail pane.
PluginReadmeSelect(String),
/// A GitHub README fetch finished (`repo`, Markdown or error).
PluginReadmeFetched(String, Result<String, String>),
/// Install the selected release of `owner/repo`.
PluginInstall(String),
/// Install a specific newer release from an installed plugin card.
PluginUpdate(String, String),
/// Result of an install: the plugin id, or an error message.
PluginInstalled(Result<String, String>),
/// Delete an installed plugin's folder (effective next restart).
@@ -2594,8 +2611,12 @@ impl OpenCADStudio {
plugin_registry: Vec::new(),
plugin_repos: Vec::new(),
plugin_repo_input: String::new(),
plugin_search_input: String::new(),
repo_release_tags: rustc_hash::FxHashMap::default(),
repo_selected_tag: rustc_hash::FxHashMap::default(),
selected_plugin_repo: None,
plugin_readmes: rustc_hash::FxHashMap::default(),
plugin_readme_loading: rustc_hash::FxHashSet::default(),
marketplace_status: String::new(),
point_size_buf: String::new(),
point_size_relative: true,
+16 -1
View File
@@ -295,6 +295,21 @@ impl OpenCADStudio {
Task::none()
}
/// Background task: fetch a repository README from its default branch.
#[cfg(not(target_arch = "wasm32"))]
pub(in crate::app) fn fetch_plugin_readme_task(&self, repo: String) -> Task<Message> {
let label = repo.clone();
Task::perform(
async move { crate::plugin::marketplace::fetch_readme(&repo) },
move |result| Message::PluginReadmeFetched(label, result),
)
}
#[cfg(target_arch = "wasm32")]
pub(in crate::app) fn fetch_plugin_readme_task(&self, _repo: String) -> Task<Message> {
Task::none()
}
/// Background task: download and install the `tag` release of `owner/repo`.
#[cfg(not(target_arch = "wasm32"))]
pub(in crate::app) fn install_task(&self, repo: String, tag: String) -> Task<Message> {
@@ -305,7 +320,7 @@ impl OpenCADStudio {
.into_iter()
.find(|r| r.tag == tag)
.ok_or_else(|| format!("release {tag} not found"))?;
crate::plugin::marketplace::install(&rel)
crate::plugin::marketplace::install(&rel, &repo)
},
Message::PluginInstalled,
)
+126 -12
View File
@@ -3951,17 +3951,42 @@ impl OpenCADStudio {
// Refresh the on-disk external-plugin list each time the manager
// opens so newly dropped-in packages show up.
self.external_plugins = crate::plugin::external::discover();
self.marketplace_status.clear();
self.active_modal = Some(super::ModalKind::PluginManager);
// Fetch the curated registry and release lists for linked repos.
#[cfg(not(target_arch = "wasm32"))]
{
let mut tasks = vec![self.fetch_registry_task()];
let release_repos: rustc_hash::FxHashSet<String> = self
.plugin_repos
.iter()
.cloned()
.chain(
self.external_plugins
.iter()
.filter_map(|plugin| plugin.repository.clone()),
)
.collect();
tasks.extend(
self.plugin_repos
.clone()
release_repos
.into_iter()
.map(|r| self.fetch_releases_task(r)),
);
if self.selected_plugin_repo.is_none() {
self.selected_plugin_repo = self
.external_plugins
.iter()
.find_map(|plugin| plugin.repository.clone())
.or_else(|| self.plugin_registry.first().map(|entry| entry.repo.clone()))
.or_else(|| self.plugin_repos.first().cloned());
}
if let Some(repo) = self.selected_plugin_repo.clone() {
if !self.plugin_readmes.contains_key(&repo)
&& self.plugin_readme_loading.insert(repo.clone())
{
tasks.push(self.fetch_plugin_readme_task(repo));
}
}
return Task::batch(tasks);
}
#[cfg(target_arch = "wasm32")]
@@ -3985,26 +4010,66 @@ impl OpenCADStudio {
self.plugin_repo_input = s;
Task::none()
}
Message::PluginSearchInput(s) => {
self.plugin_search_input = s;
Task::none()
}
Message::PluginRepoAdd => {
let repo = self
.plugin_repo_input
.trim()
.trim_start_matches("https://github.com/")
.trim_end_matches('/')
.to_string();
if repo.is_empty() || self.plugin_repos.contains(&repo) {
let Some(repo) =
crate::plugin::external::normalize_repository(&self.plugin_repo_input)
else {
self.marketplace_status =
"Enter a GitHub URL or repository in owner/repo format.".to_string();
return Task::none();
};
if self.plugin_repos.contains(&repo)
|| self.plugin_registry.iter().any(|entry| entry.repo == repo)
{
self.marketplace_status = format!("{repo} is already in the catalog.");
self.selected_plugin_repo = Some(repo.clone());
if !self.plugin_readmes.contains_key(&repo)
&& self.plugin_readme_loading.insert(repo.clone())
{
return self.fetch_plugin_readme_task(repo);
}
return Task::none();
}
if self
.external_plugins
.iter()
.any(|plugin| plugin.repository.as_deref() == Some(repo.as_str()))
{
self.marketplace_status = format!("{repo} is already installed.");
self.selected_plugin_repo = Some(repo.clone());
if !self.plugin_readmes.contains_key(&repo)
&& self.plugin_readme_loading.insert(repo.clone())
{
return self.fetch_plugin_readme_task(repo);
}
return Task::none();
}
self.plugin_repos.push(repo.clone());
self.plugin_repo_input.clear();
self.persist_settings_if_changed();
self.marketplace_status = format!("Fetching releases for {repo}…");
self.fetch_releases_task(repo)
self.selected_plugin_repo = Some(repo.clone());
self.plugin_readmes.remove(&repo);
self.plugin_readme_loading.insert(repo.clone());
Task::batch(vec![
self.fetch_releases_task(repo.clone()),
self.fetch_plugin_readme_task(repo),
])
}
Message::PluginRepoRemove(repo) => {
self.plugin_repos.retain(|r| r != &repo);
self.repo_release_tags.remove(&repo);
self.repo_selected_tag.remove(&repo);
if self.selected_plugin_repo.as_deref() == Some(repo.as_str())
&& !self.plugin_registry.iter().any(|entry| entry.repo == repo)
{
self.selected_plugin_repo =
self.plugin_registry.first().map(|entry| entry.repo.clone());
}
self.persist_settings_if_changed();
Task::none()
}
@@ -4012,11 +4077,32 @@ impl OpenCADStudio {
// Fetch releases for every curated repo so the dropdowns fill in.
#[cfg(not(target_arch = "wasm32"))]
{
let tasks: Vec<_> = entries
if self.selected_plugin_repo.is_none() {
self.selected_plugin_repo = self
.external_plugins
.iter()
.find_map(|plugin| {
plugin.repository.clone().or_else(|| {
entries
.iter()
.find(|entry| entry.name.eq_ignore_ascii_case(&plugin.name))
.map(|entry| entry.repo.clone())
})
})
.or_else(|| entries.first().map(|entry| entry.repo.clone()));
}
let mut tasks: Vec<_> = entries
.iter()
.map(|e| self.fetch_releases_task(e.repo.clone()))
.collect();
self.plugin_registry = entries;
if let Some(repo) = self.selected_plugin_repo.clone() {
if !self.plugin_readmes.contains_key(&repo)
&& self.plugin_readme_loading.insert(repo.clone())
{
tasks.push(self.fetch_plugin_readme_task(repo));
}
}
return Task::batch(tasks);
}
#[cfg(target_arch = "wasm32")]
@@ -4073,7 +4159,10 @@ impl OpenCADStudio {
.entry(repo.clone())
.or_insert_with(|| first.clone());
}
self.marketplace_status = format!("{repo}: {} installable release(s)", tags.len());
if self.marketplace_status == format!("Fetching releases for {repo}…") {
self.marketplace_status =
format!("Repository added. {} installable release(s) found.", tags.len());
}
self.repo_release_tags.insert(repo, tags);
Task::none()
}
@@ -4085,6 +4174,27 @@ impl OpenCADStudio {
self.repo_selected_tag.insert(repo, tag);
Task::none()
}
Message::PluginReadmeSelect(repo) => {
self.selected_plugin_repo = Some(repo.clone());
if self.plugin_readme_loading.contains(&repo) {
return Task::none();
}
if matches!(self.plugin_readmes.get(&repo), Some(Ok(_))) {
return Task::none();
}
// A second click on an error state acts as retry.
self.plugin_readmes.remove(&repo);
self.plugin_readme_loading.insert(repo.clone());
self.fetch_plugin_readme_task(repo)
}
Message::PluginReadmeFetched(repo, result) => {
self.plugin_readme_loading.remove(&repo);
self.plugin_readmes.insert(
repo,
result.map(|source| iced::widget::markdown::Content::parse(&source)),
);
Task::none()
}
Message::PluginInstall(repo) => {
let Some(tag) = self.repo_selected_tag.get(&repo).cloned() else {
return Task::none();
@@ -4092,6 +4202,10 @@ impl OpenCADStudio {
self.marketplace_status = format!("Installing {repo} {tag}…");
self.install_task(repo, tag)
}
Message::PluginUpdate(repo, tag) => {
self.marketplace_status = format!("Updating {repo} to {tag}…");
self.install_task(repo, tag)
}
Message::PluginInstalled(Ok(id)) => {
self.marketplace_status = format!("Installed '{id}'. Restart to load it.");
#[cfg(not(target_arch = "wasm32"))]
+1 -1
View File
@@ -1537,7 +1537,7 @@ impl OpenCADStudio {
About => (440, 360),
Shortcuts => (720, 520),
Options => (480, 190),
PluginManager => (520, 460),
PluginManager => (940, 600),
UpdateNotice => (560, 460),
Layers => (900, 360),
Plot => (760, 540),
+7 -2
View File
@@ -79,14 +79,19 @@ impl OpenCADStudio {
crate::ui::window::plugin_manager::MarketView {
registry: &self.plugin_registry,
input: &self.plugin_repo_input,
search: &self.plugin_search_input,
repos: &self.plugin_repos,
release_tags: &self.repo_release_tags,
selected_tag: &self.repo_selected_tag,
selected_repo: self.selected_plugin_repo.as_deref(),
readmes: &self.plugin_readmes,
readme_loading: &self.plugin_readme_loading,
status: &self.marketplace_status,
},
&self.active_theme,
),
520,
460,
940,
600,
),
super::super::ModalKind::UpdateNotice => {
let latest = self.update_notice_version.as_deref().unwrap_or("?");