From 0f4c1b918e9a5a878d9046385b043f608ca66cd6 Mon Sep 17 00:00:00 2001 From: James Kerr Date: Wed, 1 Nov 2023 15:22:41 -0700 Subject: [PATCH] Good Updaters for Linux and Others --- apps/zui/pages/update.tsx | 7 +- apps/zui/src/app/core/env.ts | 4 + apps/zui/src/core/on-state-change.ts | 17 +++ apps/zui/src/core/operations.ts | 4 + apps/zui/src/domain/updates/app-updater.ts | 107 ------------------ apps/zui/src/domain/updates/linux-updater.ts | 44 +++++++ .../zui/src/domain/updates/mac-win-updater.ts | 46 ++++++++ apps/zui/src/domain/updates/messages.ts | 2 +- apps/zui/src/domain/updates/operations.ts | 60 +++++----- apps/zui/src/domain/updates/scheduler.ts | 28 +++++ apps/zui/src/domain/updates/types.ts | 6 + apps/zui/src/domain/updates/updater.ts | 5 + apps/zui/src/electron/autoUpdater.test.ts | 32 ------ apps/zui/src/electron/autoUpdater.ts | 97 ---------------- apps/zui/src/electron/meta.ts | 1 + .../src/electron/ops/get-global-state-op.ts | 12 +- .../src/electron/run-main/run-operations.ts | 3 +- .../src/electron/windows/window-manager.ts | 6 + apps/zui/src/initializers/auto-update.ts | 18 ++- apps/zui/src/js/state/Updates/reducer.ts | 7 ++ apps/zui/src/js/state/Updates/selectors.ts | 13 ++- apps/zui/src/js/state/Updates/types.ts | 3 + apps/zui/src/js/state/types.ts | 2 + .../src/views/update-window/index.module.css | 1 + apps/zui/src/views/update-window/index.tsx | 3 +- 25 files changed, 234 insertions(+), 294 deletions(-) create mode 100644 apps/zui/src/core/on-state-change.ts delete mode 100644 apps/zui/src/domain/updates/app-updater.ts create mode 100644 apps/zui/src/domain/updates/linux-updater.ts create mode 100644 apps/zui/src/domain/updates/mac-win-updater.ts create mode 100644 apps/zui/src/domain/updates/scheduler.ts create mode 100644 apps/zui/src/domain/updates/types.ts create mode 100644 apps/zui/src/domain/updates/updater.ts delete mode 100644 apps/zui/src/electron/autoUpdater.test.ts delete mode 100644 apps/zui/src/electron/autoUpdater.ts create mode 100644 apps/zui/src/js/state/Updates/types.ts diff --git a/apps/zui/pages/update.tsx b/apps/zui/pages/update.tsx index b0b596bf28..d6f0cb19e4 100644 --- a/apps/zui/pages/update.tsx +++ b/apps/zui/pages/update.tsx @@ -1,16 +1,13 @@ import React, {useEffect, useState} from "react" import {AppProvider} from "src/app/core/context" -import {invoke} from "src/core/invoke" import initialize from "src/js/initializers/initialize" import {UpdateWindow} from "src/views/update-window" export default function Update() { const [app, setApp] = useState(null) + useEffect(() => { - initialize().then((app) => { - setApp(app) - invoke("updates.check") - }) + initialize().then((app) => setApp(app)) }, []) if (!app) return null diff --git a/apps/zui/src/app/core/env.ts b/apps/zui/src/app/core/env.ts index ce3bc05d17..a9730aebc6 100644 --- a/apps/zui/src/app/core/env.ts +++ b/apps/zui/src/app/core/env.ts @@ -1,4 +1,5 @@ import {app as electronApp} from "electron" +import pkg from "src/electron/pkg" const isPackaged = () => electronApp.isPackaged || process.env.NODE_ENV === "production" @@ -31,4 +32,7 @@ export default { get isLinux() { return process.platform === "linux" }, + get isInsiders() { + return pkg.name === "zui-insiders" + }, } diff --git a/apps/zui/src/core/on-state-change.ts b/apps/zui/src/core/on-state-change.ts new file mode 100644 index 0000000000..5a5320dad1 --- /dev/null +++ b/apps/zui/src/core/on-state-change.ts @@ -0,0 +1,17 @@ +import {Selector} from "@reduxjs/toolkit" +import {Store} from "src/js/state/types" + +export function onStateChange( + store: Store, + selector: Selector, + onChange: (value: any) => void +) { + let first = true + let prev = undefined + store.subscribe(() => { + const next = selector(store.getState()) + if (prev !== next || first) onChange(next) + prev = next + first = false + }) +} diff --git a/apps/zui/src/core/operations.ts b/apps/zui/src/core/operations.ts index 5784ea539d..df1e587cc5 100644 --- a/apps/zui/src/core/operations.ts +++ b/apps/zui/src/core/operations.ts @@ -1,10 +1,14 @@ import {ipcMain, IpcMainInvokeEvent} from "electron" import {OperationName} from "src/domain/messages" import {MainObject} from "./main/main-object" +import {Dispatch} from "src/js/state/types" +import {select} from "./main/select" type OperationContext = { + dispatch: Dispatch main: MainObject event?: IpcMainInvokeEvent | null + select: typeof select } let context: OperationContext | null = null diff --git a/apps/zui/src/domain/updates/app-updater.ts b/apps/zui/src/domain/updates/app-updater.ts deleted file mode 100644 index 8709796f67..0000000000 --- a/apps/zui/src/domain/updates/app-updater.ts +++ /dev/null @@ -1,107 +0,0 @@ -import {autoUpdater} from "electron-updater" -import Updates from "src/js/state/Updates" -import {Store} from "src/js/state/types" - -class AppUpdater { - static interval = 1000 * 60 * 60 * 24 // 1 day - private unlisten = () => {} - - initialize( - store: Store, - mode: "disabled" | "manual" | "startup" | "default" - ) { - this.cleanup() - this.unlisten = this.listen(store.dispatch) - - switch (mode) { - case "disabled": - autoUpdater.autoDownload = false - autoUpdater.autoInstallOnAppQuit = false - break - case "manual": - autoUpdater.autoDownload = false - autoUpdater.autoInstallOnAppQuit = false - break - case "startup": - autoUpdater.autoDownload = true - autoUpdater.autoInstallOnAppQuit = true - this.check() - break - default: - autoUpdater.autoDownload = true - autoUpdater.autoInstallOnAppQuit = true - this.check() - this.scheduleCheck() - } - } - - listen(dispatch) { - const onAvailable = (response) => { - dispatch(Updates.setIsChecking(false)) - dispatch(Updates.setNextVersion(response.version)) - } - const onNotAvailable = () => { - dispatch(Updates.setIsChecking(false)) - dispatch(Updates.setNextVersion(null)) - } - const onChecking = () => { - dispatch(Updates.setIsChecking(true)) - } - const onDownloadProgress = (response) => { - console.log("progress", response) - dispatch(Updates.setDownloadProgress(0.5)) - } - const onError = (error) => { - dispatch(Updates.setError(error)) - } - - autoUpdater - .on("update-available", onAvailable) - .on("update-not-available", onNotAvailable) - .on("checking-for-update", onChecking) - .on("download-progress", onDownloadProgress) - .on("error", onError) - - return () => { - autoUpdater - .off("update-available", onAvailable) - .off("update-not-available", onNotAvailable) - .off("checking-for-update", onChecking) - .off("download-progress", onDownloadProgress) - .off("error", onError) - } - } - - check() { - autoUpdater.checkForUpdates() - } - - download() { - return new Promise((resolve, reject) => { - autoUpdater.on("update-downloaded", resolve).on("error", reject) - autoUpdater.downloadUpdate() - }) - } - - install() { - autoUpdater.quitAndInstall() - } - - private cleanup() { - this.unlisten() - this.cancelSchedule() - } - - private scheduleId: any - private scheduleCheck() { - this.scheduleId = setTimeout(() => { - this.check() - this.scheduleCheck() - }, AppUpdater.interval) - } - private cancelSchedule() { - clearTimeout(this.scheduleId) - } -} - -export const appUpdater = new AppUpdater() diff --git a/apps/zui/src/domain/updates/linux-updater.ts b/apps/zui/src/domain/updates/linux-updater.ts new file mode 100644 index 0000000000..76fb88020e --- /dev/null +++ b/apps/zui/src/domain/updates/linux-updater.ts @@ -0,0 +1,44 @@ +import {app, shell} from "electron" +import fetch from "node-fetch" +import semver from "semver" +import env from "src/app/core/env" +import links from "src/app/core/links" +import pkg from "src/electron/pkg" +import {Updater} from "./types" + +export class LinuxUpdater implements Updater { + async check() { + const latest = await this.latest() + const current = app.getVersion() + if (semver.gte(current, latest)) { + return null + } else { + return latest + } + } + + async install() { + shell.openExternal(this.downloadUrl()) + } + + private async latest() { + const resp = await fetch(this.latestUrl()) + if (resp.status === 204) return app.getVersion() + const data = await resp.json() + return data.name + } + + private latestUrl() { + const repo = pkg.repo + const platform = "darwin-x64" // If the mac version exists, the linux does too + return `https://update.electronjs.org/${repo}/${platform}/${app.getVersion()}` + } + + private downloadUrl() { + if (env.isInsiders) { + return pkg.repo + "/releases/latest" + } else { + return links.ZUI_DOWNLOAD + } + } +} diff --git a/apps/zui/src/domain/updates/mac-win-updater.ts b/apps/zui/src/domain/updates/mac-win-updater.ts new file mode 100644 index 0000000000..5276a7c510 --- /dev/null +++ b/apps/zui/src/domain/updates/mac-win-updater.ts @@ -0,0 +1,46 @@ +import {autoUpdater} from "electron-updater" +import {Updater} from "./types" +import semver from "semver" +import {app} from "electron" + +autoUpdater.autoDownload = false +autoUpdater.autoInstallOnAppQuit = false + +export class MacWinUpdater implements Updater { + async check() { + const {updateInfo} = await autoUpdater.checkForUpdates() + const latest = updateInfo.version + const current = app.getVersion() + if (semver.lte(current, latest)) { + return latest + } else { + return null + } + } + + async install(onProgress) { + const progress = (r) => { + onProgress(r.percent / 100) + } + + let resolve + let reject + + return new Promise((res, rej) => { + resolve = res + reject = rej + autoUpdater.on("update-downloaded", resolve) + autoUpdater.on("download-progress", progress) + autoUpdater.on("error", reject) + autoUpdater.downloadUpdate() + }) + .finally(() => { + autoUpdater.off("download-progress", onProgress) + autoUpdater.off("update-downloaded", resolve) + autoUpdater.off("error", reject) + }) + .then(() => { + autoUpdater.quitAndInstall() + }) + } +} diff --git a/apps/zui/src/domain/updates/messages.ts b/apps/zui/src/domain/updates/messages.ts index 6da4cd36c3..9b5e218059 100644 --- a/apps/zui/src/domain/updates/messages.ts +++ b/apps/zui/src/domain/updates/messages.ts @@ -3,5 +3,5 @@ import * as operations from "./operations" export type UpdatesOperations = { "updates.open": typeof operations.open "updates.check": typeof operations.check - "updates.downloadAndInstall": typeof operations.downloadAndInstall + "updates.install": typeof operations.install } diff --git a/apps/zui/src/domain/updates/operations.ts b/apps/zui/src/domain/updates/operations.ts index 9d50491d53..1ceb6f3144 100644 --- a/apps/zui/src/domain/updates/operations.ts +++ b/apps/zui/src/domain/updates/operations.ts @@ -1,42 +1,38 @@ import {createOperation} from "src/core/operations" -import {appUpdater} from "./app-updater" +import {updater} from "./updater" +import Updates from "src/js/state/Updates" +import {errorToString} from "src/util/error-to-string" export const open = createOperation("updates.open", ({main}) => { - main.windows.create("update") -}) -// -export const check = createOperation("updates.check", async () => { - appUpdater.check() + main.windows.activate("update") }) -export const downloadAndInstall = createOperation( - "updates.downloadAndInstall", - async () => { +export const check = createOperation( + "updates.check", + async ({main, dispatch}) => { + dispatch(Updates.setIsChecking(true)) + const newVersion = await updater.check() + dispatch(Updates.setIsChecking(false)) + + if (newVersion) { + dispatch(Updates.setNextVersion(newVersion)) + main.windows.activate("update") + } + } +) + +export const install = createOperation( + "updates.install", + async ({dispatch}) => { + const onProgress = (n: number) => dispatch(Updates.setDownloadProgress(n)) try { - await appUpdater.download() - appUpdater.install() + dispatch(Updates.setIsDownloading(true)) + dispatch(Updates.setDownloadProgress(0)) + await updater.install(onProgress) } catch (e) { - console.log("Error", e) + dispatch(Updates.setError(errorToString(e))) + } finally { + dispatch(Updates.setIsDownloading(false)) } } ) - -// MANUAL FLOW -// 1. user click check for updates -// 2. app triggers update check -// 3. app opens update window - -// 4. update window checks state -// 5. renders progress bar if state.isChecking -// 8. renders up to date if state.isUpToDate -// 9. renders new version available if state.newVersionAvailable -// 10. Button to Install -// 11. Button to Cancel -// 12. Note that you can change update settings in Settings - -// AUTO FLOW -// app auto checks on startup, -// app downloads update in the background -// app waits until restart to install -// app udates menu with "New Version Available..." -// when the auu quits it will be updated diff --git a/apps/zui/src/domain/updates/scheduler.ts b/apps/zui/src/domain/updates/scheduler.ts new file mode 100644 index 0000000000..00705a817f --- /dev/null +++ b/apps/zui/src/domain/updates/scheduler.ts @@ -0,0 +1,28 @@ +import {UpdateMode} from "./types" + +export class Scheduler { + static interval = 1000 * 60 * 60 * 24 // 1 day + + start(mode: UpdateMode, check: () => any) { + switch (mode) { + case "default": + check() + this.schedule(check) + break + case "startup": + check() + } + } + + private scheduleId: any + private schedule(check: () => any) { + this.scheduleId = setTimeout(() => { + check() + this.schedule(check) + }, Scheduler.interval) + } + + stop() { + clearTimeout(this.scheduleId) + } +} diff --git a/apps/zui/src/domain/updates/types.ts b/apps/zui/src/domain/updates/types.ts new file mode 100644 index 0000000000..f30977009b --- /dev/null +++ b/apps/zui/src/domain/updates/types.ts @@ -0,0 +1,6 @@ +export type UpdateMode = "disabled" | "manual" | "startup" | "default" + +export interface Updater { + check(): Promise + install(onProgress: (percent: number) => void): Promise +} diff --git a/apps/zui/src/domain/updates/updater.ts b/apps/zui/src/domain/updates/updater.ts new file mode 100644 index 0000000000..0dfed41205 --- /dev/null +++ b/apps/zui/src/domain/updates/updater.ts @@ -0,0 +1,5 @@ +import env from "src/app/core/env" +import {LinuxUpdater} from "./linux-updater" +import {MacWinUpdater} from "./mac-win-updater" + +export const updater = env.isLinux ? new LinuxUpdater() : new MacWinUpdater() diff --git a/apps/zui/src/electron/autoUpdater.test.ts b/apps/zui/src/electron/autoUpdater.test.ts deleted file mode 100644 index 2a2f5d0780..0000000000 --- a/apps/zui/src/electron/autoUpdater.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @jest-envrionment jsdom - */ - -import "src/test/system/real-paths" -import {rest} from "msw" -import {setupServer} from "msw/node" -import {getLatestVersion} from "./autoUpdater" - -// @ts-ignore -global.localStorage = { - getItem: jest.fn(), - setItem: jest.fn(), -} - -const server = setupServer( - rest.get( - "https://update.electronjs.org/brimdata/zui/darwin-x64/0.0.0", - (req, res, ctx) => { - return res(ctx.status(200), ctx.json({name: "0.0.1"})) - } - ) -) - -beforeAll(() => server.listen()) -afterAll(() => server.close()) -afterEach(() => server.resetHandlers()) - -test("get latest version", async () => { - const version = await getLatestVersion("brimdata/zui") - expect(version).toBe("0.0.1") -}) diff --git a/apps/zui/src/electron/autoUpdater.ts b/apps/zui/src/electron/autoUpdater.ts deleted file mode 100644 index 00f952bccc..0000000000 --- a/apps/zui/src/electron/autoUpdater.ts +++ /dev/null @@ -1,97 +0,0 @@ -import env from "src/app/core/env" -import {app, dialog} from "electron" -import log from "electron-log" -import {autoUpdater} from "electron-updater" -import got from "got" -import get from "lodash/get" -import semver from "semver/preload" -import open from "../js/lib/open" -import {MainObject} from "../core/main/main-object" -import links from "src/app/core/links" -import pkg from "../../package.json" - -const getFeedURLForPlatform = (repo, platform) => { - return `https://update.electronjs.org/${repo}/${platform}/${app.getVersion()}` -} - -export const getLatestVersion = async (repo: string): Promise => { - // Check for updates for MacOS and if there are then we assume there is also one for our other supported OSs - const url = getFeedURLForPlatform(repo, "darwin-x64") - const resp = await got(url) - - // the update server responds with a 204 and no body if the current version is the same as the - // latest version, but will otherwise return json naming the latest version published on github - // (even if it is behind the current version) - if (resp.statusCode === 204) return app.getVersion() - - const body = JSON.parse(resp.body) - const latestVersion = get(body, "name", "") - if (!semver.valid(latestVersion)) - log.error(new Error(`Invalid latest version format: ${latestVersion}`)) - - return latestVersion -} - -const autoUpdateLinux = async (main: MainObject) => { - const latestVersion = await getLatestVersion(main.appMeta.repo) - - // up to date - if (semver.gte(app.getVersion(), latestVersion)) return - - const dialogOpts = { - type: "info", - buttons: ["Get Update", "Later"], - title: "Application Update", - message: "A new version of Zui is available.", - detail: `Zui version ${latestVersion} is available for download; you are running v${app.getVersion()}.`, - } - - dialog.showMessageBox(dialogOpts).then((returnValue) => { - const navUrl = - pkg.name == "zui-insiders" - ? pkg.repository + "/releases/latest" - : links.ZUI_DOWNLOAD - if (returnValue.response === 0) open(navUrl) - }) -} - -export async function setupAutoUpdater(main: MainObject) { - if (env.isLinux) { - setUpdateRepeater(() => { - autoUpdateLinux(main).catch((err) => log.error(err)) - }) - - return - } - - autoUpdater.on("update-downloaded", (event, releaseNotes, releaseName) => { - const dialogOpts = { - type: "info", - buttons: ["Restart", "Later"], - title: "Application Update", - // releaseNotes are not available for windows, so use name instead - message: env.isWindows ? releaseNotes : releaseName, - detail: - "A new version of Zui has been downloaded. Restart the application to apply the update.", - } - - dialog.showMessageBox(dialogOpts).then((returnValue) => { - if (returnValue.response === 0) autoUpdater.quitAndInstall() - }) - }) - - autoUpdater.on("error", (err) => { - log.error("There was a problem updating the application: " + err) - }) - - setUpdateRepeater(() => { - autoUpdater.checkForUpdates() - }) -} - -const setUpdateRepeater = (updateCb) => { - // check for updates 30s after startup - setTimeout(updateCb, 30 * 1000) - // then check for updates once a day - setInterval(updateCb, 24 * 60 * 60 * 1000) -} diff --git a/apps/zui/src/electron/meta.ts b/apps/zui/src/electron/meta.ts index 843c6a3709..8636ea9d31 100644 --- a/apps/zui/src/electron/meta.ts +++ b/apps/zui/src/electron/meta.ts @@ -9,6 +9,7 @@ export async function getAppMeta() { version: app.getVersion(), isFirstRun: await isFirstRun(), userName: os.userInfo().username, + name: pkg.name, } } diff --git a/apps/zui/src/electron/ops/get-global-state-op.ts b/apps/zui/src/electron/ops/get-global-state-op.ts index 14381a9f3f..a30f74b126 100644 --- a/apps/zui/src/electron/ops/get-global-state-op.ts +++ b/apps/zui/src/electron/ops/get-global-state-op.ts @@ -1,6 +1,16 @@ import {getPersistedGlobalState} from "src/js/state/stores/get-persistable" import {createOperation} from "../../core/operations" +import {pick} from "lodash" + +const GLOBAL_STATE_KEYS = ["updates"] export const getGlobalStateOp = createOperation("getGlobalState", ({main}) => { - return getPersistedGlobalState(main.store.getState()) + const state = main.store.getState() + // Any global state that we persist should be + // sent to new windows being created. + // But also data that is relevant, but not persisted. + return { + ...getPersistedGlobalState(state), + ...pick(state, GLOBAL_STATE_KEYS), + } }) diff --git a/apps/zui/src/electron/run-main/run-operations.ts b/apps/zui/src/electron/run-main/run-operations.ts index da4c786030..a3fe3c18fb 100644 --- a/apps/zui/src/electron/run-main/run-operations.ts +++ b/apps/zui/src/electron/run-main/run-operations.ts @@ -1,9 +1,10 @@ import {MainObject} from "src/core/main/main-object" +import {select} from "src/core/main/select" import {setOperationContext} from "src/core/operations" // Importing these will set up the listeners import "src/domain/operations" import "src/electron/ops" export function runOperations(main: MainObject) { - setOperationContext({main}) + setOperationContext({main, dispatch: main.store.dispatch, select}) } diff --git a/apps/zui/src/electron/windows/window-manager.ts b/apps/zui/src/electron/windows/window-manager.ts index 0ac4609c4f..a61879ba28 100644 --- a/apps/zui/src/electron/windows/window-manager.ts +++ b/apps/zui/src/electron/windows/window-manager.ts @@ -47,6 +47,12 @@ export class WindowManager extends EventEmitter { ) } + activate(name: WindowName) { + const [window] = this.byName(name) + if (window) window.ref.focus() + else this.create(name) + } + get focused() { return this.all.find( (f) => f.ref.webContents === BrowserWindow.getFocusedWindow()?.webContents diff --git a/apps/zui/src/initializers/auto-update.ts b/apps/zui/src/initializers/auto-update.ts index 837d886828..8df01a9eb3 100644 --- a/apps/zui/src/initializers/auto-update.ts +++ b/apps/zui/src/initializers/auto-update.ts @@ -1,16 +1,14 @@ import env from "src/app/core/env" import {MainObject} from "../core/main/main-object" -import {appUpdater} from "src/domain/updates/app-updater" -import {configurations} from "src/zui" +import ConfigPropValues from "src/js/state/ConfigPropValues" +import {Scheduler} from "src/domain/updates/scheduler" +import {check} from "src/domain/updates/operations" +import {select} from "src/core/main/select" -export function initialize(main: MainObject) { +export function initialize(_main: MainObject) { if (env.isTest) return - let prev = null - main.store.subscribe(() => { - const mode = configurations.get("application", "updateMode") - if (mode !== prev) { - appUpdater.initialize(main.store, mode) - } - }) + const mode = select(ConfigPropValues.get("application", "updateMode")) + const schedule = new Scheduler() + schedule.start(mode, check) } diff --git a/apps/zui/src/js/state/Updates/reducer.ts b/apps/zui/src/js/state/Updates/reducer.ts index 7dc6168921..32eb100f17 100644 --- a/apps/zui/src/js/state/Updates/reducer.ts +++ b/apps/zui/src/js/state/Updates/reducer.ts @@ -5,10 +5,14 @@ export const slice = createSlice({ initialState: { nextVersion: null as null | string, isChecking: false, + isDownloading: false, downloadProgress: null as null | number, error: null, }, reducers: { + reset() { + return slice.initialState() + }, setNextVersion(s, a: PayloadAction) { s.nextVersion = a.payload }, @@ -18,6 +22,9 @@ export const slice = createSlice({ setDownloadProgress(s, a: PayloadAction) { s.downloadProgress = a.payload }, + setIsDownloading(s, a: PayloadAction) { + s.isDownloading = a.payload + }, setError(s, a: PayloadAction) { s.error = a.payload }, diff --git a/apps/zui/src/js/state/Updates/selectors.ts b/apps/zui/src/js/state/Updates/selectors.ts index 5b29ff1b59..070c776d47 100644 --- a/apps/zui/src/js/state/Updates/selectors.ts +++ b/apps/zui/src/js/state/Updates/selectors.ts @@ -1,7 +1,8 @@ -import {isNumber} from "lodash" +import {State} from "../types" -export const isChecking = (state) => state.updates.isChecking -export const getNextVersion = (state) => state.updates.nextVersion -export const getDownloadProgress = (state) => state.updates.downloadProgress -export const isDownloading = (state) => isNumber(state.updates.downloadProgress) -export const getError = (state) => state.updates.error +export const isChecking = (state: State) => state.updates.isChecking +export const getNextVersion = (state: State) => state.updates.nextVersion +export const getDownloadProgress = (state: State) => + state.updates.downloadProgress +export const isDownloading = (state: State) => state.updates.isDownloading +export const getError = (state: State) => state.updates.error diff --git a/apps/zui/src/js/state/Updates/types.ts b/apps/zui/src/js/state/Updates/types.ts new file mode 100644 index 0000000000..2c697e78bd --- /dev/null +++ b/apps/zui/src/js/state/Updates/types.ts @@ -0,0 +1,3 @@ +import {slice} from "./reducer" + +export type UpdatesState = ReturnType diff --git a/apps/zui/src/js/state/types.ts b/apps/zui/src/js/state/types.ts index ce1da456cb..da41e400a5 100644 --- a/apps/zui/src/js/state/types.ts +++ b/apps/zui/src/js/state/types.ts @@ -20,6 +20,7 @@ import {SessionHistoriesState} from "./SessionHistories/types" import {PoolSettingsState} from "./PoolSettings/types" import {WindowState} from "./Window/types" import {LoadDataFormState} from "./LoadDataForm/types" +import {UpdatesState} from "./Updates/types" export type ThunkExtraArg = { api: ZuiApi @@ -54,4 +55,5 @@ export type State = { tabHistories: TabHistoriesState toolbars: ToolbarsState window: WindowState + updates: UpdatesState } diff --git a/apps/zui/src/views/update-window/index.module.css b/apps/zui/src/views/update-window/index.module.css index e7d2aca2bd..86e0ddc3da 100644 --- a/apps/zui/src/views/update-window/index.module.css +++ b/apps/zui/src/views/update-window/index.module.css @@ -53,6 +53,7 @@ .progress { margin: 0.25rem 0; + padding: 0 2rem; } @media (prefers-color-scheme: dark) { diff --git a/apps/zui/src/views/update-window/index.tsx b/apps/zui/src/views/update-window/index.tsx index de9c6b505c..69f0f4e1a0 100644 --- a/apps/zui/src/views/update-window/index.tsx +++ b/apps/zui/src/views/update-window/index.tsx @@ -27,9 +27,8 @@ function useTemplate() { const downloadProgress = useSelector(Updates.getDownloadProgress) const error = useSelector(Updates.getError) const closeWindow = () => invoke("window.close", globalThis.windowId) - const install = () => invoke("updates.downloadAndInstall") + const install = () => invoke("updates.install") const check = () => invoke("updates.check") - switch (status) { case "error": return {