diff --git a/Cargo.toml b/Cargo.toml index ea079c82..9e688aa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,7 @@ js-sys = "0.3" # window.open (external URLs) + Blob/anchor file downloads (Save). web-sys = { version = "0.3", features = [ "Window", + "Navigator", "Document", "Element", "HtmlAnchorElement", diff --git a/src/app/commands.rs b/src/app/commands.rs index 578dba80..53086d66 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -36,7 +36,7 @@ impl OpenCADStudio { && !matches!( cmd, "NEW" | "OPEN" | "EXIT" | "QUIT" | "REPORT" | "CHANGELOG" | "ABOUT" - | "PLUGINS" | "PLUGINMANAGER" | "DONATE" + | "PLUGINS" | "PLUGINMANAGER" | "DONATE" | "WEBVERSION" ) { self.command_line @@ -2505,6 +2505,11 @@ impl OpenCADStudio { self.command_line.push_info("Opening Patreon page..."); } + "WEBVERSION" => { + crate::sys::open_url("https://hakanseven12.github.io/OpenCADStudio/"); + self.command_line.push_info("Opening OCS Web..."); + } + // ── DWGPROPS — print round-trip-only HeaderVariables ───────── // No UI dialog for these yet; the command surfaces them so // users can confirm the values that the parser populated and @@ -2681,7 +2686,19 @@ impl OpenCADStudio { } "REPORT" => { - crate::sys::open_url("https://github.com/HakanSeven12/OpenCADStudio/issues/new"); + // Pre-fill the GitHub issue body with version + platform so + // reports arrive with the basics already filled in. + let body = format!( + "\n\n\n\ + ---\n- Open CAD Studio: v{}\n- Platform: {}\n", + env!("CARGO_PKG_VERSION"), + crate::sys::platform_info(), + ); + let url = format!( + "https://github.com/HakanSeven12/OpenCADStudio/issues/new?body={}", + crate::sys::percent_encode(&body) + ); + crate::sys::open_url(&url); self.command_line.push_info("Opening feedback page..."); } diff --git a/src/app/view.rs b/src/app/view.rs index 4cc03498..ac8b8f6c 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -3779,7 +3779,7 @@ pub(super) fn start_page_view<'a>() -> Element<'a, Message> { .spacing(12) .align_y(iced::Center); - let secondary_row = row![ + let mut secondary_row = row![ outline_btn( "Send Feedback", Message::RibbonToolClick { @@ -3795,10 +3795,50 @@ pub(super) fn start_page_view<'a>() -> Element<'a, Message> { }, ), outline_btn("Plugins", Message::PluginManagerOpen), - outline_btn("About", Message::AboutOpen), - ] - .spacing(12) - .align_y(iced::Center); + ]; + // The web build is already in the browser, so only the desktop offers a + // link to the web version. + #[cfg(not(target_arch = "wasm32"))] + { + // Bright ribbon blue (matches the active-tool accent), filled. + secondary_row = secondary_row.push( + button(text("OCS Web").size(14).color(Color::WHITE)) + .on_press(Message::RibbonToolClick { + tool_id: "WEBVERSION".to_string(), + event: crate::modules::ModuleEvent::Command("WEBVERSION".to_string()), + }) + .padding([10, 22]) + .style(|_: &Theme, status| button::Style { + background: Some(Background::Color(match status { + button::Status::Hovered => Color { + r: 0.15, + g: 0.45, + b: 0.78, + a: 1.0, + }, + _ => Color { + r: 0.20, + g: 0.55, + b: 0.90, + a: 1.0, + }, + })), + text_color: Color::WHITE, + border: Border { + color: Color { + r: 0.20, + g: 0.55, + b: 0.90, + a: 1.0, + }, + width: 1.0, + radius: 6.0.into(), + }, + ..Default::default() + }), + ); + } + let secondary_row = secondary_row.spacing(12).align_y(iced::Center); let card_section = |heading: &'static str, sub: &'static str, body: &'static str| { container( diff --git a/src/sys.rs b/src/sys.rs index 8cd889b8..4443ea92 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -59,3 +59,33 @@ pub fn download_bytes(name: &str, bytes: &[u8]) { } let _ = web_sys::Url::revoke_object_url(&url); } + +/// Short platform string for bug reports: OS + architecture on the desktop, +/// the browser user-agent on the web. +#[cfg(not(target_arch = "wasm32"))] +pub fn platform_info() -> String { + format!("{} {}", std::env::consts::OS, std::env::consts::ARCH) +} + +#[cfg(target_arch = "wasm32")] +pub fn platform_info() -> String { + web_sys::window() + .and_then(|w| w.navigator().user_agent().ok()) + .map(|ua| format!("Web — {ua}")) + .unwrap_or_else(|| "Web (wasm)".to_string()) +} + +/// Percent-encode `s` for use in a URL query value (e.g. a GitHub issue +/// `?body=`). Encodes everything outside the unreserved set. +pub fn percent_encode(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for &b in s.as_bytes() { + match b { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + out.push(b as char) + } + _ => out.push_str(&format!("%{b:02X}")), + } + } + out +}