mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-17 20:51:55 +01:00
multi-window support (#62)
This commit is contained in:
parent
5c6cfbc112
commit
fb668fd4e5
242
emain/emain.ts
242
emain/emain.ts
@ -21,12 +21,14 @@ const AuthKeyFile = "waveterm.authkey";
|
|||||||
const DevServerEndpoint = "http://127.0.0.1:8190";
|
const DevServerEndpoint = "http://127.0.0.1:8190";
|
||||||
const ProdServerEndpoint = "http://127.0.0.1:1719";
|
const ProdServerEndpoint = "http://127.0.0.1:1719";
|
||||||
|
|
||||||
type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string };
|
type WaveBrowserWindow = Electron.BrowserWindow & { waveWindowId: string; readyPromise: Promise<void> };
|
||||||
|
|
||||||
let waveSrvReadyResolve = (value: boolean) => {};
|
let waveSrvReadyResolve = (value: boolean) => {};
|
||||||
let waveSrvReady: Promise<boolean> = new Promise((resolve, _) => {
|
let waveSrvReady: Promise<boolean> = new Promise((resolve, _) => {
|
||||||
waveSrvReadyResolve = resolve;
|
waveSrvReadyResolve = resolve;
|
||||||
});
|
});
|
||||||
|
let globalIsQuitting = false;
|
||||||
|
let globalIsStarting = true;
|
||||||
|
|
||||||
let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null;
|
let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null;
|
||||||
electronApp.setName(isDev ? "NextWave (Dev)" : "NextWave");
|
electronApp.setName(isDev ? "NextWave (Dev)" : "NextWave");
|
||||||
@ -74,6 +76,11 @@ function getWaveSrvCwd(): string {
|
|||||||
return getWaveHomeDir();
|
return getWaveHomeDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWindowForEvent(event: Electron.IpcMainEvent): Electron.BrowserWindow {
|
||||||
|
const windowId = event.sender.id;
|
||||||
|
return electron.BrowserWindow.fromId(windowId);
|
||||||
|
}
|
||||||
|
|
||||||
function runWaveSrv(): Promise<boolean> {
|
function runWaveSrv(): Promise<boolean> {
|
||||||
let pResolve: (value: boolean) => void;
|
let pResolve: (value: boolean) => void;
|
||||||
let pReject: (reason?: any) => void;
|
let pReject: (reason?: any) => void;
|
||||||
@ -99,6 +106,9 @@ function runWaveSrv(): Promise<boolean> {
|
|||||||
env: envCopy,
|
env: envCopy,
|
||||||
});
|
});
|
||||||
proc.on("exit", (e) => {
|
proc.on("exit", (e) => {
|
||||||
|
if (globalIsQuitting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log("wavesrv exited, shutting down");
|
console.log("wavesrv exited, shutting down");
|
||||||
electronApp.quit();
|
electronApp.quit();
|
||||||
});
|
});
|
||||||
@ -189,31 +199,21 @@ function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNa
|
|||||||
console.log("frame navigation canceled");
|
console.log("frame navigation canceled");
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow {
|
function createBrowserWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow {
|
||||||
const primaryDisplay = electron.screen.getPrimaryDisplay();
|
let winBounds = {
|
||||||
let winHeight = waveWindow.winsize.height;
|
x: waveWindow.pos.x,
|
||||||
let winWidth = waveWindow.winsize.width;
|
y: waveWindow.pos.y,
|
||||||
if (winHeight > primaryDisplay.workAreaSize.height) {
|
width: waveWindow.winsize.width,
|
||||||
winHeight = primaryDisplay.workAreaSize.height;
|
height: waveWindow.winsize.height,
|
||||||
}
|
};
|
||||||
if (winWidth > primaryDisplay.workAreaSize.width) {
|
winBounds = ensureBoundsAreVisible(winBounds);
|
||||||
winWidth = primaryDisplay.workAreaSize.width;
|
|
||||||
}
|
|
||||||
let winX = waveWindow.pos.x;
|
|
||||||
let winY = waveWindow.pos.y;
|
|
||||||
if (winX + winWidth > primaryDisplay.workAreaSize.width) {
|
|
||||||
winX = Math.floor((primaryDisplay.workAreaSize.width - winWidth) / 2);
|
|
||||||
}
|
|
||||||
if (winY + winHeight > primaryDisplay.workAreaSize.height) {
|
|
||||||
winY = Math.floor((primaryDisplay.workAreaSize.height - winHeight) / 2);
|
|
||||||
}
|
|
||||||
const bwin = new electron.BrowserWindow({
|
const bwin = new electron.BrowserWindow({
|
||||||
x: winX,
|
|
||||||
y: winY,
|
|
||||||
titleBarStyle: "hiddenInset",
|
titleBarStyle: "hiddenInset",
|
||||||
width: winWidth,
|
x: winBounds.x,
|
||||||
height: winHeight,
|
y: winBounds.y,
|
||||||
minWidth: 500,
|
width: winBounds.width,
|
||||||
|
height: winBounds.height,
|
||||||
|
minWidth: 400,
|
||||||
minHeight: 300,
|
minHeight: 300,
|
||||||
icon:
|
icon:
|
||||||
unamePlatform == "linux"
|
unamePlatform == "linux"
|
||||||
@ -227,10 +227,11 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow
|
|||||||
backgroundColor: "#000000",
|
backgroundColor: "#000000",
|
||||||
});
|
});
|
||||||
(bwin as any).waveWindowId = waveWindow.oid;
|
(bwin as any).waveWindowId = waveWindow.oid;
|
||||||
const win: WaveBrowserWindow = bwin as WaveBrowserWindow;
|
let readyResolve: (value: void) => void;
|
||||||
win.once("ready-to-show", () => {
|
(bwin as any).readyPromise = new Promise((resolve, _) => {
|
||||||
win.show();
|
readyResolve = resolve;
|
||||||
});
|
});
|
||||||
|
const win: WaveBrowserWindow = bwin as WaveBrowserWindow;
|
||||||
// const indexHtml = isDev ? "index-dev.html" : "index.html";
|
// const indexHtml = isDev ? "index-dev.html" : "index.html";
|
||||||
let usp = new URLSearchParams();
|
let usp = new URLSearchParams();
|
||||||
usp.set("clientid", client.oid);
|
usp.set("clientid", client.oid);
|
||||||
@ -243,7 +244,9 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow
|
|||||||
console.log("running as file");
|
console.log("running as file");
|
||||||
win.loadFile(path.join(getElectronAppBasePath(), "frontend", indexHtml), { search: usp.toString() });
|
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-navigate", shNavHandler);
|
||||||
win.webContents.on("will-frame-navigate", shFrameNavHandler);
|
win.webContents.on("will-frame-navigate", shFrameNavHandler);
|
||||||
win.on(
|
win.on(
|
||||||
@ -254,6 +257,33 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow
|
|||||||
"move",
|
"move",
|
||||||
debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
|
debounce(400, (e) => mainResizeHandler(e, waveWindow.oid, win))
|
||||||
);
|
);
|
||||||
|
win.on("focus", () => {
|
||||||
|
if (globalIsStarting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("focus", waveWindow.oid);
|
||||||
|
services.ClientService.FocusWindow(waveWindow.oid);
|
||||||
|
});
|
||||||
|
win.on("close", (e) => {
|
||||||
|
if (globalIsQuitting) {
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
services.WindowService.CloseWindow(waveWindow.oid);
|
||||||
|
});
|
||||||
win.webContents.on("zoom-changed", (e) => {
|
win.webContents.on("zoom-changed", (e) => {
|
||||||
win.webContents.send("zoom-changed");
|
win.webContents.send("zoom-changed");
|
||||||
});
|
});
|
||||||
@ -268,6 +298,86 @@ function createWindow(client: Client, waveWindow: WaveWindow): WaveBrowserWindow
|
|||||||
return win;
|
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, y) {
|
||||||
|
for (let 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;
|
||||||
|
}
|
||||||
|
|
||||||
electron.ipcMain.on("isDev", (event) => {
|
electron.ipcMain.on("isDev", (event) => {
|
||||||
event.returnValue = isDev;
|
event.returnValue = isDev;
|
||||||
});
|
});
|
||||||
@ -287,7 +397,13 @@ electron.ipcMain.on("getCursorPoint", (event) => {
|
|||||||
event.returnValue = retVal;
|
event.returnValue = retVal;
|
||||||
});
|
});
|
||||||
|
|
||||||
electron.ipcMain.on("openNewWindow", (event) => {});
|
async function createNewWaveWindow() {
|
||||||
|
let clientData = await services.ClientService.GetClientData();
|
||||||
|
const newWindow = await services.ClientService.MakeWindow();
|
||||||
|
createBrowserWindow(clientData, newWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
electron.ipcMain.on("openNewWindow", createNewWaveWindow);
|
||||||
|
|
||||||
electron.ipcMain.on("context-editmenu", (_, { x, y }, opts) => {
|
electron.ipcMain.on("context-editmenu", (_, { x, y }, opts) => {
|
||||||
if (opts == null) {
|
if (opts == null) {
|
||||||
@ -337,7 +453,45 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
|
|||||||
return electron.Menu.buildFromTemplate(menuItems);
|
return electron.Menu.buildFromTemplate(menuItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
function makeAppMenu() {
|
||||||
|
let fileMenu: Electron.MenuItemConstructorOptions[] = [];
|
||||||
|
fileMenu.push({
|
||||||
|
label: "New Window",
|
||||||
|
click: createNewWaveWindow,
|
||||||
|
});
|
||||||
|
fileMenu.push({
|
||||||
|
label: "Close Window",
|
||||||
|
click: () => {
|
||||||
|
electron.BrowserWindow.getFocusedWindow()?.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const menuTemplate: Electron.MenuItemConstructorOptions[] = [
|
||||||
|
{
|
||||||
|
role: "appMenu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "fileMenu",
|
||||||
|
submenu: fileMenu,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "editMenu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "viewMenu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "windowMenu",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const menu = electron.Menu.buildFromTemplate(menuTemplate);
|
||||||
|
electron.Menu.setApplicationMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
electron.app.on("before-quit", () => {
|
||||||
|
globalIsQuitting = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function appMain() {
|
||||||
const startTs = Date.now();
|
const startTs = Date.now();
|
||||||
const instanceLock = electronApp.requestSingleInstanceLock();
|
const instanceLock = electronApp.requestSingleInstanceLock();
|
||||||
if (!instanceLock) {
|
if (!instanceLock) {
|
||||||
@ -349,6 +503,7 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
|
|||||||
if (!fs.existsSync(waveHomeDir)) {
|
if (!fs.existsSync(waveHomeDir)) {
|
||||||
fs.mkdirSync(waveHomeDir);
|
fs.mkdirSync(waveHomeDir);
|
||||||
}
|
}
|
||||||
|
makeAppMenu();
|
||||||
try {
|
try {
|
||||||
await runWaveSrv();
|
await runWaveSrv();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -358,17 +513,30 @@ function convertMenuDefArrToMenu(menuDefArr: ElectronContextMenuItem[]): electro
|
|||||||
console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms");
|
console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms");
|
||||||
|
|
||||||
console.log("get client data");
|
console.log("get client data");
|
||||||
let clientData = (await services.ClientService.GetClientData().catch((e) => console.log(e))) as Client;
|
let clientData = await services.ClientService.GetClientData();
|
||||||
console.log("client data ready");
|
console.log("client data ready");
|
||||||
let windowData: WaveWindow = (await services.ObjectService.GetObject(
|
|
||||||
"window:" + clientData.mainwindowid
|
|
||||||
)) as WaveWindow;
|
|
||||||
await electronApp.whenReady();
|
await electronApp.whenReady();
|
||||||
createWindow(clientData, windowData);
|
let wins: WaveBrowserWindow[] = [];
|
||||||
|
for (let windowId of clientData.windowids.slice().reverse()) {
|
||||||
|
let windowData: WaveWindow = (await services.ObjectService.GetObject("window:" + windowId)) as WaveWindow;
|
||||||
|
const win = createBrowserWindow(clientData, windowData);
|
||||||
|
wins.push(win);
|
||||||
|
}
|
||||||
|
for (let win of wins) {
|
||||||
|
await win.readyPromise;
|
||||||
|
console.log("show", win.waveWindowId);
|
||||||
|
win.show();
|
||||||
|
}
|
||||||
|
globalIsStarting = false;
|
||||||
|
|
||||||
electronApp.on("activate", () => {
|
electronApp.on("activate", () => {
|
||||||
if (electron.BrowserWindow.getAllWindows().length === 0) {
|
if (electron.BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow(clientData, windowData);
|
createNewWaveWindow();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
appMain().catch((e) => {
|
||||||
|
console.log("appMain error", e);
|
||||||
|
electronApp.quit();
|
||||||
|
});
|
||||||
|
@ -60,6 +60,10 @@
|
|||||||
|
|
||||||
&.block-preview {
|
&.block-preview {
|
||||||
background-color: var(--main-bg-color);
|
background-color: var(--main-bg-color);
|
||||||
|
|
||||||
|
.block-frame-tech-close {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.block-focused {
|
&.block-focused {
|
||||||
@ -68,10 +72,6 @@
|
|||||||
.block-frame-tech-header {
|
.block-frame-tech-header {
|
||||||
color: var(--main-text-color);
|
color: var(--main-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-frame-tech-close {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-frame-tech-header {
|
.block-frame-tech-header {
|
||||||
|
@ -21,18 +21,24 @@ export const BlockService = new BlockServiceType()
|
|||||||
|
|
||||||
// clientservice.ClientService (client)
|
// clientservice.ClientService (client)
|
||||||
class ClientServiceType {
|
class ClientServiceType {
|
||||||
|
FocusWindow(arg2: string): Promise<void> {
|
||||||
|
return WOS.callBackendService("client", "FocusWindow", Array.from(arguments))
|
||||||
|
}
|
||||||
GetClientData(): Promise<Client> {
|
GetClientData(): Promise<Client> {
|
||||||
return WOS.callBackendService("client", "GetClientData", Array.from(arguments))
|
return WOS.callBackendService("client", "GetClientData", Array.from(arguments))
|
||||||
}
|
}
|
||||||
GetTab(arg1: string): Promise<Tab> {
|
GetTab(arg1: string): Promise<Tab> {
|
||||||
return WOS.callBackendService("client", "GetTab", Array.from(arguments))
|
return WOS.callBackendService("client", "GetTab", Array.from(arguments))
|
||||||
}
|
}
|
||||||
GetWindow(arg1: string): Promise<Window> {
|
GetWindow(arg1: string): Promise<WaveWindow> {
|
||||||
return WOS.callBackendService("client", "GetWindow", Array.from(arguments))
|
return WOS.callBackendService("client", "GetWindow", Array.from(arguments))
|
||||||
}
|
}
|
||||||
GetWorkspace(arg1: string): Promise<Workspace> {
|
GetWorkspace(arg1: string): Promise<Workspace> {
|
||||||
return WOS.callBackendService("client", "GetWorkspace", Array.from(arguments))
|
return WOS.callBackendService("client", "GetWorkspace", Array.from(arguments))
|
||||||
}
|
}
|
||||||
|
MakeWindow(): Promise<WaveWindow> {
|
||||||
|
return WOS.callBackendService("client", "MakeWindow", Array.from(arguments))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ClientService = new ClientServiceType()
|
export const ClientService = new ClientServiceType()
|
||||||
@ -59,11 +65,6 @@ class ObjectServiceType {
|
|||||||
return WOS.callBackendService("object", "AddTabToWorkspace", Array.from(arguments))
|
return WOS.callBackendService("object", "AddTabToWorkspace", Array.from(arguments))
|
||||||
}
|
}
|
||||||
|
|
||||||
// @returns object updates
|
|
||||||
CloseTab(tabId: string): Promise<void> {
|
|
||||||
return WOS.callBackendService("object", "CloseTab", Array.from(arguments))
|
|
||||||
}
|
|
||||||
|
|
||||||
// @returns blockId (and object updates)
|
// @returns blockId (and object updates)
|
||||||
CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<string> {
|
CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<string> {
|
||||||
return WOS.callBackendService("object", "CreateBlock", Array.from(arguments))
|
return WOS.callBackendService("object", "CreateBlock", Array.from(arguments))
|
||||||
@ -109,6 +110,14 @@ export const ObjectService = new ObjectServiceType()
|
|||||||
|
|
||||||
// windowservice.WindowService (window)
|
// windowservice.WindowService (window)
|
||||||
class WindowServiceType {
|
class WindowServiceType {
|
||||||
|
// @returns object updates
|
||||||
|
CloseTab(arg3: string): Promise<void> {
|
||||||
|
return WOS.callBackendService("window", "CloseTab", Array.from(arguments))
|
||||||
|
}
|
||||||
|
CloseWindow(arg2: string): Promise<void> {
|
||||||
|
return WOS.callBackendService("window", "CloseWindow", Array.from(arguments))
|
||||||
|
}
|
||||||
|
|
||||||
// @returns object updates
|
// @returns object updates
|
||||||
SetWindowPosAndSize(arg2: string, arg3: Point, arg4: WinSize): Promise<void> {
|
SetWindowPosAndSize(arg2: string, arg3: Point, arg4: WinSize): Promise<void> {
|
||||||
return WOS.callBackendService("window", "SetWindowPosAndSize", Array.from(arguments))
|
return WOS.callBackendService("window", "SetWindowPosAndSize", Array.from(arguments))
|
||||||
|
@ -313,7 +313,7 @@ const TabBar = ({ workspace }: TabBarProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseTab = (tabId: string) => {
|
const handleCloseTab = (tabId: string) => {
|
||||||
services.ObjectService.CloseTab(tabId);
|
services.WindowService.CloseTab(tabId);
|
||||||
deleteLayoutStateAtomForTab(tabId);
|
deleteLayoutStateAtomForTab(tabId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
27
frontend/types/gotypes.d.ts
vendored
27
frontend/types/gotypes.d.ts
vendored
@ -31,7 +31,7 @@ declare global {
|
|||||||
|
|
||||||
type BlockCommand = {
|
type BlockCommand = {
|
||||||
command: string;
|
command: string;
|
||||||
} & ( BlockAppendIJsonCommand | ResolveIdsCommand | BlockInputCommand | BlockSetViewCommand | BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockAppendFileCommand );
|
} & ( BlockSetMetaCommand | BlockGetMetaCommand | BlockMessageCommand | BlockAppendFileCommand | BlockAppendIJsonCommand | ResolveIdsCommand | BlockInputCommand | BlockSetViewCommand );
|
||||||
|
|
||||||
// wstore.BlockDef
|
// wstore.BlockDef
|
||||||
type BlockDef = {
|
type BlockDef = {
|
||||||
@ -84,6 +84,7 @@ declare global {
|
|||||||
// wstore.Client
|
// wstore.Client
|
||||||
type Client = WaveObj & {
|
type Client = WaveObj & {
|
||||||
mainwindowid: string;
|
mainwindowid: string;
|
||||||
|
windowids: string[];
|
||||||
meta: MetaType;
|
meta: MetaType;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -253,6 +254,18 @@ declare global {
|
|||||||
obj?: WaveObj;
|
obj?: WaveObj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// wstore.Window
|
||||||
|
type WaveWindow = WaveObj & {
|
||||||
|
workspaceid: string;
|
||||||
|
activetabid: string;
|
||||||
|
activeblockid?: string;
|
||||||
|
activeblockmap: {[key: string]: string};
|
||||||
|
pos: Point;
|
||||||
|
winsize: WinSize;
|
||||||
|
lastfocusts: number;
|
||||||
|
meta: MetaType;
|
||||||
|
};
|
||||||
|
|
||||||
// service.WebCallType
|
// service.WebCallType
|
||||||
type WebCallType = {
|
type WebCallType = {
|
||||||
service: string;
|
service: string;
|
||||||
@ -275,18 +288,6 @@ declare global {
|
|||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// wstore.Window
|
|
||||||
type WaveWindow = WaveObj & {
|
|
||||||
workspaceid: string;
|
|
||||||
activetabid: string;
|
|
||||||
activeblockid?: string;
|
|
||||||
activeblockmap: {[key: string]: string};
|
|
||||||
pos: Point;
|
|
||||||
winsize: WinSize;
|
|
||||||
lastfocusts: number;
|
|
||||||
meta: MetaType;
|
|
||||||
};
|
|
||||||
|
|
||||||
// wstore.Workspace
|
// wstore.Workspace
|
||||||
type Workspace = WaveObj & {
|
type Workspace = WaveObj & {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -27,6 +27,8 @@ function matchViewportSize() {
|
|||||||
document.body.style.height = window.visualViewport.height + "px";
|
document.body.style.height = window.visualViewport.height + "px";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.title = `The Next Wave (${windowId.substring(0, 8)})`;
|
||||||
|
|
||||||
matchViewportSize();
|
matchViewportSize();
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -54,3 +55,21 @@ func (cs *ClientService) GetWindow(windowId string) (*wstore.Window, error) {
|
|||||||
}
|
}
|
||||||
return window, nil
|
return window, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cs *ClientService) MakeWindow(ctx context.Context) (*wstore.Window, error) {
|
||||||
|
return wstore.CreateWindow(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// moves the window to the front of the windowId stack
|
||||||
|
func (cs *ClientService) FocusWindow(ctx context.Context, windowId string) error {
|
||||||
|
client, err := cs.GetClientData()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
winIdx := utilfn.SliceIdx(client.WindowIds, windowId)
|
||||||
|
if winIdx == -1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
client.WindowIds = utilfn.MoveSliceIdxToFront(client.WindowIds, winIdx)
|
||||||
|
return wstore.DBUpdate(ctx, client)
|
||||||
|
}
|
||||||
|
@ -205,41 +205,6 @@ func (svc *ObjectService) CloseTab_Meta() tsgenmeta.MethodMeta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ObjectService) CloseTab(uiContext wstore.UIContext, tabId string) (wstore.UpdatesRtnType, error) {
|
|
||||||
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
|
||||||
defer cancelFn()
|
|
||||||
ctx = wstore.ContextWithUpdates(ctx)
|
|
||||||
window, err := wstore.DBMustGet[*wstore.Window](ctx, uiContext.WindowId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error getting window: %w", err)
|
|
||||||
}
|
|
||||||
tab, err := wstore.DBMustGet[*wstore.Tab](ctx, tabId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error getting tab: %w", err)
|
|
||||||
}
|
|
||||||
for _, blockId := range tab.BlockIds {
|
|
||||||
blockcontroller.StopBlockController(blockId)
|
|
||||||
}
|
|
||||||
err = wstore.CloseTab(ctx, window.WorkspaceId, tabId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error closing tab: %w", err)
|
|
||||||
}
|
|
||||||
if window.ActiveTabId == tabId {
|
|
||||||
ws, err := wstore.DBMustGet[*wstore.Workspace](ctx, window.WorkspaceId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error getting workspace: %w", err)
|
|
||||||
}
|
|
||||||
var newActiveTabId string
|
|
||||||
if len(ws.TabIds) > 0 {
|
|
||||||
newActiveTabId = ws.TabIds[0]
|
|
||||||
} else {
|
|
||||||
newActiveTabId = ""
|
|
||||||
}
|
|
||||||
wstore.SetActiveTab(ctx, uiContext.WindowId, newActiveTabId)
|
|
||||||
}
|
|
||||||
return wstore.ContextGetUpdatesRtn(ctx), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *ObjectService) UpdateObjectMeta_Meta() tsgenmeta.MethodMeta {
|
func (svc *ObjectService) UpdateObjectMeta_Meta() tsgenmeta.MethodMeta {
|
||||||
return tsgenmeta.MethodMeta{
|
return tsgenmeta.MethodMeta{
|
||||||
ArgNames: []string{"uiContext", "oref", "meta"},
|
ArgNames: []string{"uiContext", "oref", "meta"},
|
||||||
|
@ -5,10 +5,15 @@ package windowservice
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DefaultTimeout = 2 * time.Second
|
||||||
|
|
||||||
type WindowService struct{}
|
type WindowService struct{}
|
||||||
|
|
||||||
func (ws *WindowService) SetWindowPosAndSize(ctx context.Context, windowId string, pos *wstore.Point, size *wstore.WinSize) (wstore.UpdatesRtnType, error) {
|
func (ws *WindowService) SetWindowPosAndSize(ctx context.Context, windowId string, pos *wstore.Point, size *wstore.WinSize) (wstore.UpdatesRtnType, error) {
|
||||||
@ -32,3 +37,64 @@ func (ws *WindowService) SetWindowPosAndSize(ctx context.Context, windowId strin
|
|||||||
}
|
}
|
||||||
return wstore.ContextGetUpdatesRtn(ctx), nil
|
return wstore.ContextGetUpdatesRtn(ctx), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *WindowService) CloseTab(ctx context.Context, uiContext wstore.UIContext, tabId string) (wstore.UpdatesRtnType, error) {
|
||||||
|
ctx = wstore.ContextWithUpdates(ctx)
|
||||||
|
window, err := wstore.DBMustGet[*wstore.Window](ctx, uiContext.WindowId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting window: %w", err)
|
||||||
|
}
|
||||||
|
tab, err := wstore.DBMustGet[*wstore.Tab](ctx, tabId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting tab: %w", err)
|
||||||
|
}
|
||||||
|
for _, blockId := range tab.BlockIds {
|
||||||
|
blockcontroller.StopBlockController(blockId)
|
||||||
|
}
|
||||||
|
err = wstore.DeleteTab(ctx, window.WorkspaceId, tabId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error closing tab: %w", err)
|
||||||
|
}
|
||||||
|
if window.ActiveTabId == tabId {
|
||||||
|
ws, err := wstore.DBMustGet[*wstore.Workspace](ctx, window.WorkspaceId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting workspace: %w", err)
|
||||||
|
}
|
||||||
|
var newActiveTabId string
|
||||||
|
if len(ws.TabIds) > 0 {
|
||||||
|
newActiveTabId = ws.TabIds[0]
|
||||||
|
} else {
|
||||||
|
newActiveTabId = ""
|
||||||
|
}
|
||||||
|
wstore.SetActiveTab(ctx, uiContext.WindowId, newActiveTabId)
|
||||||
|
}
|
||||||
|
return wstore.ContextGetUpdatesRtn(ctx), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *WindowService) CloseWindow(ctx context.Context, windowId string) error {
|
||||||
|
ctx = wstore.ContextWithUpdates(ctx)
|
||||||
|
window, err := wstore.DBMustGet[*wstore.Window](ctx, windowId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting window: %w", err)
|
||||||
|
}
|
||||||
|
workspace, err := wstore.DBMustGet[*wstore.Workspace](ctx, window.WorkspaceId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting workspace: %w", err)
|
||||||
|
}
|
||||||
|
for _, tabId := range workspace.TabIds {
|
||||||
|
uiContext := wstore.UIContext{WindowId: windowId}
|
||||||
|
_, err := svc.CloseTab(ctx, uiContext, tabId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error closing tab: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = wstore.DBDelete(ctx, wstore.OType_Workspace, window.WorkspaceId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error deleting workspace: %w", err)
|
||||||
|
}
|
||||||
|
err = wstore.DBDelete(ctx, wstore.OType_Window, windowId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error deleting window: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -101,7 +101,11 @@ func TypeToTSType(t reflect.Type, tsTypesMap map[reflect.Type]string) (string, [
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("{[key: string]: %s}", elemType), subTypes
|
return fmt.Sprintf("{[key: string]: %s}", elemType), subTypes
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
return t.Name(), []reflect.Type{t}
|
name := t.Name()
|
||||||
|
if tsRename := tsRenameMap[name]; tsRename != "" {
|
||||||
|
name = tsRename
|
||||||
|
}
|
||||||
|
return name, []reflect.Type{t}
|
||||||
case reflect.Ptr:
|
case reflect.Ptr:
|
||||||
return TypeToTSType(t.Elem(), tsTypesMap)
|
return TypeToTSType(t.Elem(), tsTypesMap)
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
|
@ -740,3 +740,25 @@ func DoMapStucture(out any, input any) error {
|
|||||||
}
|
}
|
||||||
return decoder.Decode(input)
|
return decoder.Decode(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SliceIdx[T comparable](arr []T, elem T) int {
|
||||||
|
for idx, e := range arr {
|
||||||
|
if e == elem {
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func MoveSliceIdxToFront[T any](arr []T, idx int) []T {
|
||||||
|
// create and return a new slice with idx moved to the front
|
||||||
|
if idx == 0 || idx >= len(arr) {
|
||||||
|
// make a copy still
|
||||||
|
return append([]T(nil), arr...)
|
||||||
|
}
|
||||||
|
rtn := make([]T, 0, len(arr))
|
||||||
|
rtn = append(rtn, arr[idx])
|
||||||
|
rtn = append(rtn, arr[0:idx]...)
|
||||||
|
rtn = append(rtn, arr[idx+1:]...)
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
@ -259,7 +259,7 @@ func DeleteBlock(ctx context.Context, tabId string, blockId string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func CloseTab(ctx context.Context, workspaceId string, tabId string) error {
|
func DeleteTab(ctx context.Context, workspaceId string, tabId string) error {
|
||||||
return WithTx(ctx, func(tx *TxWrap) error {
|
return WithTx(ctx, func(tx *TxWrap) error {
|
||||||
ws, _ := DBGet[*Workspace](tx.Context(), workspaceId)
|
ws, _ := DBGet[*Workspace](tx.Context(), workspaceId)
|
||||||
if ws == nil {
|
if ws == nil {
|
||||||
@ -319,29 +319,11 @@ func UpdateObjectMeta(ctx context.Context, oref waveobj.ORef, meta map[string]an
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnsureInitialData() error {
|
func CreateWindow(ctx context.Context) (*Window, error) {
|
||||||
// does not need to run in a transaction since it is called on startup
|
|
||||||
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
|
||||||
defer cancelFn()
|
|
||||||
clientCount, err := DBGetCount[*Client](ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error getting client count: %w", err)
|
|
||||||
}
|
|
||||||
if clientCount > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
windowId := uuid.NewString()
|
windowId := uuid.NewString()
|
||||||
workspaceId := uuid.NewString()
|
workspaceId := uuid.NewString()
|
||||||
tabId := uuid.NewString()
|
tabId := uuid.NewString()
|
||||||
layoutNodeId := uuid.NewString()
|
layoutNodeId := uuid.NewString()
|
||||||
client := &Client{
|
|
||||||
OID: uuid.NewString(),
|
|
||||||
MainWindowId: windowId,
|
|
||||||
}
|
|
||||||
err = DBInsert(ctx, client)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error inserting client: %w", err)
|
|
||||||
}
|
|
||||||
window := &Window{
|
window := &Window{
|
||||||
OID: windowId,
|
OID: windowId,
|
||||||
WorkspaceId: workspaceId,
|
WorkspaceId: workspaceId,
|
||||||
@ -356,28 +338,28 @@ func EnsureInitialData() error {
|
|||||||
Height: 600,
|
Height: 600,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err = DBInsert(ctx, window)
|
err := DBInsert(ctx, window)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error inserting window: %w", err)
|
return nil, fmt.Errorf("error inserting window: %w", err)
|
||||||
}
|
}
|
||||||
ws := &Workspace{
|
ws := &Workspace{
|
||||||
OID: workspaceId,
|
OID: workspaceId,
|
||||||
Name: "default",
|
Name: "w" + workspaceId[0:8],
|
||||||
TabIds: []string{tabId},
|
TabIds: []string{tabId},
|
||||||
}
|
}
|
||||||
err = DBInsert(ctx, ws)
|
err = DBInsert(ctx, ws)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error inserting workspace: %w", err)
|
return nil, fmt.Errorf("error inserting workspace: %w", err)
|
||||||
}
|
}
|
||||||
tab := &Tab{
|
tab := &Tab{
|
||||||
OID: tabId,
|
OID: tabId,
|
||||||
Name: "Tab-1",
|
Name: "T1",
|
||||||
BlockIds: []string{},
|
BlockIds: []string{},
|
||||||
LayoutNode: layoutNodeId,
|
LayoutNode: layoutNodeId,
|
||||||
}
|
}
|
||||||
err = DBInsert(ctx, tab)
|
err = DBInsert(ctx, tab)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error inserting tab: %w", err)
|
return nil, fmt.Errorf("error inserting tab: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
layoutNode := &LayoutNode{
|
layoutNode := &LayoutNode{
|
||||||
@ -385,7 +367,62 @@ func EnsureInitialData() error {
|
|||||||
}
|
}
|
||||||
err = DBInsert(ctx, layoutNode)
|
err = DBInsert(ctx, layoutNode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error inserting layout node: %w", err)
|
return nil, fmt.Errorf("error inserting layout node: %w", err)
|
||||||
|
}
|
||||||
|
client, err := DBGetSingleton[*Client](ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting client: %w", err)
|
||||||
|
}
|
||||||
|
client.WindowIds = append(client.WindowIds, windowId)
|
||||||
|
err = DBUpdate(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error updating client: %w", err)
|
||||||
|
}
|
||||||
|
return DBMustGet[*Window](ctx, windowId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateClient(ctx context.Context) (*Client, error) {
|
||||||
|
client := &Client{
|
||||||
|
OID: uuid.NewString(),
|
||||||
|
WindowIds: []string{},
|
||||||
|
}
|
||||||
|
err := DBInsert(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error inserting client: %w", err)
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureInitialData() error {
|
||||||
|
// does not need to run in a transaction since it is called on startup
|
||||||
|
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancelFn()
|
||||||
|
client, err := DBGetSingleton[*Client](ctx)
|
||||||
|
if err == ErrNotFound {
|
||||||
|
client, err = CreateClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating client: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if client.MainWindowId != "" {
|
||||||
|
// convert to windowIds
|
||||||
|
client.WindowIds = []string{client.MainWindowId}
|
||||||
|
client.MainWindowId = ""
|
||||||
|
err = DBUpdate(ctx, client)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error updating client: %w", err)
|
||||||
|
}
|
||||||
|
client, err = DBGetSingleton[*Client](ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting client (after main window update): %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(client.WindowIds) > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err = CreateWindow(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating window: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,10 @@ func DBGetSingletonByType(ctx context.Context, otype string) (waveobj.WaveObj, e
|
|||||||
table := tableNameFromOType(otype)
|
table := tableNameFromOType(otype)
|
||||||
query := fmt.Sprintf("SELECT oid, version, data FROM %s LIMIT 1", table)
|
query := fmt.Sprintf("SELECT oid, version, data FROM %s LIMIT 1", table)
|
||||||
var row idDataType
|
var row idDataType
|
||||||
tx.Get(&row, query)
|
found := tx.Get(&row, query)
|
||||||
|
if !found {
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
||||||
rtn, err := waveobj.FromJson(row.Data)
|
rtn, err := waveobj.FromJson(row.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rtn, err
|
return rtn, err
|
||||||
|
@ -115,7 +115,8 @@ func (update *WaveObjUpdate) UnmarshalJSON(data []byte) error {
|
|||||||
type Client struct {
|
type Client struct {
|
||||||
OID string `json:"oid"`
|
OID string `json:"oid"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
MainWindowId string `json:"mainwindowid"`
|
MainWindowId string `json:"mainwindowid"` // deprecated
|
||||||
|
WindowIds []string `json:"windowids"`
|
||||||
Meta map[string]any `json:"meta"`
|
Meta map[string]any `json:"meta"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user