#include "mainwindow.h" #include #include #include #include #include "../core/globals.h" #include "../resource.h" #include "../utils/crashlog.h" #include "../utils/helpers.h" #include "../utils/config.h" #include "../mpv/player.h" #include "../tray/tray.h" #include "../ui/splash.h" #include "../webview/webview.h" #include "../updater/updater.h" #include "../utils/discord.h" // Single-instance bool FocusExistingInstance(const std::wstring &protocolArg) { HWND hExistingWnd = FindWindowW(APP_CLASS, nullptr); if(hExistingWnd) { if(IsIconic(hExistingWnd)) { ShowWindow(hExistingWnd, SW_RESTORE); } else if(!IsWindowVisible(hExistingWnd)) { ShowWindow(hExistingWnd, SW_SHOW); } SetForegroundWindow(hExistingWnd); SetFocus(hExistingWnd); if(!protocolArg.empty()) { COPYDATASTRUCT cds; cds.dwData = 1; cds.cbData = static_cast(protocolArg.size() * sizeof(wchar_t)); cds.lpData = (PVOID)protocolArg.c_str(); SendMessage(hExistingWnd, WM_COPYDATA, 0, (LPARAM)&cds); } return true; } return false; } bool CheckSingleInstance(int argc, char* argv[], std::wstring &outProtocolArg) { g_hMutex = CreateMutexW(nullptr, FALSE, L"SingleInstanceMtx_StremioWebShell"); if(!g_hMutex){ std::wcerr << L"CreateMutex failed => fallback to multi.\n"; AppendToCrashLog("CreateMutex failed => fallback to multi."); return true; } std::wstring protocolArg; for(int i=1; i 0){ std::wstring argW(size_needed - 1, 0); MultiByteToWideChar(CP_UTF8, 0, argv[i], -1, &argW[0], size_needed); if(argW.rfind(L"stremio://",0)==0 || argW.rfind(L"magnet:",0)==0 || FileExists(argW)) { protocolArg = argW; break; } } } if(GetLastError()==ERROR_ALREADY_EXISTS){ FocusExistingInstance(protocolArg); return false; } outProtocolArg = protocolArg; return true; } void ToggleFullScreen(HWND hWnd, bool enable) { static WINDOWPLACEMENT prevPlc={sizeof(prevPlc)}; if(enable==g_isFullscreen) return; g_isFullscreen = enable; if(enable){ GetWindowPlacement(hWnd, &prevPlc); MONITORINFO mi={sizeof(mi)}; if(GetMonitorInfoW(MonitorFromWindow(hWnd,MONITOR_DEFAULTTOPRIMARY), &mi)){ SetWindowLongW(hWnd,GWL_STYLE,WS_POPUP|WS_VISIBLE); SetWindowPos(hWnd,HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left, mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_FRAMECHANGED|SWP_SHOWWINDOW); } } else { SetWindowLongW(hWnd,GWL_STYLE,WS_OVERLAPPEDWINDOW|WS_VISIBLE); SetWindowPlacement(hWnd,&prevPlc); SetWindowPos(hWnd,nullptr,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE|SWP_FRAMECHANGED|SWP_SHOWWINDOW); } } // Dark/Light theme #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #endif static void UpdateTheme(HWND hWnd) { if(g_useDarkTheme){ BOOL dark = TRUE; DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark, sizeof(dark)); } else { BOOL dark = FALSE; DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &dark, sizeof(dark)); } } // Handling inbound/outbound Messages void SendToJS(const std::string &eventName, const nlohmann::json &eventData) { static int nextId = 1; nlohmann::json msg; msg["type"] = 1; msg["object"] = "transport"; msg["id"] = nextId++; msg["args"] = { eventName, eventData }; // Serialize to wstring + Post std::string payload = msg.dump(); std::wstring wpayload(payload.begin(), payload.end()); g_webview->PostWebMessageAsString(wpayload.c_str()); #ifdef DEBUG_LOG std::cout << "[Native->JS] " << payload << "\n"; #endif } void HandleEvent(const std::string &ev, std::vector &args) { if(ev=="mpv-command"){ if(!args.empty() && args[0] == "loadfile" && args.size() > 1) { if (args[1].rfind("http://", 0) != 0 && args[1].rfind("https://", 0) != 0) { args[1] = decodeURIComponent(args[1]); } std::vector voArgs = {"vo",g_initialVO}; HandleMpvSetProp(voArgs); std::vector volumeArgs = {"volume", std::to_string(g_currentVolume)}; HandleMpvSetProp(volumeArgs); g_initialSet = true; } HandleMpvCommand(args); } else if(ev=="mpv-set-prop"){ HandleMpvSetProp(args); } else if(ev=="mpv-observe-prop"){ HandleMpvObserveProp(args); } else if(ev=="app-ready"){ g_isAppReady=true; HideSplash(); PostMessage(g_hWnd, WM_NOTIFY_FLUSH, 0, 0); } else if(ev=="update-requested"){ RunInstallerAndExit(); } else if(ev == "seek-hover") { if (g_thumbFastHeight == 0) return; if(g_ignoreHover) { auto now = std::chrono::steady_clock::now(); if(now < g_ignoreUntil) { return; } g_ignoreHover = false; } // Expecting arguments: hovered_seconds, x, y if(args.size() < 3) { std::cerr << "seek-hover requires at least 3 arguments.\n"; return; } // Convert the y-coordinate from string to an integer int yCoord = 0; try { yCoord = std::stoi(args[2]); } catch(const std::exception &e) { std::cerr << "Error converting y coordinate: " << e.what() << "\n"; return; } // Subtract the thumb fast height from y int adjustedY = yCoord - g_thumbFastHeight; // Prepare command for thumbfast with adjusted y-coordinate std::vector cmdArgs = { "script-message-to", "thumbfast", "thumb", args[0], // hovered_seconds args[1], // x std::to_string(adjustedY) // y with offset }; HandleMpvCommand(cmdArgs); } else if(ev == "seek-leave") { if (g_thumbFastHeight == 0) return; // Set ignore flag and calculate ignore-until timestamp g_ignoreHover = true; g_ignoreUntil = std::chrono::steady_clock::now() + IGNORE_DURATION; std::vector cmdArgs = { "script-message-to", "thumbfast", "clear" }; HandleMpvCommand(cmdArgs); } else if(ev=="start-drag"){ ReleaseCapture(); SendMessageW(g_hWnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); } else if(ev=="refresh"){ refreshWeb(args.size()>0 && args[0]=="all"); } else if(ev=="app-error"){ if(!args.empty() && args.size()>0 && args[0] == "shellComm"){ if(!g_isAppReady && !g_waitStarted.exchange(true)){ WaitAndRefreshIfNeeded(); } } } else if (ev=="open-external") { std::wstring uri(args[0].begin(), args[0].end()); ShellExecuteW(nullptr, L"open", uri.c_str(), nullptr, nullptr, SW_SHOWNORMAL); } else if (ev=="navigate") { std::wstring uri(args[0].begin(), args[0].end()); if (args[0] == "home") { g_webview->Navigate(g_webuiUrl.c_str()); } else { g_webview->Navigate(uri.c_str()); } } else if (ev == "activity") { SetDiscordPresenceFromArgs(args); } else { std::cout<<"Unknown event="< NATIVE]: " << msg << std::endl; #endif auto j = nlohmann::json::parse(msg); int type = 0; if (j.contains("type") && j["type"].is_number()) { type = j["type"].get(); } if (type == 3) { // 3 = Init event nlohmann::json root; root["id"] = 0; nlohmann::json transportObj; json extData = {}; if (!g_extensionMap.empty()) { for (auto& [name, id] : g_extensionMap) { extData[WStringToUtf8(name)] = WStringToUtf8(id); } } transportObj["properties"] = { 1, nlohmann::json::array({0, "shellVersion", 0, APP_VERSION}), nlohmann::json::array({0, "BrowserExtensions", 0, extData}), }; transportObj["signals"] = { nlohmann::json::array({0, "handleInboundJSONSignal"}), }; nlohmann::json methods = nlohmann::json::array(); methods.push_back(nlohmann::json::array({"onEvent", "handleInboundJSON"})); transportObj["methods"] = methods; root["data"]["transport"] = transportObj; std::string payload = root.dump(); std::wstring wpayload(payload.begin(), payload.end()); g_webview->PostWebMessageAsString(wpayload.c_str()); return; } if (type == 6 && j.contains("method")) { std::string methodName = j["method"].get(); if (methodName == "handleInboundJSON" || methodName == "onEvent") { if (j["args"].is_array() && !j["args"].empty()) { std::string ev; if (j["args"][0].is_string()) { ev = j["args"][0].get(); } else { ev = "Unknown"; } std::vector argVec; if (j["args"].size() > 1) { auto &second = j["args"][1]; if (second.is_array()) { for (auto &x: second) { if (x.is_string()) argVec.push_back(x.get()); else argVec.push_back(x.dump()); } } else if (second.is_string()) { argVec.push_back(second.get()); } else { argVec.push_back(second.dump()); } } HandleEvent(ev, argVec); } else { std::cout << "[WARN] invokeMethod=handleInboundJSON => no args array?\n"; } } return; } std::cout<<"Unknown Inbound event="<(lParam); SetWindowPos(hWnd, NULL, newRect->left, newRect->top, newRect->right - newRect->left, newRect->bottom - newRect->top, SWP_NOZORDER | SWP_NOACTIVATE); break; } case WM_NOTIFY_FLUSH: { if (g_isAppReady) { for(const auto& pendingMsg : g_outboundMessages) { SendToJS(pendingMsg["type"], pendingMsg); } g_outboundMessages.clear(); if(!g_launchProtocol.empty()) { COPYDATASTRUCT cds; cds.dwData = 1; cds.cbData = static_cast((g_launchProtocol.size()+1) * sizeof(wchar_t)); cds.lpData = (PVOID)g_launchProtocol.c_str(); SendMessage(g_hWnd, WM_COPYDATA, (WPARAM)g_hWnd, (LPARAM)&cds); g_launchProtocol.clear(); } } break; } case WM_REACHABILITY_DONE: { // wParam is a pointer to a std::wstring we allocated in the thread std::wstring* pUrl = reinterpret_cast(wParam); if(pUrl) { if (!pUrl->empty() && g_webview) { std::wcout << L"[WEBVIEW]: Navigating to " << *pUrl << std::endl; g_webview->Navigate(pUrl->c_str()); } else { MessageBoxW(nullptr, L"All endpoints are unreachable", L"WebView2 Initialization Error", MB_ICONERROR | MB_OK); } delete pUrl; } break; } case WM_SETTINGCHANGE: { UpdateTheme(hWnd); break; } case WM_TRAYICON: { if(LOWORD(lParam)==WM_RBUTTONUP) { ShowTrayMenu(hWnd); } if(lParam==WM_LBUTTONDBLCLK){ ShowWindow(hWnd, SW_RESTORE); SetForegroundWindow(hWnd); } break; } case WM_COMMAND: { switch(LOWORD(wParam)) { case ID_TRAY_SHOWWINDOW: g_showWindow = !g_showWindow; ShowWindow(hWnd, g_showWindow?SW_SHOW:SW_HIDE); break; case ID_TRAY_ALWAYSONTOP: g_alwaysOnTop=!g_alwaysOnTop; SetWindowPos(hWnd, g_alwaysOnTop?HWND_TOPMOST:HWND_NOTOPMOST, 0,0,0,0, SWP_NOMOVE|SWP_NOSIZE); break; case ID_TRAY_CLOSE_ON_EXIT: g_closeOnExit=!g_closeOnExit; SaveSettings(); break; case ID_TRAY_USE_DARK_THEME: g_useDarkTheme=!g_useDarkTheme; SaveSettings(); UpdateTheme(hWnd); break; case ID_TRAY_PICTURE_IN_PICTURE: TogglePictureInPicture(hWnd, !g_isPipMode); break; case ID_TRAY_PAUSE_FOCUS_LOST: g_pauseOnLostFocus=!g_pauseOnLostFocus; SaveSettings(); break; case ID_TRAY_PAUSE_MINIMIZED: g_pauseOnMinimize=!g_pauseOnMinimize; SaveSettings(); break; case ID_TRAY_QUIT: if(g_mpv) mpv_command_string(g_mpv,"quit"); WINDOWPLACEMENT wp; wp.length = sizeof(wp); if (GetWindowPlacement(hWnd, &wp)) { SaveWindowPlacement(wp); } DestroyWindow(hWnd); break; } break; } case WM_COPYDATA: { PCOPYDATASTRUCT pcds = (PCOPYDATASTRUCT)lParam; if (pcds && pcds->dwData == 1 && pcds->lpData) { // Assuming data is a wide string containing the URL or file path std::wstring receivedUrl((wchar_t*)pcds->lpData, pcds->cbData / sizeof(wchar_t)); std::wcout << L"Received URL in main instance: " << receivedUrl << std::endl; // Check if received URL is a file and exists if (FileExists(receivedUrl)) { // Extract file extension size_t dotPos = receivedUrl.find_last_of(L"."); std::wstring extension = (dotPos != std::wstring::npos) ? receivedUrl.substr(dotPos) : L""; if (extension == L".torrent") { // Handle .torrent files std::string utf8FilePath = WStringToUtf8(receivedUrl); std::ifstream ifs(utf8FilePath, std::ios::binary); if (!ifs) { std::cerr << "Error: Could not open torrent file.\n"; break; } std::vector fileBuffer( (std::istreambuf_iterator(ifs)), (std::istreambuf_iterator()) ); json j; j["type"] = "OpenTorrent"; j["data"] = fileBuffer; SendToJS("OpenTorrent", j); } else { // Handle other media files std::string utf8FilePath = WStringToUtf8(receivedUrl); json j; j["type"] = "OpenFile"; j["path"] = utf8FilePath; SendToJS("OpenFile", j); } } else if (receivedUrl.rfind(L"stremio://detail", 0) == 0) { std::string utf8Url = WStringToUtf8(receivedUrl); json j; j["type"] = "ReplaceLocation"; j["path"] = utf8Url; SendToJS("ReplaceLocation", j); } else if (receivedUrl.rfind(L"stremio://", 0) == 0) { // Handle stremio:// protocol std::string utf8Url = WStringToUtf8(receivedUrl); json j; j["type"] = "AddonInstall"; j["path"] = utf8Url; SendToJS("AddonInstall", j); } else if (receivedUrl.rfind(L"magnet:", 0) == 0) { std::string utf8Url = WStringToUtf8(receivedUrl); json j; j["type"] = "OpenTorrent"; j["magnet"] = utf8Url; SendToJS("OpenTorrent", j); } else { std::wcout << L"Received URL is neither a valid file nor a stremio:// protocol." << std::endl; } } return 0; } case WM_CLOSE: { WINDOWPLACEMENT wp; wp.length = sizeof(wp); if (GetWindowPlacement(hWnd, &wp)) { // Save to ini SaveWindowPlacement(wp); } if(g_closeOnExit) { DestroyWindow(hWnd); } else { ShowWindow(hWnd, SW_HIDE); pauseMPV(g_pauseOnMinimize); g_showWindow=false; } return 0; } case WM_ACTIVATE: { if(LOWORD(wParam)==WA_INACTIVE){ pauseMPV(g_pauseOnLostFocus); } if (LOWORD(wParam) != WA_INACTIVE) { SetFocus(hWnd); if (g_webview && g_webviewController) { g_webviewController->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); } } break; } case WM_SIZE: { if(wParam==SIZE_MINIMIZED){ pauseMPV(g_pauseOnMinimize); } if(g_webviewController){ RECT rc; GetClientRect(hWnd,&rc); g_webviewController->put_Bounds(rc); } if(g_hSplash){ int w = LOWORD(lParam); int h = HIWORD(lParam); SetWindowPos(g_hSplash,nullptr,0,0,w,h,SWP_NOZORDER); } break; } case WM_MPV_WAKEUP: HandleMpvEvents(); break; case WM_DESTROY: { // release mutex if(g_hMutex) { CloseHandle(g_hMutex); g_hMutex=nullptr; } PostQuitMessage(0); break; } default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; }