From e1397e2dbb1ae553f10e26ce71608711a24f80c8 Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Sat, 21 Oct 2023 12:55:02 +1100 Subject: [PATCH] Implement display: transient/persistent --- client/main.js | 14 ++ client/package.json | 2 +- client/package.sh | 4 +- client/preload.js | 9 + .../layer-editor/layers/traceLayerSource.tsx | 16 +- .../components/renderer/parser/parseTrace.ts | 2 +- .../renderer/parser/parseTrace.worker.ts | 26 +- client/src/hooks/useTitleBar.tsx | 13 + client/src/public/coi.js | 232 ++++++++++-------- 9 files changed, 204 insertions(+), 114 deletions(-) create mode 100644 client/preload.js 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); + } + ); + } + })(); +}