mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
Add banner for app updates, clean up updater logic (#200)
This improves the app updater so that it doesn't rely on unreliable system notifications. Now, a banner in the tab bar will display when an update is available. Clicking this will prompt the user to restart the app and complete the installation. This also updates the tab bar to move to the smaller tab size earlier so we don't need to make the tab bar scrollable as much. ![image](https://github.com/user-attachments/assets/79e24617-d609-4554-bdb2-979f810a9b66)
This commit is contained in:
parent
11db01ebde
commit
824a8540ff
177
emain/emain.ts
177
emain/emain.ts
@ -1,10 +1,7 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { initGlobal } from "@/app/store/global";
|
||||
import { WaveDevVarName, WaveDevViteVarName } from "@/util/isdev";
|
||||
import * as electron from "electron";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import fs from "fs";
|
||||
import * as child_process from "node:child_process";
|
||||
import os from "os";
|
||||
@ -14,10 +11,13 @@ import { sprintf } from "sprintf-js";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import * as util from "util";
|
||||
import winston from "winston";
|
||||
import { initGlobal } from "../frontend/app/store/global";
|
||||
import * as services from "../frontend/app/store/services";
|
||||
import { WSServerEndpointVarName, WebServerEndpointVarName, getWebServerEndpoint } from "../frontend/util/endpoints";
|
||||
import { WaveDevVarName, WaveDevViteVarName } from "../frontend/util/isdev";
|
||||
import * as keyutil from "../frontend/util/keyutil";
|
||||
import { fireAndForget } from "../frontend/util/util";
|
||||
import { configureAutoUpdater, updater } from "./updater";
|
||||
|
||||
const electronApp = electron.app;
|
||||
|
||||
@ -624,12 +624,7 @@ function makeAppMenu() {
|
||||
{
|
||||
label: "Check for Updates",
|
||||
click: () => {
|
||||
const checkingNotification = new electron.Notification({
|
||||
title: "Wave Terminal",
|
||||
body: "Checking for updates.",
|
||||
});
|
||||
checkingNotification.show();
|
||||
fireAndForget(() => checkForUpdates());
|
||||
fireAndForget(() => updater?.checkForUpdates(true));
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -760,170 +755,6 @@ process.on("uncaughtException", (error) => {
|
||||
electronApp.quit();
|
||||
});
|
||||
|
||||
// ====== AUTO-UPDATER ====== //
|
||||
let autoUpdateLock = false;
|
||||
let autoUpdateInterval: NodeJS.Timeout | null = null;
|
||||
let availableUpdateReleaseName: string | null = null;
|
||||
let availableUpdateReleaseNotes: string | null = null;
|
||||
let appUpdateStatus = "unavailable";
|
||||
let lastUpdateCheck: Date = null;
|
||||
|
||||
/**
|
||||
* Sets the app update status and sends it to the main window
|
||||
* @param status The AppUpdateStatus to set, either "ready" or "unavailable"
|
||||
*/
|
||||
function setAppUpdateStatus(status: string) {
|
||||
appUpdateStatus = status;
|
||||
electron.BrowserWindow.getAllWindows().forEach((window) => {
|
||||
window.webContents.send("app-update-status", appUpdateStatus);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an hour has passed since the last update check, and if so, checks for updates using the `autoUpdater` object
|
||||
*/
|
||||
async function checkForUpdates() {
|
||||
const autoUpdateOpts = (await services.FileService.GetSettingsConfig()).autoupdate;
|
||||
|
||||
if (!autoUpdateOpts.enabled) {
|
||||
console.log("Auto update is disabled in settings. Removing the auto update interval.");
|
||||
clearInterval(autoUpdateInterval);
|
||||
autoUpdateInterval = null;
|
||||
return;
|
||||
}
|
||||
const now = new Date();
|
||||
if (!lastUpdateCheck || Math.abs(now.getTime() - lastUpdateCheck.getTime()) > autoUpdateOpts.intervalms) {
|
||||
fireAndForget(() => autoUpdater.checkForUpdates());
|
||||
lastUpdateCheck = now;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the updater and sets up event listeners
|
||||
*/
|
||||
function initUpdater() {
|
||||
if (isDev) {
|
||||
console.log("skipping auto-updater in dev mode");
|
||||
return null;
|
||||
}
|
||||
|
||||
setAppUpdateStatus("unavailable");
|
||||
|
||||
autoUpdater.removeAllListeners();
|
||||
|
||||
autoUpdater.on("error", (err) => {
|
||||
console.log("updater error");
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
console.log("checking-for-update");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-available", () => {
|
||||
console.log("update-available; downloading...");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
console.log("update-not-available");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", (event) => {
|
||||
console.log("update-downloaded", [event]);
|
||||
availableUpdateReleaseName = event.releaseName;
|
||||
availableUpdateReleaseNotes = event.releaseNotes as string | null;
|
||||
|
||||
// Display the update banner and create a system notification
|
||||
setAppUpdateStatus("ready");
|
||||
const updateNotification = new electron.Notification({
|
||||
title: "Wave Terminal",
|
||||
body: "A new version of Wave Terminal is ready to install.",
|
||||
});
|
||||
updateNotification.on("click", () => {
|
||||
fireAndForget(() => installAppUpdate());
|
||||
});
|
||||
updateNotification.show();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the auto update check interval.
|
||||
* @returns The timeout object for the auto update checker.
|
||||
*/
|
||||
function startAutoUpdateInterval(): NodeJS.Timeout {
|
||||
// check for updates right away and keep checking later
|
||||
checkForUpdates();
|
||||
return setInterval(() => {
|
||||
checkForUpdates();
|
||||
}, 600000); // intervals are unreliable when an app is suspended so we will check every 10 mins if an hour has passed.
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user to install the downloaded application update and restarts the application
|
||||
*/
|
||||
async function installAppUpdate() {
|
||||
const dialogOpts: Electron.MessageBoxOptions = {
|
||||
type: "info",
|
||||
buttons: ["Restart", "Later"],
|
||||
title: "Application Update",
|
||||
message: process.platform === "win32" ? availableUpdateReleaseNotes : availableUpdateReleaseName,
|
||||
detail: "A new version has been downloaded. Restart the application to apply the updates.",
|
||||
};
|
||||
|
||||
const allWindows = electron.BrowserWindow.getAllWindows();
|
||||
if (allWindows.length > 0) {
|
||||
await electron.dialog
|
||||
.showMessageBox(electron.BrowserWindow.getFocusedWindow() ?? allWindows[0], dialogOpts)
|
||||
.then(({ response }) => {
|
||||
if (response === 0) autoUpdater.quitAndInstall();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
electron.ipcMain.on("install-app-update", () => fireAndForget(() => installAppUpdate()));
|
||||
electron.ipcMain.on("get-app-update-status", (event) => {
|
||||
event.returnValue = appUpdateStatus;
|
||||
});
|
||||
|
||||
/**
|
||||
* Configures the auto-updater based on the user's preference
|
||||
* @param enabled Whether the auto-updater should be enabled
|
||||
*/
|
||||
async function configureAutoUpdater() {
|
||||
// simple lock to prevent multiple auto-update configuration attempts, this should be very rare
|
||||
if (autoUpdateLock) {
|
||||
console.log("auto-update configuration already in progress, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
autoUpdateLock = true;
|
||||
|
||||
const autoUpdateEnabled = (await services.FileService.GetSettingsConfig()).autoupdate.enabled;
|
||||
|
||||
try {
|
||||
console.log("Configuring updater");
|
||||
initUpdater();
|
||||
} catch (e) {
|
||||
console.warn("error configuring updater", e.toString());
|
||||
}
|
||||
|
||||
if (autoUpdateEnabled && autoUpdateInterval == null) {
|
||||
lastUpdateCheck = null;
|
||||
try {
|
||||
console.log("configuring auto update interval");
|
||||
autoUpdateInterval = startAutoUpdateInterval();
|
||||
} catch (e) {
|
||||
console.log("error configuring auto update interval", e.toString());
|
||||
}
|
||||
} else if (!autoUpdateEnabled && autoUpdateInterval != null) {
|
||||
console.log("disabling auto updater");
|
||||
clearInterval(autoUpdateInterval);
|
||||
autoUpdateInterval = null;
|
||||
}
|
||||
autoUpdateLock = false;
|
||||
}
|
||||
// ====== AUTO-UPDATER ====== //
|
||||
|
||||
async function relaunchBrowserWindows() {
|
||||
globalIsRelaunching = true;
|
||||
const windows = electron.BrowserWindow.getAllWindows();
|
||||
|
@ -21,6 +21,9 @@ contextBridge.exposeInMainWorld("api", {
|
||||
getEnv: (varName) => ipcRenderer.sendSync("getEnv", varName),
|
||||
onFullScreenChange: (callback) =>
|
||||
ipcRenderer.on("fullscreen-change", (_event, isFullScreen) => callback(isFullScreen)),
|
||||
onUpdaterStatusChange: (callback) => ipcRenderer.on("app-update-status", (_event, status) => callback(status)),
|
||||
getUpdaterStatus: () => ipcRenderer.sendSync("get-app-update-status"),
|
||||
installAppUpdate: () => ipcRenderer.send("install-app-update"),
|
||||
});
|
||||
|
||||
// Custom event for "new-window"
|
||||
|
191
emain/updater.ts
Normal file
191
emain/updater.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import * as electron from "electron";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import * as services from "../frontend/app/store/services";
|
||||
import { isDev } from "../frontend/util/isdev";
|
||||
import { fireAndForget } from "../frontend/util/util";
|
||||
|
||||
let autoUpdateLock = false;
|
||||
|
||||
export let updater: Updater;
|
||||
|
||||
export class Updater {
|
||||
interval: NodeJS.Timeout | null;
|
||||
availableUpdateReleaseName: string | null;
|
||||
availableUpdateReleaseNotes: string | null;
|
||||
_status: UpdaterStatus;
|
||||
lastUpdateCheck: Date;
|
||||
|
||||
constructor() {
|
||||
if (isDev) {
|
||||
console.log("skipping auto-updater in dev mode");
|
||||
return null;
|
||||
}
|
||||
|
||||
autoUpdater.removeAllListeners();
|
||||
|
||||
autoUpdater.on("error", (err) => {
|
||||
console.log("updater error");
|
||||
console.log(err);
|
||||
this.status = "error";
|
||||
});
|
||||
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
console.log("checking-for-update");
|
||||
this.status = "checking";
|
||||
});
|
||||
|
||||
autoUpdater.on("update-available", () => {
|
||||
console.log("update-available; downloading...");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
console.log("update-not-available");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", (event) => {
|
||||
console.log("update-downloaded", [event]);
|
||||
this.availableUpdateReleaseName = event.releaseName;
|
||||
this.availableUpdateReleaseNotes = event.releaseNotes as string | null;
|
||||
|
||||
// Display the update banner and create a system notification
|
||||
this.status = "ready";
|
||||
const updateNotification = new electron.Notification({
|
||||
title: "Wave Terminal",
|
||||
body: "A new version of Wave Terminal is ready to install.",
|
||||
});
|
||||
updateNotification.on("click", () => {
|
||||
fireAndForget(() => this.installAppUpdate());
|
||||
});
|
||||
updateNotification.show();
|
||||
});
|
||||
|
||||
this._status = "up-to-date";
|
||||
this.lastUpdateCheck = new Date(0);
|
||||
this.interval = null;
|
||||
this.availableUpdateReleaseName = null;
|
||||
}
|
||||
|
||||
get status(): UpdaterStatus {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the app update status and sends it to the main window
|
||||
* @param value The AppUpdateStatus to set, either "ready" or "unavailable"
|
||||
*/
|
||||
private set status(value: UpdaterStatus) {
|
||||
this._status = value;
|
||||
electron.BrowserWindow.getAllWindows().forEach((window) => {
|
||||
window.webContents.send("app-update-status", value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the auto update check interval.
|
||||
* @returns The timeout object for the auto update checker.
|
||||
*/
|
||||
startAutoUpdateInterval(): NodeJS.Timeout {
|
||||
// check for updates right away and keep checking later
|
||||
this.checkForUpdates(false);
|
||||
return setInterval(() => {
|
||||
this.checkForUpdates(false);
|
||||
}, 600000); // intervals are unreliable when an app is suspended so we will check every 10 mins if an hour has passed.
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an hour has passed since the last update check, and if so, checks for updates using the `autoUpdater` object
|
||||
* @param userInput Whether the user is requesting this. If so, an alert will report the result of the check.
|
||||
*/
|
||||
async checkForUpdates(userInput: boolean) {
|
||||
const autoUpdateOpts = (await services.FileService.GetSettingsConfig()).autoupdate;
|
||||
|
||||
if (!autoUpdateOpts.enabled) {
|
||||
console.log("Auto update is disabled in settings. Removing the auto update interval.");
|
||||
clearInterval(this.interval);
|
||||
this.interval = null;
|
||||
return;
|
||||
}
|
||||
const now = new Date();
|
||||
if (
|
||||
!this.lastUpdateCheck ||
|
||||
Math.abs(now.getTime() - this.lastUpdateCheck.getTime()) > autoUpdateOpts.intervalms
|
||||
) {
|
||||
fireAndForget(() =>
|
||||
autoUpdater.checkForUpdates().then(() => {
|
||||
if (userInput && this.status === "up-to-date") {
|
||||
const dialogOpts: Electron.MessageBoxOptions = {
|
||||
type: "info",
|
||||
message: "There are currently no updates available.",
|
||||
};
|
||||
electron.dialog.showMessageBox(electron.BrowserWindow.getFocusedWindow(), dialogOpts);
|
||||
}
|
||||
})
|
||||
);
|
||||
this.lastUpdateCheck = now;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user to install the downloaded application update and restarts the application
|
||||
*/
|
||||
async installAppUpdate() {
|
||||
const dialogOpts: Electron.MessageBoxOptions = {
|
||||
type: "info",
|
||||
buttons: ["Restart", "Later"],
|
||||
title: "Application Update",
|
||||
message: process.platform === "win32" ? this.availableUpdateReleaseNotes : this.availableUpdateReleaseName,
|
||||
detail: "A new version has been downloaded. Restart the application to apply the updates.",
|
||||
};
|
||||
|
||||
const allWindows = electron.BrowserWindow.getAllWindows();
|
||||
if (allWindows.length > 0) {
|
||||
await electron.dialog
|
||||
.showMessageBox(electron.BrowserWindow.getFocusedWindow() ?? allWindows[0], dialogOpts)
|
||||
.then(({ response }) => {
|
||||
if (response === 0) autoUpdater.quitAndInstall();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
electron.ipcMain.on("install-app-update", () => fireAndForget(() => updater?.installAppUpdate()));
|
||||
electron.ipcMain.on("get-app-update-status", (event) => {
|
||||
event.returnValue = updater?.status;
|
||||
});
|
||||
|
||||
/**
|
||||
* Configures the auto-updater based on the user's preference
|
||||
* @param enabled Whether the auto-updater should be enabled
|
||||
*/
|
||||
export async function configureAutoUpdater() {
|
||||
// simple lock to prevent multiple auto-update configuration attempts, this should be very rare
|
||||
if (autoUpdateLock) {
|
||||
console.log("auto-update configuration already in progress, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
autoUpdateLock = true;
|
||||
|
||||
const autoUpdateEnabled = (await services.FileService.GetSettingsConfig()).autoupdate.enabled;
|
||||
|
||||
try {
|
||||
console.log("Configuring updater");
|
||||
updater = new Updater();
|
||||
} catch (e) {
|
||||
console.warn("error configuring updater", e.toString());
|
||||
}
|
||||
|
||||
if (autoUpdateEnabled && updater?.interval == null) {
|
||||
try {
|
||||
console.log("configuring auto update interval");
|
||||
updater?.startAutoUpdateInterval();
|
||||
} catch (e) {
|
||||
console.log("error configuring auto update interval", e.toString());
|
||||
}
|
||||
} else if (!autoUpdateEnabled && updater?.interval != null) {
|
||||
console.log("disabling auto updater");
|
||||
clearInterval(updater.interval);
|
||||
updater = null;
|
||||
}
|
||||
autoUpdateLock = false;
|
||||
}
|
@ -104,6 +104,15 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
||||
return windowData.activetabid;
|
||||
});
|
||||
const cmdShiftDelayAtom = jotai.atom(false);
|
||||
const updateStatusAtom = jotai.atom<UpdaterStatus>("up-to-date") as jotai.PrimitiveAtom<UpdaterStatus>;
|
||||
try {
|
||||
getApi().onUpdaterStatusChange((status) => {
|
||||
console.log("updater status change", status);
|
||||
globalStore.set(updateStatusAtom, status);
|
||||
});
|
||||
} catch (_) {
|
||||
// do nothing
|
||||
}
|
||||
atoms = {
|
||||
// initialized in wave.ts (will not be null inside of application)
|
||||
windowId: windowIdAtom,
|
||||
@ -117,6 +126,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
|
||||
activeTabId: activeTabIdAtom,
|
||||
isFullScreen: isFullScreenAtom,
|
||||
cmdShiftDelayAtom: cmdShiftDelayAtom,
|
||||
updaterStatusAtom: updateStatusAtom,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,6 @@
|
||||
|
||||
&.right {
|
||||
flex-grow: 1;
|
||||
min-width: 74px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,4 +84,19 @@
|
||||
--os-handle-interactive-area-offset: 0px;
|
||||
--os-handle-border-radius: 2px;
|
||||
}
|
||||
|
||||
.update-available-label {
|
||||
height: 80%;
|
||||
opacity: 0.7;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: auto 4px;
|
||||
color: black;
|
||||
background-color: var(--accent-color);
|
||||
padding: 0 5px;
|
||||
border-radius: var(--block-border-radius);
|
||||
flex: 0 0 fit-content;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { WindowDrag } from "@/element/windowdrag";
|
||||
import { atoms, isDev } from "@/store/global";
|
||||
import { atoms, getApi, isDev } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import { deleteLayoutStateAtomForTab } from "frontend/layout/lib/layoutAtom";
|
||||
import { useAtomValue } from "jotai";
|
||||
@ -70,12 +70,15 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
||||
const draggerLeftRef = useRef<HTMLDivElement>(null);
|
||||
const tabWidthRef = useRef<number>(TAB_DEFAULT_WIDTH);
|
||||
const scrollableRef = useRef<boolean>(false);
|
||||
const updateStatusLabelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const windowData = useAtomValue(atoms.waveWindow);
|
||||
const { activetabid } = windowData;
|
||||
|
||||
const isFullScreen = useAtomValue(atoms.isFullScreen);
|
||||
|
||||
const appUpdateStatus = useAtomValue(atoms.updaterStatusAtom);
|
||||
|
||||
let prevDelta: number;
|
||||
let prevDragDirection: string;
|
||||
|
||||
@ -125,7 +128,9 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
||||
const tabbarWrapperWidth = tabbarWrapperRef.current.getBoundingClientRect().width;
|
||||
const windowDragLeftWidth = draggerLeftRef.current.getBoundingClientRect().width;
|
||||
const addBtnWidth = addBtnRef.current.getBoundingClientRect().width;
|
||||
const spaceForTabs = tabbarWrapperWidth - (windowDragLeftWidth + DRAGGER_RIGHT_MIN_WIDTH + addBtnWidth);
|
||||
const updateStatusLabelWidth = updateStatusLabelRef.current?.getBoundingClientRect().width ?? 0;
|
||||
const spaceForTabs =
|
||||
tabbarWrapperWidth - (windowDragLeftWidth + DRAGGER_RIGHT_MIN_WIDTH + addBtnWidth + updateStatusLabelWidth);
|
||||
|
||||
const numberOfTabs = tabIds.length;
|
||||
const totalDefaultTabWidth = numberOfTabs * TAB_DEFAULT_WIDTH;
|
||||
@ -137,7 +142,9 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
||||
|
||||
console.log("spaceForTabs", spaceForTabs, minTotalTabWidth);
|
||||
|
||||
if (minTotalTabWidth > spaceForTabs) {
|
||||
if (spaceForTabs < totalDefaultTabWidth && spaceForTabs > minTotalTabWidth) {
|
||||
newTabWidth = TAB_MIN_WIDTH;
|
||||
} else if (minTotalTabWidth > spaceForTabs) {
|
||||
// Case where tabs cannot shrink further, make the tab bar scrollable
|
||||
newTabWidth = TAB_MIN_WIDTH;
|
||||
newScrollable = true;
|
||||
@ -487,6 +494,19 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
function onUpdateAvailableClick() {
|
||||
getApi().installAppUpdate();
|
||||
}
|
||||
|
||||
let updateAvailableLabel: React.ReactNode = null;
|
||||
if (appUpdateStatus === "ready") {
|
||||
updateAvailableLabel = (
|
||||
<div ref={updateStatusLabelRef} className="update-available-label" onClick={onUpdateAvailableClick}>
|
||||
Update Available: Click to Install
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={tabbarWrapperRef} className="tab-bar-wrapper">
|
||||
<WindowDrag ref={draggerLeftRef} className="left" />
|
||||
@ -518,6 +538,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => {
|
||||
<i className="fa fa-solid fa-plus fa-fw" />
|
||||
</div>
|
||||
<WindowDrag ref={draggerRightRef} className="right" />
|
||||
{updateAvailableLabel}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
6
frontend/types/custom.d.ts
vendored
6
frontend/types/custom.d.ts
vendored
@ -17,6 +17,7 @@ declare global {
|
||||
activeTabId: jotai.Atom<string>; // derrived from windowDataAtom
|
||||
isFullScreen: jotai.PrimitiveAtom<boolean>;
|
||||
cmdShiftDelayAtom: jotai.PrimitiveAtom<boolean>;
|
||||
updaterStatusAtom: jotai.PrimitiveAtom<UpdaterStatus>;
|
||||
};
|
||||
|
||||
type TabLayoutData = {
|
||||
@ -44,6 +45,9 @@ declare global {
|
||||
downloadFile: (path: string) => void;
|
||||
openExternal: (url: string) => void;
|
||||
onFullScreenChange: (callback: (isFullScreen: boolean) => void) => void;
|
||||
onUpdaterStatusChange: (callback: (status: UpdaterStatus) => void) => void;
|
||||
getUpdaterStatus: () => UpdaterStatus;
|
||||
installAppUpdate: () => void;
|
||||
};
|
||||
|
||||
type ElectronContextMenuItem = {
|
||||
@ -178,6 +182,8 @@ declare global {
|
||||
giveFocus?: () => boolean;
|
||||
}
|
||||
|
||||
type UpdaterStatus = "up-to-date" | "checking" | "downloading" | "ready" | "error";
|
||||
|
||||
// jotai doesn't export this type :/
|
||||
type Loadable<T> = { state: "loading" } | { state: "hasData"; data: T } | { state: "hasError"; error: unknown };
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user