#include "webview.h" #include #include #include #include #include #include #include #include "../core/globals.h" #include "../utils/crashlog.h" #include "../utils/helpers.h" #include "../ui/mainwindow.h" #include "../utils/extensions.h" static const wchar_t* EXEC_SHELL_SCRIPT = LR"JS_CODE( try { console.log('Shell JS injected'); if (window.self === window.top && !window.qt) { window.qt = { webChannelTransport: { send: window.chrome.webview.postMessage, onmessage: (ev) => { // Will be overwritten by ShellTransport console.log('Received message from WebView2:', ev); } } }; window.chrome.webview.addEventListener('message', (ev) => { window.qt.webChannelTransport.onmessage(ev); }); window.onload = () => { try { initShellComm(); } catch (e) { const errorMessage = { type: 6, object: "transport", method: "handleInboundJSON", id: 888, args: [ "app-error", [ "shellComm" ] ] }; window.chrome.webview.postMessage(JSON.stringify(errorMessage)); } }; } } catch(e) { console.error("Error exec initShellComm:", e); const errorMessage = { type: 6, object: "transport", method: "handleInboundJSON", id: 888, args: [ "app-error", [ "shellComm" ] ] }; if(window.chrome && window.chrome.webview && window.chrome.webview.postMessage) { window.chrome.webview.postMessage(JSON.stringify(errorMessage)); } }; )JS_CODE"; static const wchar_t* INJECTED_KEYDOWN_SCRIPT = LR"JS( (function() { window.addEventListener('keydown', function(event) { if (event.code === 'F5') { event.preventDefault(); const ctrlPressed = event.ctrlKey || event.metaKey; const msg = { type: 6, object: "transport", method: "handleInboundJSON", id: 999, args: [ "refresh", [ ctrlPressed ? "all" : "no" ] ] }; window.chrome.webview.postMessage(JSON.stringify(msg)); } }); })(); )JS"; static const wchar_t* INJECTED_BUTTON_SCRIPT = LR"JS( (function() { // Create the button element var btn = document.createElement('button'); btn.id = 'goBackStremioBtn'; // Style the button: btn.style.position = 'fixed'; btn.style.bottom = '15px'; btn.style.right = '15px'; btn.style.zIndex = '9999'; btn.style.backgroundColor = '#121024'; // Purple btn.style.color = 'white'; btn.style.border = 'none'; btn.style.borderRadius = '30px'; btn.style.padding = '12px 20px'; btn.style.fontSize = '16px'; btn.style.fontWeight = 'bold'; btn.style.display = 'flex'; btn.style.alignItems = 'center'; btn.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)'; btn.style.cursor = 'pointer'; btn.style.transition = 'background-color 0.3s ease'; // Hover effect: btn.addEventListener('mouseenter', function() { btn.style.backgroundColor = '#211e39'; // Gray }); btn.addEventListener('mouseleave', function() { btn.style.backgroundColor = '#121024'; }); // Create an image element for the logo var img = document.createElement('img'); img.src = 'https://stremio.zarg.me/images/stremio_symbol.png'; img.alt = 'Logo'; img.style.height = '24px'; img.style.width = '24px'; img.style.marginRight = '8px'; img.addEventListener('error', function() { img.style.display = 'none'; }); // Create a text element var txt = document.createElement('span'); txt.textContent = 'Back to Stremio'; // Append the logo and text to the button btn.appendChild(img); btn.appendChild(txt); // On click go home btn.addEventListener('click', function() { const payload = { type: 6, object: "transport", method: "handleInboundJSON", id: 666, args: [ "navigate", [ "home" ] ] }; window.chrome.webview.postMessage(JSON.stringify(payload)); }); // Append the button to the document body document.body.appendChild(btn); })(); )JS"; void WaitAndRefreshIfNeeded() { std::thread([](){ const int maxAttempts = 10; const int initialWaitTime = 5; const int maxWaitTime = 60; std::cout << "[WEBVIEW]: Web Page could not be reached, retrying..." << std::endl; for(int attempt=0; attemptmaxWaitTime) waitTime = maxWaitTime; std::this_thread::sleep_for(std::chrono::seconds(waitTime)); if(g_isAppReady){ std::cout << "[WEBVIEW]: Web Page ready!" << std::endl; g_waitStarted.store(false); return; } std::cout << "[WEBVIEW]: Refreshing attempt " << (attempt+1) << std::endl; refreshWeb(false); } if(!g_isAppReady) { AppendToCrashLog("[WEBVIEW]: Could not load after attempts"); MessageBoxW(nullptr, L"Web page could not be loaded after multiple attempts. Make sure the Web UI is reachable.", L"WebView2 Page load fail", MB_ICONERROR | MB_OK ); PostQuitMessage(1); exit(1); } }).detach(); } void InitWebView2(HWND hWnd) { std::cout << "[WEBVIEW]: Starting webview..." << std::endl; // Setup environment Microsoft::WRL::ComPtr options = Microsoft::WRL::Make(); if(options){ options->put_AdditionalBrowserArguments( L"--autoplay-policy=no-user-gesture-required --disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection" ); Microsoft::WRL::ComPtr options6; if(SUCCEEDED(options.As(&options6))) { options6->put_AreBrowserExtensionsEnabled(TRUE); } Microsoft::WRL::ComPtr options5; if(SUCCEEDED(options.As(&options5))) { options5->put_EnableTrackingPrevention(TRUE); } } // Check for local Edge runtime in "portable_config/EdgeWebView" std::wstring exeDir; { wchar_t buf[MAX_PATH]; GetModuleFileNameW(nullptr, buf, MAX_PATH); exeDir = buf; size_t pos = exeDir.find_last_of(L"\\/"); if(pos!=std::wstring::npos) exeDir.erase(pos); } std::wstring browserDir = exeDir + L"\\portable_config\\EdgeWebView"; const wchar_t* browserExecutableFolder = nullptr; if(DirectoryExists(browserDir)) { browserExecutableFolder = browserDir.c_str(); std::wcout << L"[WEBVIEW]: Using local WebView2: " << browserDir << std::endl; } HRESULT hr = CreateCoreWebView2EnvironmentWithOptions( browserExecutableFolder, nullptr, options.Get(), Microsoft::WRL::Callback( [hWnd](HRESULT res, ICoreWebView2Environment* env)->HRESULT { if(!env) return E_FAIL; env->CreateCoreWebView2Controller( hWnd, Microsoft::WRL::Callback( [hWnd](HRESULT result, ICoreWebView2Controller* rawController)->HRESULT { if (FAILED(result) || !rawController) return E_FAIL; std::cout << "[WEBVIEW]: Initializing WebView..." << std::endl; wil::com_ptr m_webviewController = rawController; if (!m_webviewController) return E_FAIL; g_webviewController = m_webviewController.try_query(); if (!g_webviewController) return E_FAIL; wil::com_ptr coreWebView; g_webviewController->get_CoreWebView2(&coreWebView); g_webview = coreWebView.try_query(); if (!g_webview) return E_FAIL; wil::com_ptr webView2Profile; g_webview->get_Profile(&webView2Profile); g_webviewProfile = webView2Profile.try_query(); if (!g_webviewProfile) return E_FAIL; wil::com_ptr webView2Settings; g_webview->get_Settings(&webView2Settings); auto settings = webView2Settings.try_query(); if (!settings) return E_FAIL; if(settings) { #ifndef DEBUG_LOG settings->put_AreDevToolsEnabled(FALSE); #endif settings->put_IsStatusBarEnabled(FALSE); settings->put_AreBrowserAcceleratorKeysEnabled(FALSE); std::wstring customUA = std::wstring(L"StremioShell/") + Utf8ToWstring(APP_VERSION); settings->put_UserAgent(customUA.c_str()); if(!g_allowZoom) { settings->put_IsZoomControlEnabled(FALSE); settings->put_IsPinchZoomEnabled(FALSE); } } // Set background color COREWEBVIEW2_COLOR col={0,0,0,0}; g_webviewController->put_DefaultBackgroundColor(col); RECT rc; GetClientRect(hWnd,&rc); g_webviewController->put_Bounds(rc); g_webview->AddScriptToExecuteOnDocumentCreated(EXEC_SHELL_SCRIPT,nullptr); g_webview->AddScriptToExecuteOnDocumentCreated(INJECTED_KEYDOWN_SCRIPT,nullptr); SetupWebMods(); SetupExtensions(); SetupWebMessageHandler(); std::thread([](){ std::wcout << L"[WEBVIEW]: Checking web ui endpoints..." << std::endl; std::wstring foundUrl = GetFirstReachableUrl(); std::wstring* pResult = new std::wstring(foundUrl); g_webuiUrl = foundUrl; PostMessage(g_hWnd, WM_REACHABILITY_DONE, (WPARAM)pResult, 0); FetchAndParseWhitelist(); }).detach(); return S_OK; }).Get() ); return S_OK; }).Get() ); if(FAILED(hr)) { std::wstring msg = L"[WEBVIEW]: CreateCoreWebView2EnvironmentWithOptions failed => " + std::to_wstring(hr); AppendToCrashLog(msg); MessageBoxW(nullptr, msg.c_str(), L"WebView2 Initialization Error", MB_ICONERROR | MB_OK); PostQuitMessage(1); exit(1); } } static void SetupWebMessageHandler() { if(!g_webview) return; EventRegistrationToken navToken; g_webview->add_NavigationCompleted( Microsoft::WRL::Callback( [](ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs* args)->HRESULT { BOOL isSuccess; args->get_IsSuccess(&isSuccess); // Retrieve the final URL wil::unique_cotaskmem_string rawUri; sender->get_Source(&rawUri); std::wstring finalUri = rawUri ? rawUri.get() : L""; std::wcout << L"[WEBVIEW]: Navigation try to " << finalUri << std::endl; // Add back to stremio button if not on stremio if (finalUri.find(g_webuiUrl) == std::wstring::npos) { sender->ExecuteScript(INJECTED_BUTTON_SCRIPT, nullptr); } if(isSuccess) { std::cout<<"[WEBVIEW]: Navigation Complete - Success\n"; sender->ExecuteScript(EXEC_SHELL_SCRIPT, nullptr); // Flush the script queue. if (!g_scriptQueue.empty()) { for (const auto &script : g_scriptQueue) { sender->ExecuteScript(script.c_str(), nullptr); } g_scriptQueue.clear(); } } else { std::cout<<"[WEBVIEW]: Navigation failed\n"; if(g_hSplash && !g_waitStarted.exchange(true)) { WaitAndRefreshIfNeeded(); } HandleExtensions(finalUri); } return S_OK; }).Get(), &navToken ); EventRegistrationToken contentToken; g_webview->add_ContentLoading( Microsoft::WRL::Callback( [](ICoreWebView2* sender, ICoreWebView2ContentLoadingEventArgs* args) -> HRESULT { std::cout<<"[WEBVIEW]: Content loaded\n"; sender->ExecuteScript(EXEC_SHELL_SCRIPT, nullptr); return S_OK; } ).Get(), &contentToken ); EventRegistrationToken domToken; g_webview->add_DOMContentLoaded( Microsoft::WRL::Callback( [](ICoreWebView2* sender, ICoreWebView2DOMContentLoadedEventArgs* args)->HRESULT { sender->ExecuteScript(EXEC_SHELL_SCRIPT, nullptr); return S_OK; }).Get(), &domToken ); EventRegistrationToken contextMenuToken; g_webview->add_ContextMenuRequested( Microsoft::WRL::Callback( [](ICoreWebView2* sender, ICoreWebView2ContextMenuRequestedEventArgs* args) -> HRESULT { // Existing variable declarations wil::com_ptr items; HRESULT hr = args->get_MenuItems(&items); if (FAILED(hr) || !items) return hr; #ifdef DEBUG_LOG return S_OK; //DEV TOOLS DEBUG ONLY #endif // Get current URL wil::unique_cotaskmem_string currentUri; sender->get_Source(¤tUri); std::wstring uri(currentUri.get()); bool isExtensionUrl = uri.starts_with(L"chrome-extension://"); // Get context menu target wil::com_ptr target; hr = args->get_ContextMenuTarget(&target); BOOL isEditable = FALSE; if (SUCCEEDED(hr) && target) { target->get_IsEditable(&isEditable); } UINT count = 0; items->get_Count(&count); if (!isEditable) { // Allow only Back command (ID 33000) for extension URLs std::set allowedCommands = isExtensionUrl ? std::set{33000} : std::set{}; for (UINT i = 0; i < count;) { wil::com_ptr item; hr = items->GetValueAtIndex(i, &item); if (FAILED(hr)) { i++; continue; } INT32 commandId; item->get_CommandId(&commandId); if (allowedCommands.find(commandId) == allowedCommands.end()) { items->RemoveValueAtIndex(i); items->get_Count(&count); } else { i++; } } return S_OK; } // Define allowed command IDs for filtering std::set allowedCommandIds = { 50151, // Cut 50150, // Copy 50152, // Paste 50157, // Paste as plain text 50156 // Select all }; for (UINT i = 0; i < count;) { wil::com_ptr item; hr = items->GetValueAtIndex(i, &item); if (FAILED(hr)) { i++; continue; } INT32 commandId; item->get_CommandId(&commandId); if (allowedCommandIds.find(commandId) == allowedCommandIds.end()) { items->RemoveValueAtIndex(i); items->get_Count(&count); } else { i++; } } return S_OK; }).Get(), &contextMenuToken ); EventRegistrationToken msgToken; g_webview->add_WebMessageReceived( Microsoft::WRL::Callback( [](ICoreWebView2* /*sender*/, ICoreWebView2WebMessageReceivedEventArgs* args)->HRESULT { wil::unique_cotaskmem_string msgRaw; args->TryGetWebMessageAsString(&msgRaw); if(!msgRaw) return S_OK; std::wstring wstr(msgRaw.get()); std::string str = WStringToUtf8(wstr); HandleInboundJSON(str); return S_OK; }).Get(), &msgToken ); EventRegistrationToken newWindowToken; g_webview->add_NewWindowRequested( Microsoft::WRL::Callback( [](ICoreWebView2* /*sender*/, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT { // Mark the event as handled to prevent default behavior args->put_Handled(TRUE); wil::unique_cotaskmem_string uri; if (SUCCEEDED(args->get_Uri(&uri)) && uri) { std::wstring wuri(uri.get()); // Check if the URI is a local file (starts with "file://") if (wuri.rfind(L"file://", 0) == 0) { std::wstring filePath = wuri.substr(8); std::string utf8FilePath = WStringToUtf8(filePath); std::string decodedFilePathUtf8 = decodeURIComponent(utf8FilePath); std::string baseName = std::filesystem::path(decodedFilePathUtf8).filename().string(); if (isSubtitle(filePath)) { std::vector subaddArgs = {"sub-add",decodedFilePathUtf8, "select", baseName + " External", "Other Tracks"}; HandleEvent("mpv-command", subaddArgs); json j; j["type"] = "SubtitleDropped"; j["path"] = utf8FilePath; SendToJS("SubtitleDropped", j); return S_OK; } json j; j["type"] = "FileDropped"; j["path"] = decodedFilePathUtf8; SendToJS("FileDropped", j); return S_OK; } if (URLContainsAny(wuri)) { g_webview->Navigate(wuri.c_str()); return S_OK; } // For non-file URIs, open externally ShellExecuteW(nullptr, L"open", uri.get(), nullptr, nullptr, SW_SHOWNORMAL); } return S_OK; } ).Get(), &newWindowToken ); // For redirects like window.location.replace EventRegistrationToken navstartedToken; g_webview->add_NavigationStarting( Microsoft::WRL::Callback( [](ICoreWebView2* sender, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT { wil::unique_cotaskmem_string uri; args->get_Uri(&uri); std::wstring destination(uri.get()); if (!URLContainsAny(destination)) { args->put_Cancel(TRUE); ShellExecuteW(nullptr, L"open", uri.get(), nullptr, nullptr, SW_SHOWNORMAL); } return S_OK; } ).Get(), &navstartedToken ); // FullScreen EventRegistrationToken cfeToken; g_webview->add_ContainsFullScreenElementChanged( Microsoft::WRL::Callback( [](ICoreWebView2* sender, IUnknown* /*args*/) -> HRESULT { // FullScreen Toggle Handle BOOL inFull = FALSE; sender->get_ContainsFullScreenElement(&inFull); ToggleFullScreen(g_hWnd, inFull != FALSE); return S_OK; } ).Get(), &cfeToken ); } static void SetupExtensions() { if(!g_webview || !g_webviewProfile) return; // e.g. from "portable_config/extensions" std::wstring exeDir; { wchar_t buf[MAX_PATH]; GetModuleFileNameW(nullptr, buf, MAX_PATH); exeDir = buf; size_t pos = exeDir.find_last_of(L"\\/"); if(pos!=std::wstring::npos) exeDir.erase(pos); } std::wstring extensionsRoot = exeDir + L"\\portable_config\\extensions"; try { for(const auto& entry : std::filesystem::directory_iterator(extensionsRoot)) { if(entry.is_directory()) { std::wstring folderName = entry.path().filename().wstring(); HRESULT hr = g_webviewProfile->AddBrowserExtension( entry.path().wstring().c_str(), Microsoft::WRL::Callback( [folderName](HRESULT result, ICoreWebView2BrowserExtension* extension)->HRESULT { if (SUCCEEDED(result) && extension) { wil::unique_cotaskmem_string extId; HRESULT hrId = extension->get_Id(&extId); if (SUCCEEDED(hrId) && extId) { // Store extension ID in the global map g_extensionMap[folderName] = extId.get(); std::wcout << L"[EXTENSIONS]: " << folderName << L" => " << extId.get() << std::endl; } std::wcout << L"[EXTENSIONS]: Added extension " << folderName << std::endl; } else { std::wstring err = L"[EXTENSIONS]: Failed to add extension => " + std::to_wstring(result); AppendToCrashLog(err); } return S_OK; }).Get() ); if(FAILED(hr)) { std::wstring err = L"[EXTENSIONS]: AddBrowserExtension failed => " + std::to_wstring(hr); AppendToCrashLog(err); } } } } catch(...) { std::cout<<"[EXTENSIONS]: No extensions folder or iteration failed.\n"; } } static void SetupWebMods() { if (!g_webview) return; wchar_t buf[MAX_PATH]; GetModuleFileNameW(nullptr, buf, MAX_PATH); std::wstring exeDir = buf; size_t pos = exeDir.find_last_of(L"\\/"); if (pos != std::wstring::npos) exeDir.erase(pos); const std::filesystem::path root = std::filesystem::path(exeDir) / L"portable_config" / L"webmods"; if (!std::filesystem::exists(root) || !std::filesystem::is_directory(root)) { std::wcout << L"[WEBMODS] Folder not found: " << root.wstring() << std::endl; return; } std::vector cssFiles, jsFiles; for (const auto& e : std::filesystem::recursive_directory_iterator(root)) { if (!e.is_regular_file()) continue; auto ext = e.path().extension().wstring(); std::transform(ext.begin(), ext.end(), ext.begin(), ::towlower); if (ext == L".map" || ext == L".bak" || ext == L".tmp") continue; if (ext == L".css") cssFiles.push_back(e.path()); else if (ext == L".js") jsFiles.push_back(e.path()); } auto relStr = [&](const std::filesystem::path& p){ try { return std::filesystem::relative(p, root).wstring(); } catch(...) { return p.wstring(); } }; auto sorter = [&](const std::filesystem::path& a, const std::filesystem::path& b){ auto ra = relStr(a), rb = relStr(b); return _wcsicmp(ra.c_str(), rb.c_str()) < 0; }; std::sort(cssFiles.begin(), cssFiles.end(), sorter); std::sort(jsFiles.begin(), jsFiles.end(), sorter); auto makeId = [&](const std::filesystem::path& p){ std::wstring id = relStr(p); for (auto& ch : id) if (!iswalnum(ch)) ch = L'_'; return id; }; for (const auto& p : cssFiles) { std::string content; if (!ReadFileUtf8(p.wstring(), content)) continue; const std::wstring id = makeId(p); const std::wstring script = MakeInjectCssScript(id, content); g_webview->AddScriptToExecuteOnDocumentCreated(script.c_str(), nullptr); std::wcout << L"[WEBMODS] CSS: " << relStr(p) << std::endl; } for (const auto& p : jsFiles) { std::string content; if (!ReadFileUtf8(p.wstring(), content)) continue; const std::wstring id = makeId(p); const std::wstring script = MakeInjectJsScript(id, content); g_webview->AddScriptToExecuteOnDocumentCreated(script.c_str(), nullptr); std::wcout << L"[WEBMODS] JS: " << relStr(p) << std::endl; } } void refreshWeb(const bool refreshAll) { if (g_webviewProfile && refreshAll) { HRESULT hr = g_webviewProfile->ClearBrowsingData( COREWEBVIEW2_BROWSING_DATA_KINDS_DISK_CACHE | COREWEBVIEW2_BROWSING_DATA_KINDS_CACHE_STORAGE | COREWEBVIEW2_BROWSING_DATA_KINDS_SERVICE_WORKERS | COREWEBVIEW2_BROWSING_DATA_KINDS_FILE_SYSTEMS | COREWEBVIEW2_BROWSING_DATA_KINDS_WEB_SQL | COREWEBVIEW2_BROWSING_DATA_KINDS_INDEXED_DB, Microsoft::WRL::Callback( [](HRESULT result) -> HRESULT { std::cout << "[BROWSER]: Cleared browser cache successfully" << std::endl; return S_OK; } ).Get() ); if (FAILED(hr)) { std::cout << "[BROWSER]: Could not clear browser cache" << std::endl; } } if (g_webview) { g_webview->Reload(); } }