From 552e16a1c1fc86ecdf103eb1c0d97ec4cb93209b Mon Sep 17 00:00:00 2001 From: Darrell van Swinderen Date: Tue, 22 Sep 2026 23:36:34 +0200 Subject: [PATCH] revert: open the session in the editor from the post-game card (#34) Reverts #34 (#19) on request. The overlay card goes back to what it was before: no button, no preload, `setIgnoreMouseEvents(true)` without forwarding, so nothing drawn over a game can take a click. `src/preload/toast.ts`, the route token, `?cut=highlights` in the editor and `scripts/toast-button-check.mjs` go with it. The main preload goes back to `index.mjs` as ESM. `.cjs` was only ever needed because the toast's preload was sandboxed, and that preload is gone. Conflicts resolved in `electron.vite.config.ts` (the pre-#34 single entry) and `CLAUDE.md` (the commands added since #34 are kept). `npm run check`: 689 tests. `npm run build` green. `toast.spec.ts` and `window-state.spec.ts`: 4 passed. Co-Authored-By: Claude Opus 5.5 (1M context) --- CLAUDE.md | 25 -- electron.vite.config.ts | 40 +-- scripts/toast-button-check.mjs | 134 ---------- src/main/index.ts | 19 +- src/main/services/capture/sessionWatch.ts | 10 +- src/main/services/clipToast.ts | 291 +--------------------- src/preload/toast.ts | 38 --- src/renderer/src/views/EditorPage.vue | 20 +- 8 files changed, 10 insertions(+), 567 deletions(-) delete mode 100644 scripts/toast-button-check.mjs delete mode 100644 src/preload/toast.ts diff --git a/CLAUDE.md b/CLAUDE.md index cc2b7606..4b32271f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,6 @@ node scripts/foreground-track-check.mjs # prove the helper tells a running proc node scripts/trim-check.mjs # run the shipped trim on a real clip and check where it landed node scripts/clip-audio-check.mjs # prove muting a track mutes it, through the real ffmpeg node scripts/compress-check.mjs # squeeze a real recording and read back what landed -node scripts/toast-button-check.mjs # press the overlay card's button and see where the app goes node scripts/publisher-views-check.mjs # run the real publisher and see what it counts node scripts/discord-webhook-check.mjs # run the publisher against a fake Discord and read what it sent node scripts/streamdeck-check.mjs # start the app with the Stream Deck server on and knock on its door @@ -577,30 +576,6 @@ clip takes seconds to reach the library. **Off by opting out**, in Settings, Rec Borderless windowed, the default in most modern games and what OBS display capture wants anyway, is fine. The Settings text says so. -**The card can be pressed, in exactly one rectangle.** The sweep's card offers *Open in the editor*, -which opens the session already cut. That undoes three of the rules above, narrowly, and each -narrowing is the load-bearing part: - -- `setIgnoreMouseEvents(true, { forward: true })`, so clicks still pass through the card to the game - and the page still sees the pointer. The page says when the pointer is over the button and main - turns the ignoring off for exactly that long. Blanket-enabling the mouse would put a window in - front of a game that can swallow a click. -- **`focusable: false` stays.** A non-focusable window still receives mouse events, so nothing here - needs it relaxed, and relaxing it is the one change that could put somebody at their desktop - mid-match. -- A preload, which this window deliberately did not have. `src/preload/toast.ts`, two functions, and - **not** `window.goodbit`: a sandboxed page over somebody's game has no business with the API that - deletes clips. Main holds the route and hands the page an opaque token, so the window cannot - choose what the button does, which is the same rule `deeplink.ts` applies to a `goodbit://` link. - -**Preloads are CommonJS, `.cjs`, and both facts are load-bearing.** A sandboxed preload must be -CommonJS or it dies on its first `import`, and `out/` sits under a `"type": "module"` package, so a -`.js` file there is read as ESM whatever is inside it. **Electron reports neither failure**: the -card drew its button, the click did nothing, and `window.goodbitToast` was undefined with nothing in -any log. The path is handed in from `index.ts` for the same class of reason: `clipToast.ts` is a -dynamic import, so it lands in `out/main/chunks/` and its own `import.meta.dirname` is not where it -looks like it is. - Both windows are real, so **`BrowserWindow.getAllWindows()[0]` is no longer the app**, and neither is Playwright's `firstWindow()`. `tests/e2e/app.ts` and `window-state.spec.ts` pick the window whose URL is not a `data:` one. Ten tests across six specs failed at once when that was missed. diff --git a/electron.vite.config.ts b/electron.vite.config.ts index c96b3c8e..a740831e 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -45,45 +45,7 @@ export default defineConfig({ build: { rollupOptions: { external: ['electron'], - /* - * Two bridges, deliberately. - * - * `index` is the whole `window.goodbit` API, for the app's own window. - * `toast` is two functions for the overlay card that sits over - * somebody's game, which had no preload at all until it grew a button. - * Handing that sandboxed page the app's API to use two of its calls - * would be the opposite of what that window is for. - */ - input: { - index: resolve('src/preload/index.ts'), - toast: resolve('src/preload/toast.ts'), - }, - /* - * CommonJS, and that is not a preference. - * - * The toast overlay runs with `sandbox: true`, and Electron loads a - * sandboxed preload as CommonJS only: an ESM one throws on its first - * `import` and is dropped **silently**, which is exactly how it - * presented: the card rendered its button and `window.goodbitToast` - * was undefined. - * - * Both entries rather than one, because two formats out of one rollup - * build is not a thing, and CJS is the form that works in a sandboxed - * preload and an unsandboxed one alike. `src/main/index.ts` points at - * `.js` accordingly. - */ - output: { - format: 'cjs', - /* - * `.cjs`, not `.js`. - * - * The root `package.json` says `type: module`, and `out/` sits under - * it, so a `.js` file there is read as ESM whatever is inside it and - * a CommonJS preload dies on its first `require`. The extension is - * the only thing that overrides that. - */ - entryFileNames: '[name].cjs', - }, + input: { index: resolve('src/preload/index.ts') }, }, }, }, diff --git a/scripts/toast-button-check.mjs b/scripts/toast-button-check.mjs deleted file mode 100644 index 6f7211bd..00000000 --- a/scripts/toast-button-check.mjs +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Press the card's one button, and see what the app does about it. - * - * The overlay is the window whose entire job is to never disturb a game: - * `focusable: false`, `showInactive()`, always on top at the screen-saver - * level, and until this feature it ignored the mouse completely and had no - * preload at all. Giving it a button undoes part of that, narrowly, and this - * is what says the narrow part still holds. - * - * Four questions: - * - * **Is the bridge the small one?** The card gets `window.goodbitToast` with - * two functions on it, and must not get `window.goodbit`, which is the API - * that deletes clips. A sandboxed page sitting over somebody's game has no - * business with that. - * - * **Can the window choose what the button does?** It must not. Main holds the - * route and hands the page an opaque token; a token main did not issue is - * ignored. Same rule `deeplink.ts` applies to a `goodbit://` link. - * - * **Does the press actually reach the app?** Through `openIn`, which handles a - * closed, a minimised and an already-open window. - * - * **Is the offer spent afterwards?** A second click on a fading card must not - * open a second editor. - * - * The one thing this cannot answer is the one that could kill the feature: - * whether the card still refuses to steal focus **over a real borderless - * fullscreen game**. That needs a game running and a person watching, and it - * is on the hand-test list for `dev`. - * - * node scripts/toast-button-check.mjs - * - * Throw-away profile, throw-away library, three generated clips. The real - * library is never opened. - */ -import { execFileSync } from 'node:child_process'; -import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { tmpdir } from 'node:os'; -import { _electron as electron } from 'playwright'; - -const ffmpeg = (await import('ffmpeg-static')).default; - -const dataDir = mkdtempSync(join(tmpdir(), 'goodbit-toast-')); -const library = mkdtempSync(join(tmpdir(), 'goodbit-toast-lib-')); -mkdirSync(join(library, 'Battlefield 6'), { recursive: true }); - -for (const n of [1, 2, 3]) { - execFileSync(ffmpeg, [ - '-v', 'error', '-y', - '-f', 'lavfi', '-i', `testsrc=size=320x180:rate=30:duration=2`, - '-c:v', 'libx264', '-pix_fmt', 'yuv420p', - join(library, 'Battlefield 6', `clip-${n}.mp4`), - ]); -} - -writeFileSync( - join(dataDir, 'settings.json'), - JSON.stringify({ videosRoot: library, audioRoot: join(library, '.audio'), clipToast: true }), - 'utf-8', -); - -const app = await electron.launch({ - args: ['out/main/index.js'], - env: { ...process.env, GOODBIT_USER_DATA: dataDir }, -}); - -const appPage = - app.windows().find((w) => !w.url().startsWith('data:')) ?? (await app.waitForEvent('window')); -await appPage.waitForLoadState('domcontentloaded'); -await appPage.waitForTimeout(5000); - -const fail = []; -const ok = (label, passed, detail = '') => { - console.log(`${passed ? 'ok ' : 'FAIL'} ${label}${detail ? ` ${detail}` : ''}`); - if (!passed) fail.push(label); -}; - -const indexed = await appPage.evaluate(() => - window.goodbit.apiRequest({ method: 'GET', path: '/clips', query: { pageSize: 10 } }), -); -console.log(` library: ${indexed.body?.total ?? 0} clips`); - -void appPage.evaluate(() => window.goodbit.previewSweepToast()); -await appPage.waitForTimeout(4500); - -const overlay = app.windows().find((w) => w.url().startsWith('data:')); -ok('the card is a window of its own', Boolean(overlay)); - -const bridge = await overlay.evaluate(() => ({ - toast: typeof window.goodbitToast, - keys: window.goodbitToast ? Object.keys(window.goodbitToast).sort() : [], - app: typeof window.goodbit, -})); -console.log(' bridge:', JSON.stringify(bridge)); -ok('the overlay has the narrow bridge', bridge.toast === 'object', bridge.toast); -ok('with exactly two calls on it', JSON.stringify(bridge.keys) === '["action","hover"]', bridge.keys.join(',')); -ok("and not the app's own API", bridge.app === 'undefined', bridge.app); - -const button = await overlay.evaluate(() => { - const el = document.getElementById('action'); - return el ? { hidden: el.hidden, text: el.textContent, hasToken: Boolean(el.dataset.token) } : null; -}); -console.log(' button:', JSON.stringify(button)); -ok('it is offering a button', button && !button.hidden, button?.text ?? 'none'); -ok('with a token main issued', button?.hasToken === true); - -// A made-up token must be refused: the window cannot choose what happens. -const before = appPage.url(); -await overlay.evaluate(() => window.goodbitToast.action('not-the-token')); -await appPage.waitForTimeout(800); -ok('a token main did not issue does nothing', appPage.url() === before); - -await overlay.evaluate(() => document.getElementById('action').click()); -await appPage.waitForTimeout(2500); - -const where = appPage.url(); -console.log(' app url after the press:', where); -ok('pressing it opens the editor', where.includes('/editor'), where); -ok('carrying the session clips', /clips=\d+(,\d+)*/.test(where), where); -ok('and asking for them cut', where.includes('cut=highlights'), where); - -// The offer is spent: a second press must not open anything else. -await overlay.evaluate(() => document.getElementById('action')?.click()); -await appPage.waitForTimeout(800); -ok('and the offer cannot be redeemed twice', appPage.url() === where); - -await app.close(); -if (fail.length) { - console.error('\nFAILED'); - process.exit(1); -} -console.log('\nOK'); diff --git a/src/main/index.ts b/src/main/index.ts index b7bb80e8..c9f21519 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -129,7 +129,7 @@ function createWindow(): BrowserWindow { height: TITLEBAR_HEIGHT, }, webPreferences: { - preload: join(import.meta.dirname, '../preload/index.cjs'), + preload: join(import.meta.dirname, '../preload/index.mjs'), sandbox: false, // The renderer is ours, but it also renders filenames and notes that came // off disk; there is no reason for it to reach Node directly. @@ -364,23 +364,6 @@ app.whenReady().then(async () => { }); onSettingsChange(() => void refreshTrayMenu()); - /* - * The card's one button, wired to the same opener the tray uses. - * - * Registered here rather than inside the toast, because a toast reaching for - * a window opener on its own is the wrong direction: that module is also - * loaded by the preview in Settings and by benches, neither of which has a - * window to show. `openIn` already handles a closed, a minimised and an - * already-open window. - */ - void (async () => { - const { listenForToastActions } = await import('./services/clipToast.js'); - // The path is resolved here rather than in the toast: that module is a - // dynamic import and lands in a chunk, so its own `import.meta.dirname` - // points at `out/main/chunks`. This file is always `out/main/index.js`. - listenForToastActions(openIn, join(import.meta.dirname, '../preload/toast.cjs')); - })(); - // A first run with nowhere to look for clips still opens, so the window can // ask for a folder. Only a configured install starts watching. if (isConfigured()) { diff --git a/src/main/services/capture/sessionWatch.ts b/src/main/services/capture/sessionWatch.ts index 4c3b01cf..158ca34a 100644 --- a/src/main/services/capture/sessionWatch.ts +++ b/src/main/services/capture/sessionWatch.ts @@ -328,14 +328,6 @@ async function readClips(clips: Clip[], game: string): Promise { * was worth drawing because it says why the machine is busy; this half is * only worth drawing when it resolves into something. */ - /* - * The clips, so the card can offer to open them. - * - * Every clip in the session, not only the ones something was found in. The - * person asked for the session, and a clip the detector could not read is - * the one most likely to be worth watching by hand: 76 of the 174 clips in - * the reference library hold nothing the screen can name. - */ - if (found > 0) await showSweepFinished(found, clips.length, clips.map((clip) => clip.id)); + if (found > 0) await showSweepFinished(found, clips.length); else dismissClipToast(); } diff --git a/src/main/services/clipToast.ts b/src/main/services/clipToast.ts index fa1cd0e9..36aad1d6 100644 --- a/src/main/services/clipToast.ts +++ b/src/main/services/clipToast.ts @@ -1,6 +1,4 @@ -import { join } from 'node:path'; -import { randomUUID } from 'node:crypto'; -import { BrowserWindow, ipcMain, screen } from 'electron'; +import { BrowserWindow, screen } from 'electron'; import { loadSettings } from '../settings.js'; /** @@ -111,87 +109,6 @@ const MARGIN = 32; let overlay: BrowserWindow | null = null; let hideTimer: ReturnType | null = null; -/** - * What the card currently on screen is offering, if anything. - * - * A token and the route it stands for. The route never leaves main: the page - * is handed the token alone, so the one message it can send names something - * main already decided to allow rather than somewhere the page chose. - * - * Cleared when the card goes, so a token cannot be redeemed after the card - * that carried it has faded. - */ -let pending: { token: string; route: string } | null = null; - -/** Set once, by `listenForToastActions`, and read to route a press. */ -let onAction: ((route: string) => void) | null = null; - -/** - * Where the overlay's preload is, handed in rather than worked out here. - * - * This module is reached by a dynamic import, so electron-vite emits it as - * `out/main/chunks/clipToast-*.js` and `import.meta.dirname` is the chunks - * folder: `'../preload/toast.cjs'` from here resolved to - * `out/main/preload/toast.cjs`, which does not exist. `app.getAppPath()` is - * `out/main`, so that is no better. - * - * Electron says **nothing at all** about a preload it cannot find, which is - * how this presented: the card drew its button, the click did nothing, and - * `window.goodbitToast` was undefined with no error anywhere. So the path - * comes from `index.ts`, which is the one file whose own location is stable. - * - * Null until then, and a window built before it is simply the card as it was: - * no bridge, and `showSweepFinished` offers no button because nothing could - * press it. - */ -let preloadPath: string | null = null; - -/** - * Wire up the card's one button. - * - * Called from boot with whatever knows how to open a route, which is - * `openIn` in `index.ts`. Kept as a callback rather than importing that - * directly, because this file is also loaded by the preview in Settings and by - * benches, and a window opener is not something a toast should reach for on - * its own. - */ -export function listenForToastActions( - open: (route: string) => void, - preload: string, -): void { - onAction = open; - preloadPath = preload; - - /* - * The pointer is over the button, or it is not. - * - * The window ignores the mouse with `forward: true`, so this is the only - * signal that the pointer is somewhere clickable. Anything else the page - * might say is ignored: `over` is a boolean and the window either takes the - * mouse or does not. - */ - ipcMain.on('toast:hover', (event, over: unknown) => { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win || win !== overlay || win.isDestroyed()) return; - // Never take the mouse for a card that is not offering anything, whatever - // the page says. - win.setIgnoreMouseEvents(!(over === true && pending !== null), { forward: true }); - }); - - ipcMain.on('toast:action', (event, token: unknown) => { - const win = BrowserWindow.fromWebContents(event.sender); - if (!win || win !== overlay || win.isDestroyed()) return; - if (!pending || typeof token !== 'string' || token !== pending.token) return; - - const route = pending.route; - // One press per offer. A second click on a card that is fading out should - // not open a second window. - pending = null; - dismissClipToast(); - onAction?.(route); - }); -} - /** * The whole overlay, as one page. * @@ -248,33 +165,6 @@ function page(): string { } #card.in { opacity: 1; transform: translateY(0) scale(1); } - /* - The only interactive rectangle on the card. - - Sized and placed like any other button, but its geometry is what the - hover forwarding is measured against: the window ignores the mouse - everywhere, and only turns that off while the pointer is inside this - element. So it has to be a real box with real bounds rather than - anything that overflows or animates its size. - */ - #action { - flex: 0 0 auto; - margin-left: 2px; - padding: 7px 12px; - border-radius: 9px; - border: 1px solid rgba(255, 255, 255, 0.14); - background: rgba(255, 255, 255, 0.08); - color: #fff; - font: inherit; - font-size: 12.5px; - font-weight: 500; - white-space: nowrap; - cursor: pointer; - transition: background-color 150ms ease, border-color 150ms ease; - } - #action:hover { background: rgba(255, 255, 255, 0.16); } - #action[hidden] { display: none; } - /* The badge holds both states and crossfades between them, so the card does not jump when the spinner becomes a tick. */ #badge { @@ -410,11 +300,6 @@ function page(): string {
- -
@@ -646,41 +466,14 @@ function build(): BrowserWindow { hasShadow: false, // Never takes focus. A window that alt-tabs somebody out of a firefight to // tell them their clip saved has done more harm than the clip was worth. - // - // **Still false now that the card has a button.** A non-focusable window - // does receive mouse events, so the button works without this being - // relaxed, and relaxing it is the one change here that could put somebody - // back at their desktop mid-match. focusable: false, alwaysOnTop: true, - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - sandbox: true, - /* - * Two functions, and nothing else. See `src/preload/toast.ts`. - * - * This window had no preload on purpose. The button is what changed - * that, and the bridge is deliberately not `window.goodbit`: a sandboxed - * page sitting over somebody's game should not be handed the API that - * deletes clips. - */ - ...(preloadPath ? { preload: preloadPath } : {}), - }, + webPreferences: { nodeIntegration: false, contextIsolation: true, sandbox: true }, }); // Above a maximised game, not merely above ordinary windows. win.setAlwaysOnTop(true, 'screen-saver'); - /* - * Clicks pass through, `mousemove` does not. - * - * `forward: true` is the documented arrangement for a window that has one - * clickable thing on it: the card stays transparent to the pointer, so a - * click during a game lands on the game, while the page still sees the - * pointer and can say when it is over the button. `toast:hover` then turns - * the ignoring off for exactly as long as that is true. - */ - win.setIgnoreMouseEvents(true, { forward: true }); + win.setIgnoreMouseEvents(true); win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); void win.loadURL(page()); @@ -756,23 +549,13 @@ interface RenderOptions { * `analyzeOnGameCloseToast` for a sweep. */ enabled: boolean; - /** - * A button on the card, and what pressing it does. - * - * The route is held **here**, in main, and never given to the page. The - * window gets an opaque token it can hand back; it cannot name a - * destination. Same rule `deeplink.ts` applies to a `goodbit://` link, and - * for the same reason: anything that can reach that channel should not get - * to choose what it does. - */ - offer?: { label: string; route: string }; } async function render( state: ToastState, title: string, subtitle: string, - { sound = false, lingerMs, enabled, offer }: RenderOptions, + { sound = false, lingerMs, enabled }: RenderOptions, ): Promise { const began = Date.now(); const settings = loadSettings(); @@ -800,13 +583,6 @@ async function render( `window.toast(${JSON.stringify(state)}, ${JSON.stringify(title)}, ${JSON.stringify(subtitle)}, ${sound}, ${Number(settings.clipToastVolume ?? 75)});`, ); - // A new card retires whatever the last one offered, so a token cannot be - // pressed after the card that carried it has gone. - pending = offer ? { token: randomUUID(), route: offer.route } : null; - await win.webContents.executeJavaScript( - `window.offer(${JSON.stringify(offer?.label ?? null)}, ${JSON.stringify(pending?.token ?? null)});`, - ); - // The one number worth watching in this file. Everything above is arranged // around it, so it says out loud whether the arrangement is working. console.log(`[toast] ${state} in ${Date.now() - began}ms`); @@ -826,9 +602,6 @@ async function render( if (!win.isDestroyed()) win.hide(); } })(); - // The offer dies with the card it was on. - pending = null; - if (!win.isDestroyed()) win.setIgnoreMouseEvents(true, { forward: true }); }, linger); } @@ -900,41 +673,16 @@ export async function showSweepStarted( * interruption with no payload. The promise half is still worth drawing while * the work runs: it says why the machine is busy. */ -export async function showSweepFinished( - found: number, - clips: number, - clipIds: number[] = [], -): Promise { +export async function showSweepFinished(found: number, clips: number): Promise { try { const settings = loadSettings(); const title = `Found ${found} ${found === 1 ? 'GoodBit' : 'GoodBits'}`; const subtitle = `in ${clips} ${clips === 1 ? 'clip' : 'clips'}, ready to trim`; - /* - * The one card in the app that is worth pressing. - * - * It arrives when a game has closed, which is the moment somebody is most - * likely to want a montage and least likely to go looking for one, and the - * session's clips are already in hand. `cut=highlights` is what turns it - * from "open these" into "open these, already cut". - * - * Only on this card. The opening one fires as somebody closes a game, and - * is often the moment they get up. - */ - // No bridge, no button. A control that cannot report a press is worse - // than no control: it looks broken rather than absent. - const offer = clipIds.length && preloadPath - ? { - label: 'Open in the editor', - route: `/editor?clips=${clipIds.join(',')}&cut=highlights`, - } - : undefined; - await render('found', title, subtitle, { sound: settings.analyzeOnGameCloseSound !== false, enabled: settings.analyzeOnGameClose !== false && settings.analyzeOnGameCloseToast !== false, - offer, }); } catch (error) { console.error('[toast]', error instanceof Error ? error.message : error); @@ -979,34 +727,7 @@ export async function previewClipToast(): Promise { export async function previewSweepToast(): Promise { await showSweepStarted(12, 'Battlefield 6', 8000); await new Promise((resolve) => setTimeout(resolve, 1900)); - - /* - * The preview shows the real card, button and all. - * - * With the newest few clips behind it, so pressing it does what the real one - * does rather than opening an editor full of clips that do not exist. On an - * empty library there is nothing to offer and the card appears without a - * button, which is also what the real one does. - */ - const latest = await latestClipIds(3); - await showSweepFinished(4, 12, latest); -} - -/** The newest few clip ids, for the preview. Never throws: it is a preview. */ -async function latestClipIds(count: number): Promise { - try { - const { AppDataSource } = await import('../data-source.js'); - const { Clip } = await import('../entity/Clip.js'); - if (!AppDataSource?.isInitialized) return []; - - const rows = await AppDataSource.getRepository(Clip).find({ - order: { recordedAt: 'DESC', id: 'DESC' }, - take: count, - }); - return rows.map((clip) => clip.id); - } catch { - return []; - } + await showSweepFinished(4, 12); } export function closeClipToast(): void { diff --git a/src/preload/toast.ts b/src/preload/toast.ts deleted file mode 100644 index f5c74fe8..00000000 --- a/src/preload/toast.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { contextBridge, ipcRenderer } from 'electron'; - -/** - * The only way the overlay card can say anything back to main. - * - * That window had **no preload at all**, deliberately: it is a `data:text/html` - * page driven one way from main with `executeJavaScript`, and nothing it could - * possibly want to say was worth a channel. One button changes that, and this - * is the narrowest thing that makes the button work. - * - * Two calls, no payload the window invents: - * - * - `hover` toggles whether the window takes the mouse at all. The card is - * created with `setIgnoreMouseEvents(true, { forward: true })`, which is the - * documented arrangement for exactly this: clicks pass straight through the - * card to whatever is behind it, and the page still receives `mousemove`, so - * it can say when the pointer is over the one rectangle that is meant to be - * clickable. Blanket-enabling mouse events would put a window in front of a - * game that can swallow a click. - * - `action` carries a token **main issued**, and nothing else. No path, no - * clip ids, no route. The window cannot ask for anything main did not - * already offer, which is the same rule `deeplink.ts` applies to a - * `goodbit://` link and for the same reason: anything that can reach this - * channel should not be able to choose what it does. - * - * Kept out of `src/preload/index.ts` on purpose. That bridge is the whole - * `window.goodbit` API, and the overlay is a sandboxed page over somebody's - * game; handing it the app's API to use two functions would be the opposite of - * what this window is for. - */ -contextBridge.exposeInMainWorld('goodbitToast', { - hover: (over: boolean): void => { - ipcRenderer.send('toast:hover', over === true); - }, - action: (token: string): void => { - ipcRenderer.send('toast:action', String(token ?? '')); - }, -}); diff --git a/src/renderer/src/views/EditorPage.vue b/src/renderer/src/views/EditorPage.vue index e5fe890b..37dc6546 100644 --- a/src/renderer/src/views/EditorPage.vue +++ b/src/renderer/src/views/EditorPage.vue @@ -716,24 +716,6 @@ async function loadClipsFromQuery(): Promise { if (missing > 0) { toastStore.warning(`${missing} ${pluralize(missing, 'clip')} could not be loaded`); } - - /* - * Arrive already cut, when the caller asked for that and only then. - * - * The post-game card opens a whole session this way: a montage is then one - * press from closing a game. Every other caller of `?clips=` opens the - * editor from the library, where somebody expects their clips whole, so this - * is a flag rather than a default. - * - * `trimAllToHighlights` unchanged, which matters for two reasons it already - * gets right: a clip with no confident highlight is left **whole** rather - * than dropped, and it takes the undo snapshot once before trimming, so the - * first Ctrl+Z after landing here undoes the whole auto-cut rather than one - * clip of it. - */ - if (resolved.length > 0 && route.query.cut === 'highlights') { - await trimAllToHighlights(); - } } useKeyboardShortcuts({ @@ -798,7 +780,7 @@ onMounted(async () => { }); watch( - () => [route.query.clip, route.query.clips, route.query.cut], + () => [route.query.clip, route.query.clips], () => loadClipsFromQuery() );