use crate::stremio_app::constants::SERVER_IPC_KEY; use crate::stremio_app::ipc; use native_windows_gui::{self as nwg, PartialUi}; use once_cell::unsync::OnceCell; use serde_json::json; use std::borrow::Cow; use std::cell::RefCell; use std::collections::VecDeque; use std::mem; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::thread; use url::Url; use urlencoding::decode; use webview2::Controller; use winapi::shared::windef::HWND; use winapi::um::winuser::{GetClientRect, VK_F7, WM_APPCOMMAND, WM_SETFOCUS}; const APPCOMMAND_MEDIA_NEXTTRACK: u32 = 11; const APPCOMMAND_MEDIA_PREVIOUSTRACK: u32 = 12; const APPCOMMAND_MEDIA_PLAY_PAUSE: u32 = 14; const APPCOMMAND_MEDIA_PLAY: u32 = 46; const APPCOMMAND_MEDIA_PAUSE: u32 = 47; use super::constants::{WARNING_URL, WHITELISTED_HOSTS}; #[derive(Default)] pub struct WebView { pub endpoint: Rc>, pub dev_tools: Rc>, pub controller: Rc>, pub channel: ipc::Channel, notice: nwg::Notice, compute: RefCell>>, message_queue: Arc>>, } impl WebView { pub fn fit_to_window(&self, hwnd: Option) { if let Some(hwnd) = hwnd { unsafe { let mut rect = mem::zeroed(); GetClientRect(hwnd, &mut rect); self.controller .get() .and_then(|controller| controller.put_bounds(rect).ok()); } } } fn resize_to_window_bounds(controller: Option<&Controller>, hwnd: Option) { if let (Some(controller), Some(hwnd)) = (controller, hwnd) { unsafe { let mut rect = mem::zeroed(); GetClientRect(hwnd, &mut rect); controller.put_bounds(rect).ok(); } } } } impl PartialUi for WebView { fn build_partial>( data: &mut Self, parent: Option, ) -> Result<(), nwg::NwgError> { println!("Building WebView"); let (tx, rx) = flume::unbounded(); let tx_drag_drop = tx.clone(); let tx_media = tx.clone(); let (tx_web, rx_web) = flume::unbounded(); let tx_fs = tx_web.clone(); data.channel = RefCell::new(Some((tx, rx_web))); let parent = parent.expect("No parent window").into(); let hwnd = parent.hwnd().expect("Cannot obtain window handle"); nwg::Notice::builder() .parent(parent) .build(&mut data.notice) .ok(); let controller_clone = data.controller.clone(); let endpoint = data.endpoint.clone(); let dev_tools = data.dev_tools.clone(); let webview_flags = "--autoplay-policy=no-user-gesture-required --disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection"; let result = webview2::EnvironmentBuilder::new() .with_additional_browser_arguments(webview_flags) .build(move |env| { env.expect("Cannot obtain webview environment") .create_controller(hwnd, move |controller| { let controller = controller.expect("Cannot obtain webview controller"); if let Ok(controller2) = controller.get_controller2() { controller2 .put_default_background_color(webview2_sys::Color { r: 255, g: 255, b: 255, a: 0, }) .ok(); } else { eprintln!("failed to get interface to controller2"); } let webview = controller .get_webview() .expect("Cannot obtain webview from controller"); let settings = webview.get_settings().unwrap(); settings.put_is_status_bar_enabled(false).ok(); settings.put_are_dev_tools_enabled(*dev_tools.get().unwrap()).ok(); settings.put_is_zoom_control_enabled(false).ok(); settings.put_is_built_in_error_page_enabled(false).ok(); settings.put_are_host_objects_allowed(false).ok(); settings.put_are_default_script_dialogs_enabled(false).ok(); // Handle window.open and href webview.add_new_window_requested(move |_webview, event| { if let Ok(uri) = event.get_uri() { if let Ok(url) = Url::parse(&uri) { let is_whitelisted = url.host().is_some_and(|host| { WHITELISTED_HOSTS.iter().any(|whitelisted_host| host.to_string().ends_with(whitelisted_host)) }); let final_url = if is_whitelisted { url.to_string() } else { format!("{}{}", WARNING_URL, urlencoding::encode(url.as_ref())) }; if let Err(e) = open::that(final_url) { eprintln!("Failed to open URL: {e}"); } } } Ok(()) })?; if let Some(endpoint) = endpoint.get() { if webview .navigate(endpoint.as_str()).is_err() { tx_web.clone().send(ipc::RPCResponse::response_message(Some(json!(["app-error", format!("Cannot load WEB UI at '{}'", &endpoint)])))).ok(); }; } webview.add_web_message_received(move |_w, msg| { let msg = msg.try_get_web_message_as_string()?; tx_web.send(msg).ok(); Ok(()) }).expect("Cannot add web message received"); webview.add_new_window_requested(move |_w, msg| { if let Some(file) = msg.get_uri().ok().and_then(|str| {decode(str.as_str()).ok().map(Cow::into_owned)}) { tx_drag_drop.send(ipc::RPCResponse::response_message(Some(json!(["dragdrop" ,[file]])))).ok(); msg.put_handled(true).ok(); } Ok(()) }).expect("Cannot add D&D handler"); webview.add_contains_full_screen_element_changed(move |wv| { if let Ok(visibility) = wv.get_contains_full_screen_element() { tx_fs.send(ipc::RPCResponse::response_message(Some(json!(["win-set-visibility" , {"fullscreen": visibility}])))).ok(); } Ok(()) }).expect("Cannot add full screen element changed"); webview.add_content_loading(move |wv, _| { wv.execute_script(format!( "window.stremio_server_ipc_key='{}'", std::env::var(SERVER_IPC_KEY).unwrap_or_default() ).as_str(), |_| Ok(()) ).expect("Cannot add SERVER_IPC_KEY to webview"); wv.execute_script(r##" try{ /* Disable context menus */ document.addEventListener('contextmenu', (e) => { if(!(e.target.tagName == "INPUT" && ['text', 'password', 'number', 'week', 'month', 'email'].includes(e.target.type.toLowerCase()))) { e.stopPropagation();e.preventDefault() } }) }catch(e){} try{console.log('Shell JS injected');if(window.self === window.top) { window.qt={webChannelTransport:{send:window.chrome.webview.postMessage}}; window.chrome.webview.addEventListener('message',ev=>window.qt.webChannelTransport.onmessage(ev)); }}catch(e){} window.addEventListener("load", function() {if(initShellComm) try { initShellComm() } catch(e) {}}, false) "##, |_| Ok(())).expect("Cannot add script to webview"); Ok(()) }).expect("Cannot add content loading"); WebView::resize_to_window_bounds(Some(&controller), Some(hwnd)); controller.put_is_visible(true).ok(); controller .move_focus(webview2::MoveFocusReason::Programmatic) .ok(); controller.add_accelerator_key_pressed(move |_, e| { // Block F7, Ctrl+F, and Ctrl+G let k = e.get_virtual_key()?; if k == VK_F7 as u32 || k == 70 & 0x7F || k == 71 & 0x7F { e.put_handled(true) } else { Ok(()) } }) .unwrap(); controller_clone .set(controller) .expect("Cannot update the controller"); Ok(()) }) }); if let Err(e) = result { nwg::modal_fatal_message( parent, "Failed to Create WebView2 Environment", &format!("{e}"), ); } let sender = data.notice.sender(); let message = data.message_queue.clone(); *data.compute.borrow_mut() = Some(thread::spawn(move || loop { if let Ok(msg) = rx.recv() { let mut message = message.lock().unwrap(); message.push_back(msg); sender.notice(); } })); // handler ids equal or smaller than 0xFFFF are reserved by NWG let handler_id = 0x10000; let controller_clone = data.controller.clone(); nwg::bind_raw_event_handler(&parent, handler_id, move |_hwnd, msg, _w, l| { if msg == WM_SETFOCUS { controller_clone.get().and_then(|controller| { controller .move_focus(webview2::MoveFocusReason::Programmatic) .ok() }); } else if msg == WM_APPCOMMAND { let cmd = ((l >> 16) & 0xFFF) as u32; let action = match cmd { APPCOMMAND_MEDIA_PLAY_PAUSE | APPCOMMAND_MEDIA_PLAY | APPCOMMAND_MEDIA_PAUSE => { Some("play-pause") } APPCOMMAND_MEDIA_NEXTTRACK => Some("next-track"), APPCOMMAND_MEDIA_PREVIOUSTRACK => Some("previous-track"), _ => None, }; if let Some(action) = action { tx_media.send(ipc::RPCResponse::media_key(action)).ok(); return Some(1); } } None }) .ok(); Ok(()) } fn process_event<'a>( &self, evt: nwg::Event, _evt_data: &nwg::EventData, handle: nwg::ControlHandle, ) { use nwg::Event as E; if evt == E::OnNotice && handle == self.notice.handle { let message_queue = self.message_queue.clone(); if let Some(controller) = self.controller.get() { let webview = controller.get_webview().expect("Cannot get vebview"); let mut message_queue = message_queue.lock().unwrap(); for msg in message_queue.drain(..) { webview.post_web_message_as_string(msg.as_str()).ok(); } } } } }