mirror of
https://github.com/Zaarrg/stremio-community-v5.git
synced 2026-04-21 00:52:01 +00:00
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:
parent
37a20b1fe7
commit
1976366f44
26 changed files with 2969 additions and 2912 deletions
|
|
@ -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
90
src/core/globals.cpp
Normal 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
144
src/core/globals.h
Normal 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
|
||||
2990
src/main.cpp
2990
src/main.cpp
File diff suppressed because it is too large
Load diff
277
src/mpv/player.cpp
Normal file
277
src/mpv/player.cpp
Normal 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
21
src/mpv/player.h
Normal 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
128
src/node/server.cpp
Normal 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
7
src/node/server.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#ifndef SERVER_H
|
||||
#define SERVER_H
|
||||
|
||||
bool StartNodeServer();
|
||||
void StopNodeServer();
|
||||
|
||||
#endif // SERVER_H
|
||||
|
|
@ -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
344
src/tray/tray.cpp
Normal 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
14
src/tray/tray.h
Normal 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
454
src/ui/mainwindow.cpp
Normal 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
21
src/ui/mainwindow.h
Normal 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
180
src/ui/splash.cpp
Normal 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
11
src/ui/splash.h
Normal 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
283
src/updater/updater.cpp
Normal 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
7
src/updater/updater.h
Normal 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
154
src/utils/config.cpp
Normal 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 §ion, 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 §ion, 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
8
src/utils/config.h
Normal 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
63
src/utils/crashlog.cpp
Normal 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
10
src/utils/crashlog.h
Normal 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
112
src/utils/helpers.cpp
Normal 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
17
src/utils/helpers.h
Normal 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
496
src/webview/webview.cpp
Normal 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
13
src/webview/webview.h
Normal 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
|
||||
BIN
utils/chocolatey/stremio-desktop-v5.5.0.14.nupkg
Normal file
BIN
utils/chocolatey/stremio-desktop-v5.5.0.14.nupkg
Normal file
Binary file not shown.
Loading…
Reference in a new issue