mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-09-10 22:04:11 +00:00
feat(panes): pane pop-out windows + the system tray
feedBack core gained a pane system (window.feedBack.panes): live UI — a mixer, a camera rig, a readout — authored once and hostable anywhere. In a browser it pops out via window.open(). This gives it the desktop treatment: a real BrowserWindow that remembers where you put it, can float above everything, and lives in the system tray. First Tray in the app. It exists because a popped-out pane is furniture: you want it out of the way while you play and back instantly when you don't — not hunted for behind the main window, and not cluttering the taskbar. Minimizing a pane sends it to the tray; the tray menu lists every pane with a checkmark and toggles it. ## The renderer owns the truth Main never looks inside a pane. It owns OS surfaces only — windows and their geometry — and learns what panes exist from a `pane:sync` push. The tray menu is a VIEW of the renderer's registry, not a second copy of it. When the tray toggles a pane it has no window for, it asks the renderer, because only the renderer knows what opening one means (it might belong in the dock). ## The pane window loads OUR origin, and that is load-bearing A pane is fed over BroadcastChannel, which only reaches windows in the same Chromium instance and origin. Push the URL anywhere else and the pane opens looking perfect and never updates again. So `pane:open` validates the URL against the same origin predicate the navigation guards use (makeRendererOriginPredicate) and refuses anything else outright — which also means we can never open arbitrary web content with the full preload bridge attached. It is the same reason main.ts's setWindowOpenHandler answers same-origin URLs with `allow` rather than `deny` + openExternal. ## Details that bite - sanitizeWindowBounds hard-floored at the MAIN window's 800x600. A 380x560 pane restored through it would be silently inflated threefold. It now takes a WindowSizing; the main window passes its old values as the default, so every existing call site and the existing test are byte-for-byte unchanged. - Pane geometry lives in the DESKTOP config, not the renderer's localStorage — localStorage is shared with the pane windows themselves (same origin), so a second writer there would race. setDesktopConfig merges shallowly, so paneWindows is read-modify-written or one pane's save would drop the rest. - Pane windows are destroyed when the main window closes. Without the renderer there is nothing on the other end of their channel, so they would sit showing a frozen playhead forever — and a pane HIDDEN in the tray is still an open window, which would stop `window-all-closed` from ever firing and leave the app running as an invisible process. - Geometry is persisted on move/resize, not only on close: a pane window can outlive the app in a crash, and the entire point is that you never place it twice. - Electron's 'minimize' is not cancellable here (the listener takes no event), so a pane hides right after minimizing rather than preventing it. The window is skipTaskbar, so there is no animation to see. - The tray icon is copied to dist/main/ by build:ts, the same trick splash.html and spinner.json use — so __dirname resolves it identically in dev and inside a packaged asar, with no app.isPackaged branch and nothing added to electron-builder's extraResources. An unreadable icon logs and skips the tray rather than creating an invisible one whose menu no one can ever reach. Needs the matching core change (got-feedback/feedback#928), which registers the `desktop` host when this bridge is present and falls back to a browser pop-up when it isn't. An older core simply never calls these channels. Signed-off-by: topkoa <topkoa@gmail.com>
This commit is contained in:
@@ -126,6 +126,8 @@ import * as updateManager from './update-manager';
|
||||
import type { UpdateChannel } from './update-manager';
|
||||
import { installAppMenu } from './app-menu';
|
||||
import { sanitizeWindowBounds, MIN_WIDTH, MIN_HEIGHT } from './window-bounds';
|
||||
import { initPaneHosts, closeAllPanes } from './pane-hosts';
|
||||
import { initTray, destroyTray } from './pane-tray';
|
||||
|
||||
// Linux: enable Chromium's PipeWire capturer feature so getUserMedia can see
|
||||
// audio devices on PipeWire-only distros (Fedora 36+, recent Ubuntu, Arch).
|
||||
@@ -785,6 +787,13 @@ function createWindow(port: number): void {
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
// Pane windows cannot outlive the window that feeds them: without the
|
||||
// renderer there is nothing on the other end of their BroadcastChannel,
|
||||
// so they would sit there showing a frozen playhead forever. Worse, a
|
||||
// pane HIDDEN in the tray is still an open window — leaving one behind
|
||||
// would stop `window-all-closed` from ever firing and the app would
|
||||
// linger as an invisible process.
|
||||
closeAllPanes();
|
||||
});
|
||||
|
||||
// Dev tools in development
|
||||
@@ -1167,6 +1176,19 @@ async function startup(): Promise<void> {
|
||||
// Create the main window
|
||||
createWindow(port);
|
||||
|
||||
// Detachable panes: real BrowserWindows for popped-out panes, plus the tray
|
||||
// that lists them. Must come after createWindow — the pane host and the tray
|
||||
// both reach the renderer through mainWindow, and Tray requires a ready app.
|
||||
// The origin predicate is the same one the navigation guards use, so a pane
|
||||
// window can only ever load OUR renderer, never arbitrary web content with
|
||||
// the preload bridge attached.
|
||||
initPaneHosts({
|
||||
getMainWindow: () => mainWindow,
|
||||
isRendererOrigin: makeRendererOriginPredicate(port),
|
||||
webPreferences: rendererWebPreferences,
|
||||
});
|
||||
initTray({ getMainWindow: () => mainWindow });
|
||||
|
||||
// Install our application menu (replaces Electron's default so View →
|
||||
// Zoom In also accepts the unshifted Ctrl+= key — see app-menu.ts).
|
||||
installAppMenu();
|
||||
@@ -1373,6 +1395,7 @@ function shutdown(): void {
|
||||
try {
|
||||
console.log('[main] Shutting down...');
|
||||
} catch { /* console may already be gone mid-teardown */ }
|
||||
destroyTray();
|
||||
powerAwakeRenderers.clear();
|
||||
syncPowerBlocker();
|
||||
updateManager.shutdown();
|
||||
|
||||
Reference in New Issue
Block a user