stremio-shell-ng/src/stremio_app/stremio_wevbiew/wevbiew.rs
2026-04-22 18:18:07 +03:00

280 lines
12 KiB
Rust

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<OnceCell<String>>,
pub dev_tools: Rc<OnceCell<bool>>,
pub controller: Rc<OnceCell<Controller>>,
pub channel: ipc::Channel,
notice: nwg::Notice,
compute: RefCell<Option<thread::JoinHandle<()>>>,
message_queue: Arc<Mutex<VecDeque<String>>>,
}
impl WebView {
pub fn fit_to_window(&self, hwnd: Option<HWND>) {
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<HWND>) {
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<W: Into<nwg::ControlHandle>>(
data: &mut Self,
parent: Option<W>,
) -> 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();
}
}
}
}
}