use native_windows_derive::NwgUi; use native_windows_gui as nwg; use rand::Rng; use serde_json; use std::{ cell::RefCell, io::Read, os::windows::process::CommandExt, path::{Path, PathBuf}, process::{self, Command}, str, sync::{Arc, Mutex}, thread, time, }; use url::Url; use winapi::um::{winbase::CREATE_BREAKAWAY_FROM_JOB, winuser::WS_EX_TOPMOST}; use crate::stremio_app::{ constants::{APP_NAME, UPDATE_ENDPOINT, UPDATE_INTERVAL, WINDOW_MIN_HEIGHT, WINDOW_MIN_WIDTH}, ipc::{RPCRequest, RPCResponse}, splash::SplashImage, stremio_player::Player, stremio_wevbiew::WebView, systray::SystemTray, updater, window_helper::WindowStyle, PipeServer, }; use super::stremio_server::StremioServer; #[derive(Default, NwgUi)] pub struct MainWindow { pub command: String, pub commands_path: Option, pub webui_url: String, pub dev_tools: bool, pub start_hidden: bool, pub autoupdater_endpoint: Option, pub force_update: bool, pub release_candidate: bool, pub autoupdater_setup_file: Arc>>, pub saved_window_style: RefCell, #[nwg_resource] pub embed: nwg::EmbedResource, #[nwg_resource(source_embed: Some(&data.embed), source_embed_str: Some("MAINICON"))] pub window_icon: nwg::Icon, #[nwg_control(icon: Some(&data.window_icon), title: APP_NAME, flags: "MAIN_WINDOW")] #[nwg_events( OnWindowClose: [Self::on_quit(SELF, EVT_DATA)], OnInit: [Self::on_init], OnPaint: [Self::on_paint], OnMinMaxInfo: [Self::on_min_max(SELF, EVT_DATA)], OnWindowMinimize: [Self::transmit_window_state_change], OnWindowMaximize: [Self::transmit_window_state_change] )] pub window: nwg::Window, #[nwg_partial(parent: window)] #[nwg_events((tray, MousePressLeftUp): [Self::on_show], (tray_exit, OnMenuItemSelected): [nwg::stop_thread_dispatch()], (tray_show_hide, OnMenuItemSelected): [Self::on_show_hide], (tray_topmost, OnMenuItemSelected): [Self::on_toggle_topmost]) ] pub tray: SystemTray, #[nwg_partial(parent: window)] pub splash_screen: SplashImage, #[nwg_partial(parent: window)] pub server: StremioServer, #[nwg_partial(parent: window)] pub player: Player, #[nwg_partial(parent: window)] pub webview: WebView, #[nwg_control] #[nwg_events(OnNotice: [Self::on_toggle_fullscreen_notice] )] pub toggle_fullscreen_notice: nwg::Notice, #[nwg_control] #[nwg_events(OnNotice: [nwg::stop_thread_dispatch()] )] pub quit_notice: nwg::Notice, #[nwg_control] #[nwg_events(OnNotice: [Self::on_hide_splash_notice] )] pub hide_splash_notice: nwg::Notice, #[nwg_control] #[nwg_events(OnNotice: [Self::on_focus_notice] )] pub focus_notice: nwg::Notice, } impl MainWindow { fn transmit_window_visibility_change(&self) { if let (Ok(web_channel), Ok(style)) = ( self.webview.channel.try_borrow(), self.saved_window_style.try_borrow(), ) { let (web_tx, _) = web_channel .as_ref() .expect("Cannont obtain communication channel for the Web UI"); let web_tx_app = web_tx.clone(); web_tx_app .send(RPCResponse::visibility_change( self.window.visible(), style.full_screen as u32, style.full_screen, )) .ok(); } else { eprintln!("Cannot obtain communication channel or window style"); } } fn transmit_window_state_change(&self) { if let (Some(hwnd), Ok(web_channel), Ok(style)) = ( self.window.handle.hwnd(), self.webview.channel.try_borrow(), self.saved_window_style.try_borrow(), ) { let state = style.clone().get_window_state(hwnd); drop(style); let (web_tx, _) = web_channel .as_ref() .expect("Cannont obtain communication channel for the Web UI"); let web_tx_app = web_tx.clone(); web_tx_app.send(RPCResponse::state_change(state)).ok(); } else { eprintln!("Cannot obtain window handle or communication channel"); } } fn on_init(&self) { self.webview.endpoint.set(self.webui_url.clone()).ok(); self.webview.dev_tools.set(self.dev_tools).ok(); if let Some(hwnd) = self.window.handle.hwnd() { if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() { saved_style.center_window(hwnd, WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT); } } self.window.set_visible(!self.start_hidden); self.tray.tray_show_hide.set_checked(!self.start_hidden); let player_channel = self.player.channel.borrow(); let (player_tx, player_rx) = player_channel .as_ref() .expect("Cannont obtain communication channel for the Player"); let player_tx = player_tx.clone(); let player_rx = player_rx.clone(); let web_channel = self.webview.channel.borrow(); let (web_tx, web_rx) = web_channel .as_ref() .expect("Cannont obtain communication channel for the Web UI"); let web_tx_player = web_tx.clone(); let web_tx_web = web_tx.clone(); let web_tx_arg = web_tx.clone(); let web_tx_upd = web_tx.clone(); let web_rx = web_rx.clone(); let (updater_tx, updater_rx) = flume::unbounded::(); let updater_tx_web = updater_tx.clone(); let command_clone = self.command.clone(); // Single application IPC let socket_path = Path::new( self.commands_path .as_ref() .expect("Cannot initialie the single application IPC"), ); let autoupdater_endpoint = self.autoupdater_endpoint.clone(); let force_update = self.force_update; let release_candidate = self.release_candidate; let autoupdater_setup_file = self.autoupdater_setup_file.clone(); thread::spawn(move || { loop { if let Ok(msg) = updater_rx.recv() { if msg == "check_for_update" { break; } } } loop { let current_version = env!("CARGO_PKG_VERSION") .parse() .expect("Should always be valid"); let updater_endpoint = if let Some(ref endpoint) = autoupdater_endpoint { endpoint.clone() } else { let mut rng = rand::thread_rng(); let index = rng.gen_range(0..UPDATE_ENDPOINT.len()); let mut url = Url::parse(UPDATE_ENDPOINT[index]).unwrap(); if release_candidate { url.query_pairs_mut().append_pair("rc", "true"); } url }; let updater = updater::Updater::new(current_version, &updater_endpoint, force_update); match updater.autoupdate() { Ok(Some(update)) => { println!("New version ready to install v{}", update.version); let mut autoupdater_setup_file = autoupdater_setup_file.lock().unwrap(); *autoupdater_setup_file = Some(update.file.clone()); web_tx_upd.send(RPCResponse::update_available()).ok(); } Ok(None) => println!("No new updates found"), Err(e) => eprintln!("Failed to fetch updates: {e}"), } thread::sleep(time::Duration::from_secs(UPDATE_INTERVAL)); } }); // thread if let Ok(mut listener) = PipeServer::bind(socket_path) { let focus_sender = self.focus_notice.sender(); thread::spawn(move || loop { if let Ok(mut stream) = listener.accept() { let mut buf = vec![]; stream.read_to_end(&mut buf).ok(); if let Ok(s) = str::from_utf8(&buf) { focus_sender.notice(); // ['open-media', url] web_tx_arg.send(RPCResponse::open_media(s.to_string())).ok(); println!("{}", s); } } }); } // Read message from player thread::spawn(move || loop { player_rx .iter() .map(|msg| web_tx_player.send(msg)) .for_each(drop); }); // thread let toggle_fullscreen_sender = self.toggle_fullscreen_notice.sender(); let quit_sender = self.quit_notice.sender(); let hide_splash_sender = self.hide_splash_notice.sender(); let focus_sender = self.focus_notice.sender(); let autoupdater_setup_mutex = self.autoupdater_setup_file.clone(); thread::spawn(move || loop { if let Some(msg) = web_rx .recv() .ok() .and_then(|s| serde_json::from_str::(&s).ok()) { match msg.get_method() { // The handshake. Here we send some useful data to the WEB UI None if msg.is_handshake() => { web_tx_web.send(RPCResponse::get_handshake()).ok(); } Some("win-set-visibility") => toggle_fullscreen_sender.notice(), Some("quit") => quit_sender.notice(), Some("app-ready") => { hide_splash_sender.notice(); web_tx_web .send(RPCResponse::visibility_change(true, 1, false)) .ok(); updater_tx_web .send("check_for_update".to_owned()) .expect("Failed to send value to updater channel"); let command_ref = command_clone.clone(); if !command_ref.is_empty() { web_tx_web.send(RPCResponse::open_media(command_ref)).ok(); } } Some("app-error") => { hide_splash_sender.notice(); if let Some(arg) = msg.get_params() { // TODO: Make this modal dialog eprintln!("Web App Error: {}", arg); } } Some("open-external") => { if let Some(arg) = msg.get_params() { // FIXME: THIS IS NOT SAFE BY ANY MEANS // open::that("calc").ok(); does exactly that let arg = arg.as_str().unwrap_or(""); let arg_lc = arg.to_lowercase(); if arg_lc.starts_with("http://") || arg_lc.starts_with("https://") || arg_lc.starts_with("rtp://") || arg_lc.starts_with("rtps://") || arg_lc.starts_with("ftp://") || arg_lc.starts_with("ipfs://") { open::that(arg).ok(); } } } Some("win-focus") => { focus_sender.notice(); } Some("autoupdater-notif-clicked") => { // We've shown the "Update Available" notification // and the user clicked on "Restart And Update" let autoupdater_setup_file = autoupdater_setup_mutex.lock().unwrap().clone(); match autoupdater_setup_file { Some(file_path) => { println!("Running the setup at {:?}", file_path); let command = Command::new(file_path) .args([ "/SILENT", "/NOCANCEL", "/FORCECLOSEAPPLICATIONS", "/TASKS=runapp", ]) .creation_flags(CREATE_BREAKAWAY_FROM_JOB) .stdin(process::Stdio::null()) .stdout(process::Stdio::null()) .stderr(process::Stdio::null()) .spawn(); match command { Ok(process) => { println!("Updater started. (PID {:?})", process.id()); quit_sender.notice(); } Err(err) => eprintln!("Updater couldn't be started: {err}"), }; } _ => { println!("Cannot obtain the setup file path"); } } } Some(player_command) if player_command.starts_with("mpv-") => { let resp_json = serde_json::to_string( &msg.args.expect("Cannot have method without args"), ) .expect("Cannot build response"); player_tx.send(resp_json).ok(); } Some(unknown) => { eprintln!("Unsupported command {}({:?})", unknown, msg.get_params()) } None => {} } } // recv }); // thread } fn on_min_max(&self, data: &nwg::EventData) { let data = data.on_min_max(); data.set_min_size(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT); } fn on_paint(&self) { if !self.splash_screen.visible() { self.webview.fit_to_window(self.window.handle.hwnd()); } } fn on_toggle_fullscreen_notice(&self) { if let Some(hwnd) = self.window.handle.hwnd() { if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() { saved_style.toggle_full_screen(hwnd); self.tray.tray_topmost.set_enabled(!saved_style.full_screen); self.tray .tray_topmost .set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST); self.transmit_window_visibility_change(); } } } fn on_hide_splash_notice(&self) { self.splash_screen.hide(); } fn on_focus_notice(&self) { self.window.set_visible(true); if let Some(hwnd) = self.window.handle.hwnd() { if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() { saved_style.set_active(hwnd); } } } fn on_toggle_topmost(&self) { if let Some(hwnd) = self.window.handle.hwnd() { if let Ok(mut saved_style) = self.saved_window_style.try_borrow_mut() { saved_style.toggle_topmost(hwnd); self.tray .tray_topmost .set_checked((saved_style.ex_style as u32 & WS_EX_TOPMOST) == WS_EX_TOPMOST); } } } fn on_show(&self) { self.window.set_visible(true); if let (Some(hwnd), Ok(mut saved_style)) = ( self.window.handle.hwnd(), self.saved_window_style.try_borrow_mut(), ) { if saved_style.is_window_minimized(hwnd) { self.window.restore(); } saved_style.set_active(hwnd); } self.tray.tray_show_hide.set_checked(self.window.visible()); self.transmit_window_state_change(); self.transmit_window_visibility_change(); } fn on_show_hide(&self) { if self.window.visible() { self.window.set_visible(false); self.tray.tray_show_hide.set_checked(self.window.visible()); self.transmit_window_state_change(); self.transmit_window_visibility_change(); } else { self.on_show(); } } fn on_quit(&self, data: &nwg::EventData) { if let nwg::EventData::OnWindowClose(data) = data { data.close(false); } self.window.set_visible(false); self.tray.tray_show_hide.set_checked(self.window.visible()); self.transmit_window_visibility_change(); } }