2
0
mirror of https://github.com/Laupetin/OpenAssetTools.git synced 2026-07-03 06:18:11 +00:00

refactor: use new webwindowed api (#831)

* chore: update webview with new api

* chore: update modman to use new webview api

* chore: use title handler plugin from webview lib

* chore: use favicon plugin from webview lib

* chore: use vite-plugin-cpp-header from webview repo

* chore: use asset handler from webview lib

* chore: make webview utility

* chore: rename webview to webwindowed

* chore: Rename code usages to webwindowed
This commit is contained in:
Jan
2026-06-16 09:50:34 +02:00
committed by GitHub
parent b2aa4749c1
commit 8dba13f913
41 changed files with 352 additions and 1116 deletions
@@ -1,223 +0,0 @@
import type { Plugin, UserConfig } from "vite";
import type { OutputAsset, OutputChunk } from "rolldown";
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>;
interface PublicDirFile {
fullPath: string;
relativePath: string;
}
const textEncoder = new TextEncoder();
function getPublicDirFiles(publicDir?: string): PublicDirFile[] {
if (!publicDir) return [];
const result: PublicDirFile[] = [];
const files = fs.readdirSync(publicDir, { recursive: true, withFileTypes: true });
for (const file of files) {
if (!file.isFile()) continue;
const fullPath = path.join(file.parentPath, file.name);
let relativePath = path.relative(publicDir, fullPath).replaceAll(/\\/g, "/");
if (relativePath.startsWith("./")) {
relativePath = relativePath.substring(2);
}
result.push({
fullPath,
relativePath,
});
}
return result;
}
function createVarName(fileName: string) {
return fileName.replaceAll(/[\\/]/g, "__").replaceAll(/[\.-]/g, "_").toUpperCase();
}
function transformAsset(asset: MinimalOutputAsset) {
const varName = createVarName(asset.fileName);
let buffer: Uint8Array;
if (typeof asset.source === "string") {
buffer = textEncoder.encode(asset.source);
} else {
buffer = asset.source;
}
const bytes = [...buffer].map((v) => String(v)).join(",");
return `constexpr const unsigned char ${varName}[] {${bytes}};
`;
}
function transformChunk(chunk: MinimalOutputChunk) {
const varName = createVarName(chunk.fileName);
const buffer = textEncoder.encode(chunk.code);
const bytes = [...buffer].map((v) => String(v)).join(",");
return `constexpr const unsigned char ${varName}[] {${bytes}};
`;
}
function transformPublicFile(publicFile: PublicDirFile) {
const varName = createVarName(publicFile.relativePath);
const bytes = [...fs.readFileSync(publicFile.fullPath)].map((v) => String(v)).join(",");
return `constexpr const unsigned char ${varName}[] {${bytes}};
`;
}
function writeHeader(
bundle: MinimalOutputBundle,
outputDir?: string,
options?: HeaderTransformationPluginOptions,
publicDir?: string,
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"};
`,
);
const fileNames: string[] = [];
for (const curBundle of Object.values(bundle)) {
if (curBundle.type === "asset") {
fs.writeSync(fd, transformAsset(curBundle));
} else {
fs.writeSync(fd, transformChunk(curBundle));
}
fileNames.push(curBundle.fileName);
}
for (const publicDirFile of getPublicDirFiles(publicDir)) {
fs.writeSync(fd, transformPublicFile(publicDirFile));
fileNames.push(publicDirFile.relativePath);
}
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 fileName of fileNames) {
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;
let publicDir: string | undefined = undefined;
return {
name: "vite-plugin-header-transformation",
enforce: "post",
config(userOptions: UserConfig, env) {
if (env.command === "serve") {
writeServerActive = true;
} else {
writeBundleActive = true;
}
if (typeof userOptions.publicDir === "string") {
publicDir = userOptions.publicDir;
}
},
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,
publicDir,
server.config.server.port,
);
});
},
writeBundle(outputOptions, bundle) {
if (!writeBundleActive) {
return;
}
writeHeader(bundle, outputOptions.dir, options, publicDir);
},
};
}
+1
View File
@@ -22,6 +22,7 @@
"vue-router": "5.1.0"
},
"devDependencies": {
"@laupetin/vite-plugin-cpp-header": "1.1.0",
"@tsconfig/node24": "24.0.4",
"@types/jsdom": "28.0.3",
"@types/node": "25.9.2",
+10 -8
View File
@@ -7,20 +7,22 @@ export type NativeMethods = AssetBinds & DialogBinds & UnlinkingBinds & ZoneBind
type NativeEventMap = UnlinkingEventMap & ZoneEventMap;
type WebViewExtensions = {
webviewBinds: NativeMethods;
webviewAddEventListener<K extends keyof NativeEventMap>(
type WebWindowedExtensions = {
webwindowedBinds: NativeMethods;
webwindowedAddEventListener<K extends keyof NativeEventMap>(
eventKey: K,
callback: (payload: NativeEventMap[K]) => void,
): void;
webviewRemoveEventListener<K extends keyof NativeEventMap>(
webwindowedRemoveEventListener<K extends keyof NativeEventMap>(
eventKey: K,
callback: (payload: NativeEventMap[K]) => void,
): boolean;
};
const windowWithWebViewExtensions = window as typeof window & WebViewExtensions;
const windowWithWebWindowedExtensions = window as typeof window & WebWindowedExtensions;
export const webviewBinds = windowWithWebViewExtensions.webviewBinds;
export const webviewAddEventListener = windowWithWebViewExtensions.webviewAddEventListener;
export const webviewRemoveEventListener = windowWithWebViewExtensions.webviewRemoveEventListener;
export const webwindowedBinds = windowWithWebWindowedExtensions.webwindowedBinds;
export const webwindowedAddEventListener =
windowWithWebWindowedExtensions.webwindowedAddEventListener;
export const webwindowedRemoveEventListener =
windowWithWebWindowedExtensions.webwindowedRemoveEventListener;
+2 -2
View File
@@ -1,7 +1,7 @@
import { computed, ref } from "vue";
import { defineStore } from "pinia";
import type { ZoneAssetsDto } from "@/native/AssetBinds";
import { webviewBinds } from "@/native";
import { webwindowedBinds } from "@/native";
export const useAssetStore = defineStore("asset", () => {
const zoneName = ref<string | null>(null);
@@ -21,7 +21,7 @@ export const useAssetStore = defineStore("asset", () => {
// Only load assets when there is a new zone name specified
if (!newZoneName) return;
webviewBinds.getAssetsForZone(newZoneName).then((res) => {
webwindowedBinds.getAssetsForZone(newZoneName).then((res) => {
if (zoneName.value === newZoneName) {
assetsOfZone.value = res;
}
+3 -3
View File
@@ -1,6 +1,6 @@
import { ref } from "vue";
import { defineStore } from "pinia";
import { webviewAddEventListener, webviewBinds } from "@/native";
import { webwindowedAddEventListener, webwindowedBinds } from "@/native";
export const useUnlinkingStore = defineStore("unlinking", () => {
const isUnlinking = ref(false);
@@ -11,7 +11,7 @@ export const useUnlinkingStore = defineStore("unlinking", () => {
isUnlinking.value = true;
lastPercentage.value = 0;
failureMessage.value = null;
return webviewBinds
return webwindowedBinds
.unlinkZone(zoneName)
.catch((e: string) => {
console.error("Failed to unlink fastfile:", e);
@@ -23,7 +23,7 @@ export const useUnlinkingStore = defineStore("unlinking", () => {
});
}
webviewAddEventListener("zoneUnlinkProgress", (dto) => {
webwindowedAddEventListener("zoneUnlinkProgress", (dto) => {
lastPercentage.value = dto.percentage;
});
+6 -6
View File
@@ -1,6 +1,6 @@
import { computed, ref } from "vue";
import { defineStore } from "pinia";
import { webviewAddEventListener, webviewBinds } from "@/native";
import { webwindowedAddEventListener, webwindowedBinds } from "@/native";
import type { ZoneDto, ZoneLoadedDto } from "@/native/ZoneBinds";
export const useZoneStore = defineStore("zone", () => {
@@ -21,7 +21,7 @@ export const useZoneStore = defineStore("zone", () => {
zonesCurrentlyBeingLoaded.value.push(expectedZoneName);
lastPercentageByZoneName.value[expectedZoneName] = 0;
return webviewBinds
return webwindowedBinds
.loadFastFile(fastFilePath)
.catch((e: string) => {
console.error("Failed to load fastfile:", e);
@@ -44,21 +44,21 @@ export const useZoneStore = defineStore("zone", () => {
}
// Initially get all loaded zones
webviewBinds.getZones().then((allZones) => {
webwindowedBinds.getZones().then((allZones) => {
loadedZones.value = allZones;
});
webviewAddEventListener("zoneLoadProgress", (dto) => {
webwindowedAddEventListener("zoneLoadProgress", (dto) => {
if (lastPercentageByZoneName.value[dto.zoneName] !== undefined) {
lastPercentageByZoneName.value[dto.zoneName] = dto.percentage;
}
});
webviewAddEventListener("zoneLoaded", (dto) => {
webwindowedAddEventListener("zoneLoaded", (dto) => {
loadedZones.value.push(dto.zone);
});
webviewAddEventListener("zoneUnloaded", (dto) => {
webwindowedAddEventListener("zoneUnloaded", (dto) => {
const index = loadedZones.value.findIndex((zone) => zone.name === dto.zoneName);
if (index >= 0) {
loadedZones.value.splice(index, 1);
@@ -4,7 +4,7 @@ import ProgressBar from "primevue/progressbar";
import Listbox from "primevue/listbox";
import { computed } from "vue";
import { useZoneStore } from "@/stores/ZoneStore";
import { webviewBinds } from "@/native";
import { webwindowedBinds } from "@/native";
interface SelectableZone {
isLoading: boolean;
@@ -15,7 +15,9 @@ const zoneStore = useZoneStore();
const selectedZone = defineModel<string | null>("selectedZone");
async function openFastFileSelect() {
return await webviewBinds.openFileDialog({ filters: [{ name: "Fastfiles", filter: "*.ff" }] });
return await webwindowedBinds.openFileDialog({
filters: [{ name: "Fastfiles", filter: "*.ff" }],
});
}
async function onOpenFastFileClick() {
@@ -51,7 +53,7 @@ const availableZones = computed<SelectableZone[]>(() => {
function onUnloadClicked() {
if (!selectedZone.value) return;
webviewBinds.unloadZone(selectedZone.value).catch((e: string) => {
webwindowedBinds.unloadZone(selectedZone.value).catch((e: string) => {
console.error("Failed to unload zone:", e);
});
}
+3 -3
View File
@@ -3,13 +3,13 @@ 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";
import PluginCppHeader from "@laupetin/vite-plugin-cpp-header";
// https://vite.dev/config/
export default defineConfig({
build: {
emptyOutDir: true,
rollupOptions: {
rolldownOptions: {
output: {
assetFileNames: "[name][extname]",
entryFileNames: "[name].js",
@@ -21,7 +21,7 @@ export default defineConfig({
plugins: [
vue(),
vueDevTools(),
headerTransformationPlugin({
PluginCppHeader({
outputPath: fileURLToPath(
new URL("../../build/src/ModMan/Web/ViteAssets.h", import.meta.url),
),