mirror of
				https://github.com/Laupetin/OpenAssetTools.git
				synced 2025-10-30 18:17:15 +00:00 
			
		
		
		
	Merge pull request #533 from Laupetin/webview
chore: add initial technical demonstration for ModMan
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,5 @@ | ||||
| local/ | ||||
| build/ | ||||
| /build/ | ||||
| .vscode | ||||
| .idea | ||||
| user*.* | ||||
							
								
								
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							| @@ -19,3 +19,6 @@ | ||||
| [submodule "thirdparty/lz4"] | ||||
| 	path = thirdparty/lz4 | ||||
| 	url = https://github.com/lz4/lz4.git | ||||
| [submodule "thirdparty/webview"] | ||||
| 	path = thirdparty/webview | ||||
| 	url = https://github.com/Laupetin/webview.git | ||||
|   | ||||
							
								
								
									
										16
									
								
								premake5.lua
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								premake5.lua
									
									
									
									
									
								
							| @@ -95,27 +95,32 @@ include "thirdparty/catch2.lua" | ||||
| include "thirdparty/eigen.lua" | ||||
| include "thirdparty/libtomcrypt.lua" | ||||
| include "thirdparty/libtommath.lua" | ||||
| include "thirdparty/lz4.lua" | ||||
| include "thirdparty/lzx.lua" | ||||
| include "thirdparty/json.lua" | ||||
| include "thirdparty/minilzo.lua" | ||||
| include "thirdparty/minizip.lua" | ||||
| include "thirdparty/salsa20.lua" | ||||
| include "thirdparty/webview.lua" | ||||
| include "thirdparty/zlib.lua" | ||||
| include "thirdparty/lz4.lua" | ||||
|  | ||||
| -- ThirdParty group: All projects that are external dependencies | ||||
| group "ThirdParty" | ||||
|     catch2:project() | ||||
|     eigen:project() | ||||
|     libtommath:project() | ||||
|     libtomcrypt:project() | ||||
|     libtommath:project() | ||||
|     lz4:project() | ||||
|     lzx:project() | ||||
|     json:project() | ||||
|     minilzo:project() | ||||
|     minizip:project() | ||||
|     salsa20:project() | ||||
|     zlib:project() | ||||
|     lz4:project() | ||||
|      | ||||
|     if _OPTIONS["modman"] then | ||||
|         webview:project() | ||||
|     end | ||||
| group "" | ||||
|  | ||||
| -- ======================== | ||||
| @@ -125,6 +130,7 @@ include "src/Common.lua" | ||||
| include "src/Cryptography.lua" | ||||
| include "src/ImageConverter.lua" | ||||
| include "src/Linker.lua" | ||||
| include "src/ModMan.lua" | ||||
| include "src/Parser.lua" | ||||
| include "src/RawTemplater.lua" | ||||
| include "src/Unlinker.lua" | ||||
| @@ -172,6 +178,10 @@ group "Tools" | ||||
|     Linker:project() | ||||
|     Unlinker:project() | ||||
|     ImageConverter:project() | ||||
|  | ||||
|     if _OPTIONS["modman"] then | ||||
|         ModMan:project() | ||||
|     end | ||||
| group "" | ||||
|  | ||||
| group "Raw" | ||||
|   | ||||
							
								
								
									
										56
									
								
								src/ModMan.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/ModMan.lua
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| ModMan = {} | ||||
|  | ||||
| function ModMan:include(includes) | ||||
| 	if includes:handle(self:name()) then | ||||
| 		includedirs { | ||||
| 			path.join(ProjectFolder(), "ModMan") | ||||
| 		} | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function ModMan:link(links) | ||||
|      | ||||
| end | ||||
|  | ||||
| function ModMan:use() | ||||
|     dependson(self:name()) | ||||
| end | ||||
|  | ||||
| function ModMan:name() | ||||
| 	return "ModMan" | ||||
| end | ||||
|  | ||||
| function ModMan:project() | ||||
| 	local folder = ProjectFolder() | ||||
| 	local includes = Includes:create() | ||||
| 	local links = Links:create() | ||||
|  | ||||
| 	project(self:name()) | ||||
|         targetdir(TargetDirectoryBin) | ||||
| 		location "%{wks.location}/src/%{prj.name}" | ||||
| 		kind "WindowedApp" | ||||
| 		language "C++" | ||||
| 		 | ||||
| 		files { | ||||
| 			path.join(folder, "ModMan/**.h"),  | ||||
| 			path.join(folder, "ModMan/**.cpp")  | ||||
| 		} | ||||
|  | ||||
| 		includedirs { | ||||
| 			"%{wks.location}/src/ModMan" | ||||
| 		} | ||||
|  | ||||
| 		filter { "system:linux", "action:gmake" } | ||||
|   			buildoptions { "`pkg-config --cflags gtk4 webkitgtk-6.0`" } | ||||
|   			linkoptions { "`pkg-config --libs gtk4 webkitgtk-6.0`" } | ||||
| 		filter {} | ||||
| 		 | ||||
| 		self:include(includes) | ||||
| 		Utils:include(includes) | ||||
| 		json:include(includes) | ||||
| 		webview:include(includes) | ||||
|  | ||||
| 		links:linkto(Utils) | ||||
| 		links:linkto(webview) | ||||
| 		links:linkall() | ||||
| end | ||||
							
								
								
									
										41
									
								
								src/ModMan/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/ModMan/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| # ModMan | ||||
|  | ||||
| ModMan is the experimental GUI for OpenAssetTools. | ||||
|  | ||||
| ## How do I test it | ||||
|  | ||||
| Currently ModMan is not compiled by default. | ||||
| To enable it, you have to generate with the appropriate premake5 flag: | ||||
|  | ||||
| ```shell | ||||
| # On Windows | ||||
| ./generate.bat --modman | ||||
|  | ||||
| # On Linux | ||||
| ./generate.sh --modman | ||||
| ``` | ||||
|  | ||||
| **Before** building the C++ solution, the ui has to be built. | ||||
| This will require NodeJS to be installed on your machine. | ||||
|  | ||||
| ```shell | ||||
| # Download dependencies | ||||
| npm --prefix src/ModManUi install | ||||
|  | ||||
| # Build frontend | ||||
| npm --prefix src/ModManUi run build | ||||
|  | ||||
| # Optional: Dev Server for UI development | ||||
| npm --prefix src/ModManUi run dev | ||||
| ``` | ||||
|  | ||||
| ## How does it work | ||||
|  | ||||
| ModMan uses [`webview`](https://github.com/Laupetin/webview) for providing a web frontend as a native application. | ||||
| Unlike frameworks like Electron this does not ship a browser engine alongside it, but instead relies on browser APIs of your OS. | ||||
| On Windows, this makes use of [WebView2](https://learn.microsoft.com/en-us/microsoft-edge/webview2), on Linux it uses [WebKitGTK](https://webkitgtk.org). | ||||
|  | ||||
| This adds the following dependencies: | ||||
| * **Windows**: An up-to-date OS with at the very least Windows10. The WebView2 library for development is downloaded by premake. | ||||
| * **Linux**: Developing and using ModMan requires the following dependencies to be installed: `gtk4 webkitgtk-6.0` | ||||
|  | ||||
							
								
								
									
										187
									
								
								src/ModMan/Web/Edge/AssetHandlerEdge.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								src/ModMan/Web/Edge/AssetHandlerEdge.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,187 @@ | ||||
| #include "AssetHandlerEdge.h" | ||||
|  | ||||
| #if defined(WEBVIEW_PLATFORM_WINDOWS) && defined(WEBVIEW_EDGE) | ||||
|  | ||||
| #include "Web/UiAssets.h" | ||||
|  | ||||
| #include <Windows.h> | ||||
| #include <format> | ||||
| #include <iostream> | ||||
| #include <sstream> | ||||
| #include <webview/detail/backends/win32_edge.hh> | ||||
| #include <wrl/event.h> | ||||
|  | ||||
| namespace | ||||
| { | ||||
|     constexpr auto LOCALHOST_PREFIX = "http://localhost:"; | ||||
|  | ||||
|     std::unordered_map<std::string, UiFile> assetLookup; | ||||
|  | ||||
|     std::string WideStringToString(const std::wstring& wideString) | ||||
|     { | ||||
|         if (wideString.empty()) | ||||
|             return ""; | ||||
|  | ||||
|         const auto sizeNeeded = WideCharToMultiByte(CP_UTF8, 0, wideString.data(), static_cast<int>(wideString.size()), nullptr, 0, nullptr, nullptr); | ||||
|         if (sizeNeeded <= 0) | ||||
|             throw std::runtime_error(std::format("WideCharToMultiByte() failed: {}", sizeNeeded)); | ||||
|  | ||||
|         std::string result(sizeNeeded, 0); | ||||
|         WideCharToMultiByte(CP_UTF8, 0, wideString.data(), static_cast<int>(wideString.size()), result.data(), sizeNeeded, nullptr, nullptr); | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     std::wstring StringToWideString(const std::string& string) | ||||
|     { | ||||
|         if (string.empty()) | ||||
|             return L""; | ||||
|  | ||||
|         const auto sizeNeeded = MultiByteToWideChar(CP_UTF8, 0, string.data(), static_cast<int>(string.size()), nullptr, 0); | ||||
|         if (sizeNeeded <= 0) | ||||
|             throw std::runtime_error(std::format("MultiByteToWideChar() failed: {}", sizeNeeded)); | ||||
|  | ||||
|         std::wstring result(sizeNeeded, 0); | ||||
|         MultiByteToWideChar(CP_UTF8, 0, string.data(), static_cast<int>(string.size()), result.data(), sizeNeeded); | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     std::wstring HeadersForAssetName(const std::string& assetName, const size_t contentLength) | ||||
|     { | ||||
|         std::wstringstream wss; | ||||
|  | ||||
|         wss << std::format(L"Content-Length: {}\n", contentLength); | ||||
|         wss << L"Content-Type: " << StringToWideString(ui::GetMimeTypeForFileName(assetName)); | ||||
|  | ||||
|         return wss.str(); | ||||
|     } | ||||
|  | ||||
|     HRESULT HandleResourceRequested(ICoreWebView2_22* core22, IUnknown* args) | ||||
|     { | ||||
|         Microsoft::WRL::ComPtr<ICoreWebView2WebResourceRequestedEventArgs2> 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<ICoreWebView2WebResourceRequest> 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<ICoreWebView2Environment> environment; | ||||
|             if (!SUCCEEDED(core22->get_Environment(&environment))) | ||||
|             { | ||||
|                 std::cerr << "Failed to get environment\n"; | ||||
|                 return S_FALSE; | ||||
|             } | ||||
|  | ||||
|             Microsoft::WRL::ComPtr<ICoreWebView2WebResourceResponse> response; | ||||
|  | ||||
|             const auto uri = WideStringToString(wUri); | ||||
|             bool fileFound = false; | ||||
|  | ||||
| #ifdef _DEBUG | ||||
|             // Allow dev server access | ||||
|             if (uri.starts_with(LOCALHOST_PREFIX)) | ||||
|                 return S_OK; | ||||
| #endif | ||||
|  | ||||
|             if (uri.starts_with(edge::URL_PREFIX)) | ||||
|             { | ||||
|                 const auto asset = uri.substr(std::char_traits<char>::length(edge::URL_PREFIX) - 1); | ||||
|  | ||||
|                 const auto foundUiFile = assetLookup.find(asset); | ||||
|                 if (foundUiFile != assetLookup.end()) | ||||
|                 { | ||||
|                     const Microsoft::WRL::ComPtr<IStream> responseStream = | ||||
|                         SHCreateMemStream(static_cast<const BYTE*>(foundUiFile->second.data), foundUiFile->second.dataSize); | ||||
|  | ||||
|                     const auto headers = HeadersForAssetName(asset, foundUiFile->second.dataSize); | ||||
|                     if (!SUCCEEDED(environment->CreateWebResourceResponse(responseStream.Get(), 200, L"OK", headers.data(), &response))) | ||||
|                     { | ||||
|                         std::cerr << "Failed to create web resource\n"; | ||||
|                         return S_FALSE; | ||||
|                     } | ||||
|  | ||||
|                     fileFound = true; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (!fileFound) | ||||
|             { | ||||
|                 if (!SUCCEEDED(environment->CreateWebResourceResponse(nullptr, 404, L"Not found", L"", &response))) | ||||
|                 { | ||||
|                     std::cerr << "Failed to create web resource\n"; | ||||
|                     return S_FALSE; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (!SUCCEEDED(webResourceRequestArgs->put_Response(response.Get()))) | ||||
|             { | ||||
|                 std::cerr << "Failed to put response\n"; | ||||
|                 return S_FALSE; | ||||
|             } | ||||
|  | ||||
|             return S_OK; | ||||
|         } | ||||
|  | ||||
|         return S_FALSE; | ||||
|     } | ||||
| } // namespace | ||||
|  | ||||
| namespace edge | ||||
| { | ||||
|     void InstallCustomProtocolHandler(webview::webview& wv) | ||||
|     { | ||||
|         assetLookup = ui::BuildUiFileLookup(); | ||||
|  | ||||
|         const auto controller = static_cast<ICoreWebView2Controller*>(wv.browser_controller().value()); | ||||
|         Microsoft::WRL::ComPtr<ICoreWebView2> core; | ||||
|         if (!SUCCEEDED(controller->get_CoreWebView2(&core))) | ||||
|         { | ||||
|             std::cerr << "Failed to get webview\n"; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         Microsoft::WRL::ComPtr<ICoreWebView2_22> 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; | ||||
|         if (!SUCCEEDED(core->add_WebResourceRequested(Microsoft::WRL::Callback<ICoreWebView2WebResourceRequestedEventHandler>( | ||||
|                                                           [core22](ICoreWebView2* sender, IUnknown* args) -> HRESULT | ||||
|                                                           { | ||||
|                                                               return HandleResourceRequested(core22.Get(), args); | ||||
|                                                           }) | ||||
|                                                           .Get(), | ||||
|                                                       &token))) | ||||
|         { | ||||
|             std::cerr << "Failed to add resource requested filter\n"; | ||||
|         } | ||||
|     } | ||||
| } // namespace edge | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										16
									
								
								src/ModMan/Web/Edge/AssetHandlerEdge.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/ModMan/Web/Edge/AssetHandlerEdge.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <webview/macros.h> | ||||
|  | ||||
| #if defined(WEBVIEW_PLATFORM_WINDOWS) && defined(WEBVIEW_EDGE) | ||||
|  | ||||
| #include "Web/WebViewLib.h" | ||||
|  | ||||
| namespace edge | ||||
| { | ||||
|     constexpr auto URL_PREFIX = "http://modman.local/"; | ||||
|  | ||||
|     void InstallCustomProtocolHandler(webview::webview& wv); | ||||
| } // namespace edge | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										51
									
								
								src/ModMan/Web/Gtk/AssetHandlerGtk.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/ModMan/Web/Gtk/AssetHandlerGtk.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| #include "AssetHandlerGtk.h" | ||||
|  | ||||
| #if defined(WEBVIEW_PLATFORM_LINUX) && defined(WEBVIEW_GTK) | ||||
|  | ||||
| #include "Web/UiAssets.h" | ||||
|  | ||||
| #include <format> | ||||
| #include <iostream> | ||||
|  | ||||
| namespace | ||||
| { | ||||
|     std::unordered_map<std::string, UiFile> assetLookup; | ||||
|  | ||||
|     void ModManUriSchemeRequestCb(WebKitURISchemeRequest* request, gpointer user_data) | ||||
|     { | ||||
|         const gchar* asset = webkit_uri_scheme_request_get_path(request); | ||||
|  | ||||
|         const auto foundUiFile = assetLookup.find(asset); | ||||
|         if (foundUiFile != assetLookup.end()) | ||||
|         { | ||||
|             gsize stream_length = foundUiFile->second.dataSize; | ||||
|             GInputStream* stream = g_memory_input_stream_new_from_data(foundUiFile->second.data, foundUiFile->second.dataSize, nullptr); | ||||
|  | ||||
|             webkit_uri_scheme_request_finish(request, stream, stream_length, ui::GetMimeTypeForFileName(foundUiFile->second.filename)); | ||||
|             g_object_unref(stream); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             GError* error = g_error_new(G_SPAWN_ERROR, 123, "Could not find %s.", asset); | ||||
|             webkit_uri_scheme_request_finish_error(request, error); | ||||
|             g_error_free(error); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
| } // namespace | ||||
|  | ||||
| namespace gtk | ||||
| { | ||||
|     void InstallCustomProtocolHandler(webview::webview& wv) | ||||
|     { | ||||
|         const auto widget = static_cast<GtkWidget*>(wv.browser_controller().value()); | ||||
|         const auto webView = WEBKIT_WEB_VIEW(widget); | ||||
|         const auto context = webkit_web_view_get_context(webView); | ||||
|  | ||||
|         assetLookup = ui::BuildUiFileLookup(); | ||||
|  | ||||
|         webkit_web_context_register_uri_scheme(context, "modman", ModManUriSchemeRequestCb, NULL, nullptr); | ||||
|     } | ||||
| } // namespace gtk | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										16
									
								
								src/ModMan/Web/Gtk/AssetHandlerGtk.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/ModMan/Web/Gtk/AssetHandlerGtk.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <webview/macros.h> | ||||
|  | ||||
| #if defined(WEBVIEW_PLATFORM_LINUX) && defined(WEBVIEW_GTK) | ||||
|  | ||||
| #include "Web/WebViewLib.h" | ||||
|  | ||||
| namespace gtk | ||||
| { | ||||
|     constexpr auto URL_PREFIX = "modman://localhost/"; | ||||
|  | ||||
|     void InstallCustomProtocolHandler(webview::webview& wv); | ||||
| } // namespace gtk | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										34
									
								
								src/ModMan/Web/UiAssets.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/ModMan/Web/UiAssets.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| #include "UiAssets.h" | ||||
|  | ||||
| #include <format> | ||||
|  | ||||
| namespace ui | ||||
| { | ||||
|     std::unordered_map<std::string, UiFile> BuildUiFileLookup() | ||||
|     { | ||||
|         std::unordered_map<std::string, UiFile> result; | ||||
|  | ||||
|         for (const auto& asset : MOD_MAN_UI_FILES) | ||||
|         { | ||||
|             result.emplace(std::format("/{}", asset.filename), asset); | ||||
|         } | ||||
|  | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     const char* GetMimeTypeForFileName(const std::string& fileName) | ||||
|     { | ||||
|         const char* mimeType; | ||||
|  | ||||
|         if (fileName.ends_with(".html")) | ||||
|             mimeType = "text/html"; | ||||
|         else if (fileName.ends_with(".js")) | ||||
|             mimeType = "text/javascript"; | ||||
|         else if (fileName.ends_with(".css")) | ||||
|             mimeType = "text/css"; | ||||
|         else | ||||
|             mimeType = "application/octet-stream"; | ||||
|  | ||||
|         return mimeType; | ||||
|     } | ||||
| } // namespace ui | ||||
							
								
								
									
										12
									
								
								src/ModMan/Web/UiAssets.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/ModMan/Web/UiAssets.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "Web/ViteAssets.h" | ||||
|  | ||||
| #include <string> | ||||
| #include <unordered_map> | ||||
|  | ||||
| namespace ui | ||||
| { | ||||
|     std::unordered_map<std::string, UiFile> BuildUiFileLookup(); | ||||
|     const char* GetMimeTypeForFileName(const std::string& fileName); | ||||
| } // namespace ui | ||||
							
								
								
									
										0
									
								
								src/ModMan/Web/UiCommunication.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/ModMan/Web/UiCommunication.cpp
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										142
									
								
								src/ModMan/Web/UiCommunication.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								src/ModMan/Web/UiCommunication.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "Utils/Logging/Log.h" | ||||
| #include "WebViewLib.h" | ||||
|  | ||||
| #pragma warning(push, 0) | ||||
| #include <nlohmann/json.hpp> | ||||
| #pragma warning(pop) | ||||
|  | ||||
| namespace ui | ||||
| { | ||||
|     inline void Bind(webview::webview& wv, const std::string& name, std::function<void()> fn) | ||||
|     { | ||||
|         wv.bind(name, | ||||
|                 [fn](const std::string& req) -> std::string | ||||
|                 { | ||||
|                     fn(); | ||||
|                     return ""; | ||||
|                 }); | ||||
|     } | ||||
|  | ||||
|     template<typename TInput> void Bind(webview::webview& wv, const std::string& name, std::function<void(TInput)> fn) | ||||
|     { | ||||
|         wv.bind(name, | ||||
|                 [fn](const std::string& req) -> std::string | ||||
|                 { | ||||
|                     TInput param; | ||||
|                     try | ||||
|                     { | ||||
|                         const auto json = nlohmann::json::parse(req); | ||||
|                         if (!json.is_array()) | ||||
|                         { | ||||
|                             con::error("Webview params are not an array: {}", req); | ||||
|                             return ""; | ||||
|                         } | ||||
|                         param = json.at(0).get<TInput>(); | ||||
|                     } | ||||
|                     catch (const nlohmann::json::exception& e) | ||||
|                     { | ||||
|                         con::error("Failed to parse json of webview call: {}", e.what()); | ||||
|                         return ""; | ||||
|                     } | ||||
|  | ||||
|                     fn(std::move(param)); | ||||
|  | ||||
|                     return ""; | ||||
|                 }); | ||||
|     } | ||||
|  | ||||
|     template<typename TReturn> void BindRetOnly(webview::webview& wv, const std::string& name, std::function<TReturn()> fn) | ||||
|     { | ||||
|         wv.bind(name, | ||||
|                 [fn](const std::string& req) -> std::string | ||||
|                 { | ||||
|                     auto result = fn(); | ||||
|  | ||||
|                     return nlohmann::json(result).dump(); | ||||
|                 }); | ||||
|     } | ||||
|  | ||||
|     template<typename TInput, typename TReturn> void Bind(webview::webview& wv, const std::string& name, std::function<TReturn(TInput)> fn) | ||||
|     { | ||||
|         wv.bind(name, | ||||
|                 [fn](const std::string& req) -> std::string | ||||
|                 { | ||||
|                     TInput param; | ||||
|                     try | ||||
|                     { | ||||
|                         const auto json = nlohmann::json::parse(req); | ||||
|                         if (!json.is_array()) | ||||
|                         { | ||||
|                             con::error("Webview params are not an array: {}", req); | ||||
|                             return ""; | ||||
|                         } | ||||
|                         param = json.at(0).get<TInput>(); | ||||
|                     } | ||||
|                     catch (const nlohmann::json::exception& e) | ||||
|                     { | ||||
|                         con::error("Failed to parse json of webview call: {}", e.what()); | ||||
|                         return ""; | ||||
|                     } | ||||
|  | ||||
|                     auto result = fn(std::move(param)); | ||||
|  | ||||
|                     return nlohmann::json(result).dump(); | ||||
|                 }); | ||||
|     } | ||||
|  | ||||
|     inline void BindAsync(webview::webview& wv, const std::string& name, std::function<void(const std::string& id)> fn) | ||||
|     { | ||||
|         wv.bind( | ||||
|             name, | ||||
|             [fn](const std::string& id, const std::string& req, void* /* arg */) | ||||
|             { | ||||
|                 fn(id); | ||||
|             }, | ||||
|             nullptr); | ||||
|     } | ||||
|  | ||||
|     template<typename TInput> void BindAsync(webview::webview& wv, const std::string& name, std::function<void(const std::string& id, TInput)> fn) | ||||
|     { | ||||
|         wv.bind( | ||||
|             name, | ||||
|             [fn](const std::string& id, const std::string& req, void* /* arg */) | ||||
|             { | ||||
|                 TInput param; | ||||
|                 try | ||||
|                 { | ||||
|                     const auto json = nlohmann::json::parse(req); | ||||
|                     if (!json.is_array()) | ||||
|                     { | ||||
|                         con::error("Webview params are not an array: {}", req); | ||||
|                         return ""; | ||||
|                     } | ||||
|                     param = json.at(0).get<TInput>(); | ||||
|                 } | ||||
|                 catch (const nlohmann::json::exception& e) | ||||
|                 { | ||||
|                     con::error("Failed to parse json of webview call: {}", e.what()); | ||||
|                     return ""; | ||||
|                 } | ||||
|  | ||||
|                 fn(id, std::move(param)); | ||||
|             }, | ||||
|             nullptr); | ||||
|     } | ||||
|  | ||||
|     template<typename TPayload> void PromiseResolve(webview::webview& wv, const std::string& id, const TPayload& payload) | ||||
|     { | ||||
|         wv.resolve(id, 0, nlohmann::json(payload).dump()); | ||||
|     } | ||||
|  | ||||
|     template<typename TPayload> void PromiseReject(webview::webview& wv, const std::string& id, const TPayload& payload) | ||||
|     { | ||||
|         wv.resolve(id, 1, nlohmann::json(payload).dump()); | ||||
|     } | ||||
|  | ||||
|     template<typename TPayload> void Notify(webview::webview& wv, const std::string& eventKey, const TPayload& payload) | ||||
|     { | ||||
|         wv.notify(eventKey, nlohmann::json(payload).dump()); | ||||
|     } | ||||
| } // namespace ui | ||||
							
								
								
									
										20
									
								
								src/ModMan/Web/WebViewLib.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/ModMan/Web/WebViewLib.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef _MSC_VER | ||||
| #pragma warning(push, 0) | ||||
| #else | ||||
| #pragma GCC diagnostic push | ||||
| #pragma GCC diagnostic ignored "-Wdeprecated-declarations" | ||||
| #endif | ||||
|  | ||||
| #include <webview/webview.h> | ||||
|  | ||||
| #ifdef _MSC_VER | ||||
| #pragma warning(pop) | ||||
| #else | ||||
| #pragma GCC diagnostic pop | ||||
| #endif | ||||
|  | ||||
| #ifdef ERROR | ||||
| #undef ERROR | ||||
| #endif | ||||
							
								
								
									
										140
									
								
								src/ModMan/main.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/ModMan/main.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| #include "GitVersion.h" | ||||
| #include "Web/Edge/AssetHandlerEdge.h" | ||||
| #include "Web/Gtk/AssetHandlerGtk.h" | ||||
| #include "Web/UiCommunication.h" | ||||
| #include "Web/ViteAssets.h" | ||||
| #include "Web/WebViewLib.h" | ||||
|  | ||||
| #include <chrono> | ||||
| #include <format> | ||||
| #include <iostream> | ||||
| #include <optional> | ||||
| #include <string> | ||||
| #include <thread> | ||||
|  | ||||
| using namespace std::string_literals; | ||||
|  | ||||
| namespace | ||||
| { | ||||
| #ifdef _DEBUG | ||||
|     std::optional<webview::webview> devToolWindow; | ||||
|  | ||||
|     void RunDevToolsWindow() | ||||
|     { | ||||
|         con::debug("Creating dev tools window"); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             auto& newWindow = devToolWindow.emplace(false, nullptr); | ||||
|             newWindow.set_title("Devtools"); | ||||
|             newWindow.set_size(640, 480, WEBVIEW_HINT_NONE); | ||||
|             newWindow.set_size(480, 320, WEBVIEW_HINT_MIN); | ||||
|             newWindow.navigate(std::format("http://localhost:{}/__devtools__/", VITE_DEV_SERVER_PORT)); | ||||
|         } | ||||
|         catch (const webview::exception& e) | ||||
|         { | ||||
|             std::cerr << e.what() << '\n'; | ||||
|         } | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     int RunMainWindow() | ||||
|     { | ||||
|         con::debug("Creating main window"); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             webview::webview w( | ||||
| #ifdef _DEBUG | ||||
|                 true, | ||||
| #else | ||||
|                 false, | ||||
| #endif | ||||
|                 nullptr); | ||||
|             w.set_title("OpenAssetTools ModMan"); | ||||
|             w.set_size(1280, 640, WEBVIEW_HINT_NONE); | ||||
|             w.set_size(480, 320, WEBVIEW_HINT_MIN); | ||||
|  | ||||
|             // A binding that counts up or down and immediately returns the new value. | ||||
|             ui::Bind<std::string, std::string>(w, | ||||
|                                                "greet", | ||||
|                                                [&w](std::string name) -> std::string | ||||
|                                                { | ||||
|                                                    ui::Notify(w, "greeting", name); | ||||
|                                                    return std::format("Hello from C++ {}!", name); | ||||
|                                                }); | ||||
|  | ||||
|             // A binding that counts up or down and immediately returns the new value. | ||||
|             ui::Bind(w, | ||||
|                      "debug", | ||||
|                      []() | ||||
|                      { | ||||
|                          con::info("Debug"); | ||||
|                      }); | ||||
|  | ||||
|             // A binding that creates a new thread and returns the result at a later time. | ||||
|             ui::BindAsync(w, | ||||
|                           "compute", | ||||
|                           [&](const std::string& id) | ||||
|                           { | ||||
|                               // Create a thread and forget about it for the sake of simplicity. | ||||
|                               std::thread( | ||||
|                                   [&, id] | ||||
|                                   { | ||||
|                                       // Simulate load. | ||||
|                                       std::this_thread::sleep_for(std::chrono::seconds(5)); | ||||
|                                       ui::PromiseResolve(w, id, 42); | ||||
|                                   }) | ||||
|                                   .detach(); | ||||
|                           }); | ||||
|  | ||||
| #if defined(WEBVIEW_PLATFORM_WINDOWS) && defined(WEBVIEW_EDGE) | ||||
|             edge::InstallCustomProtocolHandler(w); | ||||
|             constexpr auto urlPrefix = edge::URL_PREFIX; | ||||
| #elif defined(WEBVIEW_PLATFORM_LINUX) && defined(WEBVIEW_GTK) | ||||
|             gtk::InstallCustomProtocolHandler(w); | ||||
|             constexpr auto urlPrefix = gtk::URL_PREFIX; | ||||
| #else | ||||
| #error Unsupported platform | ||||
| #endif | ||||
|  | ||||
| #ifdef _DEBUG | ||||
|             w.navigate(VITE_DEV_SERVER ? std::format("http://localhost:{}", VITE_DEV_SERVER_PORT) : std::format("{}index.html", urlPrefix)); | ||||
|  | ||||
|             if (VITE_DEV_SERVER) | ||||
|             { | ||||
|                 w.dispatch( | ||||
|                     [] | ||||
|                     { | ||||
|                         RunDevToolsWindow(); | ||||
|                     }); | ||||
|             } | ||||
| #else | ||||
|             w.navigate(std::format("{}index.html", urlPrefix)); | ||||
| #endif | ||||
|             w.run(); | ||||
|         } | ||||
|         catch (const webview::exception& e) | ||||
|         { | ||||
|             std::cerr << e.what() << '\n'; | ||||
|             return 1; | ||||
|         } | ||||
|  | ||||
|         return 0; | ||||
|     } | ||||
| } // namespace | ||||
|  | ||||
| #ifdef _WIN32 | ||||
| int WINAPI WinMain(HINSTANCE /*hInst*/, HINSTANCE /*hPrevInst*/, LPSTR /*lpCmdLine*/, int /*nCmdShow*/) | ||||
| { | ||||
| #else | ||||
| int main() | ||||
| { | ||||
| #endif | ||||
|  | ||||
|     con::info("Starting ModMan " GIT_VERSION); | ||||
|  | ||||
|     const auto result = RunMainWindow(); | ||||
|  | ||||
|     return result; | ||||
| } | ||||
							
								
								
									
										24
									
								
								src/ModManUi/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/ModManUi/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| # Logs | ||||
| logs | ||||
| *.log | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| pnpm-debug.log* | ||||
| lerna-debug.log* | ||||
|  | ||||
| node_modules | ||||
| dist | ||||
| dist-ssr | ||||
| *.local | ||||
|  | ||||
| # Editor directories and files | ||||
| .vscode/* | ||||
| !.vscode/extensions.json | ||||
| .idea | ||||
| .DS_Store | ||||
| *.suo | ||||
| *.ntvs* | ||||
| *.njsproj | ||||
| *.sln | ||||
| *.sw? | ||||
							
								
								
									
										1
									
								
								src/ModManUi/.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/ModManUi/.prettierignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| src-tauri | ||||
							
								
								
									
										4
									
								
								src/ModManUi/.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/ModManUi/.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| { | ||||
|   "$schema": "https://json.schemastore.org/prettierrc", | ||||
|   "printWidth": 100 | ||||
| } | ||||
							
								
								
									
										171
									
								
								src/ModManUi/build/HeaderTransformationPlugin.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								src/ModManUi/build/HeaderTransformationPlugin.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,171 @@ | ||||
| import type { Plugin } from "vite"; | ||||
| import type { OutputAsset, OutputChunk } from "rollup"; | ||||
| import path from "node:path"; | ||||
| import fs from "node:fs"; | ||||
|  | ||||
| type MinimalOutputAsset = Pick<OutputAsset, "type" | "fileName" | "source">; | ||||
| type MinimalOutputChunk = Pick<OutputChunk, "type" | "fileName" | "code">; | ||||
| type MinimalOutputBundle = Record<string, MinimalOutputAsset | MinimalOutputChunk>; | ||||
|  | ||||
| function createVarName(fileName: string) { | ||||
|   return fileName.replaceAll(".", "_").toUpperCase(); | ||||
| } | ||||
|  | ||||
| function transformAsset(asset: MinimalOutputAsset) { | ||||
|   const varName = createVarName(asset.fileName); | ||||
|  | ||||
|   let bytes: string; | ||||
|   if (typeof asset.source === "string") { | ||||
|     bytes = [...asset.source].map((v) => String(v.charCodeAt(0))).join(","); | ||||
|   } else { | ||||
|     bytes = [...asset.source].map((v) => String(v)).join(","); | ||||
|   } | ||||
|  | ||||
|   return `constexpr const unsigned char ${varName}[] {${bytes}}; | ||||
| `; | ||||
| } | ||||
|  | ||||
| function transformChunk(chunk: MinimalOutputChunk) { | ||||
|   const varName = createVarName(chunk.fileName); | ||||
|   const bytes = [...chunk.code].map((v) => String(v.charCodeAt(0))).join(","); | ||||
|  | ||||
|   return `constexpr const unsigned char ${varName}[] {${bytes}}; | ||||
| `; | ||||
| } | ||||
|  | ||||
| function writeHeader( | ||||
|   bundle: MinimalOutputBundle, | ||||
|   outputDir?: string, | ||||
|   options?: HeaderTransformationPluginOptions, | ||||
|   devServerPort?: number, | ||||
| ) { | ||||
|   const outputPath = options?.outputPath ?? path.join(outputDir ?? "dist", "ViteAssets.h"); | ||||
|   const outputPathParentDir = path.dirname(outputPath); | ||||
|  | ||||
|   fs.mkdirSync(outputPathParentDir, { recursive: true }); | ||||
|  | ||||
|   const fd = fs.openSync(outputPath, "w"); | ||||
|   const includeFileEnumeration = options?.includeFileEnumeration ?? true; | ||||
|  | ||||
|   fs.writeSync( | ||||
|     fd, | ||||
|     `#pragma once | ||||
|  | ||||
| `, | ||||
|   ); | ||||
|  | ||||
|   if (includeFileEnumeration) { | ||||
|     fs.writeSync( | ||||
|       fd, | ||||
|       `#include <cstdlib> | ||||
| #include <type_traits> | ||||
|  | ||||
| `, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   fs.writeSync( | ||||
|     fd, | ||||
|     `constexpr auto VITE_DEV_SERVER = ${devServerPort ? "true" : "false"}; | ||||
| constexpr auto VITE_DEV_SERVER_PORT = ${devServerPort ? String(devServerPort) : "-1"}; | ||||
| `, | ||||
|   ); | ||||
|  | ||||
|   for (const curBundle of Object.values(bundle)) { | ||||
|     if (curBundle.type === "asset") { | ||||
|       fs.writeSync(fd, transformAsset(curBundle)); | ||||
|     } else { | ||||
|       fs.writeSync(fd, transformChunk(curBundle)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (includeFileEnumeration) { | ||||
|     fs.writeSync( | ||||
|       fd, | ||||
|       ` | ||||
| struct UiFile | ||||
| { | ||||
|     const char* filename; | ||||
|     const void* data; | ||||
|     const size_t dataSize; | ||||
| }; | ||||
|  | ||||
| static inline const UiFile MOD_MAN_UI_FILES[] { | ||||
| `, | ||||
|     ); | ||||
|  | ||||
|     let index = 0; | ||||
|     for (const curBundle of Object.values(bundle)) { | ||||
|       const fileName = curBundle.fileName; | ||||
|       const varName = createVarName(fileName); | ||||
|  | ||||
|       let prefix = "  "; | ||||
|       if (index > 0) { | ||||
|         prefix = `, | ||||
|   `; | ||||
|       } | ||||
|  | ||||
|       fs.writeSync( | ||||
|         fd, | ||||
|         `${prefix}{ "${fileName}", ${varName}, std::extent_v<decltype(${varName})> }`, | ||||
|       ); | ||||
|       index++; | ||||
|     } | ||||
|  | ||||
|     fs.writeSync( | ||||
|       fd, | ||||
|       ` | ||||
| }; | ||||
| `, | ||||
|     ); | ||||
|  | ||||
|     fs.closeSync(fd); | ||||
|   } | ||||
| } | ||||
|  | ||||
| export interface HeaderTransformationPluginOptions { | ||||
|   outputPath?: string; | ||||
|   includeFileEnumeration?: boolean; | ||||
| } | ||||
|  | ||||
| export default function headerTransformationPlugin( | ||||
|   options?: HeaderTransformationPluginOptions, | ||||
| ): Plugin { | ||||
|   let writeServerActive = false; | ||||
|   let writeBundleActive = false; | ||||
|  | ||||
|   return { | ||||
|     name: "vite-plugin-header-transformation", | ||||
|     enforce: "post", | ||||
|     config(_userOptions, env) { | ||||
|       if (env.command === "serve") { | ||||
|         writeServerActive = true; | ||||
|       } else { | ||||
|         writeBundleActive = true; | ||||
|       } | ||||
|     }, | ||||
|     configureServer(server) { | ||||
|       if (!writeServerActive) { | ||||
|         return; | ||||
|       } | ||||
|       server.httpServer?.once("listening", () => { | ||||
|         writeHeader( | ||||
|           { | ||||
|             // We need at least one array entry for MSVC | ||||
|             dummyfile: { type: "chunk", fileName: "dummyfile", code: "dummy" }, | ||||
|           }, | ||||
|           server.config.build.outDir, | ||||
|           options, | ||||
|           server.config.server.port, | ||||
|         ); | ||||
|       }); | ||||
|     }, | ||||
|     writeBundle(outputOptions, bundle) { | ||||
|       if (!writeBundleActive) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       writeHeader(bundle, outputOptions.dir, options); | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										1
									
								
								src/ModManUi/env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/ModManUi/env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| /// <reference types="vite/client" /> | ||||
							
								
								
									
										28
									
								
								src/ModManUi/eslint.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/ModManUi/eslint.config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| import { globalIgnores } from "eslint/config"; | ||||
| import { defineConfigWithVueTs, vueTsConfigs } from "@vue/eslint-config-typescript"; | ||||
| import pluginVue from "eslint-plugin-vue"; | ||||
| import pluginVitest from "@vitest/eslint-plugin"; | ||||
| import skipFormatting from "@vue/eslint-config-prettier/skip-formatting"; | ||||
|  | ||||
| // To allow more languages other than `ts` in `.vue` files, uncomment the following lines: | ||||
| // import { configureVueProject } from '@vue/eslint-config-typescript' | ||||
| // configureVueProject({ scriptLangs: ['ts', 'tsx'] }) | ||||
| // More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup | ||||
|  | ||||
| export default defineConfigWithVueTs( | ||||
|   { | ||||
|     name: "app/files-to-lint", | ||||
|     files: ["**/*.{ts,mts,tsx,vue}"], | ||||
|   }, | ||||
|  | ||||
|   globalIgnores(["**/dist/**", "**/dist-ssr/**", "**/coverage/**"]), | ||||
|  | ||||
|   pluginVue.configs["flat/essential"], | ||||
|   vueTsConfigs.recommended, | ||||
|  | ||||
|   { | ||||
|     ...pluginVitest.configs.recommended, | ||||
|     files: ["src/**/__tests__/*"], | ||||
|   }, | ||||
|   skipFormatting, | ||||
| ); | ||||
							
								
								
									
										13
									
								
								src/ModManUi/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/ModManUi/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| <!doctype html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <link rel="icon" href="/favicon.ico" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|     <title>Tauri + Vue + Typescript App</title> | ||||
|   </head> | ||||
|   <body> | ||||
|     <div id="app"></div> | ||||
|     <script type="module" src="/src/main.ts"></script> | ||||
|   </body> | ||||
| </html> | ||||
							
								
								
									
										7100
									
								
								src/ModManUi/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7100
									
								
								src/ModManUi/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										42
									
								
								src/ModManUi/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/ModManUi/package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| { | ||||
|   "name": "openassettools", | ||||
|   "private": true, | ||||
|   "version": "0.1.0", | ||||
|   "type": "module", | ||||
|   "scripts": { | ||||
|     "dev": "vite", | ||||
|     "build": "run-p type-check \"build-only {@}\" --", | ||||
|     "preview": "vite preview", | ||||
|     "build-only": "vite build", | ||||
|     "type-check": "vue-tsc --build", | ||||
|     "lint": "eslint . --fix", | ||||
|     "format": "prettier --write **/*.{js,ts,vue,html,json,yml,yaml,md}" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "pinia": "3.0.3", | ||||
|     "vue": "3.5.22" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@tsconfig/node22": "^22.0.2", | ||||
|     "@types/jsdom": "^21.1.7", | ||||
|     "@types/node": "^22.18.6", | ||||
|     "@vitejs/plugin-vue": "6.0.1", | ||||
|     "@vitest/eslint-plugin": "^1.3.13", | ||||
|     "@vue/eslint-config-prettier": "^10.2.0", | ||||
|     "@vue/eslint-config-typescript": "^14.6.0", | ||||
|     "@vue/test-utils": "^2.4.6", | ||||
|     "@vue/tsconfig": "^0.8.1", | ||||
|     "eslint": "^9.33.0", | ||||
|     "eslint-plugin-vue": "~10.4.0", | ||||
|     "jiti": "^2.5.1", | ||||
|     "jsdom": "^27.0.0", | ||||
|     "npm-run-all2": "^8.0.4", | ||||
|     "prettier": "3.6.2", | ||||
|     "sass": "1.93.2", | ||||
|     "typescript": "~5.9.3", | ||||
|     "vite": "7.1.7", | ||||
|     "vite-plugin-vue-devtools": "^8.0.2", | ||||
|     "vitest": "^3.2.4", | ||||
|     "vue-tsc": "3.1.0" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								src/ModManUi/public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/ModManUi/public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 4.2 KiB | 
							
								
								
									
										44
									
								
								src/ModManUi/src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/ModManUi/src/App.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| <script setup lang="ts"> | ||||
| import { onUnmounted, ref } from "vue"; | ||||
| import { webviewBinds, webviewAddEventListener, webviewRemoveEventListener } from "./native"; | ||||
|  | ||||
| const greetMsg = ref(""); | ||||
| const lastPersonGreeted = ref(""); | ||||
| const name = ref(""); | ||||
|  | ||||
| async function greet() { | ||||
|   greetMsg.value = await webviewBinds.greet(name.value); | ||||
| } | ||||
|  | ||||
| function onPersonGreeted(person: string) { | ||||
|   lastPersonGreeted.value = person; | ||||
| } | ||||
|  | ||||
| webviewAddEventListener("greeting", onPersonGreeted); | ||||
|  | ||||
| onUnmounted(() => webviewRemoveEventListener("greeting", onPersonGreeted)); | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <main class="container"> | ||||
|     <h1>Welcome to ModMan</h1> | ||||
|     <small>Nothing to see here yet, this is mainly for testing</small> | ||||
|  | ||||
|     <form class="row" @submit.prevent="greet"> | ||||
|       <input id="greet-input" v-model="name" placeholder="Enter a name..." autocomplete="off" /> | ||||
|       <button type="submit">Greet</button> | ||||
|     </form> | ||||
|     <p>{{ greetMsg }}</p> | ||||
|     <p>The last person greeted is: {{ lastPersonGreeted }}</p> | ||||
|   </main> | ||||
| </template> | ||||
|  | ||||
| <style scoped> | ||||
| .logo.vite:hover { | ||||
|   filter: drop-shadow(0 0 2em #747bff); | ||||
| } | ||||
|  | ||||
| .logo.vue:hover { | ||||
|   filter: drop-shadow(0 0 2em #249b73); | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										109
									
								
								src/ModManUi/src/main.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/ModManUi/src/main.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| :root { | ||||
|   font-family: Inter, Avenir, Helvetica, Arial, sans-serif; | ||||
|   font-size: 16px; | ||||
|   line-height: 24px; | ||||
|   font-weight: 400; | ||||
|  | ||||
|   color: #0f0f0f; | ||||
|   background-color: #f6f6f6; | ||||
|  | ||||
|   font-synthesis: none; | ||||
|   text-rendering: optimizeLegibility; | ||||
|   -webkit-font-smoothing: antialiased; | ||||
|   -moz-osx-font-smoothing: grayscale; | ||||
|   -webkit-text-size-adjust: 100%; | ||||
| } | ||||
|  | ||||
| .container { | ||||
|   margin: 0; | ||||
|   padding-top: 10vh; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   justify-content: center; | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| .logo { | ||||
|   height: 6em; | ||||
|   padding: 1.5em; | ||||
|   will-change: filter; | ||||
|   transition: 0.75s; | ||||
| } | ||||
|  | ||||
| .logo.tauri:hover { | ||||
|   filter: drop-shadow(0 0 2em #24c8db); | ||||
| } | ||||
|  | ||||
| .row { | ||||
|   display: flex; | ||||
|   justify-content: center; | ||||
| } | ||||
|  | ||||
| a { | ||||
|   font-weight: 500; | ||||
|   color: #646cff; | ||||
|   text-decoration: inherit; | ||||
| } | ||||
|  | ||||
| a:hover { | ||||
|   color: #535bf2; | ||||
| } | ||||
|  | ||||
| h1 { | ||||
|   text-align: center; | ||||
| } | ||||
|  | ||||
| input, | ||||
| button { | ||||
|   border-radius: 8px; | ||||
|   border: 1px solid transparent; | ||||
|   padding: 0.6em 1.2em; | ||||
|   font-size: 1em; | ||||
|   font-weight: 500; | ||||
|   font-family: inherit; | ||||
|   color: #0f0f0f; | ||||
|   background-color: #ffffff; | ||||
|   transition: border-color 0.25s; | ||||
|   box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); | ||||
| } | ||||
|  | ||||
| button { | ||||
|   cursor: pointer; | ||||
| } | ||||
|  | ||||
| button:hover { | ||||
|   border-color: #396cd8; | ||||
| } | ||||
| button:active { | ||||
|   border-color: #396cd8; | ||||
|   background-color: #e8e8e8; | ||||
| } | ||||
|  | ||||
| input, | ||||
| button { | ||||
|   outline: none; | ||||
| } | ||||
|  | ||||
| #greet-input { | ||||
|   margin-right: 5px; | ||||
| } | ||||
|  | ||||
| @media (prefers-color-scheme: dark) { | ||||
|   :root { | ||||
|     color: #f6f6f6; | ||||
|     background-color: #2f2f2f; | ||||
|   } | ||||
|  | ||||
|   a:hover { | ||||
|     color: #24c8db; | ||||
|   } | ||||
|  | ||||
|   input, | ||||
|   button { | ||||
|     color: #ffffff; | ||||
|     background-color: #0f0f0f98; | ||||
|   } | ||||
|   button:active { | ||||
|     background-color: #0f0f0f69; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13
									
								
								src/ModManUi/src/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/ModManUi/src/main.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import "../public/favicon.ico"; | ||||
| import "./main.scss"; | ||||
|  | ||||
| import { createApp } from "vue"; | ||||
| import { createPinia } from "pinia"; | ||||
|  | ||||
| import App from "./App.vue"; | ||||
|  | ||||
| const app = createApp(App); | ||||
|  | ||||
| app.use(createPinia()); | ||||
|  | ||||
| app.mount("#app"); | ||||
							
								
								
									
										25
									
								
								src/ModManUi/src/native.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/ModManUi/src/native.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| export interface NativeMethods { | ||||
|   greet: (name: string) => Promise<string>; | ||||
| } | ||||
|  | ||||
| interface NativeEventMap { | ||||
|   greeting: string; | ||||
| } | ||||
|  | ||||
| type WebViewExtensions = { | ||||
|   webviewBinds: NativeMethods; | ||||
|   webviewAddEventListener<K extends keyof NativeEventMap>( | ||||
|     eventKey: K, | ||||
|     callback: (payload: NativeEventMap[K]) => void, | ||||
|   ): void; | ||||
|   webviewRemoveEventListener<K extends keyof NativeEventMap>( | ||||
|     eventKey: K, | ||||
|     callback: (payload: NativeEventMap[K]) => void, | ||||
|   ): boolean; | ||||
| }; | ||||
|  | ||||
| const windowWithWebViewExtensions = window as typeof window & WebViewExtensions; | ||||
|  | ||||
| export const webviewBinds = windowWithWebViewExtensions.webviewBinds; | ||||
| export const webviewAddEventListener = windowWithWebViewExtensions.webviewAddEventListener; | ||||
| export const webviewRemoveEventListener = windowWithWebViewExtensions.webviewRemoveEventListener; | ||||
							
								
								
									
										12
									
								
								src/ModManUi/src/stores/counter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/ModManUi/src/stores/counter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import { ref, computed } from "vue"; | ||||
| import { defineStore } from "pinia"; | ||||
|  | ||||
| export const useCounterStore = defineStore("counter", () => { | ||||
|   const count = ref(0); | ||||
|   const doubleCount = computed(() => count.value * 2); | ||||
|   function increment() { | ||||
|     count.value++; | ||||
|   } | ||||
|  | ||||
|   return { count, doubleCount, increment }; | ||||
| }); | ||||
							
								
								
									
										12
									
								
								src/ModManUi/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/ModManUi/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| { | ||||
|   "extends": "@vue/tsconfig/tsconfig.dom.json", | ||||
|   "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], | ||||
|   "exclude": ["src/**/__tests__/*"], | ||||
|   "compilerOptions": { | ||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", | ||||
|  | ||||
|     "paths": { | ||||
|       "@/*": ["./src/*"] | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										14
									
								
								src/ModManUi/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/ModManUi/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| { | ||||
|   "files": [], | ||||
|   "references": [ | ||||
|     { | ||||
|       "path": "./tsconfig.node.json" | ||||
|     }, | ||||
|     { | ||||
|       "path": "./tsconfig.app.json" | ||||
|     }, | ||||
|     { | ||||
|       "path": "./tsconfig.vitest.json" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/ModManUi/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/ModManUi/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| { | ||||
|   "extends": "@tsconfig/node22/tsconfig.json", | ||||
|   "include": [ | ||||
|     "vite.config.*", | ||||
|     "vitest.config.*", | ||||
|     "cypress.config.*", | ||||
|     "nightwatch.conf.*", | ||||
|     "playwright.config.*", | ||||
|     "eslint.config.*", | ||||
|     "build/**/*" | ||||
|   ], | ||||
|   "compilerOptions": { | ||||
|     "noEmit": true, | ||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", | ||||
|  | ||||
|     "module": "ESNext", | ||||
|     "moduleResolution": "Bundler", | ||||
|     "types": ["node"] | ||||
|   } | ||||
| } | ||||
							
								
								
									
										11
									
								
								src/ModManUi/tsconfig.vitest.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/ModManUi/tsconfig.vitest.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| { | ||||
|   "extends": "./tsconfig.app.json", | ||||
|   "include": ["src/**/__tests__/*", "env.d.ts"], | ||||
|   "exclude": [], | ||||
|   "compilerOptions": { | ||||
|     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", | ||||
|  | ||||
|     "lib": [], | ||||
|     "types": ["node", "jsdom"] | ||||
|   } | ||||
| } | ||||
							
								
								
									
										35
									
								
								src/ModManUi/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/ModManUi/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import { fileURLToPath, URL } from "node:url"; | ||||
|  | ||||
| import { defineConfig } from "vite"; | ||||
| import vue from "@vitejs/plugin-vue"; | ||||
| import vueDevTools from "vite-plugin-vue-devtools"; | ||||
| import headerTransformationPlugin from "./build/HeaderTransformationPlugin"; | ||||
|  | ||||
| // https://vite.dev/config/ | ||||
| export default defineConfig({ | ||||
|   build: { | ||||
|     copyPublicDir: false, | ||||
|     emptyOutDir: true, | ||||
|     rollupOptions: { | ||||
|       output: { | ||||
|         assetFileNames: "[name][extname]", | ||||
|         entryFileNames: "[name].js", | ||||
|         chunkFileNames: "[name].js", | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   plugins: [ | ||||
|     vue(), | ||||
|     vueDevTools(), | ||||
|     headerTransformationPlugin({ | ||||
|       outputPath: fileURLToPath( | ||||
|         new URL("../../build/src/ModMan/Web/ViteAssets.h", import.meta.url), | ||||
|       ), | ||||
|     }), | ||||
|   ], | ||||
|   resolve: { | ||||
|     alias: { | ||||
|       "@": fileURLToPath(new URL("./src", import.meta.url)), | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										13
									
								
								src/ModManUi/vitest.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/ModManUi/vitest.config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import { fileURLToPath } from "node:url"; | ||||
| import { mergeConfig, defineConfig } from "vitest/config"; | ||||
| import viteConfig from "./vite.config"; | ||||
|  | ||||
| export default mergeConfig( | ||||
|   viteConfig, | ||||
|   defineConfig({ | ||||
|     test: { | ||||
|       environment: "jsdom", | ||||
|       root: fileURLToPath(new URL("./", import.meta.url)), | ||||
|     }, | ||||
|   }), | ||||
| ); | ||||
							
								
								
									
										1
									
								
								thirdparty/webview
									
									
									
									
										vendored
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								thirdparty/webview
									
									
									
									
										vendored
									
									
										Submodule
									
								
							 Submodule thirdparty/webview added at 0ae23fc2da
									
								
							
							
								
								
									
										123
									
								
								thirdparty/webview.lua
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								thirdparty/webview.lua
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| webview = {} | ||||
|  | ||||
| function webview:include(includes) | ||||
| 	if includes:handle(self:name()) then | ||||
| 		includedirs { | ||||
| 			path.join(ThirdPartyFolder(), "webview/core/include"), | ||||
| 			path.join(self:msWebviewDir(), "build/native/include") | ||||
| 		} | ||||
| 	end | ||||
| end | ||||
|  | ||||
| function webview:link(links) | ||||
|  | ||||
| 	if os.host() == "windows" then | ||||
| 		links:add("WebView2LoaderStatic") | ||||
| 		 | ||||
| 		filter "platforms:x86" | ||||
| 			libdirs { | ||||
| 				path.join(self:msWebviewDir(), "build/native/x86") | ||||
| 			} | ||||
| 		filter {} | ||||
| 		filter "platforms:x64" | ||||
| 			libdirs { | ||||
| 				path.join(self:msWebviewDir(), "build/native/x64") | ||||
| 			} | ||||
| 		filter {} | ||||
| 	end | ||||
|  | ||||
| 	links:add(self:name()) | ||||
| end | ||||
|  | ||||
| function webview:use() | ||||
| 	 | ||||
| end | ||||
|  | ||||
| function webview:name() | ||||
| 	return "webview" | ||||
| end | ||||
|  | ||||
| function webview:project() | ||||
| 	local folder = ThirdPartyFolder() | ||||
| 	local includes = Includes:create() | ||||
|  | ||||
| 	project(self:name()) | ||||
|         targetdir(TargetDirectoryLib) | ||||
| 		location "%{wks.location}/thirdparty/%{prj.name}" | ||||
| 		kind "StaticLib" | ||||
| 		language "C++" | ||||
| 		 | ||||
| 		files {  | ||||
| 			path.join(folder, "webview/core/include/**.h"), | ||||
| 			path.join(folder, "webview/core/include/**.hh"), | ||||
| 			path.join(folder, "webview/core/src/**.cc") | ||||
| 		} | ||||
| 		 | ||||
| 		defines { | ||||
| 			"WEBVIEW_STATIC" | ||||
| 		} | ||||
|  | ||||
| 		if os.host() == "windows" then | ||||
| 			self:installWebview2() | ||||
| 		end | ||||
|  | ||||
| 		filter { "system:linux", "action:gmake" } | ||||
|   			buildoptions { "`pkg-config --cflags gtk4 webkitgtk-6.0`" } | ||||
|   			linkoptions { "`pkg-config --libs gtk4 webkitgtk-6.0`" } | ||||
| 		filter {} | ||||
| 		 | ||||
| 		self:include(includes) | ||||
|  | ||||
| 		-- Disable warnings. They do not have any value to us since it is not our code. | ||||
| 		warnings "off" | ||||
| end | ||||
|  | ||||
| function webview:msWebviewDir() | ||||
| 	return path.join(BuildFolder(), "thirdparty/ms-webview2") | ||||
| end | ||||
|  | ||||
| function webview:installWebview2() | ||||
| 	local version = "1.0.3485.44" | ||||
| 	local webviewDir = self:msWebviewDir() | ||||
| 	local versionFile = path.join(webviewDir, "ms-webview2.txt") | ||||
| 	local nuspecFile = path.join(webviewDir, "Microsoft.Web.WebView2.nuspec") | ||||
| 	local nupkgFile = path.join(webviewDir, "microsoft.web.webview2.nupkg.zip") | ||||
| 	local url = "https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/" .. version | ||||
|  | ||||
| 	if not os.isdir(webviewDir) then | ||||
| 		os.mkdir(webviewDir) | ||||
| 	end | ||||
|  | ||||
| 	local installedVersion = io.readfile(versionFile) | ||||
| 	if installedVersion == version and os.isfile(nuspecFile) then | ||||
| 		return | ||||
| 	end | ||||
|  | ||||
| 	function progress(total, current)		 | ||||
| 		local ratio = current / total; | ||||
| 		ratio = math.min(math.max(ratio, 0), 1); | ||||
| 		local percent = math.floor(ratio * 100); | ||||
| 		io.write("\rDownload progress (" .. percent .. "%/100%)") | ||||
| 	end | ||||
|  | ||||
| 	print("Downloading Microsoft.Web.WebView2 " .. version .. "...") | ||||
| 	local result_str, response_code = http.download(url, nupkgFile, { | ||||
| 		progress = progress | ||||
| 	}) | ||||
|  | ||||
| 	io.write("\n") | ||||
| 	 | ||||
| 	if result_str ~= "OK" then | ||||
| 		premake.error("Failed to download Microsoft.Web.WebView2") | ||||
| 	end | ||||
|  | ||||
| 	-- local hash = string.sha1(io.readfile(nupkgFile)) | ||||
| 	-- print(hash) | ||||
|  | ||||
| 	print("Extracting Microsoft.Web.WebView2 " .. version .. "...") | ||||
| 	zip.extract(nupkgFile, webviewDir) | ||||
|  | ||||
| 	os.remove(nupkgFile) | ||||
|  | ||||
| 	io.writefile(versionFile, version) | ||||
| end | ||||
| @@ -16,3 +16,10 @@ newoption { | ||||
|     trigger = "experimental-material-compilation", | ||||
|     description = "Activate experimental material compilation support" | ||||
| } | ||||
|  | ||||
| -- ModMan is currently experimental and deactivated by default. | ||||
| -- For more information see src/ModMan/README.md | ||||
| newoption { | ||||
|     trigger = "modman", | ||||
|     description = "Activate experimental compilation of ModMan (OAT GUI)" | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user