diff --git a/enjoy/src/i18n/en.json b/enjoy/src/i18n/en.json index cb7fe01d0..3290efb4a 100644 --- a/enjoy/src/i18n/en.json +++ b/enjoy/src/i18n/en.json @@ -1,4 +1,19 @@ { + "menu": { + "about": "About", + "toggleFullScreen": "Toggle Full Screen", + "hide": "Hide", + "unhide": "Unhide", + "quit": "Quit", + "undo": "Undo", + "redo": "Redo", + "cut": "Cut", + "copy": "Copy", + "paste": "Paste", + "selectAll": "Select All", + "checkForUpdates": "Check for Updates", + "reportIssue": "Report Issue" + }, "models": { "user": { "id": "ID", @@ -360,7 +375,6 @@ "currentVersion": "Current version", "checkUpdate": "Check update", "checkingLatestVersion": "Checking latest version", - "updateAvailable": "Update available", "updateDownloaded": "A new version has been downloaded. Restart the application to apply the updates.", "restart": "Restart", "later": "Later", @@ -938,5 +952,8 @@ "horizontal": "Horizontal", "vertical": "Vertical", "copied": "Copied", - "copyFullText": "Copy full text" + "copyFullText": "Copy full text", + "checkingForUpdate": "Checking for update", + "updateAvailable": "Update available", + "quitAndInstall": "Quit and install" } diff --git a/enjoy/src/i18n/zh-CN.json b/enjoy/src/i18n/zh-CN.json index 85d28b1ed..019affa1a 100644 --- a/enjoy/src/i18n/zh-CN.json +++ b/enjoy/src/i18n/zh-CN.json @@ -1,4 +1,19 @@ { + "menu": { + "about": "关于", + "toggleFullScreen": "全屏", + "hide": "隐藏", + "unhide": "显示", + "quit": "退出", + "undo": "撤销", + "redo": "重做", + "cut": "剪切", + "copy": "复制", + "paste": "粘贴", + "selectAll": "全选", + "checkForUpdates": "检查更新", + "reportIssue": "报告问题" + }, "models": { "user": { "id": "ID", @@ -360,7 +375,6 @@ "currentVersion": "当前版本", "checkUpdate": "检查更新", "checkingLatestVersion": "正在检查最新版本", - "updateAvailable": "有新版本可用", "updateDownloaded": "新版本已下载,点击重新启动以更新", "restart": "Restart", "later": "Later", @@ -938,5 +952,8 @@ "horizontal": "水平", "vertical": "垂直", "copied": "已复制", - "copyFullText": "复制全文" + "copyFullText": "复制全文", + "checkingForUpdate": "正在检查更新", + "updateAvailable": "有新版本", + "quitAndInstall": "退出并安装新版本" } diff --git a/enjoy/src/main/window.ts b/enjoy/src/main/window.ts index 5821451b3..4b8ea56e1 100644 --- a/enjoy/src/main/window.ts +++ b/enjoy/src/main/window.ts @@ -8,6 +8,7 @@ import { dialog, systemPreferences, MenuItemConstructorOptions, + autoUpdater, } from "electron"; import path from "path"; import db from "@main/db"; @@ -25,7 +26,9 @@ import dict from "./dict"; import mdict from "./mdict"; import decompresser from "./decompresser"; import { UserSetting } from "@main/db/models"; -import { platform } from "os"; +import { t } from "i18next"; +import { format } from "util"; +import pkg from "../../package.json" assert { type: "json" }; const __dirname = import.meta.dirname; @@ -37,6 +40,25 @@ const youtubeProvider = new YoutubeProvider(); const ffmpeg = new Ffmpeg(); const waveform = new Waveform(); +const FEED_BASE_URL = `https://dl.enjoy.bot/app/${process.platform}/${process.arch}`; +autoUpdater.setFeedURL({ + url: + process.platform === "darwin" + ? `${FEED_BASE_URL}/RELEASES.json` + : FEED_BASE_URL, + headers: { + "X-App-Version": app.getVersion(), + "User-Agent": format( + "%s/%s (%s: %s)", + pkg.name, + pkg.version, + process.platform, + process.arch + ), + }, + serverType: process.platform === "darwin" ? "json" : "default", +}); + const main = { win: null as BrowserWindow | null, init: () => {}, @@ -386,6 +408,44 @@ main.init = async () => { app.quit(); }); + ipcMain.handle("app-check-for-updates", () => { + autoUpdater.checkForUpdates(); + }); + + ipcMain.handle("app-quit-and-install", () => { + autoUpdater.quitAndInstall(); + }); + + ipcMain.on("app-on-updater", () => { + autoUpdater.on("error", (error) => { + mainWindow.webContents.send("app-on-updater", "error", [error]); + }); + autoUpdater.on("checking-for-update", () => { + mainWindow.webContents.send("app-on-updater", "checking-for-update", []); + }); + autoUpdater.on("update-available", () => { + mainWindow.webContents.send("app-on-updater", "update-available", []); + }); + autoUpdater.on( + "update-downloaded", + (_event, releaseNotes, releaseName, releaseDate, updateURL) => { + logger.info( + "update-downloaded", + releaseNotes, + releaseName, + releaseDate, + updateURL + ); + mainWindow.webContents.send("app-on-updater", "update-downloaded", [ + releaseNotes, + releaseName, + releaseDate, + updateURL, + ]); + } + ); + }); + ipcMain.handle("app-open-dev-tools", () => { mainWindow.webContents.openDevTools(); }); @@ -646,38 +706,38 @@ ${log} { label: app.name, submenu: [ - { role: "about" }, + { role: "about", label: t("menu.about") }, { type: "separator" }, - { role: "togglefullscreen" }, - { role: "hide" }, - { role: "unhide" }, + { role: "togglefullscreen", label: t("menu.toggleFullScreen") }, + { role: "hide", label: t("menu.hide") }, + { role: "unhide", label: t("menu.unhide") }, { type: "separator" }, - { role: "quit" }, + { role: "quit", label: t("menu.quit") }, ], }, { label: "Edit", submenu: [ - { role: "undo" }, - { role: "redo" }, + { role: "undo", label: t("menu.undo") }, + { role: "redo", label: t("menu.redo") }, { type: "separator" }, - { role: "cut" }, - { role: "copy" }, - { role: "paste" }, - { role: "selectAll" }, + { role: "cut", label: t("menu.cut") }, + { role: "copy", label: t("menu.copy") }, + { role: "paste", label: t("menu.paste") }, + { role: "selectAll", label: t("menu.selectAll") }, ], }, { label: "Help", submenu: [ { - label: "Check for Updates...", + label: t("menu.checkForUpdates"), click: () => { shell.openExternal("https://1000h.org/enjoy-app/install.html"); }, }, { - label: "Report Issue...", + label: t("menu.reportIssue"), click: () => { shell.openExternal(`${REPO_URL}/issues/new`); }, diff --git a/enjoy/src/preload.ts b/enjoy/src/preload.ts index 687e5579b..4f3fe840c 100644 --- a/enjoy/src/preload.ts +++ b/enjoy/src/preload.ts @@ -37,6 +37,22 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", { quit: () => { ipcRenderer.invoke("app-quit"); }, + checkForUpdates: () => { + ipcRenderer.invoke("app-check-for-updates"); + }, + quitAndInstall: () => { + ipcRenderer.invoke("app-quit-and-install"); + }, + onUpdater: ( + callback: ( + event: IpcRendererEvent, + eventType: string, + args: any[] + ) => void + ) => ipcRenderer.on("app-on-updater", callback), + removeUpdaterListeners: () => { + ipcRenderer.removeAllListeners("app-on-updater"); + }, openDevTools: () => { ipcRenderer.invoke("app-open-dev-tools"); }, diff --git a/enjoy/src/renderer/components/layouts/title-bar.tsx b/enjoy/src/renderer/components/layouts/title-bar.tsx index 101a257a7..f84efbb6e 100644 --- a/enjoy/src/renderer/components/layouts/title-bar.tsx +++ b/enjoy/src/renderer/components/layouts/title-bar.tsx @@ -32,7 +32,6 @@ import { LightbulbIcon, LightbulbOffIcon, MaximizeIcon, - MenuIcon, MinimizeIcon, MinusIcon, SettingsIcon, @@ -40,16 +39,31 @@ import { } from "lucide-react"; import { useContext, useEffect, useState } from "react"; +const INSTALL_URL = "https://1000h.org/enjoy-app/install.html"; + export const TitleBar = () => { const [isMaximized, setIsMaximized] = useState(false); const [isFullScreen, setIsFullScreen] = useState(false); const [platform, setPlatform] = useState<"darwin" | "win32" | "linux">(); + const [updaterState, setUpdaterState] = useState< + "checking-for-update" | "update-available" | "update-downloaded" | "error" + >(); const { EnjoyApp, setDisplayPreferences, initialized } = useContext( AppSettingsProviderContext ); const { active, setActive } = useContext(CopilotProviderContext); + const checkUpdate = () => { + if (platform === "linux") { + EnjoyApp.shell.openExternal(INSTALL_URL); + } else if (updaterState === "update-downloaded") { + EnjoyApp.app.quitAndInstall(); + } else { + EnjoyApp.app.checkForUpdates(); + } + }; + const onWindowChange = ( _event: IpcRendererEvent, state: { event: string } @@ -65,14 +79,28 @@ export const TitleBar = () => { } }; + const onUpdater = ( + _event: IpcRendererEvent, + eventType: + | "checking-for-update" + | "update-available" + | "update-downloaded" + | "error", + args: any[] + ) => { + setUpdaterState(eventType); + }; + useEffect(() => { EnjoyApp.window.onChange(onWindowChange); EnjoyApp.app.getPlatformInfo().then((info) => { setPlatform(info.platform as "darwin" | "win32" | "linux"); }); + EnjoyApp.app.onUpdater(onUpdater); return () => { EnjoyApp.window.removeListener(onWindowChange); + EnjoyApp.app.removeUpdaterListeners(); }; }, []); @@ -123,9 +151,12 @@ export const TitleBar = () => { @@ -181,14 +212,20 @@ export const TitleBar = () => { - EnjoyApp.shell.openExternal( - "https://1000h.org/enjoy-app/install.html" - ) - } - className="cursor-pointer" + onClick={checkUpdate} + className="cursor-pointer relative" > - {t("checkUpdate")} + {updaterState && ( + + )} + + {updaterState === "checking-for-update" && + t("checkingForUpdate")} + {updaterState === "update-available" && t("updateAvailable")} + {updaterState === "update-downloaded" && t("quitAndInstall")} + {!updaterState || + (updaterState === "error" && t("checkUpdate"))} + diff --git a/enjoy/src/renderer/components/preferences/about.tsx b/enjoy/src/renderer/components/preferences/about.tsx index 82024d88b..97faf4ce6 100644 --- a/enjoy/src/renderer/components/preferences/about.tsx +++ b/enjoy/src/renderer/components/preferences/about.tsx @@ -7,7 +7,13 @@ export const About = () => { const { version, EnjoyApp } = useContext(AppSettingsProviderContext); const checkUpdate = async () => { - EnjoyApp.shell.openExternal("https://1000h.org/enjoy-app/install.html"); + const platformInfo = await EnjoyApp.app.getPlatformInfo(); + if (platformInfo.platform === "linux") { + EnjoyApp.shell.openExternal("https://1000h.org/enjoy-app/install.html"); + } else { + EnjoyApp.app.checkForUpdates(); + toast.info(t("checkingForUpdate")); + } }; return ( diff --git a/enjoy/src/types/enjoy-app.d.ts b/enjoy/src/types/enjoy-app.d.ts index db815fdab..d985c10cf 100644 --- a/enjoy/src/types/enjoy-app.d.ts +++ b/enjoy/src/types/enjoy-app.d.ts @@ -13,6 +13,16 @@ type EnjoyAppType = { createIssue: (title: string, body: string) => Promise; onCmdOutput: (callback: (event, output: string) => void) => void; removeCmdOutputListeners: () => void; + checkForUpdates: () => Promise; + quitAndInstall: () => Promise; + onUpdater: ( + callback: ( + event: IpcRendererEvent, + eventType: string, + args: any[] + ) => void + ) => void; + removeUpdaterListeners: () => void; diskUsage: () => Promise; version: string; };