From 804e6cf1cd51c42eff30466fb94bf9746355e892 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Fri, 3 Oct 2025 11:36:11 +0100 Subject: [PATCH] chore: implement custom url handler for edge to serve assets --- src/ModMan/main.cpp | 15 +- .../edge/CustomProtocolHandlerEdge.cpp | 162 ++++++++++++++++++ .../webview/edge/CustomProtocolHandlerEdge.h | 18 ++ .../webview/gtk/CustomProtocolHandlerGtk.cpp | 6 + .../webview/gtk/CustomProtocolHandlerGtk.h | 18 ++ .../build/HeaderTransformationPlugin.ts | 67 +++----- 6 files changed, 234 insertions(+), 52 deletions(-) create mode 100644 src/ModMan/webview/edge/CustomProtocolHandlerEdge.cpp create mode 100644 src/ModMan/webview/edge/CustomProtocolHandlerEdge.h create mode 100644 src/ModMan/webview/gtk/CustomProtocolHandlerGtk.cpp create mode 100644 src/ModMan/webview/gtk/CustomProtocolHandlerGtk.h diff --git a/src/ModMan/main.cpp b/src/ModMan/main.cpp index d7b5b3af..ae58b638 100644 --- a/src/ModMan/main.cpp +++ b/src/ModMan/main.cpp @@ -1,9 +1,10 @@ -#include "ui/modmanui.h" - -#pragma warning(push, 0) +#pragma warning(push, 0) #include "webview/webview.h" #pragma warning(pop) +#include "webview/edge/CustomProtocolHandlerEdge.h" +#include "webview/gtk/CustomProtocolHandlerGtk.h" + #include #include #include @@ -20,8 +21,8 @@ int main() { long count = 0; webview::webview w(true, nullptr); - w.set_title("Basic Example"); - w.set_size(480, 320, WEBVIEW_HINT_NONE); + w.set_title("OpenAssetTools ModMan"); + w.set_size(480, 320, WEBVIEW_HINT_MIN); // A binding that counts up or down and immediately returns the new value. w.bind("count", @@ -51,7 +52,9 @@ int main() }, nullptr); - w.set_html(std::string(reinterpret_cast(INDEX_HTML), std::extent_v)); + edge::InstallCustomProtocolHandler(w); + + w.navigate("http://modman/index.html"); w.run(); } catch (const webview::exception& e) diff --git a/src/ModMan/webview/edge/CustomProtocolHandlerEdge.cpp b/src/ModMan/webview/edge/CustomProtocolHandlerEdge.cpp new file mode 100644 index 00000000..8e2f351a --- /dev/null +++ b/src/ModMan/webview/edge/CustomProtocolHandlerEdge.cpp @@ -0,0 +1,162 @@ +#include "CustomProtocolHandlerEdge.h" + +#pragma warning(push, 0) +#include +#include +#pragma warning(pop) + +#if defined(WEBVIEW_PLATFORM_WINDOWS) && defined(WEBVIEW_EDGE) + +#include "ui/modmanui.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + constexpr auto MOD_MAN_URL_PREFIX = "http://modman/"; + + std::string wide_string_to_string(const std::wstring& wide_string) + { + if (wide_string.empty()) + { + return ""; + } + + const auto size_needed = WideCharToMultiByte(CP_UTF8, 0, wide_string.data(), (int)wide_string.size(), nullptr, 0, nullptr, nullptr); + if (size_needed <= 0) + { + throw std::runtime_error("WideCharToMultiByte() failed: " + std::to_string(size_needed)); + } + + std::string result(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, wide_string.data(), (int)wide_string.size(), result.data(), size_needed, nullptr, nullptr); + return result; + } + + std::wstring HeadersForAssetName(const std::string& assetName, const size_t contentLength) + { + std::wstringstream wss; + + wss << std::format(L"Content-Length: {}\n", contentLength); + + if (assetName.ends_with(".html")) + { + wss << L"Content-Type: text/html\n"; + } + else if (assetName.ends_with(".js")) + { + wss << L"Content-Type: text/javascript\n"; + } + else if (assetName.ends_with(".css")) + { + wss << L"Content-Type: text/css\n"; + } + + return wss.str(); + } +} // namespace + +namespace edge +{ + void InstallCustomProtocolHandler(webview::webview& wv) + { + const auto controller = static_cast(wv.browser_controller().value()); + Microsoft::WRL::ComPtr core; + if (!SUCCEEDED(controller->get_CoreWebView2(&core))) + { + std::cerr << "Failed to get webview\n"; + return; + } + + Microsoft::WRL::ComPtr core22; + if (!SUCCEEDED(core->QueryInterface(IID_PPV_ARGS(&core22)))) + { + std::cerr << "Failed to get core22\n"; + return; + } + + if (!SUCCEEDED(core22->AddWebResourceRequestedFilterWithRequestSourceKinds( + L"*", COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL, COREWEBVIEW2_WEB_RESOURCE_REQUEST_SOURCE_KINDS_ALL))) + { + std::cerr << "Failed to install request filter\n"; + return; + } + + EventRegistrationToken token; + core->add_WebResourceRequested(Microsoft::WRL::Callback( + [core22](ICoreWebView2* sender, IUnknown* args) -> HRESULT + { + Microsoft::WRL::ComPtr webResourceRequestArgs; + if (SUCCEEDED(args->QueryInterface(IID_PPV_ARGS(&webResourceRequestArgs)))) + { + COREWEBVIEW2_WEB_RESOURCE_REQUEST_SOURCE_KINDS requestSourceKind = + COREWEBVIEW2_WEB_RESOURCE_REQUEST_SOURCE_KINDS_ALL; + if (!SUCCEEDED(webResourceRequestArgs->get_RequestedSourceKind(&requestSourceKind))) + { + std::cerr << "Failed to get requested source kind\n"; + return S_FALSE; + } + + Microsoft::WRL::ComPtr request; + if (!SUCCEEDED(webResourceRequestArgs->get_Request(&request))) + { + std::cerr << "Failed to get request\n"; + return S_FALSE; + } + + LPWSTR wUri; + if (!SUCCEEDED(request->get_Uri(&wUri))) + { + std::cerr << "Failed to get uri\n"; + return S_FALSE; + } + + Microsoft::WRL::ComPtr environment; + if (!SUCCEEDED(core22->get_Environment(&environment))) + { + std::cerr << "Failed to get environment\n"; + return S_FALSE; + } + + Microsoft::WRL::ComPtr response; + + const auto uri = wide_string_to_string(wUri); + bool fileFound = false; + if (uri.starts_with(MOD_MAN_URL_PREFIX)) + { + const auto asset = uri.substr(std::char_traits::length(MOD_MAN_URL_PREFIX)); + + const auto foundUiFile = MOD_MAN_UI_FILES.find(asset); + if (foundUiFile != MOD_MAN_UI_FILES.end()) + { + Microsoft::WRL::ComPtr response_stream = SHCreateMemStream( + static_cast(foundUiFile->second.data), foundUiFile->second.dataSize); + + const auto headers = HeadersForAssetName(asset, foundUiFile->second.dataSize); + environment->CreateWebResourceResponse(response_stream.Get(), 200, L"OK", headers.data(), &response); + fileFound = true; + } + } + + if (!fileFound) + { + environment->CreateWebResourceResponse(nullptr, 404, L"Not found", L"", &response); + } + + webResourceRequestArgs->put_Response(response.Get()); + return S_OK; + } + + return S_FALSE; + }) + .Get(), + &token); + } +} // namespace edge + +#endif diff --git a/src/ModMan/webview/edge/CustomProtocolHandlerEdge.h b/src/ModMan/webview/edge/CustomProtocolHandlerEdge.h new file mode 100644 index 00000000..3c6efe2f --- /dev/null +++ b/src/ModMan/webview/edge/CustomProtocolHandlerEdge.h @@ -0,0 +1,18 @@ +#pragma once + +#pragma warning(push, 0) +#include +#pragma warning(pop) + +#if defined(WEBVIEW_PLATFORM_WINDOWS) && defined(WEBVIEW_EDGE) + +#pragma warning(push, 0) +#include +#pragma warning(pop) + +namespace edge +{ + void InstallCustomProtocolHandler(webview::webview& wv); +} + +#endif diff --git a/src/ModMan/webview/gtk/CustomProtocolHandlerGtk.cpp b/src/ModMan/webview/gtk/CustomProtocolHandlerGtk.cpp new file mode 100644 index 00000000..6cb05149 --- /dev/null +++ b/src/ModMan/webview/gtk/CustomProtocolHandlerGtk.cpp @@ -0,0 +1,6 @@ +#pragma once + +namespace webkitgtk +{ + void InstallCustomProtocolHandler(); +} diff --git a/src/ModMan/webview/gtk/CustomProtocolHandlerGtk.h b/src/ModMan/webview/gtk/CustomProtocolHandlerGtk.h new file mode 100644 index 00000000..85e2231a --- /dev/null +++ b/src/ModMan/webview/gtk/CustomProtocolHandlerGtk.h @@ -0,0 +1,18 @@ +#pragma once + +#pragma warning(push, 0) +#include +#pragma warning(pop) + +#if defined(WEBVIEW_PLATFORM_LINUX) && defined(WEBVIEW_GTK) + +#pragma warning(push, 0) +#include +#pragma warning(pop) + +namespace gtk +{ + void InstallCustomProtocolHandler(webview::webview& wv); +} + +#endif diff --git a/src/ModManUi/build/HeaderTransformationPlugin.ts b/src/ModManUi/build/HeaderTransformationPlugin.ts index 60b29c91..eb862985 100644 --- a/src/ModManUi/build/HeaderTransformationPlugin.ts +++ b/src/ModManUi/build/HeaderTransformationPlugin.ts @@ -1,21 +1,12 @@ import type { Plugin, ViteDevServer } from "vite"; import type { OutputOptions, OutputBundle, OutputAsset, OutputChunk } from "rollup"; -import { createHmac } from "node:crypto"; import path from "node:path"; import fs from "node:fs"; -function createTransformedTextSource2(varName: string, previousSource: string) { - const hash = createHmac("sha256", previousSource); - const digest = hash.digest("hex").substring(0, 16); - - return `#pragma once - -static inline const char* ${varName} = R"${digest}(${previousSource})${digest}"; -`; -} - function createTransformedTextSource(varName: string, previousSource: string) { - const str = [...previousSource].map((v) => `0x${v.charCodeAt(0).toString(16).padStart(2, "0")}`).join(", "); + const str = [...previousSource] + .map((v) => `0x${v.charCodeAt(0).toString(16).padStart(2, "0")}`) + .join(", "); return `#pragma once static inline const unsigned char ${varName}[] { @@ -33,8 +24,6 @@ function transformAsset(asset: OutputAsset) { if (typeof asset.source === "string") { asset.source = createTransformedTextSource(varName, asset.source); - return `${varName}, std::extent_v`; - // return `${varName}, std::char_traits::length(${varName})`; } else { const str = [...asset.source].map((v) => `0x${v.toString(16).padStart(2, "0")}`).join(", "); asset.source = `#pragma once @@ -43,15 +32,15 @@ static inline const unsigned char ${varName}[] { ${str} }; `; - return `${varName}, std::extent_v`; } + + return varName; } function transformChunk(chunk: OutputChunk) { const varName = createVarName(chunk.fileName); chunk.code = createTransformedTextSource(varName, chunk.code); - return `${varName}, std::extent_v`; -// return `${varName}, std::char_traits::length(${varName})`; + return varName; } export function headerTransformationPlugin(): Plugin { @@ -59,30 +48,35 @@ export function headerTransformationPlugin(): Plugin { name: "header-transformation", apply: "build", config(config) { - config.base = "http://modman-resource/"; + config.base = "http://modman"; }, generateBundle(options: OutputOptions, bundle: OutputBundle, isWrite: boolean) { - const includesStr: string[] = []; - const uiFilesStr: string[] = []; + const includesStr: string[] = [`#include "index.html.h"`]; + const uiFilesStr: string[] = [ + `{ "index.html", { "index.html", INDEX_HTML, std::extent_v } }`, + ]; + for (const curBundle of Object.values(bundle)) { - let varStr: string; + let varName: string; if (curBundle.type === "asset") { - varStr = transformAsset(curBundle); + varName = transformAsset(curBundle); } else { - varStr = transformChunk(curBundle); + varName = transformChunk(curBundle); } includesStr.push(`#include "${curBundle.fileName}.h"`); - uiFilesStr.push(`{ "${curBundle.fileName}", { "${curBundle.fileName}", ${varStr} } }`); + uiFilesStr.push( + `{ "${curBundle.fileName}", { "${curBundle.fileName}", ${varName}, std::extent_v } }`, + ); curBundle.fileName = `${curBundle.fileName}.h`; } + this.emitFile({ type: "asset", fileName: "modmanui.h", source: `#pragma once -#include "index.html.h" ${includesStr.join("\n")} #include @@ -111,9 +105,8 @@ ${uiFilesStr.join(",\n")} chunk?: OutputChunk; }, ) { - html = html - .replaceAll("index.js.h", "index.js") - .replaceAll("http://modman-resource/", "modman-resource://"); + html = html.replaceAll("index.js.h", "index.js"); + // .replaceAll("http://modman-resource/", "modman-resource://"); html = createTransformedTextSource(createVarName("index.html"), html); ctx.filename = `${ctx.filename}.h`; @@ -128,24 +121,6 @@ ${uiFilesStr.join(",\n")} curBundle.fileName += ".h"; } } - - this.emitFile({ - type: "asset", - fileName: "index.h", - source: ` - - - - - Title - - - - - - - `, - }); }, }; }