mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
Save
This commit is contained in:
parent
f115dcbc62
commit
6db414a0f0
43
emain/appstate.ts
Normal file
43
emain/appstate.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||||
|
import { BrowserWindow } from "electron";
|
||||||
|
|
||||||
|
class AppStateType {
|
||||||
|
isQuitting: boolean;
|
||||||
|
isStarting: boolean;
|
||||||
|
isRelaunching: boolean;
|
||||||
|
|
||||||
|
wasInFg: boolean;
|
||||||
|
wasActive: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.isQuitting = false;
|
||||||
|
this.isStarting = false;
|
||||||
|
this.isRelaunching = false;
|
||||||
|
this.wasInFg = false;
|
||||||
|
this.wasActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async logActiveState() {
|
||||||
|
const activeState = { fg: this.wasInFg, active: this.wasActive, open: true };
|
||||||
|
const url = new URL(getWebServerEndpoint() + "/wave/log-active-state");
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url, { method: "post", body: JSON.stringify(activeState) });
|
||||||
|
if (!resp.ok) {
|
||||||
|
console.log("error logging active state", resp.status, resp.statusText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error logging active state", e);
|
||||||
|
} finally {
|
||||||
|
// for next iteration
|
||||||
|
this.wasInFg = BrowserWindow.getFocusedWindow()?.isFocused() ?? false;
|
||||||
|
this.wasActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runActiveTimer() {
|
||||||
|
this.logActiveState().then(() => setTimeout(this.runActiveTimer, 60000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppState = new AppStateType();
|
456
emain/emain.ts
456
emain/emain.ts
@ -2,25 +2,22 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import * as electron from "electron";
|
import * as electron from "electron";
|
||||||
import { FastAverageColor } from "fast-average-color";
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import * as child_process from "node:child_process";
|
import * as child_process from "node:child_process";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { PNG } from "pngjs";
|
|
||||||
import * as readline from "readline";
|
import * as readline from "readline";
|
||||||
import { sprintf } from "sprintf-js";
|
import { sprintf } from "sprintf-js";
|
||||||
import { debounce } from "throttle-debounce";
|
|
||||||
import * as util from "util";
|
import * as util from "util";
|
||||||
import winston from "winston";
|
import winston from "winston";
|
||||||
import { initGlobal } from "../frontend/app/store/global";
|
import { initGlobal } from "../frontend/app/store/global";
|
||||||
import * as services from "../frontend/app/store/services";
|
import * as services from "../frontend/app/store/services";
|
||||||
import { initElectronWshrpc } from "../frontend/app/store/wshrpcutil";
|
import { initElectronWshrpc } from "../frontend/app/store/wshrpcutil";
|
||||||
import { WSServerEndpointVarName, WebServerEndpointVarName, getWebServerEndpoint } from "../frontend/util/endpoints";
|
import { WSServerEndpointVarName, WebServerEndpointVarName, getWebServerEndpoint } from "../frontend/util/endpoints";
|
||||||
import { fetch } from "../frontend/util/fetchutil";
|
|
||||||
import * as keyutil from "../frontend/util/keyutil";
|
import * as keyutil from "../frontend/util/keyutil";
|
||||||
import { fireAndForget } from "../frontend/util/util";
|
import { AppState } from "./appstate";
|
||||||
import { AuthKey, AuthKeyEnv, configureAuthKeyRequestInjection } from "./authkey";
|
import { AuthKey, AuthKeyEnv, configureAuthKeyRequestInjection } from "./authkey";
|
||||||
import { ElectronWshClient, initElectronWshClient } from "./emain-wsh";
|
import { ElectronWshClient, initElectronWshClient } from "./emain-wsh";
|
||||||
|
import { configureGlobalHotkey } from "./globalhotkey";
|
||||||
import { getLaunchSettings } from "./launchsettings";
|
import { getLaunchSettings } from "./launchsettings";
|
||||||
import { getAppMenu } from "./menu";
|
import { getAppMenu } from "./menu";
|
||||||
import {
|
import {
|
||||||
@ -30,11 +27,11 @@ import {
|
|||||||
getWaveSrvCwd,
|
getWaveSrvCwd,
|
||||||
getWaveSrvPath,
|
getWaveSrvPath,
|
||||||
isDev,
|
isDev,
|
||||||
isDevVite,
|
|
||||||
unameArch,
|
unameArch,
|
||||||
unamePlatform,
|
unamePlatform,
|
||||||
} from "./platform";
|
} from "./platform";
|
||||||
import { configureAutoUpdater, updater } from "./updater";
|
import { configureAutoUpdater, updater } from "./updater";
|
||||||
|
import { createBrowserWindow, createNewWaveWindow, relaunchBrowserWindows } from "./window";
|
||||||
|
|
||||||
const electronApp = electron.app;
|
const electronApp = electron.app;
|
||||||
let WaveVersion = "unknown"; // set by WAVESRV-ESTART
|
let WaveVersion = "unknown"; // set by WAVESRV-ESTART
|
||||||
@ -44,15 +41,10 @@ const WaveAppPathVarName = "WAVETERM_APP_PATH";
|
|||||||
const WaveSrvReadySignalPidVarName = "WAVETERM_READY_SIGNAL_PID";
|
const WaveSrvReadySignalPidVarName = "WAVETERM_READY_SIGNAL_PID";
|
||||||
electron.nativeTheme.themeSource = "dark";
|
electron.nativeTheme.themeSource = "dark";
|
||||||
|
|
||||||
type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string; readyPromise: Promise<void> };
|
|
||||||
|
|
||||||
let waveSrvReadyResolve = (value: boolean) => {};
|
let waveSrvReadyResolve = (value: boolean) => {};
|
||||||
const waveSrvReady: Promise<boolean> = new Promise((resolve, _) => {
|
const waveSrvReady: Promise<boolean> = new Promise((resolve, _) => {
|
||||||
waveSrvReadyResolve = resolve;
|
waveSrvReadyResolve = resolve;
|
||||||
});
|
});
|
||||||
let globalIsQuitting = false;
|
|
||||||
let globalIsStarting = true;
|
|
||||||
let globalIsRelaunching = false;
|
|
||||||
|
|
||||||
// for activity updates
|
// for activity updates
|
||||||
let wasActive = true;
|
let wasActive = true;
|
||||||
@ -106,11 +98,6 @@ if (isDev) {
|
|||||||
|
|
||||||
initGlobal({ windowId: null, clientId: null, platform: unamePlatform, environment: "electron" });
|
initGlobal({ windowId: null, clientId: null, platform: unamePlatform, environment: "electron" });
|
||||||
|
|
||||||
function getWindowForEvent(event: Electron.IpcMainEvent): Electron.BrowserWindow {
|
|
||||||
const windowId = event.sender.id;
|
|
||||||
return electron.BrowserWindow.fromId(windowId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCtrlShift(wc: Electron.WebContents, state: boolean) {
|
function setCtrlShift(wc: Electron.WebContents, state: boolean) {
|
||||||
wc.send("control-shift-state-update", state);
|
wc.send("control-shift-state-update", state);
|
||||||
}
|
}
|
||||||
@ -165,7 +152,7 @@ function runWaveSrv(): Promise<boolean> {
|
|||||||
env: envCopy,
|
env: envCopy,
|
||||||
});
|
});
|
||||||
proc.on("exit", (e) => {
|
proc.on("exit", (e) => {
|
||||||
if (globalIsQuitting || updater?.status == "installing") {
|
if (AppState.isQuitting || updater?.status == "installing") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("wavesrv exited, shutting down");
|
console.log("wavesrv exited, shutting down");
|
||||||
@ -250,325 +237,6 @@ async function handleWSEvent(evtMsg: WSEventType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mainResizeHandler(_: any, windowId: string, win: WaveBrowserWindow) {
|
|
||||||
if (win == null || win.isDestroyed() || win.fullScreen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bounds = win.getBounds();
|
|
||||||
try {
|
|
||||||
await services.WindowService.SetWindowPosAndSize(
|
|
||||||
windowId,
|
|
||||||
{ x: bounds.x, y: bounds.y },
|
|
||||||
{ width: bounds.width, height: bounds.height }
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.log("error resizing window", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function shNavHandler(event: Electron.Event<Electron.WebContentsWillNavigateEventParams>, url: string) {
|
|
||||||
if (url.startsWith("http://127.0.0.1:5173/index.html") || url.startsWith("http://localhost:5173/index.html")) {
|
|
||||||
// this is a dev-mode hot-reload, ignore it
|
|
||||||
console.log("allowing hot-reload of index.html");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
if (url.startsWith("https://") || url.startsWith("http://") || url.startsWith("file://")) {
|
|
||||||
console.log("open external, shNav", url);
|
|
||||||
electron.shell.openExternal(url);
|
|
||||||
} else {
|
|
||||||
console.log("navigation canceled", url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNavigateEventParams>) {
|
|
||||||
if (!event.frame?.parent) {
|
|
||||||
// only use this handler to process iframe events (non-iframe events go to shNavHandler)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url = event.url;
|
|
||||||
console.log(`frame-navigation url=${url} frame=${event.frame.name}`);
|
|
||||||
if (event.frame.name == "webview") {
|
|
||||||
// "webview" links always open in new window
|
|
||||||
// this will *not* effect the initial load because srcdoc does not count as an electron navigation
|
|
||||||
console.log("open external, frameNav", url);
|
|
||||||
event.preventDefault();
|
|
||||||
electron.shell.openExternal(url);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
event.frame.name == "pdfview" &&
|
|
||||||
(url.startsWith("blob:file:///") || url.startsWith(getWebServerEndpoint() + "/wave/stream-file?"))
|
|
||||||
) {
|
|
||||||
// allowed
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
console.log("frame navigation canceled");
|
|
||||||
}
|
|
||||||
|
|
||||||
// note, this does not *show* the window.
|
|
||||||
// to show, await win.readyPromise and then win.show()
|
|
||||||
function createBrowserWindow(clientId: string, waveWindow: WaveWindow, fullConfig: FullConfigType): WaveBrowserWindow {
|
|
||||||
let winWidth = waveWindow?.winsize?.width;
|
|
||||||
let winHeight = waveWindow?.winsize?.height;
|
|
||||||
let winPosX = waveWindow.pos.x;
|
|
||||||
let winPosY = waveWindow.pos.y;
|
|
||||||
if (winWidth == null || winWidth == 0) {
|
|
||||||
const primaryDisplay = electron.screen.getPrimaryDisplay();
|
|
||||||
const { width } = primaryDisplay.workAreaSize;
|
|
||||||
winWidth = width - winPosX - 100;
|
|
||||||
if (winWidth > 2000) {
|
|
||||||
winWidth = 2000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (winHeight == null || winHeight == 0) {
|
|
||||||
const primaryDisplay = electron.screen.getPrimaryDisplay();
|
|
||||||
const { height } = primaryDisplay.workAreaSize;
|
|
||||||
winHeight = height - winPosY - 100;
|
|
||||||
if (winHeight > 1200) {
|
|
||||||
winHeight = 1200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let winBounds = {
|
|
||||||
x: winPosX,
|
|
||||||
y: winPosY,
|
|
||||||
width: winWidth,
|
|
||||||
height: winHeight,
|
|
||||||
};
|
|
||||||
winBounds = ensureBoundsAreVisible(winBounds);
|
|
||||||
const settings = fullConfig?.settings;
|
|
||||||
const winOpts: Electron.BrowserWindowConstructorOptions = {
|
|
||||||
titleBarStyle:
|
|
||||||
unamePlatform === "darwin" ? "hiddenInset" : settings["window:nativetitlebar"] ? "default" : "hidden",
|
|
||||||
titleBarOverlay:
|
|
||||||
unamePlatform !== "darwin"
|
|
||||||
? {
|
|
||||||
symbolColor: "white",
|
|
||||||
color: "#00000000",
|
|
||||||
}
|
|
||||||
: false,
|
|
||||||
x: winBounds.x,
|
|
||||||
y: winBounds.y,
|
|
||||||
width: winBounds.width,
|
|
||||||
height: winBounds.height,
|
|
||||||
minWidth: 400,
|
|
||||||
minHeight: 300,
|
|
||||||
icon:
|
|
||||||
unamePlatform == "linux"
|
|
||||||
? path.join(getElectronAppBasePath(), "public/logos/wave-logo-dark.png")
|
|
||||||
: undefined,
|
|
||||||
webPreferences: {
|
|
||||||
preload: path.join(getElectronAppBasePath(), "preload", "index.cjs"),
|
|
||||||
webviewTag: true,
|
|
||||||
},
|
|
||||||
show: false,
|
|
||||||
autoHideMenuBar: true,
|
|
||||||
};
|
|
||||||
const isTransparent = settings?.["window:transparent"] ?? false;
|
|
||||||
const isBlur = !isTransparent && (settings?.["window:blur"] ?? false);
|
|
||||||
if (isTransparent) {
|
|
||||||
winOpts.transparent = true;
|
|
||||||
} else if (isBlur) {
|
|
||||||
switch (unamePlatform) {
|
|
||||||
case "win32": {
|
|
||||||
winOpts.backgroundMaterial = "acrylic";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "darwin": {
|
|
||||||
winOpts.vibrancy = "fullscreen-ui";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
winOpts.backgroundColor = "#222222";
|
|
||||||
}
|
|
||||||
const bwin = new electron.BrowserWindow(winOpts);
|
|
||||||
(bwin as any).waveWindowId = waveWindow.oid;
|
|
||||||
let readyResolve: (value: void) => void;
|
|
||||||
(bwin as any).readyPromise = new Promise((resolve, _) => {
|
|
||||||
readyResolve = resolve;
|
|
||||||
});
|
|
||||||
const win: WaveBrowserWindow = bwin as WaveBrowserWindow;
|
|
||||||
const usp = new URLSearchParams();
|
|
||||||
usp.set("clientid", clientId);
|
|
||||||
usp.set("windowid", waveWindow.oid);
|
|
||||||
const indexHtml = "index.html";
|
|
||||||
if (isDevVite) {
|
|
||||||
console.log("running as dev server");
|
|
||||||
win.loadURL(`${process.env.ELECTRON_RENDERER_URL}/index.html?${usp.toString()}`);
|
|
||||||
} else {
|
|
||||||
console.log("running as file");
|
|
||||||
win.loadFile(path.join(getElectronAppBasePath(), "frontend", indexHtml), { search: usp.toString() });
|
|
||||||
}
|
|
||||||
win.once("ready-to-show", () => {
|
|
||||||
readyResolve();
|
|
||||||
});
|
|
||||||
win.webContents.on("will-navigate", shNavHandler);
|
|
||||||
win.webContents.on("will-frame-navigate", shFrameNavHandler);
|
|
||||||
win.webContents.on("did-attach-webview", (event, wc) => {
|
|
||||||
wc.setWindowOpenHandler((details) => {
|
|
||||||
win.webContents.send("webview-new-window", wc.id, details);
|
|
||||||
return { action: "deny" };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
win.webContents.on("before-input-event", (e, input) => {
|
|
||||||
const waveEvent = keyutil.adaptFromElectronKeyEvent(input);
|
|
||||||
// console.log("WIN bie", waveEvent.type, waveEvent.code);
|
|
||||||
handleCtrlShiftState(win.webContents, waveEvent);
|
|
||||||
if (win.isFocused()) {
|
|
||||||
wasActive = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
win.on(
|
|
||||||
"resize",
|
|
||||||
debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
|
|
||||||
);
|
|
||||||
win.on(
|
|
||||||
"move",
|
|
||||||
debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
|
|
||||||
);
|
|
||||||
win.on("focus", () => {
|
|
||||||
wasInFg = true;
|
|
||||||
wasActive = true;
|
|
||||||
if (globalIsStarting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log("focus", waveWindow.oid);
|
|
||||||
services.ClientService.FocusWindow(waveWindow.oid);
|
|
||||||
});
|
|
||||||
win.on("blur", () => {
|
|
||||||
handleCtrlShiftFocus(win.webContents, false);
|
|
||||||
});
|
|
||||||
win.on("enter-full-screen", async () => {
|
|
||||||
win.webContents.send("fullscreen-change", true);
|
|
||||||
});
|
|
||||||
win.on("leave-full-screen", async () => {
|
|
||||||
win.webContents.send("fullscreen-change", false);
|
|
||||||
});
|
|
||||||
win.on("close", (e) => {
|
|
||||||
if (globalIsQuitting || updater?.status == "installing") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const numWindows = electron.BrowserWindow.getAllWindows().length;
|
|
||||||
if (numWindows == 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const choice = electron.dialog.showMessageBoxSync(win, {
|
|
||||||
type: "question",
|
|
||||||
buttons: ["Cancel", "Yes"],
|
|
||||||
title: "Confirm",
|
|
||||||
message: "Are you sure you want to close this window (all tabs and blocks will be deleted)?",
|
|
||||||
});
|
|
||||||
if (choice === 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
win.on("closed", () => {
|
|
||||||
if (globalIsQuitting || updater?.status == "installing") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const numWindows = electron.BrowserWindow.getAllWindows().length;
|
|
||||||
if (numWindows == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
services.WindowService.CloseWindow(waveWindow.oid);
|
|
||||||
});
|
|
||||||
win.webContents.on("zoom-changed", (e) => {
|
|
||||||
win.webContents.send("zoom-changed");
|
|
||||||
});
|
|
||||||
win.webContents.setWindowOpenHandler(({ url, frameName }) => {
|
|
||||||
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) {
|
|
||||||
console.log("openExternal fallback", url);
|
|
||||||
electron.shell.openExternal(url);
|
|
||||||
}
|
|
||||||
console.log("window-open denied", url);
|
|
||||||
return { action: "deny" };
|
|
||||||
});
|
|
||||||
configureAuthKeyRequestInjection(win.webContents.session);
|
|
||||||
return win;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWindowFullyVisible(bounds: electron.Rectangle): boolean {
|
|
||||||
const displays = electron.screen.getAllDisplays();
|
|
||||||
|
|
||||||
// Helper function to check if a point is inside any display
|
|
||||||
function isPointInDisplay(x: number, y: number) {
|
|
||||||
for (const display of displays) {
|
|
||||||
const { x: dx, y: dy, width, height } = display.bounds;
|
|
||||||
if (x >= dx && x < dx + width && y >= dy && y < dy + height) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check all corners of the window
|
|
||||||
const topLeft = isPointInDisplay(bounds.x, bounds.y);
|
|
||||||
const topRight = isPointInDisplay(bounds.x + bounds.width, bounds.y);
|
|
||||||
const bottomLeft = isPointInDisplay(bounds.x, bounds.y + bounds.height);
|
|
||||||
const bottomRight = isPointInDisplay(bounds.x + bounds.width, bounds.y + bounds.height);
|
|
||||||
|
|
||||||
return topLeft && topRight && bottomLeft && bottomRight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findDisplayWithMostArea(bounds: electron.Rectangle): electron.Display {
|
|
||||||
const displays = electron.screen.getAllDisplays();
|
|
||||||
let maxArea = 0;
|
|
||||||
let bestDisplay = null;
|
|
||||||
|
|
||||||
for (let display of displays) {
|
|
||||||
const { x, y, width, height } = display.bounds;
|
|
||||||
const overlapX = Math.max(0, Math.min(bounds.x + bounds.width, x + width) - Math.max(bounds.x, x));
|
|
||||||
const overlapY = Math.max(0, Math.min(bounds.y + bounds.height, y + height) - Math.max(bounds.y, y));
|
|
||||||
const overlapArea = overlapX * overlapY;
|
|
||||||
|
|
||||||
if (overlapArea > maxArea) {
|
|
||||||
maxArea = overlapArea;
|
|
||||||
bestDisplay = display;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestDisplay;
|
|
||||||
}
|
|
||||||
|
|
||||||
function adjustBoundsToFitDisplay(bounds: electron.Rectangle, display: electron.Display): electron.Rectangle {
|
|
||||||
const { x: dx, y: dy, width: dWidth, height: dHeight } = display.workArea;
|
|
||||||
let { x, y, width, height } = bounds;
|
|
||||||
|
|
||||||
// Adjust width and height to fit within the display's work area
|
|
||||||
width = Math.min(width, dWidth);
|
|
||||||
height = Math.min(height, dHeight);
|
|
||||||
|
|
||||||
// Adjust x to ensure the window fits within the display
|
|
||||||
if (x < dx) {
|
|
||||||
x = dx;
|
|
||||||
} else if (x + width > dx + dWidth) {
|
|
||||||
x = dx + dWidth - width;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adjust y to ensure the window fits within the display
|
|
||||||
if (y < dy) {
|
|
||||||
y = dy;
|
|
||||||
} else if (y + height > dy + dHeight) {
|
|
||||||
y = dy + dHeight - height;
|
|
||||||
}
|
|
||||||
return { x, y, width, height };
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureBoundsAreVisible(bounds: electron.Rectangle): electron.Rectangle {
|
|
||||||
if (!isWindowFullyVisible(bounds)) {
|
|
||||||
let targetDisplay = findDisplayWithMostArea(bounds);
|
|
||||||
|
|
||||||
if (!targetDisplay) {
|
|
||||||
targetDisplay = electron.screen.getPrimaryDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
return adjustBoundsToFitDisplay(bounds, targetDisplay);
|
|
||||||
}
|
|
||||||
return bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for the open-external event from the renderer process
|
// Listen for the open-external event from the renderer process
|
||||||
electron.ipcMain.on("open-external", (event, url) => {
|
electron.ipcMain.on("open-external", (event, url) => {
|
||||||
if (url && typeof url === "string") {
|
if (url && typeof url === "string") {
|
||||||
@ -650,59 +318,6 @@ electron.ipcMain.on("register-global-webview-keys", (event, keys: string[]) => {
|
|||||||
webviewKeys = keys ?? [];
|
webviewKeys = keys ?? [];
|
||||||
});
|
});
|
||||||
|
|
||||||
if (unamePlatform !== "darwin") {
|
|
||||||
const fac = new FastAverageColor();
|
|
||||||
|
|
||||||
electron.ipcMain.on("update-window-controls-overlay", async (event, rect: Dimensions) => {
|
|
||||||
// Bail out if the user requests the native titlebar
|
|
||||||
const fullConfig = await services.FileService.GetFullConfig();
|
|
||||||
if (fullConfig.settings["window:nativetitlebar"]) return;
|
|
||||||
|
|
||||||
const zoomFactor = event.sender.getZoomFactor();
|
|
||||||
const electronRect: Electron.Rectangle = {
|
|
||||||
x: rect.left * zoomFactor,
|
|
||||||
y: rect.top * zoomFactor,
|
|
||||||
height: rect.height * zoomFactor,
|
|
||||||
width: rect.width * zoomFactor,
|
|
||||||
};
|
|
||||||
const overlay = await event.sender.capturePage(electronRect);
|
|
||||||
const overlayBuffer = overlay.toPNG();
|
|
||||||
const png = PNG.sync.read(overlayBuffer);
|
|
||||||
const color = fac.prepareResult(fac.getColorFromArray4(png.data));
|
|
||||||
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
||||||
window.setTitleBarOverlay({
|
|
||||||
color: unamePlatform === "linux" ? color.rgba : "#00000000", // Windows supports a true transparent overlay, so we don't need to set a background color.
|
|
||||||
symbolColor: color.isDark ? "white" : "black",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createNewWaveWindow(): Promise<void> {
|
|
||||||
const clientData = await services.ClientService.GetClientData();
|
|
||||||
const fullConfig = await services.FileService.GetFullConfig();
|
|
||||||
let recreatedWindow = false;
|
|
||||||
if (electron.BrowserWindow.getAllWindows().length === 0 && clientData?.windowids?.length >= 1) {
|
|
||||||
// reopen the first window
|
|
||||||
const existingWindowId = clientData.windowids[0];
|
|
||||||
const existingWindowData = (await services.ObjectService.GetObject("window:" + existingWindowId)) as WaveWindow;
|
|
||||||
if (existingWindowData != null) {
|
|
||||||
const win = createBrowserWindow(clientData.oid, existingWindowData, fullConfig);
|
|
||||||
await win.readyPromise;
|
|
||||||
win.show();
|
|
||||||
recreatedWindow = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (recreatedWindow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newWindow = await services.ClientService.MakeWindow();
|
|
||||||
const newBrowserWindow = createBrowserWindow(clientData.oid, newWindow, fullConfig);
|
|
||||||
await newBrowserWindow.readyPromise;
|
|
||||||
newBrowserWindow.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
electron.ipcMain.on("open-new-window", () => fireAndForget(createNewWaveWindow));
|
|
||||||
|
|
||||||
electron.ipcMain.on("contextmenu-show", (event, menuDefArr?: ElectronContextMenuItem[]) => {
|
electron.ipcMain.on("contextmenu-show", (event, menuDefArr?: ElectronContextMenuItem[]) => {
|
||||||
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
||||||
if (menuDefArr?.length === 0) {
|
if (menuDefArr?.length === 0) {
|
||||||
@ -716,30 +331,6 @@ electron.ipcMain.on("contextmenu-show", (event, menuDefArr?: ElectronContextMenu
|
|||||||
event.returnValue = true;
|
event.returnValue = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function logActiveState() {
|
|
||||||
const activeState = { fg: wasInFg, active: wasActive, open: true };
|
|
||||||
const url = new URL(getWebServerEndpoint() + "/wave/log-active-state");
|
|
||||||
try {
|
|
||||||
const resp = await fetch(url, { method: "post", body: JSON.stringify(activeState) });
|
|
||||||
if (!resp.ok) {
|
|
||||||
console.log("error logging active state", resp.status, resp.statusText);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log("error logging active state", e);
|
|
||||||
} finally {
|
|
||||||
// for next iteration
|
|
||||||
wasInFg = electron.BrowserWindow.getFocusedWindow()?.isFocused() ?? false;
|
|
||||||
wasActive = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this isn't perfect, but gets the job done without being complicated
|
|
||||||
function runActiveTimer() {
|
|
||||||
logActiveState();
|
|
||||||
setTimeout(runActiveTimer, 60000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electron.Menu {
|
function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electron.Menu {
|
||||||
const menuItems: electron.MenuItem[] = [];
|
const menuItems: electron.MenuItem[] = [];
|
||||||
for (const menuDef of menuDefArr) {
|
for (const menuDef of menuDefArr) {
|
||||||
@ -771,7 +362,7 @@ function makeAppMenu() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
electronApp.on("window-all-closed", () => {
|
electronApp.on("window-all-closed", () => {
|
||||||
if (globalIsRelaunching) {
|
if (AppState.isRelaunching) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (unamePlatform !== "darwin") {
|
if (unamePlatform !== "darwin") {
|
||||||
@ -779,7 +370,7 @@ electronApp.on("window-all-closed", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
electronApp.on("before-quit", () => {
|
electronApp.on("before-quit", () => {
|
||||||
globalIsQuitting = true;
|
AppState.isQuitting = true;
|
||||||
updater?.stop();
|
updater?.stop();
|
||||||
});
|
});
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
@ -805,36 +396,6 @@ process.on("uncaughtException", (error) => {
|
|||||||
electronApp.quit();
|
electronApp.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function relaunchBrowserWindows(): Promise<void> {
|
|
||||||
globalIsRelaunching = true;
|
|
||||||
const windows = electron.BrowserWindow.getAllWindows();
|
|
||||||
for (const window of windows) {
|
|
||||||
window.removeAllListeners();
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
globalIsRelaunching = false;
|
|
||||||
|
|
||||||
const clientData = await services.ClientService.GetClientData();
|
|
||||||
const fullConfig = await services.FileService.GetFullConfig();
|
|
||||||
const wins: WaveBrowserWindow[] = [];
|
|
||||||
for (const windowId of clientData.windowids.slice().reverse()) {
|
|
||||||
const windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow;
|
|
||||||
if (windowData == null) {
|
|
||||||
services.WindowService.CloseWindow(windowId).catch((e) => {
|
|
||||||
/* ignore */
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const win = createBrowserWindow(clientData.oid, windowData, fullConfig);
|
|
||||||
wins.push(win);
|
|
||||||
}
|
|
||||||
for (const win of wins) {
|
|
||||||
await win.readyPromise;
|
|
||||||
console.log("show", win.waveWindowId);
|
|
||||||
win.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on("uncaughtException", (error) => {
|
process.on("uncaughtException", (error) => {
|
||||||
console.error("Uncaught Exception:", error);
|
console.error("Uncaught Exception:", error);
|
||||||
console.error("Stack Trace:", error.stack);
|
console.error("Stack Trace:", error.stack);
|
||||||
@ -871,7 +432,8 @@ async function appMain() {
|
|||||||
await electronApp.whenReady();
|
await electronApp.whenReady();
|
||||||
configureAuthKeyRequestInjection(electron.session.defaultSession);
|
configureAuthKeyRequestInjection(electron.session.defaultSession);
|
||||||
await relaunchBrowserWindows();
|
await relaunchBrowserWindows();
|
||||||
setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe
|
await configureGlobalHotkey();
|
||||||
|
setTimeout(AppState.runActiveTimer, 5000); // start active timer, wait 5s just to be safe
|
||||||
try {
|
try {
|
||||||
initElectronWshClient();
|
initElectronWshClient();
|
||||||
initElectronWshrpc(ElectronWshClient, { authKey: AuthKey });
|
initElectronWshrpc(ElectronWshClient, { authKey: AuthKey });
|
||||||
@ -880,7 +442,7 @@ async function appMain() {
|
|||||||
}
|
}
|
||||||
await configureAutoUpdater();
|
await configureAutoUpdater();
|
||||||
|
|
||||||
globalIsStarting = false;
|
AppState.isStarting = false;
|
||||||
|
|
||||||
electronApp.on("activate", async () => {
|
electronApp.on("activate", async () => {
|
||||||
if (electron.BrowserWindow.getAllWindows().length === 0) {
|
if (electron.BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
21
emain/globalhotkey.ts
Normal file
21
emain/globalhotkey.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { FileService } from "@/app/store/services";
|
||||||
|
import { fireAndForget } from "@/util/util";
|
||||||
|
import { app, globalShortcut } from "electron";
|
||||||
|
|
||||||
|
async function hotkeyCallback() {
|
||||||
|
app.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function configureGlobalHotkey() {
|
||||||
|
const settings = (await FileService.GetFullConfig())?.settings;
|
||||||
|
|
||||||
|
const globalhotkey = settings["window:globalhotkey"];
|
||||||
|
if (globalhotkey) {
|
||||||
|
console.log(`Registering global hotkey: "${globalhotkey}"`);
|
||||||
|
|
||||||
|
await app.whenReady();
|
||||||
|
globalShortcut.register(globalhotkey, () => fireAndForget(() => hotkeyCallback()));
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on("before-quit", () => globalShortcut.unregisterAll());
|
||||||
|
}
|
0
emain/keybindings.ts
Normal file
0
emain/keybindings.ts
Normal file
430
emain/window.ts
Normal file
430
emain/window.ts
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
import { ClientService, FileService, ObjectService, WindowService } from "@/app/store/services";
|
||||||
|
import { getWebServerEndpoint } from "@/util/endpoints";
|
||||||
|
import { adaptFromElectronKeyEvent } from "@/util/keyutil";
|
||||||
|
import { fireAndForget } from "@/util/util";
|
||||||
|
import {
|
||||||
|
BrowserWindow,
|
||||||
|
BrowserWindowConstructorOptions,
|
||||||
|
dialog,
|
||||||
|
Display,
|
||||||
|
ipcMain,
|
||||||
|
Rectangle,
|
||||||
|
screen,
|
||||||
|
shell,
|
||||||
|
} from "electron";
|
||||||
|
import { FastAverageColor } from "fast-average-color";
|
||||||
|
import path from "node:path";
|
||||||
|
import { PNG } from "pngjs";
|
||||||
|
import { debounce } from "throttle-debounce";
|
||||||
|
import { AppState } from "./appstate";
|
||||||
|
import { configureAuthKeyRequestInjection } from "./authkey";
|
||||||
|
import { getElectronAppBasePath, isDevVite, unamePlatform } from "./platform";
|
||||||
|
import { updater } from "./updater";
|
||||||
|
|
||||||
|
type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string; readyPromise: Promise<void> };
|
||||||
|
|
||||||
|
// note, this does not *show* the window.
|
||||||
|
// to show, await win.readyPromise and then win.show()
|
||||||
|
export function createBrowserWindow(
|
||||||
|
clientId: string,
|
||||||
|
waveWindow: WaveWindow,
|
||||||
|
fullConfig: FullConfigType
|
||||||
|
): WaveBrowserWindow {
|
||||||
|
let winWidth = waveWindow?.winsize?.width;
|
||||||
|
let winHeight = waveWindow?.winsize?.height;
|
||||||
|
let winPosX = waveWindow.pos.x;
|
||||||
|
let winPosY = waveWindow.pos.y;
|
||||||
|
if (winWidth == null || winWidth == 0) {
|
||||||
|
const primaryDisplay = screen.getPrimaryDisplay();
|
||||||
|
const { width } = primaryDisplay.workAreaSize;
|
||||||
|
winWidth = width - winPosX - 100;
|
||||||
|
if (winWidth > 2000) {
|
||||||
|
winWidth = 2000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (winHeight == null || winHeight == 0) {
|
||||||
|
const primaryDisplay = screen.getPrimaryDisplay();
|
||||||
|
const { height } = primaryDisplay.workAreaSize;
|
||||||
|
winHeight = height - winPosY - 100;
|
||||||
|
if (winHeight > 1200) {
|
||||||
|
winHeight = 1200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let winBounds = {
|
||||||
|
x: winPosX,
|
||||||
|
y: winPosY,
|
||||||
|
width: winWidth,
|
||||||
|
height: winHeight,
|
||||||
|
};
|
||||||
|
winBounds = ensureBoundsAreVisible(winBounds);
|
||||||
|
const settings = fullConfig?.settings;
|
||||||
|
const winOpts: BrowserWindowConstructorOptions = {
|
||||||
|
titleBarStyle:
|
||||||
|
unamePlatform === "darwin" ? "hiddenInset" : settings["window:nativetitlebar"] ? "default" : "hidden",
|
||||||
|
titleBarOverlay:
|
||||||
|
unamePlatform !== "darwin"
|
||||||
|
? {
|
||||||
|
symbolColor: "white",
|
||||||
|
color: "#00000000",
|
||||||
|
}
|
||||||
|
: false,
|
||||||
|
x: winBounds.x,
|
||||||
|
y: winBounds.y,
|
||||||
|
width: winBounds.width,
|
||||||
|
height: winBounds.height,
|
||||||
|
minWidth: 400,
|
||||||
|
minHeight: 300,
|
||||||
|
icon:
|
||||||
|
unamePlatform == "linux"
|
||||||
|
? path.join(getElectronAppBasePath(), "public/logos/wave-logo-dark.png")
|
||||||
|
: undefined,
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(getElectronAppBasePath(), "preload", "index.cjs"),
|
||||||
|
webviewTag: true,
|
||||||
|
},
|
||||||
|
show: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
};
|
||||||
|
const isTransparent = settings?.["window:transparent"] ?? false;
|
||||||
|
const isBlur = !isTransparent && (settings?.["window:blur"] ?? false);
|
||||||
|
if (isTransparent) {
|
||||||
|
winOpts.transparent = true;
|
||||||
|
} else if (isBlur) {
|
||||||
|
switch (unamePlatform) {
|
||||||
|
case "win32": {
|
||||||
|
winOpts.backgroundMaterial = "acrylic";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "darwin": {
|
||||||
|
winOpts.vibrancy = "fullscreen-ui";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
winOpts.backgroundColor = "#222222";
|
||||||
|
}
|
||||||
|
const bwin = new BrowserWindow(winOpts);
|
||||||
|
(bwin as any).waveWindowId = waveWindow.oid;
|
||||||
|
let readyResolve: (value: void) => void;
|
||||||
|
(bwin as any).readyPromise = new Promise((resolve, _) => {
|
||||||
|
readyResolve = resolve;
|
||||||
|
});
|
||||||
|
const win: WaveBrowserWindow = bwin as WaveBrowserWindow;
|
||||||
|
const usp = new URLSearchParams();
|
||||||
|
usp.set("clientid", clientId);
|
||||||
|
usp.set("windowid", waveWindow.oid);
|
||||||
|
const indexHtml = "index.html";
|
||||||
|
if (isDevVite) {
|
||||||
|
console.log("running as dev server");
|
||||||
|
win.loadURL(`${process.env.ELECTRON_RENDERER_URL}/index.html?${usp.toString()}`);
|
||||||
|
} else {
|
||||||
|
console.log("running as file");
|
||||||
|
win.loadFile(path.join(getElectronAppBasePath(), "frontend", indexHtml), { search: usp.toString() });
|
||||||
|
}
|
||||||
|
win.once("ready-to-show", () => {
|
||||||
|
readyResolve();
|
||||||
|
});
|
||||||
|
win.webContents.on("will-navigate", shNavHandler);
|
||||||
|
win.webContents.on("will-frame-navigate", shFrameNavHandler);
|
||||||
|
win.webContents.on("did-attach-webview", (event, wc) => {
|
||||||
|
wc.setWindowOpenHandler((details) => {
|
||||||
|
win.webContents.send("webview-new-window", wc.id, details);
|
||||||
|
return { action: "deny" };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
win.webContents.on("before-input-event", (e, input) => {
|
||||||
|
const waveEvent = adaptFromElectronKeyEvent(input);
|
||||||
|
// console.log("WIN bie", waveEvent.type, waveEvent.code);
|
||||||
|
handleCtrlShiftState(win.webContents, waveEvent);
|
||||||
|
if (win.isFocused()) {
|
||||||
|
AppState.wasActive = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
win.on(
|
||||||
|
"resize",
|
||||||
|
debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
|
||||||
|
);
|
||||||
|
win.on(
|
||||||
|
"move",
|
||||||
|
debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
|
||||||
|
);
|
||||||
|
win.on("focus", () => {
|
||||||
|
AppState.wasInFg = true;
|
||||||
|
AppState.wasActive = true;
|
||||||
|
if (AppState.isStarting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("focus", waveWindow.oid);
|
||||||
|
ClientService.FocusWindow(waveWindow.oid);
|
||||||
|
});
|
||||||
|
win.on("blur", () => {
|
||||||
|
handleCtrlShiftFocus(win.webContents, false);
|
||||||
|
});
|
||||||
|
win.on("enter-full-screen", async () => {
|
||||||
|
win.webContents.send("fullscreen-change", true);
|
||||||
|
});
|
||||||
|
win.on("leave-full-screen", async () => {
|
||||||
|
win.webContents.send("fullscreen-change", false);
|
||||||
|
});
|
||||||
|
win.on("close", (e) => {
|
||||||
|
if (AppState.isQuitting || updater?.status == "installing") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const numWindows = BrowserWindow.getAllWindows().length;
|
||||||
|
if (numWindows == 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const choice = dialog.showMessageBoxSync(win, {
|
||||||
|
type: "question",
|
||||||
|
buttons: ["Cancel", "Yes"],
|
||||||
|
title: "Confirm",
|
||||||
|
message: "Are you sure you want to close this window (all tabs and blocks will be deleted)?",
|
||||||
|
});
|
||||||
|
if (choice === 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
win.on("closed", () => {
|
||||||
|
if (AppState.isQuitting || updater?.status == "installing") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const numWindows = BrowserWindow.getAllWindows().length;
|
||||||
|
if (numWindows == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WindowService.CloseWindow(waveWindow.oid);
|
||||||
|
});
|
||||||
|
win.webContents.on("zoom-changed", (e) => {
|
||||||
|
win.webContents.send("zoom-changed");
|
||||||
|
});
|
||||||
|
win.webContents.setWindowOpenHandler(({ url, frameName }) => {
|
||||||
|
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) {
|
||||||
|
console.log("openExternal fallback", url);
|
||||||
|
shell.openExternal(url);
|
||||||
|
}
|
||||||
|
console.log("window-open denied", url);
|
||||||
|
return { action: "deny" };
|
||||||
|
});
|
||||||
|
configureAuthKeyRequestInjection(win.webContents.session);
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createNewWaveWindow(): Promise<void> {
|
||||||
|
const clientData = await ClientService.GetClientData();
|
||||||
|
const fullConfig = await FileService.GetFullConfig();
|
||||||
|
let recreatedWindow = false;
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0 && clientData?.windowids?.length >= 1) {
|
||||||
|
// reopen the first window
|
||||||
|
const existingWindowId = clientData.windowids[0];
|
||||||
|
const existingWindowData = (await ObjectService.GetObject("window:" + existingWindowId)) as WaveWindow;
|
||||||
|
if (existingWindowData != null) {
|
||||||
|
const win = createBrowserWindow(clientData.oid, existingWindowData, fullConfig);
|
||||||
|
await win.readyPromise;
|
||||||
|
win.show();
|
||||||
|
recreatedWindow = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (recreatedWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newWindow = await ClientService.MakeWindow();
|
||||||
|
const newBrowserWindow = createBrowserWindow(clientData.oid, newWindow, fullConfig);
|
||||||
|
await newBrowserWindow.readyPromise;
|
||||||
|
newBrowserWindow.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function relaunchBrowserWindows(): Promise<void> {
|
||||||
|
AppState.isRelaunching = true;
|
||||||
|
const windows = BrowserWindow.getAllWindows();
|
||||||
|
for (const window of windows) {
|
||||||
|
window.removeAllListeners();
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
AppState.isRelaunching = false;
|
||||||
|
|
||||||
|
const clientData = await ClientService.GetClientData();
|
||||||
|
const fullConfig = await FileService.GetFullConfig();
|
||||||
|
const wins: WaveBrowserWindow[] = [];
|
||||||
|
for (const windowId of clientData.windowids.slice().reverse()) {
|
||||||
|
const windowData: WaveWindow = (await ObjectService.GetObject("window:" + windowId)) as WaveWindow;
|
||||||
|
if (windowData == null) {
|
||||||
|
WindowService.CloseWindow(windowId).catch((e) => {
|
||||||
|
/* ignore */
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const win = createBrowserWindow(clientData.oid, windowData, fullConfig);
|
||||||
|
wins.push(win);
|
||||||
|
}
|
||||||
|
for (const win of wins) {
|
||||||
|
await win.readyPromise;
|
||||||
|
console.log("show", win.waveWindowId);
|
||||||
|
win.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mainResizeHandler(_: any, windowId: string, win: WaveBrowserWindow) {
|
||||||
|
if (win == null || win.isDestroyed() || win.fullScreen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bounds = win.getBounds();
|
||||||
|
try {
|
||||||
|
await WindowService.SetWindowPosAndSize(
|
||||||
|
windowId,
|
||||||
|
{ x: bounds.x, y: bounds.y },
|
||||||
|
{ width: bounds.width, height: bounds.height }
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error resizing window", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.on("open-new-window", () => fireAndForget(createNewWaveWindow));
|
||||||
|
|
||||||
|
function shNavHandler(event: Electron.Event<Electron.WebContentsWillNavigateEventParams>, url: string) {
|
||||||
|
if (url.startsWith("http://127.0.0.1:5173/index.html") || url.startsWith("http://localhost:5173/index.html")) {
|
||||||
|
// this is a dev-mode hot-reload, ignore it
|
||||||
|
console.log("allowing hot-reload of index.html");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
if (url.startsWith("https://") || url.startsWith("http://") || url.startsWith("file://")) {
|
||||||
|
console.log("open external, shNav", url);
|
||||||
|
shell.openExternal(url);
|
||||||
|
} else {
|
||||||
|
console.log("navigation canceled", url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNavigateEventParams>) {
|
||||||
|
if (!event.frame?.parent) {
|
||||||
|
// only use this handler to process iframe events (non-iframe events go to shNavHandler)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = event.url;
|
||||||
|
console.log(`frame-navigation url=${url} frame=${event.frame.name}`);
|
||||||
|
if (event.frame.name == "webview") {
|
||||||
|
// "webview" links always open in new window
|
||||||
|
// this will *not* effect the initial load because srcdoc does not count as an electron navigation
|
||||||
|
console.log("open external, frameNav", url);
|
||||||
|
event.preventDefault();
|
||||||
|
shell.openExternal(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.frame.name == "pdfview" &&
|
||||||
|
(url.startsWith("blob:file:///") || url.startsWith(getWebServerEndpoint() + "/wave/stream-file?"))
|
||||||
|
) {
|
||||||
|
// allowed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
console.log("frame navigation canceled");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWindowFullyVisible(bounds: Rectangle): boolean {
|
||||||
|
const displays = screen.getAllDisplays();
|
||||||
|
|
||||||
|
// Helper function to check if a point is inside any display
|
||||||
|
function isPointInDisplay(x: number, y: number) {
|
||||||
|
for (const display of displays) {
|
||||||
|
const { x: dx, y: dy, width, height } = display.bounds;
|
||||||
|
if (x >= dx && x < dx + width && y >= dy && y < dy + height) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all corners of the window
|
||||||
|
const topLeft = isPointInDisplay(bounds.x, bounds.y);
|
||||||
|
const topRight = isPointInDisplay(bounds.x + bounds.width, bounds.y);
|
||||||
|
const bottomLeft = isPointInDisplay(bounds.x, bounds.y + bounds.height);
|
||||||
|
const bottomRight = isPointInDisplay(bounds.x + bounds.width, bounds.y + bounds.height);
|
||||||
|
|
||||||
|
return topLeft && topRight && bottomLeft && bottomRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDisplayWithMostArea(bounds: Rectangle): Display {
|
||||||
|
const displays = screen.getAllDisplays();
|
||||||
|
let maxArea = 0;
|
||||||
|
let bestDisplay = null;
|
||||||
|
|
||||||
|
for (let display of displays) {
|
||||||
|
const { x, y, width, height } = display.bounds;
|
||||||
|
const overlapX = Math.max(0, Math.min(bounds.x + bounds.width, x + width) - Math.max(bounds.x, x));
|
||||||
|
const overlapY = Math.max(0, Math.min(bounds.y + bounds.height, y + height) - Math.max(bounds.y, y));
|
||||||
|
const overlapArea = overlapX * overlapY;
|
||||||
|
|
||||||
|
if (overlapArea > maxArea) {
|
||||||
|
maxArea = overlapArea;
|
||||||
|
bestDisplay = display;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustBoundsToFitDisplay(bounds: Rectangle, display: Display): Rectangle {
|
||||||
|
const { x: dx, y: dy, width: dWidth, height: dHeight } = display.workArea;
|
||||||
|
let { x, y, width, height } = bounds;
|
||||||
|
|
||||||
|
// Adjust width and height to fit within the display's work area
|
||||||
|
width = Math.min(width, dWidth);
|
||||||
|
height = Math.min(height, dHeight);
|
||||||
|
|
||||||
|
// Adjust x to ensure the window fits within the display
|
||||||
|
if (x < dx) {
|
||||||
|
x = dx;
|
||||||
|
} else if (x + width > dx + dWidth) {
|
||||||
|
x = dx + dWidth - width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust y to ensure the window fits within the display
|
||||||
|
if (y < dy) {
|
||||||
|
y = dy;
|
||||||
|
} else if (y + height > dy + dHeight) {
|
||||||
|
y = dy + dHeight - height;
|
||||||
|
}
|
||||||
|
return { x, y, width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureBoundsAreVisible(bounds: Rectangle): Rectangle {
|
||||||
|
if (!isWindowFullyVisible(bounds)) {
|
||||||
|
let targetDisplay = findDisplayWithMostArea(bounds);
|
||||||
|
|
||||||
|
if (!targetDisplay) {
|
||||||
|
targetDisplay = screen.getPrimaryDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
return adjustBoundsToFitDisplay(bounds, targetDisplay);
|
||||||
|
}
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unamePlatform !== "darwin") {
|
||||||
|
const fac = new FastAverageColor();
|
||||||
|
|
||||||
|
ipcMain.on("update-window-controls-overlay", async (event, rect: Dimensions) => {
|
||||||
|
// Bail out if the user requests the native titlebar
|
||||||
|
const fullConfig = await FileService.GetFullConfig();
|
||||||
|
if (fullConfig.settings["window:nativetitlebar"]) return;
|
||||||
|
|
||||||
|
const zoomFactor = event.sender.getZoomFactor();
|
||||||
|
const electronRect: Rectangle = {
|
||||||
|
x: rect.left * zoomFactor,
|
||||||
|
y: rect.top * zoomFactor,
|
||||||
|
height: rect.height * zoomFactor,
|
||||||
|
width: rect.width * zoomFactor,
|
||||||
|
};
|
||||||
|
const overlay = await event.sender.capturePage(electronRect);
|
||||||
|
const overlayBuffer = overlay.toPNG();
|
||||||
|
const png = PNG.sync.read(overlayBuffer);
|
||||||
|
const color = fac.prepareResult(fac.getColorFromArray4(png.data));
|
||||||
|
const window = BrowserWindow.fromWebContents(event.sender);
|
||||||
|
window.setTitleBarOverlay({
|
||||||
|
color: unamePlatform === "linux" ? color.rgba : "#00000000", // Windows supports a true transparent overlay, so we don't need to set a background color.
|
||||||
|
symbolColor: color.isDark ? "white" : "black",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
1
frontend/types/gotypes.d.ts
vendored
1
frontend/types/gotypes.d.ts
vendored
@ -443,6 +443,7 @@ declare global {
|
|||||||
"window:tilegapsize"?: number;
|
"window:tilegapsize"?: number;
|
||||||
"window:nativetitlebar"?: boolean;
|
"window:nativetitlebar"?: boolean;
|
||||||
"window:disablehardwareacceleration"?: boolean;
|
"window:disablehardwareacceleration"?: boolean;
|
||||||
|
"window:globalhotkey"?: string;
|
||||||
"telemetry:*"?: boolean;
|
"telemetry:*"?: boolean;
|
||||||
"telemetry:enabled"?: boolean;
|
"telemetry:enabled"?: boolean;
|
||||||
};
|
};
|
||||||
|
@ -49,6 +49,7 @@ const (
|
|||||||
ConfigKey_WindowTileGapSize = "window:tilegapsize"
|
ConfigKey_WindowTileGapSize = "window:tilegapsize"
|
||||||
ConfigKey_WindowNativeTitleBar = "window:nativetitlebar"
|
ConfigKey_WindowNativeTitleBar = "window:nativetitlebar"
|
||||||
ConfigKey_WindowDisableHardwareAcceleration = "window:disablehardwareacceleration"
|
ConfigKey_WindowDisableHardwareAcceleration = "window:disablehardwareacceleration"
|
||||||
|
ConfigKey_WindowGlobalHotkey = "window:globalhotkey"
|
||||||
|
|
||||||
ConfigKey_TelemetryClear = "telemetry:*"
|
ConfigKey_TelemetryClear = "telemetry:*"
|
||||||
ConfigKey_TelemetryEnabled = "telemetry:enabled"
|
ConfigKey_TelemetryEnabled = "telemetry:enabled"
|
||||||
|
@ -83,6 +83,7 @@ type SettingsType struct {
|
|||||||
WindowTileGapSize *int64 `json:"window:tilegapsize,omitempty"`
|
WindowTileGapSize *int64 `json:"window:tilegapsize,omitempty"`
|
||||||
WindowNativeTitleBar bool `json:"window:nativetitlebar,omitempty"`
|
WindowNativeTitleBar bool `json:"window:nativetitlebar,omitempty"`
|
||||||
WindowDisableHardwareAcceleration bool `json:"window:disablehardwareacceleration,omitempty"`
|
WindowDisableHardwareAcceleration bool `json:"window:disablehardwareacceleration,omitempty"`
|
||||||
|
WindowGlobalHotkey string `json:"window:globalhotkey,omitempty"`
|
||||||
|
|
||||||
TelemetryClear bool `json:"telemetry:*,omitempty"`
|
TelemetryClear bool `json:"telemetry:*,omitempty"`
|
||||||
TelemetryEnabled bool `json:"telemetry:enabled,omitempty"`
|
TelemetryEnabled bool `json:"telemetry:enabled,omitempty"`
|
||||||
|
Loading…
Reference in New Issue
Block a user