fix(web): hide unsupported plugin manager

Fixes #567
This commit is contained in:
Hakan Seven 2026-07-29 16:17:17 +03:00
commit 2424eeb74d
10 changed files with 56 additions and 22 deletions

View file

@ -18,6 +18,7 @@ browser can't provide. This page lists the differences.
| Parallelism | Multi-threaded (rayon) | Single-threaded | | Parallelism | Multi-threaded (rayon) | Single-threaded |
| GPU backend | Vulkan / DX12 / Metal (wgpu) | WebGL2 (WebGPU where available) | | GPU backend | Vulkan / DX12 / Metal (wgpu) | WebGL2 (WebGPU where available) |
| Update check / external links | `ureq`, `open` | skipped / `window.open` | | Update check / external links | `ureq`, `open` | skipped / `window.open` |
| External plugins | Native packages + Plugin Manager | **No** — native libraries cannot run in a browser |
## Details ## Details
@ -76,6 +77,14 @@ native and sequential `std` iterators on wasm, so the same call sites run in
parallel on the desktop and serially in the browser. Large drawings are slower parallel on the desktop and serially in the browser. Large drawings are slower
on the web. on the web.
### External plugins — web: unavailable
Marketplace plugins are native `.dll`, `.so`, or `.dylib` packages launched in
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
its commands. 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.
- `download_bytes`: web only (Blob + anchor download). - `download_bytes`: web only (Blob + anchor download).

View file

@ -333,8 +333,9 @@ plugin dead, and reports a `CallTimeout` error via the normal plugin error path.
## Marketplace ## Marketplace
The **Plugin Manager** (`PLUGINS` / `PLUGINMANAGER`, or the Start-page button) The desktop **Plugin Manager** (`PLUGINS` / `PLUGINMANAGER`, or the Start-page
installs plugins from GitHub Releases: button) installs plugins from GitHub Releases. It is hidden in the browser
build 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
@ -343,7 +344,7 @@ installs plugins from GitHub Releases:
[`plugins/README.md`](../plugins/README.md)); merged PRs reach every user with [`plugins/README.md`](../plugins/README.md)); merged PRs reach every user with
no app update. no app update.
- **Manual link** — *Add a repository* (`owner/repo`) for unlisted or private - **Manual link** — *Add a repository* (`owner/repo`) for unlisted or private
dev repos; linked repos persist in `settings.txt` (`plugin_repos=`). dev repos; linked repos persist in `settings.json`.
- **Install / upgrade / reinstall** — pick a release from the dropdown and - **Install / upgrade / reinstall** — pick a release from the dropdown and
*Install*; the host downloads the platform asset + `plugin.toml` into the *Install*; the host downloads the platform asset + `plugin.toml` into the
plugins folder, checking `api_version` first. Reinstalling overwrites and plugins folder, checking `api_version` first. Reinstalling overwrites and

View file

@ -236,8 +236,6 @@ pub fn start_allowed(cmd: &str) -> bool {
| "REPORT" | "REPORT"
| "CHANGELOG" | "CHANGELOG"
| "ABOUT" | "ABOUT"
| "PLUGINS"
| "PLUGINMANAGER"
| "DONATE" | "DONATE"
| "WEBVERSION" | "WEBVERSION"
| "HELP" | "HELP"
@ -247,6 +245,8 @@ 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,10 +212,17 @@ 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

@ -1313,6 +1313,7 @@ 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

@ -291,8 +291,11 @@ impl OpenCADStudio {
} }
#[cfg(target_arch = "wasm32")] #[cfg(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> {
Task::none() Task::done(Message::PluginReleasesFetched(
repo,
Err("External plugins are available in the desktop app.".to_string()),
))
} }
/// Background task: fetch a repository README from its default branch. /// Background task: fetch a repository README from its default branch.
@ -306,8 +309,11 @@ impl OpenCADStudio {
} }
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub(in crate::app) fn fetch_plugin_readme_task(&self, _repo: String) -> Task<Message> { pub(in crate::app) fn fetch_plugin_readme_task(&self, repo: String) -> Task<Message> {
Task::none() Task::done(Message::PluginReadmeFetched(
repo,
Err("Plugin details are available in the desktop app.".to_string()),
))
} }
/// Background task: download and install the `tag` release of `owner/repo`. /// Background task: download and install the `tag` release of `owner/repo`.
@ -328,7 +334,9 @@ impl OpenCADStudio {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub(in crate::app) fn install_task(&self, _repo: String, _tag: String) -> Task<Message> { pub(in crate::app) fn install_task(&self, _repo: String, _tag: String) -> Task<Message> {
Task::none() Task::done(Message::PluginInstalled(Err(
"External plugins are available in the desktop app.".to_string(),
)))
} }
/// Gather the full persisted config (all sections) from live app state. /// Gather the full persisted config (all sections) from live app state.

View file

@ -4056,14 +4056,20 @@ impl OpenCADStudio {
// ── Plugin Manager window ───────────────────────────────────── // ── Plugin Manager window ─────────────────────────────────────
Message::PluginManagerOpen => { Message::PluginManagerOpen => {
// Refresh the on-disk external-plugin list each time the manager #[cfg(target_arch = "wasm32")]
// opens so newly dropped-in packages show up. {
self.external_plugins = crate::plugin::external::discover(); self.command_line
self.marketplace_status.clear(); .push_info("External plugins are available in the desktop app.");
self.active_modal = Some(super::ModalKind::PluginManager); return Task::none();
// Fetch the curated registry and release lists for linked repos. }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
// 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.
self.plugin_registry_loading = true; self.plugin_registry_loading = true;
self.plugin_registry_error = None; self.plugin_registry_error = None;
self.plugin_registry_error_details_open = false; self.plugin_registry_error_details_open = false;
@ -4100,8 +4106,6 @@ impl OpenCADStudio {
} }
return Task::batch(tasks); return Task::batch(tasks);
} }
#[cfg(target_arch = "wasm32")]
Task::none()
} }
Message::PluginManagerClose => { Message::PluginManagerClose => {
self.close_active_modal(); self.close_active_modal();

View file

@ -2299,12 +2299,12 @@ pub(super) fn start_page_view<'a>(
) )
.into(), .into(),
outline_btn("Options", Message::OptionsOpen).into(), outline_btn("Options", Message::OptionsOpen).into(),
outline_btn("Plugins", Message::PluginManagerOpen).into(),
]; ];
// The web build is already in the browser, so only the desktop offers a // External plugins are native dynamic libraries and the web build is
// link to the web version. // already in the browser, so both actions are desktop-only.
#[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

@ -13,6 +13,8 @@
//! <lib<name>.so | .dll | .dylib> //! <lib<name>.so | .dll | .dylib>
//! ``` //! ```
#![cfg_attr(target_arch = "wasm32", allow(dead_code))]
use std::path::PathBuf; use std::path::PathBuf;
/// One entry in the curated plugin registry (`plugins/registry.json`). /// One entry in the curated plugin registry (`plugins/registry.json`).

View file

@ -33,7 +33,9 @@ pub struct MarketView<'a> {
pub status: &'a str, pub status: &'a str,
} }
// Register the command names for autocomplete. // External plugins are native dynamic libraries, so the browser build must not
// advertise a manager it cannot use.
#[cfg(not(target_arch = "wasm32"))]
inventory::submit!(crate::command::CommandRegistration { inventory::submit!(crate::command::CommandRegistration {
names: &["PLUGINS", "PLUGINMANAGER"] names: &["PLUGINS", "PLUGINMANAGER"]
}); });