diff --git a/client/main.js b/client/main.js
index 27ebc1a4..0e28247b 100644
--- a/client/main.js
+++ b/client/main.js
@@ -54,6 +54,14 @@ server.listen(0, () => {
autoHideMenuBar: true,
center: true,
show: false,
+ titleBarStyle: "hidden",
+ titleBarOverlay: {
+ color: "#00000000",
+ symbolColor: "#00000000",
+ },
+ webPreferences: {
+ preload: path.resolve(__dirname, "preload.js"),
+ },
});
win.loadURL(`http://localhost:${port}/index.html`);
win.maximize();
@@ -61,6 +69,12 @@ server.listen(0, () => {
win.webContents.on("did-finish-load", () => {
win.webContents.setZoomFactor(0.9);
});
+ electron.ipcMain.handle("title-bar", (_e, background, foreground) => {
+ win.setTitleBarOverlay({
+ color: background,
+ symbolColor: foreground,
+ });
+ });
};
electron.app.whenReady().then(() => {
createWindow();
diff --git a/client/package.json b/client/package.json
index d356e00d..a43b8b23 100644
--- a/client/package.json
+++ b/client/package.json
@@ -48,7 +48,7 @@
},
"scripts": {
"start": "vite",
- "build": "vite build",
+ "build": "vite build --emptyOutDir",
"package": "sh ./package.sh",
"test": "vitest"
},
diff --git a/client/package.sh b/client/package.sh
index 4a40b2c8..d3fb0b57 100644
--- a/client/package.sh
+++ b/client/package.sh
@@ -1,2 +1,2 @@
-npx electron-packager . Waypoint --dir dist --platform=win32 --arch=x64 --electronVersion=26.2.1 --ignore node_modules --ignore src --overwrite --icon ./dist/favicon --out bin
-npx electron-packager . Waypoint --dir dist --platform=linux --arch=x64 --electronVersion=26.2.1 --ignore node_modules --ignore src --overwrite --icon ./dist/favicon --out bin
+npx electron-packager . Visualiser --dir dist --platform=win32 --arch=x64 --electronVersion=26.2.1 --ignore node_modules --ignore src --overwrite --icon ./dist/favicon --out bin
+npx electron-packager . Visualiser --dir dist --platform=linux --arch=x64 --electronVersion=26.2.1 --ignore node_modules --ignore src --overwrite --icon ./dist/favicon --out bin
diff --git a/client/preload.js b/client/preload.js
new file mode 100644
index 00000000..ad47cf68
--- /dev/null
+++ b/client/preload.js
@@ -0,0 +1,9 @@
+process.once("loaded", () => {
+ const { contextBridge, ipcRenderer } = require("electron");
+
+ contextBridge.exposeInMainWorld("electron", {
+ async invoke(eventName, ...params) {
+ return await ipcRenderer.invoke(eventName, ...params);
+ },
+ });
+});
diff --git a/client/src/components/layer-editor/layers/traceLayerSource.tsx b/client/src/components/layer-editor/layers/traceLayerSource.tsx
index 09f7a243..44dcc6d9 100644
--- a/client/src/components/layer-editor/layers/traceLayerSource.tsx
+++ b/client/src/components/layer-editor/layers/traceLayerSource.tsx
@@ -156,14 +156,26 @@ export const traceLayerSource: LayerSource<"trace", TraceLayerData> = {
const path = use2DPath(layer, throttledStep);
const steps = useMemo(
() =>
- map(result?.steps, (c) =>
+ map(result?.stepsPersistent, (c) =>
map(c, (d) => merge(d, { meta: { sourceLayer: layer?.key } }))
),
- [result?.steps, layer]
+ [result?.stepsPersistent, layer]
+ );
+ const steps1 = useMemo(
+ () =>
+ map(result?.stepsTransient, (c) =>
+ map(c, (d) => merge(d, { meta: { sourceLayer: layer?.key } }))
+ ),
+ [result?.stepsTransient, layer]
+ );
+ const steps2 = useMemo(
+ () => [steps1[throttledStep] ?? []],
+ [steps1, throttledStep]
);
return (
<>
+
{path}
>
);
diff --git a/client/src/components/renderer/parser/parseTrace.ts b/client/src/components/renderer/parser/parseTrace.ts
index 505ca163..2e8ac0a2 100644
--- a/client/src/components/renderer/parser/parseTrace.ts
+++ b/client/src/components/renderer/parser/parseTrace.ts
@@ -32,7 +32,7 @@ export function useTrace(params: ParseTraceWorkerParameters) {
const output = await parseTraceAsync(params);
push(
"Trace loaded",
- pluralize("step", output?.steps?.length ?? 0, true)
+ pluralize("step", output?.stepsPersistent?.length ?? 0, true)
);
return output;
}
diff --git a/client/src/components/renderer/parser/parseTrace.worker.ts b/client/src/components/renderer/parser/parseTrace.worker.ts
index bdacd397..bc5e7f68 100644
--- a/client/src/components/renderer/parser/parseTrace.worker.ts
+++ b/client/src/components/renderer/parser/parseTrace.worker.ts
@@ -1,4 +1,4 @@
-import { chain, findLast, map } from "lodash";
+import { chain, findLast, map, mapValues, negate } from "lodash";
import {
CompiledComponent,
EventContext,
@@ -17,6 +17,9 @@ type Key = string | number;
type KeyRef = Key | null | undefined;
+const isPersistent = (c: CompiledComponent>) =>
+ c.display !== "transient";
+
function parse({
trace,
context,
@@ -61,21 +64,25 @@ function parse({
.value();
const steps = chain(trace?.events)
- .map((e, i, esx) =>
- apply(e, {
+ .map((e, i, esx) => {
+ const component = apply(e, {
...context,
step: i,
parent: !isNullish(e.pId)
? esx[findLast(r[e.pId], (x) => x.step <= i)?.step ?? 0]
: undefined,
- })
- )
- .map((c) => c.filter(isVisible))
- .map((c, i) => c.map(makeEntryIteratee(i)))
+ });
+ const persistent = component.filter(isPersistent);
+ const transient = component.filter(negate(isPersistent));
+ return { persistent, transient };
+ })
+ .map((c) => mapValues(c, (b) => b.filter(isVisible)))
+ .map((c, i) => mapValues(c, (b) => b.map(makeEntryIteratee(i))))
.value();
return {
- steps,
+ stepsPersistent: map(steps, "persistent"),
+ stepsTransient: map(steps, "transient"),
};
}
@@ -86,7 +93,8 @@ export type ParseTraceWorkerParameters = {
};
export type ParseTraceWorkerReturnType = {
- steps: ComponentEntry[][];
+ stepsPersistent: ComponentEntry[][];
+ stepsTransient: ComponentEntry[][];
};
onmessage = ({ data }: MessageEvent) => {
diff --git a/client/src/hooks/useTitleBar.tsx b/client/src/hooks/useTitleBar.tsx
index 19ef876a..cae1290b 100644
--- a/client/src/hooks/useTitleBar.tsx
+++ b/client/src/hooks/useTitleBar.tsx
@@ -1,5 +1,11 @@
import { useEffect } from "react";
import { name } from "manifest.json";
+import { getContrastRatio } from "@mui/material";
+
+const getForegroundColor = (bg: string) =>
+ getContrastRatio(bg, "#ffffff") > getContrastRatio(bg, "#000000")
+ ? "#ffffff"
+ : "#000000";
export function useTitleBar(color: string) {
useEffect(() => {
@@ -7,5 +13,12 @@ export function useTitleBar(color: string) {
.querySelector('meta[name="theme-color"]')!
.setAttribute("content", color);
document.title = name;
+ if ("electron" in window) {
+ (window.electron as any).invoke(
+ "title-bar",
+ "#00000000",
+ getForegroundColor(color)
+ );
+ }
}, [color]);
}
diff --git a/client/src/public/coi.js b/client/src/public/coi.js
index 2ce52153..bae8820d 100644
--- a/client/src/public/coi.js
+++ b/client/src/public/coi.js
@@ -1,102 +1,136 @@
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
-let coepCredentialless = !1;
-"undefined" == typeof window
- ? (self.addEventListener("install", () => self.skipWaiting()),
- self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim())),
- self.addEventListener("message", (e) => {
- e.data &&
- ("deregister" === e.data.type
- ? self.registration
- .unregister()
- .then(() => self.clients.matchAll())
- .then((e) => {
- e.forEach((e) => e.navigate(e.url));
- })
- : "coepCredentialless" === e.data.type &&
- (coepCredentialless = e.data.value));
- }),
- self.addEventListener("fetch", function (e) {
- const r = e.request;
- if ("only-if-cached" === r.cache && "same-origin" !== r.mode) return;
- const s =
- coepCredentialless && "no-cors" === r.mode
- ? new Request(r, { credentials: "omit" })
- : r;
- e.respondWith(
- fetch(s)
- .then((e) => {
- if (0 === e.status) return e;
- const r = new Headers(e.headers);
- return (
- r.set(
- "Cross-Origin-Embedder-Policy",
- coepCredentialless ? "credentialless" : "require-corp"
- ),
- coepCredentialless ||
- r.set("Cross-Origin-Resource-Policy", "cross-origin"),
- r.set("Cross-Origin-Opener-Policy", "same-origin"),
- new Response(e.body, {
- status: e.status,
- statusText: e.statusText,
- headers: r,
- })
- );
+let coepCredentialless = false;
+if (typeof window === "undefined") {
+ self.addEventListener("install", () => self.skipWaiting());
+ self.addEventListener("activate", (event) =>
+ event.waitUntil(self.clients.claim())
+ );
+
+ self.addEventListener("message", (ev) => {
+ if (!ev.data) {
+ return;
+ } else if (ev.data.type === "deregister") {
+ self.registration
+ .unregister()
+ .then(() => {
+ return self.clients.matchAll();
+ })
+ .then((clients) => {
+ clients.forEach((client) => client.navigate(client.url));
+ });
+ } else if (ev.data.type === "coepCredentialless") {
+ coepCredentialless = ev.data.value;
+ }
+ });
+
+ self.addEventListener("fetch", function (event) {
+ const r = event.request;
+ if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
+ return;
+ }
+
+ const request =
+ coepCredentialless && r.mode === "no-cors"
+ ? new Request(r, {
+ credentials: "omit",
})
- .catch((e) => console.error(e))
- );
- }))
- : (() => {
- const e = {
- shouldRegister: () => !0,
- shouldDeregister: () => !1,
- coepCredentialless: () =>
- window.chrome !== undefined || window.netscape !== undefined,
- doReload: () => window.location.reload(),
- quiet: !1,
- ...window.coi,
- },
- r = navigator;
- r.serviceWorker &&
- r.serviceWorker.controller &&
- (r.serviceWorker.controller.postMessage({
- type: "coepCredentialless",
- value: e.coepCredentialless(),
- }),
- e.shouldDeregister() &&
- r.serviceWorker.controller.postMessage({ type: "deregister" })),
- !1 === window.crossOriginIsolated &&
- e.shouldRegister() &&
- (window.isSecureContext
- ? r.serviceWorker &&
- r.serviceWorker.register(window.document.currentScript.src).then(
- (s) => {
- !e.quiet &&
- console.log("COOP/COEP Service Worker registered", s.scope),
- s.addEventListener("updatefound", () => {
- !e.quiet &&
- console.log(
- "Reloading page to make use of updated COOP/COEP Service Worker."
- ),
- e.doReload();
- }),
- s.active &&
- !r.serviceWorker.controller &&
- (!e.quiet &&
- console.log(
- "Reloading page to make use of COOP/COEP Service Worker."
- ),
- e.doReload());
- },
- (r) => {
- !e.quiet &&
- console.error(
- "COOP/COEP Service Worker failed to register:",
- r
- );
- }
- )
- : !e.quiet &&
+ : r;
+ event.respondWith(
+ fetch(request)
+ .then((response) => {
+ if (response.status === 0) {
+ return response;
+ }
+
+ const newHeaders = new Headers(response.headers);
+ newHeaders.set(
+ "Cross-Origin-Embedder-Policy",
+ coepCredentialless ? "credentialless" : "require-corp"
+ );
+ if (!coepCredentialless) {
+ newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
+ }
+ newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
+
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers: newHeaders,
+ });
+ })
+ .catch((e) => console.error(e))
+ );
+ });
+} else {
+ (() => {
+ // You can customize the behavior of this script through a global `coi` variable.
+ const coi = {
+ shouldRegister: () => true,
+ shouldDeregister: () => false,
+ coepCredentialless: () =>
+ window.chrome !== undefined || window.netscape !== undefined,
+ doReload: () => window.location.reload(),
+ quiet: false,
+ ...window.coi,
+ };
+
+ const n = navigator;
+
+ if (n.serviceWorker && n.serviceWorker.controller) {
+ n.serviceWorker.controller.postMessage({
+ type: "coepCredentialless",
+ value: coi.coepCredentialless(),
+ });
+
+ if (coi.shouldDeregister()) {
+ n.serviceWorker.controller.postMessage({ type: "deregister" });
+ }
+ }
+
+ // If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
+ // already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
+ if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return;
+
+ if (!window.isSecureContext) {
+ !coi.quiet &&
+ console.log(
+ "COOP/COEP Service Worker not registered, a secure context is required."
+ );
+ return;
+ }
+
+ // In some environments (e.g. Chrome incognito mode) this won't be available
+ if (n.serviceWorker) {
+ n.serviceWorker.register(window.document.currentScript.src).then(
+ (registration) => {
+ !coi.quiet &&
+ console.log(
+ "COOP/COEP Service Worker registered",
+ registration.scope
+ );
+
+ registration.addEventListener("updatefound", () => {
+ !coi.quiet &&
console.log(
- "COOP/COEP Service Worker not registered, a secure context is required."
- ));
- })();
+ "Reloading page to make use of updated COOP/COEP Service Worker."
+ );
+ coi.doReload();
+ });
+
+ // If the registration is active, but it's not controlling the page
+ if (registration.active && !n.serviceWorker.controller) {
+ !coi.quiet &&
+ console.log(
+ "Reloading page to make use of COOP/COEP Service Worker."
+ );
+ coi.doReload();
+ }
+ },
+ (err) => {
+ !coi.quiet &&
+ console.error("COOP/COEP Service Worker failed to register:", err);
+ }
+ );
+ }
+ })();
+}