Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions electron/main/update.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { app, ipcMain } from 'electron'
import { createRequire } from 'node:module'
import { app, ipcMain } from "electron"
import { createRequire } from "node:module"
import type {
ProgressInfo,
UpdateDownloadedEvent,
UpdateInfo,
} from 'electron-updater'
import path from 'node:path';
} from "electron-updater"
import path from "node:path"

const { autoUpdater } = createRequire(import.meta.url)('electron-updater');
const { autoUpdater } = createRequire(import.meta.url)("electron-updater")

export function update(win: Electron.BrowserWindow) {

Expand All @@ -17,55 +17,55 @@ export function update(win: Electron.BrowserWindow) {
autoUpdater.allowDowngrade = false

if (process.env.DEBUG) {
autoUpdater.updateConfigPath = path.join(__dirname, "dev-app-update.yml");
autoUpdater.updateConfigPath = path.join(__dirname, "dev-app-update.yml")
}

// start check
autoUpdater.on('checking-for-update', function () { })
autoUpdater.on("checking-for-update", function () { })
// update available
autoUpdater.on('update-available', (arg: UpdateInfo) => {
win.webContents.send('update-can-available', { update: true, version: app.getVersion(), newVersion: arg?.version })
autoUpdater.on("update-available", (arg: UpdateInfo) => {
win.webContents.send("update-can-available", { update: true, version: app.getVersion(), newVersion: arg?.version })
})
// update not available
autoUpdater.on('update-not-available', (arg: UpdateInfo) => {
win.webContents.send('update-can-available', { update: false, version: app.getVersion(), newVersion: arg?.version })
autoUpdater.on("update-not-available", (arg: UpdateInfo) => {
win.webContents.send("update-can-available", { update: false, version: app.getVersion(), newVersion: arg?.version })
})

// Checking for updates
ipcMain.handle('check-update', async () => {
ipcMain.handle("check-update", async () => {
if (!app.isPackaged) {
const error = new Error('The update feature is only available after the package.')
const error = new Error("The update feature is only available after the package.")
return { message: error.message, error }
}

try {
return await autoUpdater.checkForUpdatesAndNotify()
} catch (error) {
return { message: 'Network error', error }
return { message: "Network error", error }
}
})

// Start downloading and feedback on progress
ipcMain.handle('start-download', (event: Electron.IpcMainInvokeEvent) => {
ipcMain.handle("start-download", (event: Electron.IpcMainInvokeEvent) => {
startDownload(
(error, progressInfo) => {
if (error) {
// feedback download error message
event.sender.send('update-error', { message: error.message, error })
event.sender.send("update-error", { message: error.message, error })
} else {
// feedback update progress message
event.sender.send('download-progress', progressInfo)
event.sender.send("download-progress", progressInfo)
}
},
() => {
// feedback update downloaded message
event.sender.send('update-downloaded')
event.sender.send("update-downloaded")
}
)
})

// Install now
ipcMain.handle('quit-and-install', () => {
ipcMain.handle("quit-and-install", () => {
autoUpdater.quitAndInstall(false, true)
})
}
Expand All @@ -74,8 +74,8 @@ function startDownload(
callback: (error: Error | null, info: ProgressInfo | null) => void,
complete: (event: UpdateDownloadedEvent) => void,
) {
autoUpdater.on('download-progress', (info: ProgressInfo) => callback(null, info))
autoUpdater.on('error', (error: Error) => callback(error, null))
autoUpdater.on('update-downloaded', complete)
autoUpdater.on("download-progress", (info: ProgressInfo) => callback(null, info))
autoUpdater.on("error", (error: Error) => callback(error, null))
autoUpdater.on("update-downloaded", complete)
autoUpdater.downloadUpdate()
}
8 changes: 7 additions & 1 deletion public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@
"title": "System Settings",
"language": "System Language",
"theme": "Display Mode",
"autoLaunch": "Run Dive AI on system startup",
"defaultInstructions": "speak in English",
"autoDownload": "Auto Download Update",
"autoLaunch": "Run Dive AI on system startup",
"minimalToTray": "Minimize to System Tray"
},
"toast": {
Expand All @@ -142,5 +143,10 @@
"global_close-layer": "Close Window",
"global_toggle-keymap-modal": "Toggle Keyboard Shortcut Helper"
}
},
"update": {
"downloading": "Downloading",
"readyToInstall": "Ready to Install",
"clickToInstall": "Click to Install Update"
}
}
8 changes: 7 additions & 1 deletion public/locales/zh-CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@
"title": "系统设置",
"language": "系统语言",
"theme": "显示模式",
"autoLaunch": "开机自动启动Dive AI",
"defaultInstructions": "以简体中文回答",
"autoDownload": "自动下载更新",
"autoLaunch": "开机自动启动Dive AI",
"minimalToTray": "最小化到系统托盘"
},
"toast": {
Expand All @@ -142,5 +143,10 @@
"global_close-layer": "关闭窗口",
"global_toggle-keymap-modal": "显示/隐藏快捷键说明"
}
},
"update": {
"downloading": "下载中",
"readyToInstall": "下载完成",
"clickToInstall": "点击安装更新"
}
}
8 changes: 7 additions & 1 deletion public/locales/zh-TW/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@
"title": "系統設定",
"language": "系統語言",
"theme": "顯示模式",
"autoLaunch": "開機自動啟動Dive AI",
"defaultInstructions": "以繁體中文回應",
"autoDownload": "自動下載更新",
"autoLaunch": "開機自動啟動Dive AI",
"minimalToTray": "關閉按鈕會將Dive AI最小化到背景"
},
"toast": {
Expand All @@ -142,5 +143,10 @@
"global_close-layer": "關閉視窗",
"global_toggle-keymap-modal": "顯示/隱藏快捷鍵說明"
}
},
"update": {
"downloading": "下載中",
"readyToInstall": "下載完成",
"clickToInstall": "點擊安裝更新"
}
}
3 changes: 0 additions & 3 deletions src/atoms/globalState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { atom } from "jotai"
import { loadable } from "jotai/utils"

export const platformAtom = loadable(atom(async (get) => await window.ipcRenderer.getPlatform()))

export const newVersionAtom = atom<string | null>(null)
16 changes: 2 additions & 14 deletions src/components/HistorySidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import useHotkeyEvent from "../hooks/useHotkeyEvent"
import { currentChatIdAtom } from "../atoms/chatState"
import PopupConfirm from "./PopupConfirm"
import { newVersionAtom } from "../atoms/globalState"
import UpdateButton from "./UpdateButton"

interface Props {
onNewChat?: () => void
Expand Down Expand Up @@ -224,20 +225,7 @@ const HistorySidebar = ({ onNewChat }: Props) => {
</svg>
{t("sidebar.system")}
</button>
{newVersion && (
<button
className="sidebar-footer-btn update-btn"
onClick={() => window.open("https://github.com/OpenAgentPlatform/Dive/releases/latest", "_blank")}
>
<div className="update-btn-wrap">
<span>✨</span>
<span className="update-btn-text">{t("sidebar.update")}</span>
</div>
<div className="update-btn-text">
<span>v{newVersion} &gt;</span>
</div>
</button>
)}
<UpdateButton />
</div>
</div>
{deletingChatId && (
Expand Down
11 changes: 3 additions & 8 deletions src/components/Modal/KeymapModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,15 @@ import PopupConfirm from "../PopupConfirm"
import { useTranslation } from "react-i18next"
import { rawKeymapAtom } from "../../atoms/hotkeyState"
import { useAtom, useAtomValue } from "jotai"
import { platformAtom } from "../../atoms/globalState"


const KeymapModal = () => {
const { t } = useTranslation()
const [isVisible, setIsVisible] = useAtom(keymapModalVisibleAtom)
const keyMap = useAtomValue(rawKeymapAtom)
const platform = useAtomValue(platformAtom)

const formatHotkey = (key: string): string => {
if (platform.state === "loading" || platform.state === "hasError") {
return ""
}

const metaKey = platform.data === "darwin" ? "⌘" : platform.data === "win32" ? "Win" : "Super"
const metaKey = window.PLATFORM === "darwin" ? "⌘" : window.PLATFORM === "win32" ? "Win" : "Super"
// Check if it"s a pure combination key format <c-o>
if (key.startsWith("<") && key.endsWith(">") && !key.slice(1, -1).includes("><")) {
const parts = key.slice(1, -1).split("-")
Expand Down Expand Up @@ -129,7 +124,7 @@ const KeymapModal = () => {
display: formatHotkey(hotkey)
}
})
}, [keyMap, platform])
}, [keyMap])

const hotkeyRows = useMemo(() => {
const rows = []
Expand Down
81 changes: 81 additions & 0 deletions src/components/UpdateButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useSetAtom } from "jotai"
import useUpdateProgress from "../hooks/useUpdateProgress"
import { memo, useCallback, useState } from "react"
import { useTranslation } from "react-i18next"
import { showToastAtom } from "../atoms/toastState"

const AvailableButton = ({ newVersion }: { newVersion: string }) => {
const { t } = useTranslation()

return (
<>
<div className="update-btn-wrap downloading">
<span>✨</span>
<span className="update-btn-text">{t("sidebar.update")}</span>
</div>
<div className="update-btn-text">
<span>v{newVersion} &gt;</span>
</div>
</>
)
}

const DownloadingButton = ({ progress, isCompleted }: { progress: number, isCompleted: boolean }) => {
const { t } = useTranslation()

return (
<>
<div className="update-btn-wrap">
<span>{isCompleted ? "✅" : "⏬"}</span>
<span className="update-btn-text">
{isCompleted ? t("update.readyToInstall") : t("update.downloading")}
</span>
</div>
<div className="update-progress-container">
{!isCompleted && (
<>
<div
className="update-progress-bar"
style={{ width: `${progress}%` }}
/>
<span className="update-progress-text">{Math.round(progress)}%</span>
</>
)}
{isCompleted && (
<span className="update-btn-text">{t("update.clickToInstall")}</span>
)}
</div>
</>
)
}

const UpdateButton = () => {
const showToast = useSetAtom(showToastAtom)
const [isCompleted, setIsCompleted] = useState(false)
const { newVersion, progress, update } = useUpdateProgress(
useCallback(() => {
setIsCompleted(true)
}, []),
useCallback((e) => {
showToast({
message: e.message,
type: "error",
})
}, [showToast])
)

if (!newVersion) {
return null
}

return (
<button
className={`sidebar-footer-btn update-btn ${progress === 0 ? "available" : "downloading"}`}
onClick={update}
>
{progress === 0 ? <AvailableButton newVersion={newVersion} /> : <DownloadingButton progress={progress} isCompleted={isCompleted} />}
</button>
)
}

export default memo(UpdateButton)
63 changes: 63 additions & 0 deletions src/hooks/useUpdateProgress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { getAutoDownload } from "../updater"
import { ProgressInfo } from "electron-updater"
import { useCallback, useEffect, useState } from "react"
import { newVersionAtom } from "../atoms/globalState"
import { useAtomValue } from "jotai"

export default function useUpdateProgress(onComplete: () => void, onError: (e: { message: string, error: Error }) => void) {
const [progress, setProgress] = useState(0)
const newVersion = useAtomValue(newVersionAtom)

useEffect(() => {
window.ipcRenderer.invoke("check-update")
}, [])

const update = useCallback(async () => {
if (window.PLATFORM === "darwin") {
window.open("https://github.com/OpenAgentPlatform/Dive/releases/latest", "_blank")
return
}

const autoDownload = getAutoDownload()
if (autoDownload || progress >= 100) {
window.ipcRenderer.invoke("quit-and-install")
return
}

window.ipcRenderer.invoke("start-download")
setProgress(0.1)
}, [progress])

const handleDownloadProgress = useCallback((event: Electron.IpcRendererEvent, progressInfo: ProgressInfo) => {
if (progressInfo.percent > 0) {
setProgress(progressInfo.percent)
}
}, [setProgress])

const handleError = useCallback((event: Electron.IpcRendererEvent, error: Error) => {
setProgress(0)
onError({
message: error.message,
error,
})
}, [onError])

useEffect(() => {
if (getAutoDownload()) {
setProgress(0)
return
}

window.ipcRenderer.on("download-progress", handleDownloadProgress)
window.ipcRenderer.on("update-downloaded", onComplete)
window.ipcRenderer.on("update-error", handleError)

return () => {
window.ipcRenderer.off("download-progress", handleDownloadProgress)
window.ipcRenderer.off("update-downloaded", onComplete)
window.ipcRenderer.off("update-error", handleError)
}
}, [handleDownloadProgress, onComplete, onError])

return { progress, update, newVersion }
}
Loading
Loading