Full Refactor + Transport rewrite to use default qt transport

- Full Refactor
- Full transport rewrite to use qt default transport
- Saves window position on exit and restores on start
- Added filter to ublock extension to prevent thumbnail issues (possibly)
This commit is contained in:
Zarg 2025-01-25 00:46:32 +01:00
parent 37a20b1fe7
commit 1976366f44
26 changed files with 2969 additions and 2912 deletions

View file

@ -29,6 +29,28 @@ find_package(unofficial-webview2 CONFIG REQUIRED)
set(SOURCES
src/main.cpp
stremio.rc
src/core/globals.h
src/core/globals.cpp
src/utils/helpers.h
src/utils/helpers.cpp
src/utils/config.h
src/utils/config.cpp
src/utils/crashlog.h
src/utils/crashlog.cpp
src/mpv/player.h
src/mpv/player.cpp
src/node/server.cpp
src/node/server.h
src/tray/tray.h
src/tray/tray.cpp
src/ui/splash.h
src/ui/splash.cpp
src/updater/updater.cpp
src/updater/updater.h
src/webview/webview.h
src/webview/webview.cpp
src/ui/mainwindow.h
src/ui/mainwindow.cpp
src/resource.h
)
@ -36,12 +58,10 @@ add_executable(${PROJECT_NAME} WIN32 ${SOURCES})
target_link_libraries(${PROJECT_NAME} PRIVATE
user32.lib
gdi32.lib
ole32.lib
oleaut32.lib
shell32.lib
advapi32.lib
gdiplus.lib
dwmapi.lib
Shcore.lib
Msimg32.lib
nlohmann_json::nlohmann_json
unofficial::webview2::webview2
OpenSSL::SSL

90
src/core/globals.cpp Normal file
View file

@ -0,0 +1,90 @@
#include "globals.h"
// Window & instance
TCHAR szWindowClass[] = APP_NAME;
TCHAR szTitle[] = APP_TITLE;
HINSTANCE g_hInst = nullptr;
HWND g_hWnd = nullptr;
HBRUSH g_darkBrush = nullptr;
HANDLE g_hMutex = nullptr;
HHOOK g_hMouseHook = nullptr;
std::wstring g_webuiUrl = L"https://zaarrg.github.io/stremio-web-shell-fixes/";
std::string g_updateUrl= "https://raw.githubusercontent.com/Zaarrg/stremio-desktop-v5/refs/heads/webview-windows/version/version.json";
// Command-line args
bool g_streamingServer = true;
bool g_autoupdaterForceFull = false;
// mpv
mpv_handle* g_mpv = nullptr;
std::set<std::string> g_observedProps;
// Node
std::atomic_bool g_nodeRunning = false;
std::thread g_nodeThread;
HANDLE g_nodeProcess = nullptr;
HANDLE g_nodeOutPipe = nullptr;
HANDLE g_nodeInPipe = nullptr;
// WebView2
wil::com_ptr<ICoreWebView2Controller4> g_webviewController;
wil::com_ptr<ICoreWebView2Profile8> g_webviewProfile;
wil::com_ptr<ICoreWebView2_21> g_webview;
// Tray
std::vector<MenuItem> g_menuItems;
NOTIFYICONDATA g_nid = {0};
bool g_showWindow = true;
bool g_alwaysOnTop= false;
bool g_isFullscreen = false;
bool g_closeOnExit = false;
bool g_useDarkTheme = false;
bool g_isPipMode = false;
int g_thumbFastHeight = 0;
int g_hoverIndex = -1;
HFONT g_hMenuFont = nullptr;
HANDLE g_serverJob = nullptr;
HWND g_trayHwnd = nullptr;
// Ini Settings
bool g_pauseOnMinimize = true;
bool g_pauseOnLostFocus = false;
bool g_allowZoom = false;
// Tray sizes
int g_tray_itemH = 31;
int g_tray_sepH = 8;
int g_tray_w = 200;
// Splash
HWND g_hSplash = nullptr;
HBITMAP g_hSplashImage = nullptr;
float g_splashOpacity= 1.0f;
int g_pulseDirection = -1;
ULONG_PTR g_gdiplusToken = 0;
// Pending messages
std::vector<nlohmann::json> g_inboundMessages;
std::vector<nlohmann::json> g_outboundMessages;
std::atomic<bool> g_isAppReady = false;
std::atomic<bool> g_waitStarted(false);
// Updater
std::atomic_bool g_updaterRunning = false;
std::filesystem::path g_installerPath;
std::thread g_updaterThread;
const char* public_key_pem = R"(-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoXoJRQ81xOT3Gx6+hsWM
ZiD4PwtLdxxNhEdL/iK0yp6AdO/L0kcSHk9YCPPx0XPK9sssjSV5vCbNE/2IJxnh
/mV+3GAMmXgMvTL+DZgrHafnxe1K50M+8Z2z+uM5YC9XDLppgnC6OrUjwRqNHrKI
T1vcgKf16e/TdKj8xlgadoHBECjv6dr87nbHW115bw8PVn2tSk/zC+QdUud+p6KV
zA6+FT9ZpHJvdS3R0V0l7snr2cwapXF6J36aLGjJ7UviRFVWEEsQaKtAAtTTBzdD
4B9FJ2IJb/ifdnVzeuNTDYApCSE1F89XFWN9FoDyw7Jkk+7u4rsKjpcnCDTd9ziG
kwIDAQAB
-----END PUBLIC KEY-----)";
// ThumbFast
std::atomic<bool> g_ignoreHover(false);
std::chrono::steady_clock::time_point g_ignoreUntil;

144
src/core/globals.h Normal file
View file

@ -0,0 +1,144 @@
#ifndef GLOBALS_H
#define GLOBALS_H
#include <windows.h>
#include <shellapi.h>
#include <dwmapi.h>
#include <string>
#include <thread>
#include <atomic>
#include <set>
#include <vector>
#include <chrono>
#include <filesystem>
#include <wil/com.h>
#include "nlohmann/json.hpp"
#include "mpv/client.h"
#include <WebView2.h>
#include <WebView2EnvironmentOptions.h>
// For our JSON convenience
using json = nlohmann::json;
// -----------------------------------------------------------------------------
// App info
// -----------------------------------------------------------------------------
#define APP_TITLE "Stremio - Freedom to Stream"
#define APP_NAME "Stremio"
#define APP_CLASS L"Stremio"
#define APP_VERSION "5.0.14"
// -----------------------------------------------------------------------------
// Globals
// -----------------------------------------------------------------------------
extern TCHAR szWindowClass[];
extern TCHAR szTitle[];
extern HINSTANCE g_hInst;
extern HWND g_hWnd;
extern HBRUSH g_darkBrush;
extern HANDLE g_hMutex;
extern HHOOK g_hMouseHook;
extern std::wstring g_webuiUrl;
extern std::string g_updateUrl;
// Args
extern bool g_streamingServer;
extern bool g_autoupdaterForceFull;
// mpv
extern mpv_handle* g_mpv;
extern std::set<std::string> g_observedProps;
// custom messages
#define WM_MPV_WAKEUP (WM_APP + 2)
#define WM_TRAYICON (WM_APP + 1)
// Node server
extern std::atomic_bool g_nodeRunning;
extern std::thread g_nodeThread;
extern HANDLE g_nodeProcess;
extern HANDLE g_nodeOutPipe;
extern HANDLE g_nodeInPipe;
// WebView2
extern wil::com_ptr<ICoreWebView2Controller4> g_webviewController;
extern wil::com_ptr<ICoreWebView2Profile8> g_webviewProfile;
extern wil::com_ptr<ICoreWebView2_21> g_webview;
// Tray IDs
#define ID_TRAY_SHOWWINDOW 1001
#define ID_TRAY_ALWAYSONTOP 1002
#define ID_TRAY_CLOSE_ON_EXIT 1003
#define ID_TRAY_USE_DARK_THEME 1004
#define ID_TRAY_PAUSE_MINIMIZED 1005
#define ID_TRAY_PAUSE_FOCUS_LOST 1006
#define ID_TRAY_PICTURE_IN_PICTURE 1007
#define ID_TRAY_QUIT 1008
struct MenuItem
{
UINT id;
bool checked;
bool separator;
std::wstring text;
};
extern std::vector<MenuItem> g_menuItems;
extern NOTIFYICONDATA g_nid;
extern bool g_showWindow;
extern bool g_alwaysOnTop;
extern bool g_isFullscreen;
extern bool g_closeOnExit;
extern bool g_useDarkTheme;
extern bool g_isPipMode;
extern int g_thumbFastHeight;
extern int g_hoverIndex;
extern HFONT g_hMenuFont;
extern HANDLE g_serverJob;
extern HWND g_trayHwnd;
// Ini Settings
extern bool g_pauseOnMinimize;
extern bool g_pauseOnLostFocus;
extern bool g_allowZoom;
// Tray sizes
extern int g_tray_itemH;
extern int g_tray_sepH;
extern int g_tray_w;
// Splash
extern HWND g_hSplash;
extern HBITMAP g_hSplashImage;
extern float g_splashOpacity;
extern int g_pulseDirection;
extern ULONG_PTR g_gdiplusToken;
// App Ready and Event Queue
#define WM_NOTIFY_FLUSH (WM_USER + 101)
extern std::vector<nlohmann::json> g_inboundMessages;
extern std::vector<nlohmann::json> g_outboundMessages;
extern std::atomic<bool> g_isAppReady;
extern std::atomic<bool> g_waitStarted;
// Updater
extern std::atomic_bool g_updaterRunning;
extern std::filesystem::path g_installerPath;
extern std::thread g_updaterThread;
extern const char* public_key_pem;
// Thumb Fast
extern std::atomic<bool> g_ignoreHover;
extern std::chrono::steady_clock::time_point g_ignoreUntil;
constexpr std::chrono::milliseconds IGNORE_DURATION(200);
// -----------------------------------------------------------------------------
// Functions declared here if you need them globally
// -----------------------------------------------------------------------------
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
#endif // GLOBALS_H

File diff suppressed because it is too large Load diff

277
src/mpv/player.cpp Normal file
View file

@ -0,0 +1,277 @@
#include "player.h"
#include <iostream>
#include <cctype>
#include "../core/globals.h"
#include "../utils/crashlog.h"
#include "../utils/helpers.h"
#include "../ui/mainwindow.h"
// Helper for mpv node => JSON
static nlohmann::json mpvNodeToJson(const mpv_node* node);
static nlohmann::json mpvNodeArrayToJson(const mpv_node_list* list)
{
using json = nlohmann::json;
json j = json::array();
if(!list) return j;
for(int i=0; i<list->num; i++){
j.push_back(mpvNodeToJson(&list->values[i]));
}
return j;
}
static nlohmann::json mpvNodeMapToJson(const mpv_node_list* list)
{
using json = nlohmann::json;
json j = json::object();
if(!list) return j;
for(int i=0; i<list->num; i++){
const char* key = (list->keys && list->keys[i]) ? list->keys[i] : "";
mpv_node &val = list->values[i];
j[key] = mpvNodeToJson(&val);
}
return j;
}
static nlohmann::json mpvNodeToJson(const mpv_node* node)
{
using json = nlohmann::json;
if(!node) return nullptr;
switch(node->format)
{
case MPV_FORMAT_STRING:
return node->u.string ? node->u.string : "";
case MPV_FORMAT_INT64:
return (long long)node->u.int64;
case MPV_FORMAT_DOUBLE:
return node->u.double_;
case MPV_FORMAT_FLAG:
return (bool)node->u.flag;
case MPV_FORMAT_NODE_ARRAY:
return mpvNodeArrayToJson(node->u.list);
case MPV_FORMAT_NODE_MAP:
return mpvNodeMapToJson(node->u.list);
default:
return "<unhandled mpv_node format>";
}
}
// Helper to properly capitalize mpv error
static std::string capitalizeFirstLetter(const std::string& input) {
if (input.empty()) return input;
std::string result = input;
result[0] = std::toupper(result[0]);
return result;
}
// Forward
static void MpvWakeup(void* ctx)
{
PostMessage((HWND)ctx, WM_MPV_WAKEUP, 0, 0);
}
void HandleMpvEvents()
{
if(!g_mpv) return;
while(true){
mpv_event* ev = mpv_wait_event(g_mpv, 0);
if(!ev || ev->event_id==MPV_EVENT_NONE) break;
if(ev->error<0) {
std::cerr<<"mpv event error="<<mpv_error_string(ev->error)<<"\n";
}
switch(ev->event_id)
{
case MPV_EVENT_PROPERTY_CHANGE:
{
mpv_event_property* prop=(mpv_event_property*)ev->data;
if(!prop||!prop->name)break;
json j;
j["type"] ="mpv-prop-change";
j["id"] =(int64_t)ev->reply_userdata;
j["name"] = prop->name;
if(ev->error<0)
j["error"]=mpv_error_string(ev->error);
switch(prop->format)
{
case MPV_FORMAT_INT64:
if(prop->data)
j["data"]=(long long)(*(int64_t*)prop->data);
else
j["data"]=nullptr;
break;
case MPV_FORMAT_DOUBLE:
if(prop->data)
j["data"]=*(double*)prop->data;
else
j["data"]=nullptr;
break;
case MPV_FORMAT_FLAG:
if(prop->data)
j["data"]=(*(int*)prop->data!=0);
else
j["data"]=false;
break;
case MPV_FORMAT_STRING:
if(prop->data){
const char*s=*(char**)prop->data;
j["data"]=(s? s:"");
} else {
j["data"]="";
}
break;
case MPV_FORMAT_NODE:
j["data"]=mpvNodeToJson((mpv_node*)prop->data);
break;
default:
j["data"]=nullptr;
break;
}
SendToJS("mpv-prop-change", j);
break;
}
case MPV_EVENT_END_FILE:
{
mpv_event_end_file* ef=(mpv_event_end_file*)ev->data;
nlohmann::json j;
j["type"]="mpv-event-ended";
switch(ef->reason){
case MPV_END_FILE_REASON_EOF:
j["reason"]="quit";
SendToJS("mpv-event-ended", j);
break;
case MPV_END_FILE_REASON_ERROR:
{
std::string err = mpv_error_string(ef->error);
std::string capitalized = capitalizeFirstLetter(err);
j["reason"]="error";
if(ef->error<0)
j["error"]= capitalized;
AppendToCrashLog("[MPV]: " + capitalized);
SendToJS("mpv-event-ended", j);
break;
}
default:
j["reason"]="other";
SendToJS("mpv-event-ended", j);
break;
}
PostMessage(g_hWnd, WM_NOTIFY_FLUSH, 0, 0);
break;
}
case MPV_EVENT_SHUTDOWN:
{
std::cout<<"mpv EVENT_SHUTDOWN => terminate\n";
mpv_terminate_destroy(g_mpv);
g_mpv=nullptr;
break;
}
default:
// ignore
break;
}
}
}
void HandleMpvCommand(const std::vector<std::string>& args)
{
if(!g_mpv || args.empty()) return;
std::vector<const char*> cargs;
for(auto &s: args) {
cargs.push_back(s.c_str());
}
cargs.push_back(nullptr);
mpv_command(g_mpv, cargs.data());
}
void HandleMpvSetProp(const std::vector<std::string>& args)
{
if(!g_mpv || args.size()<2) return;
std::string val=args[1];
if(val=="true") val="yes";
if(val=="false") val="no";
mpv_set_property_string(g_mpv, args[0].c_str(), val.c_str());
}
void HandleMpvObserveProp(const std::vector<std::string>& args)
{
if(!g_mpv || args.empty()) return;
std::string pname=args[0];
g_observedProps.insert(pname);
mpv_observe_property(g_mpv,0,pname.c_str(),MPV_FORMAT_NODE);
std::cout<<"Observing prop="<<pname<<"\n";
}
void pauseMPV(bool allowed)
{
if(!allowed) return;
std::vector<std::string> pauseArgs = { "pause", "true" };
HandleMpvSetProp(pauseArgs);
}
bool InitMPV(HWND hwnd)
{
g_mpv = mpv_create();
if(!g_mpv){
std::cerr<<"mpv_create failed\n";
AppendToCrashLog("[MPV]: Create failed");
return false;
}
// portable_config
std::wstring exeDir = GetExeDirectory();
std::wstring cfg = exeDir + L"\\portable_config";
CreateDirectoryW(cfg.c_str(), nullptr);
// Convert config path to UTF-8
int needed = WideCharToMultiByte(CP_UTF8, 0, cfg.c_str(), -1, nullptr, 0, nullptr, nullptr);
std::string utf8(needed, 0);
WideCharToMultiByte(CP_UTF8, 0, cfg.c_str(), -1, &utf8[0], needed, nullptr, nullptr);
mpv_set_option_string(g_mpv, "config-dir", utf8.c_str());
mpv_set_option_string(g_mpv, "load-scripts","yes");
mpv_set_option_string(g_mpv, "config","yes");
mpv_set_option_string(g_mpv, "terminal","yes");
mpv_set_option_string(g_mpv, "msg-level","all=v");
int64_t wid=(int64_t)hwnd;
mpv_set_option(g_mpv,"wid", MPV_FORMAT_INT64, &wid);
mpv_set_wakeup_callback(g_mpv, MpvWakeup, hwnd);
if(mpv_initialize(g_mpv)<0){
std::cerr<<"mpv_initialize failed\n";
AppendToCrashLog("[MPV]: Initialize failed");
return false;
}
// Set VO
mpv_set_option_string(g_mpv,"vo","gpu-next");
// demux/caching
mpv_set_property_string(g_mpv,"demuxer-lavf-probesize", "524288");
mpv_set_property_string(g_mpv,"demuxer-lavf-analyzeduration","0.5");
mpv_set_property_string(g_mpv,"demuxer-max-bytes","300000000");
mpv_set_property_string(g_mpv,"demuxer-max-packets","150000000");
mpv_set_property_string(g_mpv,"cache","yes");
mpv_set_property_string(g_mpv,"cache-pause","no");
mpv_set_property_string(g_mpv,"cache-secs","60");
mpv_set_property_string(g_mpv,"vd-lavc-threads","0");
mpv_set_property_string(g_mpv,"ad-lavc-threads","0");
mpv_set_property_string(g_mpv,"audio-fallback-to-null","yes");
mpv_set_property_string(g_mpv,"audio-client-name",APP_NAME);
mpv_set_property_string(g_mpv,"title",APP_NAME);
return true;
}
void CleanupMPV()
{
if(g_mpv){
mpv_terminate_destroy(g_mpv);
g_mpv=nullptr;
}
}

21
src/mpv/player.h Normal file
View file

@ -0,0 +1,21 @@
#ifndef PLAYER_H
#define PLAYER_H
#include <string>
#include <vector>
#include <windows.h>
#include "nlohmann/json.hpp"
bool InitMPV(HWND hwnd);
void CleanupMPV();
void HandleMpvEvents();
// Commands
void HandleMpvCommand(const std::vector<std::string>& args);
void HandleMpvSetProp(const std::vector<std::string>& args);
void HandleMpvObserveProp(const std::vector<std::string>& args);
// For pausing
void pauseMPV(bool allowed);
#endif // PLAYER_H

128
src/node/server.cpp Normal file
View file

@ -0,0 +1,128 @@
#include "server.h"
#include <windows.h>
#include <string>
#include <thread>
#include <atomic>
#include <iostream>
#include "../core/globals.h"
#include "../ui/mainwindow.h"
#include "../utils/crashlog.h"
#include "../utils/helpers.h"
static void NodeOutputThreadProc()
{
char buf[1024];
DWORD readSz=0;
while(g_nodeRunning){
BOOL ok = ReadFile(g_nodeOutPipe, buf, sizeof(buf)-1, &readSz, nullptr);
if(!ok || readSz==0) break;
buf[readSz]='\0';
std::cout<<"[node] "<<buf;
}
std::cout<<"NodeOutputThreadProc done.\n";
}
bool StartNodeServer()
{
std::wstring exePath = L"stremio-runtime.exe";
std::wstring scriptPath = L"server.js";
if(!FileExists(exePath)){
AppendToCrashLog(L"[NODE]: Missing stremio-runtime.exe");
return false;
}
if(!FileExists(scriptPath)){
AppendToCrashLog(L"[NODE]: Missing server.js");
return false;
}
if (!g_serverJob) {
g_serverJob = CreateJobObject(nullptr, nullptr);
if(!g_serverJob){
AppendToCrashLog(L"[NODE]: Failed to create Job Object.");
return false;
}
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {0};
jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
SetInformationJobObject(g_serverJob, JobObjectExtendedLimitInformation, &jobInfo, sizeof(jobInfo));
}
SECURITY_ATTRIBUTES sa;ZeroMemory(&sa, sizeof(sa));
sa.nLength = sizeof(sa);
sa.bInheritHandle = TRUE;
HANDLE outR=nullptr, outW=nullptr;
if(!CreatePipe(&outR,&outW,&sa,0)){
AppendToCrashLog(L"[NODE]: CreatePipe fail1");
return false;
}
SetHandleInformation(outR,HANDLE_FLAG_INHERIT,0);
HANDLE inR=nullptr, inW=nullptr;
if(!CreatePipe(&inR,&inW,&sa,0)){
AppendToCrashLog(L"[NODE]: CreatePipe fail2");
CloseHandle(outR);CloseHandle(outW);
return false;
}
SetHandleInformation(inW,HANDLE_FLAG_INHERIT,0);
STARTUPINFOW si;ZeroMemory(&si,sizeof(si));
si.cb=sizeof(si);
si.hStdOutput=outW; si.hStdError=outW; si.hStdInput=inR;
si.dwFlags=STARTF_USESTDHANDLES;
PROCESS_INFORMATION pi;ZeroMemory(&pi,sizeof(pi));
std::wstring cmdLine = L"\"stremio-runtime.exe\" \"server.js\"";
SetEnvironmentVariableW(L"NO_CORS", L"1");
BOOL success = CreateProcessW(
nullptr, &cmdLine[0],
nullptr,nullptr, TRUE,
CREATE_NO_WINDOW,nullptr,nullptr,
&si, &pi
);
CloseHandle(inR);
CloseHandle(outW);
if(!success){
std::wstring err = L"Failed to launch stremio-runtime.exe\nGetLastError=" + std::to_wstring(GetLastError());
AppendToCrashLog(err);
CloseHandle(inW);CloseHandle(outR);
return false;
}
// Ensure the process belongs to the job
AssignProcessToJobObject(g_serverJob, pi.hProcess);
g_nodeProcess = pi.hProcess;
CloseHandle(pi.hThread);
g_nodeRunning = true;
g_nodeOutPipe = outR;
g_nodeInPipe = inW;
g_nodeThread = std::thread(NodeOutputThreadProc);
std::cout<<"Node server started.\n";
// Let front-end know:
nlohmann::json j;
j["type"] ="ServerStarted";
g_outboundMessages.push_back(j);
PostMessage(g_hWnd, WM_NOTIFY_FLUSH, 0, 0);
return true;
}
void StopNodeServer()
{
if(g_nodeRunning){
g_nodeRunning=false;
if(g_nodeProcess){
TerminateProcess(g_nodeProcess,0);
WaitForSingleObject(g_nodeProcess,INFINITE);
CloseHandle(g_nodeProcess); g_nodeProcess=nullptr;
}
if(g_nodeThread.joinable()) g_nodeThread.join();
if(g_nodeOutPipe){CloseHandle(g_nodeOutPipe); g_nodeOutPipe=nullptr;}
if(g_nodeInPipe){CloseHandle(g_nodeInPipe); g_nodeInPipe=nullptr;}
std::cout<<"Node server stopped.\n";
}
}

7
src/node/server.h Normal file
View file

@ -0,0 +1,7 @@
#ifndef SERVER_H
#define SERVER_H
bool StartNodeServer();
void StopNodeServer();
#endif // SERVER_H

View file

@ -1,6 +1,7 @@
#ifndef RESOURCE_H
#define RESOURCE_H
#define IDR_SPLASH_PNG 101
#define IDR_MAINFRAME 101
#define IDR_SPLASH_PNG 102
#endif
#endif // RESOURCE_H

344
src/tray/tray.cpp Normal file
View file

@ -0,0 +1,344 @@
#include "tray.h"
#include <iostream>
#include <windows.h>
#include <tchar.h>
#include <windowsx.h>
#include "../core/globals.h"
#include "../utils/crashlog.h"
#include "../utils/helpers.h"
#include "../ui/mainwindow.h"
#include "../resource.h"
static LRESULT CALLBACK DarkTrayMenuProc(HWND, UINT, WPARAM, LPARAM);
static HWND CreateDarkTrayMenuWindow();
static void ShowDarkTrayMenu();
static void CreateRoundedRegion(HWND hWnd, int w, int h, int radius);
void CreateTrayIcon(HWND hWnd)
{
g_nid.cbSize=sizeof(NOTIFYICONDATA);
g_nid.hWnd=hWnd;
g_nid.uID=1;
g_nid.uFlags=NIF_ICON|NIF_MESSAGE|NIF_TIP;
g_nid.uCallbackMessage=WM_TRAYICON;
HICON hIcon = LoadIcon(g_hInst, MAKEINTRESOURCE(IDR_MAINFRAME));
g_nid.hIcon = hIcon;
_tcscpy_s(g_nid.szTip, _T("Stremio SingleInstance"));
Shell_NotifyIcon(NIM_ADD,&g_nid);
}
void RemoveTrayIcon()
{
Shell_NotifyIcon(NIM_DELETE,&g_nid);
if(g_nid.hIcon){
DestroyIcon(g_nid.hIcon);
g_nid.hIcon=nullptr;
}
}
void LoadCustomMenuFont()
{
if (g_hMenuFont) {
DeleteObject(g_hMenuFont);
g_hMenuFont = nullptr;
}
LOGFONTW lf = { 0 };
lf.lfHeight = -12;
lf.lfWeight = FW_MEDIUM;
wcscpy_s(lf.lfFaceName, L"Arial Rounded MT");
lf.lfQuality = CLEARTYPE_QUALITY;
g_hMenuFont = CreateFontIndirectW(&lf);
// fallback to system menu font if custom is not available
if (!g_hMenuFont) {
NONCLIENTMETRICSW ncm = { sizeof(ncm) };
if (SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0))
{
ncm.lfMenuFont.lfQuality = CLEARTYPE_QUALITY;
g_hMenuFont = CreateFontIndirectW(&ncm.lfMenuFont);
}
}
if (!g_hMenuFont) {
std::cerr << "Failed to load custom menu font.\n";
AppendToCrashLog("[FONT]: Failed to load custom menu font");
}
}
void ShowTrayMenu(HWND hWnd)
{
ShowDarkTrayMenu();
}
static HWND CreateDarkTrayMenuWindow()
{
static bool s_classRegistered = false;
if (!s_classRegistered)
{
WNDCLASSEXW wcex = { sizeof(wcex) };
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = DarkTrayMenuProc;
wcex.hInstance = GetModuleHandle(nullptr);
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = nullptr;
wcex.lpszClassName = L"DarkTrayMenuWnd";
RegisterClassExW(&wcex);
s_classRegistered = true;
}
HWND hMenuWnd = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_TOPMOST,
L"DarkTrayMenuWnd",
L"",
WS_POPUP,
0, 0, 200, 200,
nullptr, nullptr, GetModuleHandle(nullptr), nullptr
);
if(!hMenuWnd) {
DWORD errorCode = GetLastError();
std::string errorMessage = "[TRAY]: Failed to create tray" + std::to_string(errorCode);
std::cerr << errorMessage << "\n";
AppendToCrashLog(errorMessage);
}
g_trayHwnd = hMenuWnd;
return hMenuWnd;
}
static void CreateRoundedRegion(HWND hWnd, int w, int h, int radius)
{
HRGN hrgn = CreateRoundRectRgn(0, 0, w, h, radius, radius);
SetWindowRgn(hWnd, hrgn, TRUE);
}
static void ShowDarkTrayMenu()
{
g_menuItems.clear();
g_menuItems.push_back({ ID_TRAY_SHOWWINDOW, g_showWindow, false, L"Show Window" });
g_menuItems.push_back({ ID_TRAY_ALWAYSONTOP, g_alwaysOnTop, false, L"Always on Top" });
g_menuItems.push_back({ ID_TRAY_PICTURE_IN_PICTURE, g_isPipMode, false, L"Picture in Picture" });
g_menuItems.push_back({ ID_TRAY_PAUSE_MINIMIZED, g_pauseOnMinimize, false, L"Pause Minimized" });
g_menuItems.push_back({ ID_TRAY_PAUSE_FOCUS_LOST, g_pauseOnLostFocus, false, L"Pause Unfocused" });
g_menuItems.push_back({ ID_TRAY_CLOSE_ON_EXIT, g_closeOnExit, false, L"Close on Exit" });
g_menuItems.push_back({ ID_TRAY_USE_DARK_THEME, g_useDarkTheme, false, L"Use Dark Theme" });
g_menuItems.push_back({ 0, false, true, L"" });
g_menuItems.push_back({ ID_TRAY_QUIT, false, false, L"Quit" });
HWND hMenuWnd = CreateDarkTrayMenuWindow();
int itemH = g_tray_itemH;
int sepH = g_tray_sepH;
int w = g_tray_w;
int totalH = 0;
for (auto &it: g_menuItems) {
totalH += it.separator ? sepH : itemH;
}
POINT cursor;
GetCursorPos(&cursor);
int posX = cursor.x;
int posY = cursor.y - totalH;
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
if (posX + w > screenWidth) {
posX = cursor.x - w;
}
if (posX < 0) posX = 0;
if (posY < 0) posY = 0;
if (posY + totalH > screenHeight) posY = screenHeight - totalH;
SetCapture(hMenuWnd);
SetWindowPos(hMenuWnd, HWND_TOPMOST, posX, posY, w, totalH, SWP_SHOWWINDOW);
CreateRoundedRegion(hMenuWnd, w, totalH, 10);
ShowWindow(hMenuWnd, SW_SHOW);
UpdateWindow(hMenuWnd);
SetForegroundWindow(hMenuWnd);
SetFocus(hMenuWnd);
}
static LRESULT CALLBACK DarkTrayMenuProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_ACTIVATE:
if (LOWORD(wParam) == WA_INACTIVE) {
DestroyWindow(hWnd);
}
break;
case WM_KILLFOCUS:
case WM_CAPTURECHANGED:
DestroyWindow(hWnd);
break;
case WM_ERASEBKGND:
return 1;
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
RECT rcClient;
GetClientRect(hWnd, &rcClient);
COLORREF bgBase, bgHover, txtNormal, txtCheck, lineColor;
if (g_useDarkTheme)
{
bgBase = RGB(30,30,30);
bgHover = RGB(50,50,50);
txtNormal= RGB(200,200,200);
txtCheck = RGB(200,200,200);
lineColor= RGB(80,80,80);
}
else
{
bgBase = RGB(240,240,240);
bgHover = RGB(200,200,200);
txtNormal= RGB(0,0,0);
txtCheck = RGB(0,0,0);
lineColor= RGB(160,160,160);
}
HDC memDC = CreateCompatibleDC(hdc);
HBITMAP memBmp = CreateCompatibleBitmap(hdc, rcClient.right, rcClient.bottom);
HGDIOBJ oldMemBmp = SelectObject(memDC, memBmp);
HBRUSH bgBrush = CreateSolidBrush(bgBase);
FillRect(memDC, &rcClient, bgBrush);
DeleteObject(bgBrush);
int y = 0;
int itemH = g_tray_itemH;
int sepH = g_tray_sepH;
for (int i=0; i<(int)g_menuItems.size(); i++)
{
auto &it = g_menuItems[i];
if(it.separator)
{
int midY = y + sepH/2;
HPEN oldPen = (HPEN)SelectObject(memDC, CreatePen(PS_SOLID,1,lineColor));
MoveToEx(memDC, 5, midY, nullptr);
LineTo(memDC, rcClient.right-5, midY);
DeleteObject(SelectObject(memDC, oldPen));
y += sepH;
}
else
{
bool hovered = (i == g_hoverIndex);
RECT itemRc = {0, y, rcClient.right, y+itemH};
HBRUSH itemBg = CreateSolidBrush(hovered ? bgHover : bgBase);
FillRect(memDC, &itemRc, itemBg);
DeleteObject(itemBg);
if(it.checked)
{
RECT cbox = { 4, y, 20, y+itemH };
SetTextColor(memDC, txtCheck);
SetBkMode(memDC, TRANSPARENT);
DrawTextW(memDC, L"\u2713", -1, &cbox, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
}
SetBkMode(memDC, TRANSPARENT);
SetTextColor(memDC, txtNormal);
HFONT oldFnt = (HFONT)SelectObject(memDC, g_hMenuFont);
RECT textRc = { 24, y, rcClient.right-5, y+itemH };
DrawTextW(memDC, it.text.c_str(), -1, &textRc, DT_SINGLELINE | DT_VCENTER | DT_LEFT);
SelectObject(memDC, oldFnt);
y += itemH;
}
}
BitBlt(hdc, 0,0, rcClient.right, rcClient.bottom, memDC, 0,0, SRCCOPY);
SelectObject(memDC, oldMemBmp);
DeleteObject(memBmp);
DeleteDC(memDC);
EndPaint(hWnd, &ps);
return 0;
}
case WM_MOUSEMOVE:
{
int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);
int itemH = g_tray_itemH, sepH = g_tray_sepH;
int curY = 0, hover = -1;
for(int i=0; i<(int)g_menuItems.size(); i++)
{
auto &it = g_menuItems[i];
int h = it.separator ? sepH : itemH;
if(!it.separator && yPos>=curY && yPos<(curY+h)) {
hover = i;
break;
}
curY += h;
}
if(hover != g_hoverIndex){
g_hoverIndex = hover;
InvalidateRect(hWnd, nullptr, FALSE);
}
SetCursor(LoadCursor(nullptr, hover!=-1 ? IDC_HAND : IDC_ARROW));
break;
}
case WM_LBUTTONUP:
{
POINT pt; GetCursorPos(&pt);
RECT rc; GetWindowRect(hWnd, &rc);
bool inside = PtInRect(&rc, pt);
if(g_hMouseHook) {
UnhookWindowsHookEx(g_hMouseHook);
g_hMouseHook = nullptr;
}
if(inside && g_hoverIndex >= 0 && g_hoverIndex < (int)g_menuItems.size())
{
auto &it = g_menuItems[g_hoverIndex];
if (!it.separator)
{
PostMessage(g_hWnd, WM_COMMAND, it.id, 0);
}
}
DestroyWindow(hWnd);
break;
}
case WM_DESTROY:
g_hoverIndex = -1;
if (g_hMouseHook) {
UnhookWindowsHookEx(g_hMouseHook);
g_hMouseHook = nullptr;
}
ReleaseCapture();
break;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
void TogglePictureInPicture(HWND hWnd, bool enable)
{
LONG style = GetWindowLong(hWnd, GWL_STYLE);
if(enable) {
g_alwaysOnTop = true;
style &= ~WS_CAPTION;
SetWindowLong(hWnd, GWL_STYLE, style);
SetWindowPos(hWnd, HWND_TOPMOST,0,0,0,0, SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE|SWP_FRAMECHANGED);
} else {
g_alwaysOnTop = false;
style |= WS_CAPTION;
SetWindowLong(hWnd, GWL_STYLE, style);
SetWindowPos(hWnd, HWND_NOTOPMOST,0,0,0,0, SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE|SWP_FRAMECHANGED);
}
g_isPipMode = enable;
if(g_webview) {
nlohmann::json j;
if(enable)
SendToJS("showPictureInPicture", j);
else
SendToJS("hidePictureInPicture", j);
}
}

14
src/tray/tray.h Normal file
View file

@ -0,0 +1,14 @@
#ifndef TRAY_H
#define TRAY_H
#include <windows.h>
void CreateTrayIcon(HWND hWnd);
void RemoveTrayIcon();
void ShowTrayMenu(HWND hWnd);
void LoadCustomMenuFont();
void TogglePictureInPicture(HWND hWnd, bool enable);
#endif // TRAY_H

454
src/ui/mainwindow.cpp Normal file
View file

@ -0,0 +1,454 @@
#include "mainwindow.h"
#include <fstream>
#include <iostream>
#include <windowsx.h>
#include <ShlObj.h>
#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"
// 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<DWORD>(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[])
{
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<argc; ++i){
int size_needed = MultiByteToWideChar(CP_UTF8, 0, argv[i], -1, NULL, 0);
if(size_needed > 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;
}
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->PostWebMessageAsJson(wpayload.c_str());
#ifdef DEBUG_BUILD
std::cout << "[Native->JS] " << payload << "\n";
#endif
}
void HandleEvent(const std::string &ev, std::vector<std::string> &args)
{
if(ev=="mpv-command"){
if(!args.empty() && args[0] == "loadfile" && args.size() > 1) {
args[1] = decodeURIComponent(args[1]);
}
HandleMpvCommand(args);
} else if(ev=="mpv-set-prop"){
HandleMpvSetProp(args);
} else if(ev=="mpv-observe-prop"){
HandleMpvObserveProp(args);
} else if(ev=="app-ready"){
std::cout<<"[Native->JS] APP READY"<<"\n" << std::endl;
g_isAppReady=true;
HideSplash();
PostMessage(g_hWnd, WM_NOTIFY_FLUSH, 0, 0);
} else if(ev=="update-requested"){
RunInstallerAndExit();
} 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_hSplash && !g_waitStarted.exchange(true)){
WaitAndRefreshIfNeeded();
}
}
}
else {
std::cout<<"Unknown event="<<ev<<"\n";
}
}
void HandleInboundJSON(const std::string &msg)
{
try {
std::cout << "[JS -> NATIVE]: " << msg << std::endl;
auto j = nlohmann::json::parse(msg);
int type = 0;
if (j.contains("type") && j["type"].is_number()) {
type = j["type"].get<int>();
}
if (type == 3) {
// 3 = Init event
nlohmann::json root;
root["id"] = 0;
nlohmann::json transportObj;
transportObj["properties"] = {
1,
nlohmann::json::array({0, "shellVersion", 0, APP_VERSION}),
};
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->PostWebMessageAsJson(wpayload.c_str());
return;
}
if (type == 6 && j.contains("method"))
{
std::string methodName = j["method"].get<std::string>();
if (methodName == "handleInboundJSON")
{
if (j["args"].is_array() && !j["args"].empty())
{
std::string ev;
if (j["args"][0].is_string()) {
ev = j["args"][0].get<std::string>();
} else {
ev = "Unknown";
}
std::vector<std::string> 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<std::string>());
else argVec.push_back(x.dump());
}
}
else if (second.is_string()) {
argVec.push_back(second.get<std::string>());
}
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="<<msg<<"\n";
} catch(std::exception &ex) {
std::cerr<<"JSON parse error:"<<ex.what()<<"\n";
}
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_CREATE:
{
HICON hIconBig = LoadIcon(g_hInst, MAKEINTRESOURCE(IDR_MAINFRAME));
HICON hIconSmall = LoadIcon(g_hInst, MAKEINTRESOURCE(IDR_MAINFRAME));
SendMessage(hWnd, WM_SETICON, ICON_BIG, (LPARAM)hIconBig);
SendMessage(hWnd, WM_SETICON, ICON_SMALL, (LPARAM)hIconSmall);
CreateTrayIcon(hWnd);
UpdateTheme(hWnd);
break;
}
case WM_DPICHANGED:
{
RECT* const newRect = reinterpret_cast<RECT*>(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();
}
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");
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<unsigned char> fileBuffer(
(std::istreambuf_iterator<char>(ifs)),
(std::istreambuf_iterator<char>())
);
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://", 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);
}
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;
}

21
src/ui/mainwindow.h Normal file
View file

@ -0,0 +1,21 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <string>
#include <windows.h>
#include "nlohmann/json.hpp"
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
// Helper for single-instance
bool CheckSingleInstance(int argc, char* argv[]);
bool FocusExistingInstance(const std::wstring& protocolArg);
// Our "ToggleFullScreen" logic
void ToggleFullScreen(HWND hWnd, bool enable);
// Webview
void HandleInboundJSON(const std::string &msg);
void SendToJS(const std::string &eventName, const nlohmann::json &eventData);
#endif // MAINWINDOW_H

180
src/ui/splash.cpp Normal file
View file

@ -0,0 +1,180 @@
#include "splash.h"
#include <gdiplus.h>
#include <iostream>
#include "../core/globals.h"
#include "../utils/crashlog.h"
#include "../resource.h"
LRESULT CALLBACK SplashWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_TIMER:
{
const float baseStep = 0.01f;
const float splashSpeed = 1.1f;
float actualStep = baseStep * splashSpeed;
g_splashOpacity += actualStep * g_pulseDirection;
if(g_splashOpacity <= 0.3f) {
g_splashOpacity = 0.3f;
g_pulseDirection = 1;
} else if(g_splashOpacity >= 1.0f) {
g_splashOpacity = 1.0f;
g_pulseDirection = -1;
}
InvalidateRect(hWnd, nullptr, FALSE);
break;
}
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
RECT rc; GetClientRect(hWnd, &rc);
int winW = rc.right - rc.left;
int winH = rc.bottom - rc.top;
HDC memDC = CreateCompatibleDC(hdc);
HBITMAP memBmp = CreateCompatibleBitmap(hdc, winW, winH);
HGDIOBJ oldMemBmp = SelectObject(memDC, memBmp);
HBRUSH bgBrush = CreateSolidBrush(RGB(12, 11, 17));
FillRect(memDC, &rc, bgBrush);
DeleteObject(bgBrush);
if(g_hSplashImage)
{
BITMAP bm;
GetObject(g_hSplashImage, sizeof(bm), &bm);
int imgWidth = bm.bmWidth;
int imgHeight= bm.bmHeight;
int destX = (winW - imgWidth)/2;
int destY = (winH - imgHeight)/2;
HDC imgDC = CreateCompatibleDC(memDC);
HGDIOBJ oldImgBmp = SelectObject(imgDC, g_hSplashImage);
BLENDFUNCTION blend = {};
blend.BlendOp = AC_SRC_OVER;
blend.SourceConstantAlpha = (BYTE)(g_splashOpacity * 255);
blend.AlphaFormat = AC_SRC_ALPHA;
HBITMAP tempBmp = CreateCompatibleBitmap(memDC, imgWidth, imgHeight);
HDC tempDC = CreateCompatibleDC(memDC);
HGDIOBJ oldTempBmp = SelectObject(tempDC, tempBmp);
BitBlt(tempDC, 0, 0, imgWidth, imgHeight, imgDC, 0, 0, SRCCOPY);
AlphaBlend(memDC, destX, destY, imgWidth, imgHeight, tempDC, 0, 0, imgWidth, imgHeight, blend);
SelectObject(tempDC, oldTempBmp);
DeleteObject(tempBmp);
DeleteDC(tempDC);
SelectObject(imgDC, oldImgBmp);
DeleteDC(imgDC);
}
BitBlt(hdc, 0,0, winW, winH, memDC, 0,0, SRCCOPY);
SelectObject(memDC, oldMemBmp);
DeleteObject(memBmp);
DeleteDC(memDC);
EndPaint(hWnd, &ps);
break;
}
case WM_DESTROY:
KillTimer(hWnd, 1);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
void CreateSplashScreen(HWND parent)
{
WNDCLASSEXW splashWcex = {0};
splashWcex.cbSize = sizeof(WNDCLASSEXW);
splashWcex.lpfnWndProc = SplashWndProc;
splashWcex.hInstance = g_hInst;
splashWcex.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
splashWcex.lpszClassName = L"SplashScreenClass";
RegisterClassExW(&splashWcex);
RECT rcClient;
GetClientRect(parent, &rcClient);
int width = rcClient.right - rcClient.left;
int height = rcClient.bottom - rcClient.top;
g_hSplash = CreateWindowExW(
0,
L"SplashScreenClass",
nullptr,
WS_CHILD | WS_VISIBLE,
0, 0, width, height,
parent,
nullptr,
g_hInst,
nullptr
);
if(!g_hSplash) {
DWORD errorCode = GetLastError();
std::string errorMessage = "[SPLASH]: Failed to create splash. Error=" + std::to_string(errorCode);
std::cerr << errorMessage << "\n";
AppendToCrashLog(errorMessage);
return;
}
HRSRC hRes = FindResource(g_hInst, MAKEINTRESOURCE(IDR_SPLASH_PNG), RT_RCDATA);
if(!hRes) {
std::cerr << "Could not find PNG resource.\n";
} else {
HGLOBAL hData = LoadResource(g_hInst, hRes);
DWORD size = SizeofResource(g_hInst, hRes);
void* pData = LockResource(hData);
if(!pData) {
std::cerr << "LockResource returned null.\n";
} else {
IStream* pStream = nullptr;
if(CreateStreamOnHGlobal(nullptr, TRUE, &pStream) == S_OK)
{
ULONG written = 0;
pStream->Write(pData, size, &written);
LARGE_INTEGER liZero = {};
pStream->Seek(liZero, STREAM_SEEK_SET, nullptr);
Gdiplus::Bitmap bitmap(pStream);
if(bitmap.GetLastStatus()==Gdiplus::Ok)
{
HBITMAP hBmp = NULL;
if(bitmap.GetHBITMAP(Gdiplus::Color(0,0,0,0), &hBmp) == Gdiplus::Ok) {
g_hSplashImage = hBmp;
} else {
std::cerr << "Failed to create HBITMAP from embedded PNG.\n";
}
} else {
std::cerr << "Failed to decode embedded PNG data.\n";
}
pStream->Release();
}
}
}
SetTimer(g_hSplash, 1, 4, nullptr);
SetWindowPos(g_hSplash, HWND_TOP, 0, 0, width, height, SWP_SHOWWINDOW);
InvalidateRect(g_hSplash, nullptr, TRUE);
}
void HideSplash()
{
if(g_hSplash) {
KillTimer(g_hSplash, 1);
DestroyWindow(g_hSplash);
g_hSplash = nullptr;
}
if(g_hSplashImage) {
DeleteObject(g_hSplashImage);
g_hSplashImage = nullptr;
}
}

11
src/ui/splash.h Normal file
View file

@ -0,0 +1,11 @@
#ifndef SPLASH_H
#define SPLASH_H
#include <windows.h>
void CreateSplashScreen(HWND parent);
void HideSplash();
LRESULT CALLBACK SplashWndProc(HWND, UINT, WPARAM, LPARAM);
#endif // SPLASH_H

283
src/updater/updater.cpp Normal file
View file

@ -0,0 +1,283 @@
#include "updater.h"
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/sha.h>
#include <curl/curl.h>
#include "../core/globals.h"
#include "../utils/crashlog.h"
#include "../utils/helpers.h"
#include "../node/server.h"
#include <fstream>
#include <sstream>
#include <iomanip>
#include <iostream>
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp)
{
std::string* s = reinterpret_cast<std::string*>(userp);
s->append(reinterpret_cast<char*>(contents), size * nmemb);
return size * nmemb;
}
// Download to string
static bool DownloadString(const std::string& url, std::string& outData)
{
CURL* curl = curl_easy_init();
if(!curl) return false;
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &outData);
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
return res == CURLE_OK;
}
// Download to file
static bool DownloadFile(const std::string& url, const std::filesystem::path& dest)
{
CURL* curl = curl_easy_init();
if(!curl) return false;
FILE* fp = _wfopen(dest.c_str(), L"wb");
if(!fp){
curl_easy_cleanup(curl);
return false;
}
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
CURLcode res = curl_easy_perform(curl);
fclose(fp);
curl_easy_cleanup(curl);
return (res == CURLE_OK);
}
// Compute sha256
static std::string FileChecksum(const std::filesystem::path& filepath)
{
std::ifstream file(filepath, std::ios::binary);
if(!file) return "";
SHA256_CTX ctx;
SHA256_Init(&ctx);
char buf[4096];
while(file.read(buf, sizeof(buf))) {
SHA256_Update(&ctx, buf, file.gcount());
}
if(file.gcount()>0) {
SHA256_Update(&ctx, buf, file.gcount());
}
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256_Final(hash, &ctx);
std::ostringstream oss;
for(int i=0; i<SHA256_DIGEST_LENGTH; ++i)
oss << std::hex << std::setw(2) << std::setfill('0') << (int)hash[i];
return oss.str();
}
static bool VerifySignature(const std::string& data, const std::string& signatureBase64)
{
// Load public key from embedded PEM
BIO* bio = BIO_new_mem_buf(public_key_pem, -1);
EVP_PKEY* pubKey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr);
BIO_free(bio);
if(!pubKey) return false;
// remove whitespace
std::string cleanedSig;
for(char c : signatureBase64){
if(!isspace((unsigned char)c)) {
cleanedSig.push_back(c);
}
}
// base64 decode
BIO* b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
BIO* bmem = BIO_new_mem_buf(cleanedSig.data(), (int)cleanedSig.size());
bmem = BIO_push(b64, bmem);
std::vector<unsigned char> signature(512);
int sig_len = BIO_read(bmem, signature.data(), (int)signature.size());
BIO_free_all(bmem);
if(sig_len <= 0) {
EVP_PKEY_free(pubKey);
return false;
}
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
EVP_PKEY_CTX* pctx = nullptr;
bool result = false;
if(EVP_DigestVerifyInit(ctx, &pctx, EVP_sha256(), NULL, pubKey)==1){
if(EVP_DigestVerifyUpdate(ctx, data.data(), data.size())==1){
result = (EVP_DigestVerifyFinal(ctx, signature.data(), sig_len)==1);
}
}
EVP_MD_CTX_free(ctx);
EVP_PKEY_free(pubKey);
return result;
}
void RunAutoUpdaterOnce()
{
g_updaterRunning = true;
std::cout<<"Checking for Updates.\n";
std::string versionContent;
if(!DownloadString(g_updateUrl, versionContent)) {
AppendToCrashLog("[UPDATER]: Failed to download version.json");
return;
}
nlohmann::json versionJson;
try {
versionJson = nlohmann::json::parse(versionContent);
} catch(...) { return; }
std::string versionDescUrl = versionJson["versionDesc"].get<std::string>();
std::string signatureBase64 = versionJson["signature"].get<std::string>();
// get version-details
std::string detailsContent;
if(!DownloadString(versionDescUrl, detailsContent)) {
AppendToCrashLog("[UPDATER]: Failed to download version details");
return;
}
if(!VerifySignature(detailsContent, signatureBase64)) {
AppendToCrashLog("[UPDATER]: Signature verification failed");
return;
}
nlohmann::json detailsJson;
try {
detailsJson = nlohmann::json::parse(detailsContent);
} catch(...) { return; }
// Compare shellVersion
std::string remoteShellVersion = detailsJson["shellVersion"].get<std::string>();
bool needsFullUpdate = (remoteShellVersion != APP_VERSION);
auto files = detailsJson["files"];
std::vector<std::string> partialUpdateKeys = { "server.js" };
// prepare temp dir
wchar_t buf[MAX_PATH];
GetTempPathW(MAX_PATH, buf);
std::filesystem::path tempDir = std::filesystem::path(buf) / L"stremio_updater";
std::filesystem::create_directories(tempDir);
// handle full update
if(needsFullUpdate || g_autoupdaterForceFull) {
bool allDownloadsSuccessful = true;
// check architecture
std::string key = "windows";
SYSTEM_INFO systemInfo;
GetNativeSystemInfo(&systemInfo);
if(systemInfo.wProcessorArchitecture==PROCESSOR_ARCHITECTURE_AMD64){
key = "windows-x64";
} else if(systemInfo.wProcessorArchitecture==PROCESSOR_ARCHITECTURE_INTEL) {
key = "windows-x86";
}
if(files.contains(key) && files[key].contains("url") && files[key].contains("checksum")) {
std::string url = files[key]["url"].get<std::string>();
std::string expectedChecksum = files[key]["checksum"].get<std::string>();
std::string filename = url.substr(url.find_last_of('/') + 1);
std::filesystem::path installerPath = tempDir / std::wstring(filename.begin(), filename.end());
if(std::filesystem::exists(installerPath)) {
if(FileChecksum(installerPath) != expectedChecksum) {
std::filesystem::remove(installerPath);
if(!DownloadFile(url, installerPath)) {
AppendToCrashLog("[UPDATER]: Failed to re-download installer");
allDownloadsSuccessful = false;
}
}
} else {
if(!DownloadFile(url, installerPath)) {
AppendToCrashLog("[UPDATER]: Failed to download installer");
allDownloadsSuccessful = false;
}
}
if(FileChecksum(installerPath) != expectedChecksum) {
AppendToCrashLog("[UPDATER]: Installer file corrupted: " + installerPath.string());
allDownloadsSuccessful = false;
}
if(allDownloadsSuccessful) {
g_installerPath = installerPath;
}
} else {
allDownloadsSuccessful = false;
}
if(allDownloadsSuccessful) {
std::cout<<"Full update needed!\n";
nlohmann::json j;
j["type"] = "requestUpdate";
g_outboundMessages.push_back(j);
PostMessage(g_hWnd, WM_NOTIFY_FLUSH, 0, 0);
} else {
std::cout<<"Installer download failed. Skipping update prompt.\n";
}
}
// partial update
if(!needsFullUpdate) {
std::wstring exeDir;
{
wchar_t pathBuf[MAX_PATH];
GetModuleFileNameW(nullptr, pathBuf, MAX_PATH);
exeDir = pathBuf;
size_t pos = exeDir.find_last_of(L"\\/");
if(pos!=std::wstring::npos) exeDir.erase(pos);
}
for(const auto& key : partialUpdateKeys) {
if(files.contains(key) && files[key].contains("url") && files[key].contains("checksum")) {
std::string url = files[key]["url"].get<std::string>();
std::string expectedChecksum = files[key]["checksum"].get<std::string>();
std::filesystem::path localFilePath = std::filesystem::path(exeDir) / std::wstring(key.begin(), key.end());
if(std::filesystem::exists(localFilePath)) {
if(FileChecksum(localFilePath) == expectedChecksum) {
continue; // no update needed
}
}
if(!DownloadFile(url, localFilePath)) {
AppendToCrashLog("[UPDATER]: Failed to download partial file " + key);
} else {
if(FileChecksum(localFilePath) != expectedChecksum) {
AppendToCrashLog("[UPDATER]: Downloaded file corrupted " + localFilePath.string());
continue;
}
if(key=="server.js") {
StopNodeServer();
StartNodeServer();
}
}
}
}
}
std::cout<<"[UPDATER]: Update check done!\n";
}
void RunInstallerAndExit()
{
if(g_installerPath.empty()) {
AppendToCrashLog("[UPDATER]: Installer path not set.");
return;
}
// pass /overrideInstallDir
wchar_t exeDir[MAX_PATH];
GetModuleFileNameW(nullptr, exeDir, MAX_PATH);
std::wstring dir(exeDir);
size_t pos = dir.find_last_of(L"\\/");
if(pos!=std::wstring::npos) {
dir.erase(pos);
}
std::wstring arguments = L"/overrideInstallDir=\"" + dir + L"\"";
HINSTANCE result = ShellExecuteW(nullptr, L"open", g_installerPath.c_str(), arguments.c_str(), nullptr, SW_HIDE);
if ((INT_PTR)result <= 32) {
AppendToCrashLog(L"[UPDATER]: Failed to start installer via ShellExecute.");
}
PostQuitMessage(0);
exit(0);
}

7
src/updater/updater.h Normal file
View file

@ -0,0 +1,7 @@
#ifndef UPDATER_H
#define UPDATER_H
void RunAutoUpdaterOnce();
void RunInstallerAndExit();
#endif // UPDATER_H

154
src/utils/config.cpp Normal file
View file

@ -0,0 +1,154 @@
#include <windows.h>
#include <string>
#include "config.h"
#include <sstream>
#include "../core/globals.h"
#include "../utils/helpers.h"
// Return the path to "portable_config/stremio-settings.ini"
static std::wstring GetIniPath()
{
std::wstring exeDir = GetExeDirectory();
std::wstring pcDir = exeDir + L"\\portable_config";
CreateDirectoryW(pcDir.c_str(), nullptr); // ensure it exists
return pcDir + L"\\stremio-settings.ini";
}
void LoadSettings()
{
std::wstring iniPath = GetIniPath();
wchar_t buffer[16];
GetPrivateProfileStringW(L"General", L"CloseOnExit", L"0", buffer, _countof(buffer), iniPath.c_str());
g_closeOnExit = (wcscmp(buffer, L"1") == 0);
GetPrivateProfileStringW(L"General", L"UseDarkTheme", L"1", buffer, _countof(buffer), iniPath.c_str());
g_useDarkTheme = (wcscmp(buffer, L"1") == 0);
g_thumbFastHeight = GetPrivateProfileIntW(L"General", L"ThumbFastHeight", 0, iniPath.c_str());
g_allowZoom = GetPrivateProfileIntW(L"General", L"AllowZoom", 0, iniPath.c_str());
g_pauseOnMinimize = (GetPrivateProfileIntW(L"General", L"PauseOnMinimize", 1, iniPath.c_str()) == 1);
g_pauseOnLostFocus = (GetPrivateProfileIntW(L"General", L"PauseOnLostFocus", 0, iniPath.c_str()) == 1);
}
void SaveSettings()
{
std::wstring iniPath = GetIniPath();
const wchar_t* closeVal = g_closeOnExit ? L"1" : L"0";
const wchar_t* darkVal = g_useDarkTheme ? L"1" : L"0";
const wchar_t* pauseMinVal = g_pauseOnMinimize ? L"1" : L"0";
const wchar_t* pauseFocVal = g_pauseOnLostFocus ? L"1" : L"0";
const wchar_t* allowZoomVal = g_allowZoom ? L"1" : L"0";
WritePrivateProfileStringW(L"General", L"CloseOnExit", closeVal, iniPath.c_str());
WritePrivateProfileStringW(L"General", L"UseDarkTheme", darkVal, iniPath.c_str());
WritePrivateProfileStringW(L"General", L"PauseOnMinimize", pauseMinVal, iniPath.c_str());
WritePrivateProfileStringW(L"General", L"PauseOnLostFocus", pauseFocVal, iniPath.c_str());
WritePrivateProfileStringW(L"General", L"AllowZoom", allowZoomVal, iniPath.c_str());
}
static void WriteIntToIni(const std::wstring &section, const std::wstring &key, int value, const std::wstring &iniPath)
{
std::wstringstream ws;
ws << value;
WritePrivateProfileStringW(section.c_str(), key.c_str(), ws.str().c_str(), iniPath.c_str());
}
// This helper reads an integer from the .ini, returning `defaultVal` if not found
static int ReadIntFromIni(const std::wstring &section, const std::wstring &key, int defaultVal, const std::wstring &iniPath)
{
return GetPrivateProfileIntW(section.c_str(), key.c_str(), defaultVal, iniPath.c_str());
}
/**
* Optional helper:
* Check if the given rectangle is on a valid monitor.
* If completely off-screen, we re-center it on the primary monitor so the user sees it.
*/
static void EnsureRectOnScreen(RECT &rc)
{
HMONITOR hMon = MonitorFromRect(&rc, MONITOR_DEFAULTTONULL);
if(hMon)
{
return;
}
MONITORINFO mi = {0};
mi.cbSize = sizeof(mi);
HMONITOR hPrimary = MonitorFromPoint({0,0}, MONITOR_DEFAULTTOPRIMARY);
if(!hPrimary || !GetMonitorInfoW(hPrimary, &mi)) {
return;
}
int width = rc.right - rc.left;
int height = rc.bottom - rc.top;
int workWidth = mi.rcWork.right - mi.rcWork.left;
int workHeight = mi.rcWork.bottom - mi.rcWork.top;
// Center the rect in the primary monitor's WORK area
int newLeft = mi.rcWork.left + (workWidth - width)/2;
int newTop = mi.rcWork.top + (workHeight - height)/2;
rc.left = newLeft;
rc.top = newTop;
rc.right = newLeft + width;
rc.bottom = newTop + height;
}
/**
* SaveWindowPlacement:
* Writes showCmd (normal vs. maximized) and the normal window rectangle
* into the [Window] section of our .ini.
*/
void SaveWindowPlacement(const WINDOWPLACEMENT &wp)
{
std::wstring iniPath = GetIniPath();
WriteIntToIni(L"Window", L"ShowCmd", (int)wp.showCmd, iniPath);
WriteIntToIni(L"Window", L"Left", wp.rcNormalPosition.left, iniPath);
WriteIntToIni(L"Window", L"Top", wp.rcNormalPosition.top, iniPath);
WriteIntToIni(L"Window", L"Right", wp.rcNormalPosition.right, iniPath);
WriteIntToIni(L"Window", L"Bottom", wp.rcNormalPosition.bottom, iniPath);
}
/**
* LoadWindowPlacement:
* Reads ShowCmd + window rectangle from [Window].
* If not found or incomplete, returns false => use default behavior.
* If found, also ensures the rect is at least partially visible on a monitor.
*/
bool LoadWindowPlacement(WINDOWPLACEMENT &wp)
{
wp.length = sizeof(wp);
wp.flags = 0;
std::wstring iniPath = GetIniPath();
// We default to SW_SHOWNORMAL if not found
int showCmd = ReadIntFromIni(L"Window", L"ShowCmd", SW_SHOWNORMAL, iniPath);
wp.showCmd = (UINT)showCmd;
// If any of these are -1 => means not found
int left = ReadIntFromIni(L"Window", L"Left", -1, iniPath);
int top = ReadIntFromIni(L"Window", L"Top", -1, iniPath);
int right = ReadIntFromIni(L"Window", L"Right", -1, iniPath);
int bottom = ReadIntFromIni(L"Window", L"Bottom", -1, iniPath);
if(left == -1 || top == -1 || right == -1 || bottom == -1)
{
// No saved geometry
return false;
}
wp.rcNormalPosition.left = left;
wp.rcNormalPosition.top = top;
wp.rcNormalPosition.right = right;
wp.rcNormalPosition.bottom = bottom;
// Edge-case: if user had it on a disconnected monitor => re-center
EnsureRectOnScreen(wp.rcNormalPosition);
return true;
}

8
src/utils/config.h Normal file
View file

@ -0,0 +1,8 @@
#ifndef CONFIG_H
#define CONFIG_H
void LoadSettings();
void SaveSettings();
void SaveWindowPlacement(const WINDOWPLACEMENT &wp);
bool LoadWindowPlacement(WINDOWPLACEMENT &wp);
#endif // CONFIG_H

63
src/utils/crashlog.cpp Normal file
View file

@ -0,0 +1,63 @@
#include "crashlog.h"
#include <fstream>
#include <iomanip>
#include <ctime>
#include "../core/globals.h"
#include "../mpv/player.h"
#include "../node/server.h"
#include "../tray/tray.h"
#include "../utils/helpers.h"
#include <gdiplus.h>
#include <sstream>
static std::wstring GetDailyCrashLogPath()
{
std::time_t t = std::time(nullptr);
std::tm localTime;
localtime_s(&localTime, &t);
std::wstringstream filename;
filename << L"\\errors-"
<< localTime.tm_mday << L"."
<< (localTime.tm_mon + 1) << L"."
<< (localTime.tm_year + 1900) << L".txt";
std::wstring exeDir = GetExeDirectory();
std::wstring pcDir = exeDir + L"\\portable_config";
return pcDir + filename.str();
}
void AppendToCrashLog(const std::wstring& message)
{
std::wofstream logFile;
logFile.open(GetDailyCrashLogPath(), std::ios::app);
if(!logFile.is_open()) {
return;
}
std::time_t t = std::time(nullptr);
std::tm localTime;
localtime_s(&localTime, &t);
logFile << L"[" << std::put_time(&localTime, L"%H:%M:%S") << L"] "
<< message << std::endl;
}
void AppendToCrashLog(const std::string& message)
{
std::wstring wmsg(message.begin(), message.end());
AppendToCrashLog(wmsg);
}
void Cleanup()
{
// Shut down mpv
CleanupMPV();
// Shut down Node
StopNodeServer();
// Remove tray icon
RemoveTrayIcon();
// GDI+ cleanup
if(g_gdiplusToken) {
Gdiplus::GdiplusShutdown(g_gdiplusToken);
}
}

10
src/utils/crashlog.h Normal file
View file

@ -0,0 +1,10 @@
#ifndef CRASHLOG_H
#define CRASHLOG_H
#include <string>
void AppendToCrashLog(const std::wstring& message);
void AppendToCrashLog(const std::string& message);
void Cleanup();
#endif // CRASHLOG_H

112
src/utils/helpers.cpp Normal file
View file

@ -0,0 +1,112 @@
#include "helpers.h"
#include <tlhelp32.h>
#include <iostream>
std::string WStringToUtf8(const std::wstring &wstr)
{
if (wstr.empty()) {
return {};
}
int neededSize = WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), nullptr, 0, nullptr, nullptr);
if (neededSize <= 0) {
return {};
}
std::string result(neededSize, '\0');
WideCharToMultiByte(CP_UTF8, 0, wstr.data(), (int)wstr.size(), &result[0], neededSize, nullptr, nullptr);
// remove trailing null
while(!result.empty() && result.back()=='\0') {
result.pop_back();
}
return result;
}
std::wstring Utf8ToWstring(const std::string& utf8Str)
{
if (utf8Str.empty()) {
return std::wstring();
}
int size_needed = MultiByteToWideChar(CP_UTF8, 0, utf8Str.data(), (int)utf8Str.size(), NULL, 0);
if (size_needed == 0) {
return std::wstring();
}
std::wstring wstr(size_needed, 0);
MultiByteToWideChar(CP_UTF8, 0, utf8Str.data(), (int)utf8Str.size(), &wstr[0], size_needed);
return wstr;
}
bool FileExists(const std::wstring& path)
{
DWORD attributes = GetFileAttributesW(path.c_str());
return (attributes != INVALID_FILE_ATTRIBUTES &&
!(attributes & FILE_ATTRIBUTE_DIRECTORY));
}
bool DirectoryExists(const std::wstring& dirPath)
{
DWORD attributes = GetFileAttributesW(dirPath.c_str());
return (attributes != INVALID_FILE_ATTRIBUTES &&
(attributes & FILE_ATTRIBUTE_DIRECTORY));
}
bool IsDuplicateProcessRunning(const std::vector<std::wstring>& targetProcesses)
{
DWORD currentPid = GetCurrentProcessId();
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
return false;
}
PROCESSENTRY32W processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32W);
if (!Process32FirstW(hSnapshot, &processEntry)) {
CloseHandle(hSnapshot);
return false;
}
do {
if (processEntry.th32ProcessID == currentPid) {
continue;
}
std::wstring exeName(processEntry.szExeFile);
for (const auto& target : targetProcesses) {
if (_wcsicmp(exeName.c_str(), target.c_str()) == 0) {
CloseHandle(hSnapshot);
return true;
}
}
} while (Process32NextW(hSnapshot, &processEntry));
CloseHandle(hSnapshot);
return false;
}
// For local files
std::string decodeURIComponent(const std::string& encoded) {
std::string result;
result.reserve(encoded.size());
for (size_t i = 0; i < encoded.size(); ++i) {
char c = encoded[i];
if (c == '%' && i + 2 < encoded.size() &&
std::isxdigit(static_cast<unsigned char>(encoded[i + 1])) &&
std::isxdigit(static_cast<unsigned char>(encoded[i + 2]))) {
// Convert the two hex digits to a character
std::string hex = encoded.substr(i + 1, 2);
char decodedChar = static_cast<char>(std::strtol(hex.c_str(), nullptr, 16));
result.push_back(decodedChar);
i += 2;
} else {
result.push_back(c);
}
}
return result;
}
std::wstring GetExeDirectory()
{
wchar_t buf[MAX_PATH];
GetModuleFileNameW(nullptr,buf,MAX_PATH);
std::wstring path(buf);
size_t pos=path.find_last_of(L"\\/");
if(pos!=std::wstring::npos)
path.erase(pos);
return path;
}

17
src/utils/helpers.h Normal file
View file

@ -0,0 +1,17 @@
#ifndef HELPERS_H
#define HELPERS_H
#include <windows.h>
#include <string>
#include <vector>
#include <filesystem>
std::string WStringToUtf8(const std::wstring &wstr);
std::wstring Utf8ToWstring(const std::string& utf8Str);
std::string decodeURIComponent(const std::string& encoded);
std::wstring GetExeDirectory();
bool FileExists(const std::wstring& path);
bool DirectoryExists(const std::wstring& dirPath);
bool IsDuplicateProcessRunning(const std::vector<std::wstring>& targetProcesses);
#endif // HELPERS_H

496
src/webview/webview.cpp Normal file
View file

@ -0,0 +1,496 @@
#include "webview.h"
#include <string>
#include <vector>
#include <thread>
#include <cmath>
#include <iostream>
#include <Shlwapi.h>
#include <wrl.h>
#include "../core/globals.h"
#include "../utils/crashlog.h"
#include "../utils/helpers.h"
#include "../ui/mainwindow.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 = {
event: "app-error",
reason: "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";
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; attempt<maxAttempts; ++attempt)
{
int waitTime = (int)(initialWaitTime * pow(1.25, attempt));
if(waitTime>maxWaitTime) 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<ICoreWebView2EnvironmentOptions> options = Microsoft::WRL::Make<CoreWebView2EnvironmentOptions>();
if(options){
options->put_AdditionalBrowserArguments(
L"--autoplay-policy=no-user-gesture-required --disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection"
);
Microsoft::WRL::ComPtr<ICoreWebView2EnvironmentOptions6> options6;
if(SUCCEEDED(options.As(&options6))) {
options6->put_AreBrowserExtensionsEnabled(TRUE);
}
Microsoft::WRL::ComPtr<ICoreWebView2EnvironmentOptions5> 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<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
[hWnd](HRESULT res, ICoreWebView2Environment* env)->HRESULT
{
if(!env) return E_FAIL;
env->CreateCoreWebView2Controller(
hWnd,
Microsoft::WRL::Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
[hWnd](HRESULT result, ICoreWebView2Controller* rawController)->HRESULT
{
if (FAILED(result) || !rawController) return E_FAIL;
std::cout << "[WEBVIEW]: Initializing WebView..." << std::endl;
wil::com_ptr<ICoreWebView2Controller> m_webviewController = rawController;
if (!m_webviewController) return E_FAIL;
g_webviewController = m_webviewController.try_query<ICoreWebView2Controller4>();
if (!g_webviewController) return E_FAIL;
wil::com_ptr<ICoreWebView2> coreWebView;
g_webviewController->get_CoreWebView2(&coreWebView);
g_webview = coreWebView.try_query<ICoreWebView2_21>();
if (!g_webview) return E_FAIL;
wil::com_ptr<ICoreWebView2Profile> webView2Profile;
g_webview->get_Profile(&webView2Profile);
g_webviewProfile = webView2Profile.try_query<ICoreWebView2Profile8>();
if (!g_webviewProfile) return E_FAIL;
wil::com_ptr<ICoreWebView2Settings> webView2Settings;
g_webview->get_Settings(&webView2Settings);
auto settings = webView2Settings.try_query<ICoreWebView2Settings8>();
if (!settings) return E_FAIL;
if(settings) {
#ifndef DEBUG_BUILD
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);
SetupExtensions();
SetupWebMessageHandler();
std::wcout << L"[WEBVIEW]: Navigating to " << g_webuiUrl << std::endl;
g_webview->Navigate(g_webuiUrl.c_str());
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<ICoreWebView2NavigationCompletedEventHandler>(
[](ICoreWebView2* sender, ICoreWebView2NavigationCompletedEventArgs* args)->HRESULT
{
BOOL isSuccess;
args->get_IsSuccess(&isSuccess);
if(isSuccess) {
std::cout<<"[WEBVIEW]: Navigation Complete - Success\n";
sender->ExecuteScript(EXEC_SHELL_SCRIPT, nullptr);
} else {
std::cout<<"[WEBVIEW]: Navigation failed\n";
if(g_hSplash && !g_waitStarted.exchange(true)) {
WaitAndRefreshIfNeeded();
}
}
return S_OK;
}).Get(),
&navToken
);
EventRegistrationToken contentToken;
g_webview->add_ContentLoading(
Microsoft::WRL::Callback<ICoreWebView2ContentLoadingEventHandler>(
[](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<ICoreWebView2DOMContentLoadedEventHandler>(
[](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<ICoreWebView2ContextMenuRequestedEventHandler>(
[](ICoreWebView2* sender, ICoreWebView2ContextMenuRequestedEventArgs* args) -> HRESULT {
wil::com_ptr<ICoreWebView2ContextMenuItemCollection> items;
HRESULT hr = args->get_MenuItems(&items);
if (FAILED(hr) || !items) {
return hr;
}
#ifdef DEBUG_BUILD
return S_OK; //DEV TOOLS DEBUG ONLY
#endif
wil::com_ptr<ICoreWebView2ContextMenuTarget> target;
hr = args->get_ContextMenuTarget(&target);
BOOL isEditable = FALSE;
if (SUCCEEDED(hr) && target) {
hr = target->get_IsEditable(&isEditable);
}
if (FAILED(hr)) {
return hr;
}
UINT count = 0;
items->get_Count(&count);
if (!isEditable) {
while(count > 0) {
wil::com_ptr<ICoreWebView2ContextMenuItem> item;
items->GetValueAtIndex(0, &item);
if(item) {
items->RemoveValueAtIndex(0);
}
items->get_Count(&count);
}
return S_OK;
}
// Define allowed command IDs for filtering
std::set<INT32> allowedCommandIds = {
50151, // Cut
50150, // Copy
50152, // Paste
50157, // Paste as plain text
50156 // Select all
};
for (UINT i = 0; i < count; )
{
wil::com_ptr<ICoreWebView2ContextMenuItem> item;
hr = items->GetValueAtIndex(i, &item);
if (FAILED(hr) || !item) {
++i;
continue;
}
INT32 commandId = 0;
hr = item->get_CommandId(&commandId);
if (FAILED(hr)) {
++i;
continue;
}
// If the commandId is not in the allowed list, remove the item
if (allowedCommandIds.find(commandId) == allowedCommandIds.end()) {
hr = items->RemoveValueAtIndex(i);
if (FAILED(hr)) {
std::wcerr << L"Failed to remove item at index " << i << std::endl;
return hr;
}
// After removal, the collection size reduces, so update count and don't increment i
items->get_Count(&count);
continue;
}
++i;
}
return S_OK;
}
).Get(),
&contextMenuToken
);
EventRegistrationToken msgToken;
g_webview->add_WebMessageReceived(
Microsoft::WRL::Callback<ICoreWebView2WebMessageReceivedEventHandler>(
[](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<ICoreWebView2NewWindowRequestedEventHandler>(
[](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);
json j;
j["type"] = "FileDropped";
j["path"] = utf8FilePath;
SendToJS("FileDropped", j);
return S_OK;
}
// For non-file URIs, open externally
ShellExecuteW(nullptr, L"open", uri.get(), nullptr, nullptr, SW_SHOWNORMAL);
}
return S_OK;
}
).Get(),
&newWindowToken
);
// FullScreen
EventRegistrationToken cfeToken;
g_webview->add_ContainsFullScreenElementChanged(
Microsoft::WRL::Callback<ICoreWebView2ContainsFullScreenElementChangedEventHandler>(
[](ICoreWebView2* sender, IUnknown*){
BOOL inFull = FALSE;
sender->get_ContainsFullScreenElement(&inFull);
g_isFullscreen = inFull;
// Toggle here or in WndProc
PostMessage(g_hWnd, WM_NOTIFY_FLUSH, 0, 0);
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()) {
HRESULT hr = g_webviewProfile->AddBrowserExtension(
entry.path().wstring().c_str(),
Microsoft::WRL::Callback<ICoreWebView2ProfileAddBrowserExtensionCompletedHandler>(
[extPath=entry.path().wstring()](HRESULT result, ICoreWebView2BrowserExtension* extension)->HRESULT
{
if(SUCCEEDED(result)) {
std::wcout<<L"[EXTENSIONS]: Added extension "<<extPath<<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";
}
}
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<ICoreWebView2ClearBrowsingDataCompletedHandler>(
[](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();
}
}

13
src/webview/webview.h Normal file
View file

@ -0,0 +1,13 @@
#ifndef WEBVIEW_H
#define WEBVIEW_H
#include <windows.h>
#include <string>
void InitWebView2(HWND hWnd);
void WaitAndRefreshIfNeeded();
void refreshWeb(bool refreshAll);
static void SetupWebMessageHandler();
static void SetupExtensions();
#endif // WEBVIEW_H

Binary file not shown.