fix(web): link plugins to desktop app

Keep browser entry points visible while explaining that native
plugins require a desktop install.

Refs #567
This commit is contained in:
Hakan Seven 2026-07-29 16:28:20 +03:00
commit fcc6e1f8e1
9 changed files with 92 additions and 48 deletions

View file

@ -81,9 +81,10 @@ on the web.
Marketplace plugins are native `.dll`, `.so`, or `.dylib` packages launched in Marketplace plugins are native `.dll`, `.so`, or `.dylib` packages launched in
an isolated child process. Browsers cannot load those libraries or spawn the an isolated child process. Browsers cannot load those libraries or spawn the
plugin runner, so the web build does not show the Plugin Manager or register plugin runner. The web Plugin button and `PLUGINS` command therefore show a
its commands. Installed desktop plugins remain in the per-user plugins folder short explanation with a link to download the desktop app instead of opening
and load again on the next desktop launch. the marketplace. Installed desktop plugins remain in the per-user plugins
folder and load again on the next desktop launch.
### Platform shims (`src/sys.rs`) ### Platform shims (`src/sys.rs`)
- `open_url`: `open::that` on native, `window.open(_blank)` on web. - `open_url`: `open::that` on native, `window.open(_blank)` on web.

View file

@ -334,8 +334,9 @@ plugin dead, and reports a `CallTimeout` error via the normal plugin error path.
## Marketplace ## Marketplace
The desktop **Plugin Manager** (`PLUGINS` / `PLUGINMANAGER`, or the Start-page The desktop **Plugin Manager** (`PLUGINS` / `PLUGINMANAGER`, or the Start-page
button) installs plugins from GitHub Releases. It is hidden in the browser button) installs plugins from GitHub Releases. In the browser, the same entry
build because marketplace packages are native dynamic libraries: points show a desktop-download notice because marketplace packages are native
dynamic libraries:
- **Curated registry** — [`plugins/registry.json`](../plugins/registry.json) in - **Curated registry** — [`plugins/registry.json`](../plugins/registry.json) in
this repo lists discoverable plugins. The host fetches it from `main` at this repo lists discoverable plugins. The host fetches it from `main` at

View file

@ -236,6 +236,8 @@ pub fn start_allowed(cmd: &str) -> bool {
| "REPORT" | "REPORT"
| "CHANGELOG" | "CHANGELOG"
| "ABOUT" | "ABOUT"
| "PLUGINS"
| "PLUGINMANAGER"
| "DONATE" | "DONATE"
| "WEBVERSION" | "WEBVERSION"
| "HELP" | "HELP"
@ -245,8 +247,6 @@ pub fn start_allowed(cmd: &str) -> bool {
| "CUILOAD" | "CUILOAD"
| "CUIIMPORT" | "CUIIMPORT"
) )
|| (!cfg!(target_arch = "wasm32")
&& matches!(cmd, "PLUGINS" | "PLUGINMANAGER"))
} }
// ── Autocomplete registry — one-shot commands ────────────────────────────── // ── Autocomplete registry — one-shot commands ──────────────────────────────

View file

@ -212,17 +212,10 @@ impl OpenCADStudio {
return Some(Task::done(Message::AboutOpen)); return Some(Task::done(Message::AboutOpen));
} }
#[cfg(not(target_arch = "wasm32"))]
"PLUGINS" | "PLUGINMANAGER" => { "PLUGINS" | "PLUGINMANAGER" => {
return Some(Task::done(Message::PluginManagerOpen)); return Some(Task::done(Message::PluginManagerOpen));
} }
#[cfg(target_arch = "wasm32")]
"PLUGINS" | "PLUGINMANAGER" => {
self.command_line
.push_info("External plugins are available in the desktop app.");
}
"CHANGELOG" => { "CHANGELOG" => {
crate::sys::open_url("https://github.com/HakanSeven12/OpenCADStudio/releases"); crate::sys::open_url("https://github.com/HakanSeven12/OpenCADStudio/releases");
self.command_line.push_info("Opening release notes..."); self.command_line.push_info("Opening release notes...");

View file

@ -521,6 +521,7 @@ pub(super) struct OpenCADStudio {
external_plugins: Vec<crate::plugin::external::ExternalPlugin>, external_plugins: Vec<crate::plugin::external::ExternalPlugin>,
/// Ids of external packages actually loaded this session (a subset of /// Ids of external packages actually loaded this session (a subset of
/// `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))]
loaded_plugin_ids: rustc_hash::FxHashSet<String>, loaded_plugin_ids: rustc_hash::FxHashSet<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>,
@ -1313,7 +1314,6 @@ impl ClipboardDeps {
pub enum ModalKind { pub enum ModalKind {
About, About,
Shortcuts, Shortcuts,
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
PluginManager, PluginManager,
UpdateNotice, UpdateNotice,
Layers, Layers,

View file

@ -4058,8 +4058,7 @@ impl OpenCADStudio {
Message::PluginManagerOpen => { Message::PluginManagerOpen => {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
{ {
self.command_line self.active_modal = Some(super::ModalKind::PluginManager);
.push_info("External plugins are available in the desktop app.");
return Task::none(); return Task::none();
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]

View file

@ -2300,11 +2300,11 @@ pub(super) fn start_page_view<'a>(
.into(), .into(),
outline_btn("Options", Message::OptionsOpen).into(), outline_btn("Options", Message::OptionsOpen).into(),
]; ];
// External plugins are native dynamic libraries and the web build is secondary_items.push(outline_btn("Plugins", Message::PluginManagerOpen).into());
// already in the browser, so both actions are desktop-only. // The web build is already in the browser, so only the desktop offers a
// link to the web version.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
secondary_items.push(outline_btn("Plugins", Message::PluginManagerOpen).into());
// Filled with the active theme's primary colour. // Filled with the active theme's primary colour.
secondary_items.push( secondary_items.push(
button(text("OCS Web").size(14)) button(text("OCS Web").size(14))

View file

@ -71,31 +71,45 @@ impl OpenCADStudio {
520, 520,
500, 500,
), ),
super::super::ModalKind::PluginManager => sized( super::super::ModalKind::PluginManager => {
crate::ui::window::plugin_manager::view_window( #[cfg(not(target_arch = "wasm32"))]
&self.disabled_plugins, {
&self.external_plugins, sized(
&self.loaded_plugin_ids, crate::ui::window::plugin_manager::view_window(
crate::ui::window::plugin_manager::MarketView { &self.disabled_plugins,
registry: &self.plugin_registry, &self.external_plugins,
registry_loading: self.plugin_registry_loading, &self.loaded_plugin_ids,
registry_error: self.plugin_registry_error.as_deref(), crate::ui::window::plugin_manager::MarketView {
registry_error_details_open: self.plugin_registry_error_details_open, registry: &self.plugin_registry,
input: &self.plugin_repo_input, registry_loading: self.plugin_registry_loading,
search: &self.plugin_search_input, registry_error: self.plugin_registry_error.as_deref(),
repos: &self.plugin_repos, registry_error_details_open: self
release_tags: &self.repo_release_tags, .plugin_registry_error_details_open,
selected_tag: &self.repo_selected_tag, input: &self.plugin_repo_input,
selected_repo: self.selected_plugin_repo.as_deref(), search: &self.plugin_search_input,
readmes: &self.plugin_readmes, repos: &self.plugin_repos,
readme_loading: &self.plugin_readme_loading, release_tags: &self.repo_release_tags,
status: &self.marketplace_status, selected_tag: &self.repo_selected_tag,
}, selected_repo: self.selected_plugin_repo.as_deref(),
&self.active_theme, readmes: &self.plugin_readmes,
), readme_loading: &self.plugin_readme_loading,
940, status: &self.marketplace_status,
600, },
), &self.active_theme,
),
940,
600,
)
}
#[cfg(target_arch = "wasm32")]
{
sized(
crate::ui::window::plugin_manager::view_web_notice(),
520,
260,
)
}
}
super::super::ModalKind::UpdateNotice => { super::super::ModalKind::UpdateNotice => {
let latest = self.update_notice_version.as_deref().unwrap_or("?"); let latest = self.update_notice_version.as_deref().unwrap_or("?");
let body = self.update_notice_body.as_deref().unwrap_or(""); let body = self.update_notice_body.as_deref().unwrap_or("");

View file

@ -4,6 +4,8 @@
//! launches). Dynamic loading still comes with the phase-2 loader; see //! launches). Dynamic loading still comes with the phase-2 loader; see
//! `docs/plugin-architecture.md`. //! `docs/plugin-architecture.md`.
#![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};
use iced::widget::{ use iced::widget::{
@ -33,9 +35,13 @@ pub struct MarketView<'a> {
pub status: &'a str, pub status: &'a str,
} }
// External plugins are native dynamic libraries, so the browser build must not /// Latest desktop builds, used by the browser-only plugin notice.
// advertise a manager it cannot use. #[cfg(target_arch = "wasm32")]
#[cfg(not(target_arch = "wasm32"))] pub const DESKTOP_DOWNLOAD_URL: &str =
"https://github.com/HakanSeven12/OpenCADStudio/releases/latest";
// Register the command names for autocomplete. On the web they open a desktop
// download notice instead of the native marketplace.
inventory::submit!(crate::command::CommandRegistration { inventory::submit!(crate::command::CommandRegistration {
names: &["PLUGINS", "PLUGINMANAGER"] names: &["PLUGINS", "PLUGINMANAGER"]
}); });
@ -844,6 +850,36 @@ pub fn view_window<'a>(
.into() .into()
} }
/// Browser replacement for the native Plugin Manager.
#[cfg(target_arch = "wasm32")]
pub fn view_web_notice<'a>() -> Element<'a, Message> {
let download = button(text("Download desktop app").size(13))
.on_press(Message::OpenUrl(DESKTOP_DOWNLOAD_URL.to_string()))
.padding([8, 16])
.style(button::primary);
container(
column![
text("Plugins require the desktop app").size(20),
text(
"Open CAD Studio plugins are native packages and cannot run inside a browser. \
Install the desktop app to browse, install, and use plugins.",
)
.size(13)
.style(muted_style),
Space::new().height(8),
download,
]
.spacing(8)
.padding(24)
.width(Fill),
)
.center(Fill)
.width(Fill)
.height(Fill)
.into()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{newest_update, registry_error_message, repository_display_name}; use super::{newest_update, registry_error_message, repository_display_name};