2024-12-02 19:56:56 +01:00
|
|
|
// Copyright 2024, Command Line Inc.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2024-12-06 09:10:17 +01:00
|
|
|
import { FileService } from "@/app/store/services";
|
2024-12-02 19:56:56 +01:00
|
|
|
import { adaptFromElectronKeyEvent } from "@/util/keyutil";
|
|
|
|
import { Rectangle, shell, WebContentsView } from "electron";
|
2024-12-07 00:42:29 +01:00
|
|
|
import { getWaveWindowById } from "emain/emain-window";
|
2024-12-02 19:56:56 +01:00
|
|
|
import path from "path";
|
|
|
|
import { configureAuthKeyRequestInjection } from "./authkey";
|
|
|
|
import { setWasActive } from "./emain-activity";
|
|
|
|
import { handleCtrlShiftFocus, handleCtrlShiftState, shFrameNavHandler, shNavHandler } from "./emain-util";
|
|
|
|
import { getElectronAppBasePath, isDevVite } from "./platform";
|
|
|
|
|
|
|
|
function computeBgColor(fullConfig: FullConfigType): string {
|
|
|
|
const settings = fullConfig?.settings;
|
|
|
|
const isTransparent = settings?.["window:transparent"] ?? false;
|
|
|
|
const isBlur = !isTransparent && (settings?.["window:blur"] ?? false);
|
|
|
|
if (isTransparent) {
|
|
|
|
return "#00000000";
|
|
|
|
} else if (isBlur) {
|
|
|
|
return "#00000000";
|
|
|
|
} else {
|
|
|
|
return "#222222";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const wcIdToWaveTabMap = new Map<number, WaveTabView>();
|
|
|
|
|
|
|
|
export function getWaveTabViewByWebContentsId(webContentsId: number): WaveTabView {
|
|
|
|
return wcIdToWaveTabMap.get(webContentsId);
|
|
|
|
}
|
|
|
|
|
|
|
|
export class WaveTabView extends WebContentsView {
|
2024-12-07 00:42:29 +01:00
|
|
|
waveWindowId: string; // this will be set for any tabviews that are initialized. (unset for the hot spare)
|
2024-12-02 19:56:56 +01:00
|
|
|
isActiveTab: boolean;
|
2024-12-06 05:03:30 +01:00
|
|
|
private _waveTabId: string; // always set, WaveTabViews are unique per tab
|
2024-12-02 19:56:56 +01:00
|
|
|
lastUsedTs: number; // ts milliseconds
|
|
|
|
createdTs: number; // ts milliseconds
|
|
|
|
initPromise: Promise<void>;
|
2024-12-06 05:03:30 +01:00
|
|
|
initResolve: () => void;
|
2024-12-02 19:56:56 +01:00
|
|
|
savedInitOpts: WaveInitOpts;
|
|
|
|
waveReadyPromise: Promise<void>;
|
|
|
|
waveReadyResolve: () => void;
|
2024-12-06 09:10:17 +01:00
|
|
|
isInitialized: boolean = false;
|
|
|
|
isWaveReady: boolean = false;
|
2024-12-07 00:42:29 +01:00
|
|
|
isDestroyed: boolean = false;
|
2024-12-06 05:03:30 +01:00
|
|
|
|
2024-12-02 19:56:56 +01:00
|
|
|
constructor(fullConfig: FullConfigType) {
|
|
|
|
console.log("createBareTabView");
|
|
|
|
super({
|
|
|
|
webPreferences: {
|
|
|
|
preload: path.join(getElectronAppBasePath(), "preload", "index.cjs"),
|
|
|
|
webviewTag: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
this.createdTs = Date.now();
|
|
|
|
this.savedInitOpts = null;
|
|
|
|
this.initPromise = new Promise((resolve, _) => {
|
|
|
|
this.initResolve = resolve;
|
|
|
|
});
|
|
|
|
this.initPromise.then(() => {
|
2024-12-06 09:10:17 +01:00
|
|
|
this.isInitialized = true;
|
2024-12-02 19:56:56 +01:00
|
|
|
console.log("tabview init", Date.now() - this.createdTs + "ms");
|
|
|
|
});
|
|
|
|
this.waveReadyPromise = new Promise((resolve, _) => {
|
|
|
|
this.waveReadyResolve = resolve;
|
|
|
|
});
|
2024-12-06 05:03:30 +01:00
|
|
|
this.waveReadyPromise.then(() => {
|
2024-12-06 09:10:17 +01:00
|
|
|
this.isWaveReady = true;
|
2024-12-06 05:03:30 +01:00
|
|
|
});
|
2024-12-02 19:56:56 +01:00
|
|
|
wcIdToWaveTabMap.set(this.webContents.id, this);
|
|
|
|
if (isDevVite) {
|
|
|
|
this.webContents.loadURL(`${process.env.ELECTRON_RENDERER_URL}/index.html}`);
|
|
|
|
} else {
|
|
|
|
this.webContents.loadFile(path.join(getElectronAppBasePath(), "frontend", "index.html"));
|
|
|
|
}
|
|
|
|
this.webContents.on("destroyed", () => {
|
|
|
|
wcIdToWaveTabMap.delete(this.webContents.id);
|
|
|
|
removeWaveTabView(this.waveTabId);
|
2024-12-07 00:42:29 +01:00
|
|
|
this.isDestroyed = true;
|
2024-12-02 19:56:56 +01:00
|
|
|
});
|
|
|
|
this.setBackgroundColor(computeBgColor(fullConfig));
|
|
|
|
}
|
|
|
|
|
2024-12-06 05:03:30 +01:00
|
|
|
get waveTabId(): string {
|
|
|
|
return this._waveTabId;
|
|
|
|
}
|
|
|
|
|
|
|
|
set waveTabId(waveTabId: string) {
|
|
|
|
this._waveTabId = waveTabId;
|
|
|
|
}
|
|
|
|
|
2024-12-02 19:56:56 +01:00
|
|
|
positionTabOnScreen(winBounds: Rectangle) {
|
|
|
|
const curBounds = this.getBounds();
|
|
|
|
if (
|
|
|
|
curBounds.width == winBounds.width &&
|
|
|
|
curBounds.height == winBounds.height &&
|
|
|
|
curBounds.x == 0 &&
|
|
|
|
curBounds.y == 0
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.setBounds({ x: 0, y: 0, width: winBounds.width, height: winBounds.height });
|
|
|
|
}
|
|
|
|
|
|
|
|
positionTabOffScreen(winBounds: Rectangle) {
|
|
|
|
this.setBounds({
|
|
|
|
x: -15000,
|
|
|
|
y: -15000,
|
|
|
|
width: winBounds.width,
|
|
|
|
height: winBounds.height,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
isOnScreen() {
|
|
|
|
const bounds = this.getBounds();
|
|
|
|
return bounds.x == 0 && bounds.y == 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy() {
|
|
|
|
console.log("destroy tab", this.waveTabId);
|
|
|
|
removeWaveTabView(this.waveTabId);
|
2024-12-07 00:42:29 +01:00
|
|
|
if (!this.isDestroyed) {
|
|
|
|
this.webContents?.close();
|
2024-12-02 19:56:56 +01:00
|
|
|
}
|
2024-12-07 00:42:29 +01:00
|
|
|
this.isDestroyed = true;
|
2024-12-02 19:56:56 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let MaxCacheSize = 10;
|
|
|
|
const wcvCache = new Map<string, WaveTabView>();
|
|
|
|
|
|
|
|
export function setMaxTabCacheSize(size: number) {
|
|
|
|
console.log("setMaxTabCacheSize", size);
|
|
|
|
MaxCacheSize = size;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getWaveTabView(waveTabId: string): WaveTabView | undefined {
|
|
|
|
const rtn = wcvCache.get(waveTabId);
|
|
|
|
if (rtn) {
|
|
|
|
rtn.lastUsedTs = Date.now();
|
|
|
|
}
|
|
|
|
return rtn;
|
|
|
|
}
|
|
|
|
|
2024-12-07 00:42:29 +01:00
|
|
|
function tryEvictEntry(waveTabId: string): boolean {
|
|
|
|
const tabView = wcvCache.get(waveTabId);
|
|
|
|
if (!tabView) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (tabView.isActiveTab) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const lastUsedDiff = Date.now() - tabView.lastUsedTs;
|
|
|
|
if (lastUsedDiff < 1000) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const ww = getWaveWindowById(tabView.waveWindowId);
|
|
|
|
if (!ww) {
|
|
|
|
// this shouldn't happen, but if it does, just destroy the tabview
|
|
|
|
console.log("[error] WaveWindow not found for WaveTabView", tabView.waveTabId);
|
|
|
|
tabView.destroy();
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
// will trigger a destroy on the tabview
|
|
|
|
ww.removeTabView(tabView.waveTabId, false);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-02 19:56:56 +01:00
|
|
|
function checkAndEvictCache(): void {
|
|
|
|
if (wcvCache.size <= MaxCacheSize) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const sorted = Array.from(wcvCache.values()).sort((a, b) => {
|
|
|
|
// Prioritize entries which are active
|
|
|
|
if (a.isActiveTab && !b.isActiveTab) {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
// Otherwise, sort by lastUsedTs
|
|
|
|
return a.lastUsedTs - b.lastUsedTs;
|
|
|
|
});
|
2024-12-07 00:42:29 +01:00
|
|
|
const now = Date.now();
|
2024-12-02 19:56:56 +01:00
|
|
|
for (let i = 0; i < sorted.length - MaxCacheSize; i++) {
|
2024-12-07 00:42:29 +01:00
|
|
|
tryEvictEntry(sorted[i].waveTabId);
|
2024-12-02 19:56:56 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function clearTabCache() {
|
|
|
|
const wcVals = Array.from(wcvCache.values());
|
|
|
|
for (let i = 0; i < wcVals.length; i++) {
|
|
|
|
const tabView = wcVals[i];
|
2024-12-07 00:42:29 +01:00
|
|
|
tryEvictEntry(tabView.waveTabId);
|
2024-12-02 19:56:56 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// returns [tabview, initialized]
|
2024-12-07 00:42:29 +01:00
|
|
|
export async function getOrCreateWebViewForTab(waveWindowId: string, tabId: string): Promise<[WaveTabView, boolean]> {
|
2024-12-02 19:56:56 +01:00
|
|
|
let tabView = getWaveTabView(tabId);
|
|
|
|
if (tabView) {
|
|
|
|
return [tabView, true];
|
|
|
|
}
|
2024-12-06 09:10:17 +01:00
|
|
|
const fullConfig = await FileService.GetFullConfig();
|
2024-12-02 19:56:56 +01:00
|
|
|
tabView = getSpareTab(fullConfig);
|
2024-12-07 00:42:29 +01:00
|
|
|
tabView.waveWindowId = waveWindowId;
|
2024-12-02 19:56:56 +01:00
|
|
|
tabView.lastUsedTs = Date.now();
|
2024-12-07 00:42:29 +01:00
|
|
|
setWaveTabView(tabId, tabView);
|
2024-12-02 19:56:56 +01:00
|
|
|
tabView.waveTabId = tabId;
|
|
|
|
tabView.webContents.on("will-navigate", shNavHandler);
|
|
|
|
tabView.webContents.on("will-frame-navigate", shFrameNavHandler);
|
|
|
|
tabView.webContents.on("did-attach-webview", (event, wc) => {
|
|
|
|
wc.setWindowOpenHandler((details) => {
|
|
|
|
tabView.webContents.send("webview-new-window", wc.id, details);
|
|
|
|
return { action: "deny" };
|
|
|
|
});
|
|
|
|
});
|
|
|
|
tabView.webContents.on("before-input-event", (e, input) => {
|
|
|
|
const waveEvent = adaptFromElectronKeyEvent(input);
|
|
|
|
// console.log("WIN bie", tabView.waveTabId.substring(0, 8), waveEvent.type, waveEvent.code);
|
|
|
|
handleCtrlShiftState(tabView.webContents, waveEvent);
|
|
|
|
setWasActive(true);
|
|
|
|
});
|
|
|
|
tabView.webContents.on("zoom-changed", (e) => {
|
|
|
|
tabView.webContents.send("zoom-changed");
|
|
|
|
});
|
|
|
|
tabView.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" };
|
|
|
|
});
|
|
|
|
tabView.webContents.on("blur", () => {
|
|
|
|
handleCtrlShiftFocus(tabView.webContents, false);
|
|
|
|
});
|
|
|
|
configureAuthKeyRequestInjection(tabView.webContents.session);
|
|
|
|
return [tabView, false];
|
|
|
|
}
|
|
|
|
|
|
|
|
export function setWaveTabView(waveTabId: string, wcv: WaveTabView): void {
|
2024-12-07 00:42:29 +01:00
|
|
|
if (waveTabId == null) {
|
|
|
|
return;
|
|
|
|
}
|
2024-12-02 19:56:56 +01:00
|
|
|
wcvCache.set(waveTabId, wcv);
|
|
|
|
checkAndEvictCache();
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeWaveTabView(waveTabId: string): void {
|
2024-12-07 00:42:29 +01:00
|
|
|
if (waveTabId == null) {
|
|
|
|
return;
|
|
|
|
}
|
2024-12-02 19:56:56 +01:00
|
|
|
wcvCache.delete(waveTabId);
|
|
|
|
}
|
|
|
|
|
|
|
|
let HotSpareTab: WaveTabView = null;
|
|
|
|
|
|
|
|
export function ensureHotSpareTab(fullConfig: FullConfigType) {
|
|
|
|
console.log("ensureHotSpareTab");
|
|
|
|
if (HotSpareTab == null) {
|
|
|
|
HotSpareTab = new WaveTabView(fullConfig);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getSpareTab(fullConfig: FullConfigType): WaveTabView {
|
|
|
|
setTimeout(ensureHotSpareTab, 500);
|
|
|
|
if (HotSpareTab != null) {
|
|
|
|
const rtn = HotSpareTab;
|
|
|
|
HotSpareTab = null;
|
|
|
|
console.log("getSpareTab: returning hotspare");
|
|
|
|
return rtn;
|
|
|
|
} else {
|
|
|
|
console.log("getSpareTab: creating new tab");
|
|
|
|
return new WaveTabView(fullConfig);
|
|
|
|
}
|
|
|
|
}
|