From fcc6e1f8e1c8b1b21eea891c088aeebc2b85e7e7 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 29 Jul 2026 16:28:20 +0300 Subject: [PATCH] fix(web): link plugins to desktop app Keep browser entry points visible while explaining that native plugins require a desktop install. Refs #567 --- docs/native-vs-web.md | 7 ++-- docs/plugin-architecture.md | 5 +-- src/app/commands/mod.rs | 4 +-- src/app/commands/view.rs | 7 ---- src/app/mod.rs | 2 +- src/app/update/mod.rs | 3 +- src/app/view/mod.rs | 6 ++-- src/app/view/modal.rs | 64 ++++++++++++++++++++------------- src/ui/window/plugin_manager.rs | 42 ++++++++++++++++++++-- 9 files changed, 92 insertions(+), 48 deletions(-) diff --git a/docs/native-vs-web.md b/docs/native-vs-web.md index 631608b4..53290168 100644 --- a/docs/native-vs-web.md +++ b/docs/native-vs-web.md @@ -81,9 +81,10 @@ on the web. 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. +plugin runner. The web Plugin button and `PLUGINS` command therefore show a +short explanation with a link to download the desktop app instead of opening +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`) - `open_url`: `open::that` on native, `window.open(_blank)` on web. diff --git a/docs/plugin-architecture.md b/docs/plugin-architecture.md index 7d6c08ca..b3a5e5a4 100644 --- a/docs/plugin-architecture.md +++ b/docs/plugin-architecture.md @@ -334,8 +334,9 @@ plugin dead, and reports a `CallTimeout` error via the normal plugin error path. ## Marketplace The desktop **Plugin Manager** (`PLUGINS` / `PLUGINMANAGER`, or the Start-page -button) installs plugins from GitHub Releases. It is hidden in the browser -build because marketplace packages are native dynamic libraries: +button) installs plugins from GitHub Releases. In the browser, the same entry +points show a desktop-download notice because marketplace packages are native +dynamic libraries: - **Curated registry** — [`plugins/registry.json`](../plugins/registry.json) in this repo lists discoverable plugins. The host fetches it from `main` at diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index c9f28b3b..a6e81750 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -236,6 +236,8 @@ pub fn start_allowed(cmd: &str) -> bool { | "REPORT" | "CHANGELOG" | "ABOUT" + | "PLUGINS" + | "PLUGINMANAGER" | "DONATE" | "WEBVERSION" | "HELP" @@ -245,8 +247,6 @@ pub fn start_allowed(cmd: &str) -> bool { | "CUILOAD" | "CUIIMPORT" ) - || (!cfg!(target_arch = "wasm32") - && matches!(cmd, "PLUGINS" | "PLUGINMANAGER")) } // ── Autocomplete registry — one-shot commands ────────────────────────────── diff --git a/src/app/commands/view.rs b/src/app/commands/view.rs index d4a1e34d..d96f4125 100644 --- a/src/app/commands/view.rs +++ b/src/app/commands/view.rs @@ -212,17 +212,10 @@ impl OpenCADStudio { return Some(Task::done(Message::AboutOpen)); } - #[cfg(not(target_arch = "wasm32"))] "PLUGINS" | "PLUGINMANAGER" => { 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" => { crate::sys::open_url("https://github.com/HakanSeven12/OpenCADStudio/releases"); self.command_line.push_info("Opening release notes..."); diff --git a/src/app/mod.rs b/src/app/mod.rs index c827c07a..c9b5cd07 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -521,6 +521,7 @@ pub(super) struct OpenCADStudio { external_plugins: Vec, /// Ids of external packages actually loaded this session (a subset of /// `external_plugins` — compatible, with a library, dlopen'd at startup). + #[cfg_attr(target_arch = "wasm32", allow(dead_code))] loaded_plugin_ids: rustc_hash::FxHashSet, /// Curated plugin registry fetched from the OpenCADStudio repo. plugin_registry: Vec, @@ -1313,7 +1314,6 @@ impl ClipboardDeps { pub enum ModalKind { About, Shortcuts, - #[cfg_attr(target_arch = "wasm32", allow(dead_code))] PluginManager, UpdateNotice, Layers, diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 876304cf..6f457937 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -4058,8 +4058,7 @@ impl OpenCADStudio { Message::PluginManagerOpen => { #[cfg(target_arch = "wasm32")] { - self.command_line - .push_info("External plugins are available in the desktop app."); + self.active_modal = Some(super::ModalKind::PluginManager); return Task::none(); } #[cfg(not(target_arch = "wasm32"))] diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index aca0b0ac..835005dd 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -2300,11 +2300,11 @@ pub(super) fn start_page_view<'a>( .into(), outline_btn("Options", Message::OptionsOpen).into(), ]; - // External plugins are native dynamic libraries and the web build is - // already in the browser, so both actions are desktop-only. + secondary_items.push(outline_btn("Plugins", Message::PluginManagerOpen).into()); + // The web build is already in the browser, so only the desktop offers a + // link to the web version. #[cfg(not(target_arch = "wasm32"))] { - secondary_items.push(outline_btn("Plugins", Message::PluginManagerOpen).into()); // Filled with the active theme's primary colour. secondary_items.push( button(text("OCS Web").size(14)) diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 2388eb72..d79f170e 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -71,31 +71,45 @@ impl OpenCADStudio { 520, 500, ), - super::super::ModalKind::PluginManager => sized( - crate::ui::window::plugin_manager::view_window( - &self.disabled_plugins, - &self.external_plugins, - &self.loaded_plugin_ids, - crate::ui::window::plugin_manager::MarketView { - registry: &self.plugin_registry, - registry_loading: self.plugin_registry_loading, - registry_error: self.plugin_registry_error.as_deref(), - registry_error_details_open: self.plugin_registry_error_details_open, - 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, - ), - 940, - 600, - ), + super::super::ModalKind::PluginManager => { + #[cfg(not(target_arch = "wasm32"))] + { + sized( + crate::ui::window::plugin_manager::view_window( + &self.disabled_plugins, + &self.external_plugins, + &self.loaded_plugin_ids, + crate::ui::window::plugin_manager::MarketView { + registry: &self.plugin_registry, + registry_loading: self.plugin_registry_loading, + registry_error: self.plugin_registry_error.as_deref(), + registry_error_details_open: self + .plugin_registry_error_details_open, + 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, + ), + 940, + 600, + ) + } + #[cfg(target_arch = "wasm32")] + { + sized( + crate::ui::window::plugin_manager::view_web_notice(), + 520, + 260, + ) + } + } super::super::ModalKind::UpdateNotice => { let latest = self.update_notice_version.as_deref().unwrap_or("?"); let body = self.update_notice_body.as_deref().unwrap_or(""); diff --git a/src/ui/window/plugin_manager.rs b/src/ui/window/plugin_manager.rs index 0b95f620..f6b505a7 100644 --- a/src/ui/window/plugin_manager.rs +++ b/src/ui/window/plugin_manager.rs @@ -4,6 +4,8 @@ //! launches). Dynamic loading still comes with the phase-2 loader; see //! `docs/plugin-architecture.md`. +#![cfg_attr(target_arch = "wasm32", allow(dead_code))] + use crate::app::Message; use crate::plugin::external::{ExternalPlugin, RegistryEntry}; use iced::widget::{ @@ -33,9 +35,13 @@ pub struct MarketView<'a> { pub status: &'a str, } -// External plugins are native dynamic libraries, so the browser build must not -// advertise a manager it cannot use. -#[cfg(not(target_arch = "wasm32"))] +/// Latest desktop builds, used by the browser-only plugin notice. +#[cfg(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 { names: &["PLUGINS", "PLUGINMANAGER"] }); @@ -844,6 +850,36 @@ pub fn view_window<'a>( .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)] mod tests { use super::{newest_update, registry_error_message, repository_display_name};