From 970d6d95380700796729310667f3061676f27051 Mon Sep 17 00:00:00 2001 From: xue jiahao Date: Mon, 16 Mar 2026 15:53:59 +0800 Subject: [PATCH] fix load app, download, upgrade --- package-lock.json | 8 +- package.json | 2 +- src/main/ipc-handlers.ts | 71 +++++++- src/preload/index.d.ts | 5 + src/preload/index.ts | 5 +- .../src/components/AppList/AppList.tsx | 78 +------- .../src/components/AppList/AppListItem.tsx | 166 ++++++++++++++++++ .../src/components/CodeViewer/CodeViewer.tsx | 78 ++++++-- .../components/DomainManager/DomainList.tsx | 3 + .../DomainManager/DomainManager.tsx | 1 - .../src/components/Settings/Settings.tsx | 126 ++++++++++++- src/renderer/src/locales/en-US/settings.json | 4 +- src/renderer/src/locales/ja-JP/settings.json | 4 +- src/renderer/src/locales/zh-CN/settings.json | 4 +- src/renderer/src/stores/domainStore.ts | 25 ++- src/shared/types/ipc.ts | 14 ++ 16 files changed, 487 insertions(+), 107 deletions(-) create mode 100644 src/renderer/src/components/AppList/AppListItem.tsx diff --git a/package-lock.json b/package-lock.json index 856be8f..498b1a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^3.0.0", "@kintone/rest-api-client": "^6.1.2", - "@lobehub/ui": "^5.5.0", + "@lobehub/ui": "^5.5.1", "@uiw/react-codemirror": "^4.23.0", "antd": "^6.1.0", "antd-style": "^4.1.0", @@ -2581,9 +2581,9 @@ } }, "node_modules/@lobehub/ui": { - "version": "5.5.0", - "resolved": "https://registry.npmmirror.com/@lobehub/ui/-/ui-5.5.0.tgz", - "integrity": "sha512-wVAAl13cYxtFSiJcKPxmQuiKA6ZqHenXDPV7h1vadMjlhTp71PV4ZhtAJffAv7Mni4EjyJL4B6+pEX+nqlxeoQ==", + "version": "5.5.1", + "resolved": "https://registry.npmmirror.com/@lobehub/ui/-/ui-5.5.1.tgz", + "integrity": "sha512-rAZ46Fc633EDgY0eBDEIifmLpSNUr5hHJIL6ra6q7nL0KiCSuIbiQmszA5QyzETKMvF1WW6EbQk4JOyq2E4kyg==", "license": "MIT", "dependencies": { "@ant-design/cssinjs": "^2.0.3", diff --git a/package.json b/package.json index b7a496f..9b2edd6 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^3.0.0", "@kintone/rest-api-client": "^6.1.2", - "@lobehub/ui": "^5.5.0", + "@lobehub/ui": "^5.5.1", "@uiw/react-codemirror": "^4.23.0", "antd": "^6.1.0", "antd-style": "^4.1.0", diff --git a/src/main/ipc-handlers.ts b/src/main/ipc-handlers.ts index cf88ada..117482e 100644 --- a/src/main/ipc-handlers.ts +++ b/src/main/ipc-handlers.ts @@ -4,7 +4,8 @@ * Based on REQUIREMENTS.md:228-268 */ -import { ipcMain, dialog } from "electron"; +import { ipcMain, dialog, app } from "electron"; +import { autoUpdater } from "electron-updater"; import { v4 as uuidv4 } from "uuid"; import { saveDomain, @@ -34,6 +35,7 @@ import type { GetVersionsParams, RollbackParams, SetLocaleParams, + CheckUpdateResult, } from "@shared/types/ipc"; import type { LocaleCode } from "@shared/types/locale"; import type { @@ -539,6 +541,69 @@ function registerSetLocale(): void { }); } +// ==================== App Version & Update IPC Handlers ==================== + +/** + * Get the current app version + */ +function registerGetAppVersion(): void { + handle("getAppVersion", async () => { + return app.getVersion(); + }); +} + +/** + * Check for app updates + */ +function registerCheckForUpdates(): void { + handle("checkForUpdates", async () => { + try { + // In development, autoUpdater.checkForUpdates will throw an error + // because there's no update server configured + if (process.env.NODE_ENV === "development" || !app.isPackaged) { + // Return mock result for development + return { + hasUpdate: false, + updateInfo: undefined, + }; + } + + const result = await autoUpdater.checkForUpdates(); + + if (result && result.updateInfo) { + const currentVersion = app.getVersion(); + const latestVersion = result.updateInfo.version; + + // Compare versions + const hasUpdate = latestVersion !== currentVersion; + + return { + hasUpdate, + updateInfo: hasUpdate + ? { + version: result.updateInfo.version, + releaseDate: result.updateInfo.releaseDate, + releaseNotes: result.releaseNotes as string | undefined, + } + : undefined, + }; + } + + return { + hasUpdate: false, + updateInfo: undefined, + }; + } catch (error) { + // If update check fails, return no update available + console.error("Update check failed:", error); + return { + hasUpdate: false, + updateInfo: undefined, + }; + } + }); +} + // ==================== Register All Handlers ==================== /** @@ -573,6 +638,10 @@ export function registerIpcHandlers(): void { registerGetLocale(); registerSetLocale(); + // App Version & Update + registerGetAppVersion(); + registerCheckForUpdates(); + // Dialog registerShowSaveDialog(); registerSaveFileContent(); diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 4cbbb19..758dce0 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -16,6 +16,7 @@ import type { SetLocaleParams, ShowSaveDialogParams, SaveFileContentParams, + CheckUpdateResult, } from "@shared/types/ipc"; import type { Domain, DomainWithStatus } from "@shared/types/domain"; import type { @@ -70,6 +71,10 @@ export interface SelfAPI { getLocale: () => Promise>; setLocale: (params: SetLocaleParams) => Promise>; + // ==================== App Version & Update ==================== + getAppVersion: () => Promise>; + checkForUpdates: () => Promise>; + // ==================== Dialog ==================== showSaveDialog: (params: ShowSaveDialogParams) => Promise>; saveFileContent: (params: SaveFileContentParams) => Promise>; diff --git a/src/preload/index.ts b/src/preload/index.ts index 8208c98..a8775d6 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -34,10 +34,13 @@ const api: SelfAPI = { getLocale: () => ipcRenderer.invoke("getLocale"), setLocale: (params) => ipcRenderer.invoke("setLocale", params), + // App Version & Update + getAppVersion: () => ipcRenderer.invoke("getAppVersion"), + checkForUpdates: () => ipcRenderer.invoke("checkForUpdates"), + // Dialog showSaveDialog: (params) => ipcRenderer.invoke("showSaveDialog", params), saveFileContent: (params) => ipcRenderer.invoke("saveFileContent", params), - }; // Use `contextBridge` APIs to expose Electron APIs to diff --git a/src/renderer/src/components/AppList/AppList.tsx b/src/renderer/src/components/AppList/AppList.tsx index 39d2b24..2171b51 100644 --- a/src/renderer/src/components/AppList/AppList.tsx +++ b/src/renderer/src/components/AppList/AppList.tsx @@ -4,15 +4,13 @@ */ import React from "react"; -import { motion } from "motion/react"; import { useTranslation } from "react-i18next"; -import { Input, Spin, Typography, Space } from "antd"; -import { Button, Tooltip, Empty, Select } from "@lobehub/ui"; +import { Spin, Typography, Space } from "antd"; +import { Button, Tooltip, Empty, Select, Input } from "@lobehub/ui"; + import { RefreshCw, Search, - LayoutGrid, - Pin, ArrowUpDown, ArrowDownUp, } from "lucide-react"; @@ -21,9 +19,9 @@ import { useAppStore } from "@renderer/stores"; import { useDomainStore } from "@renderer/stores"; import { useUIStore } from "@renderer/stores"; import type { AppDetail } from "@shared/types/kintone"; +import AppListItem from "./AppListItem"; const { Text } = Typography; - const useStyles = createStyles(({ token, css }) => ({ container: css` height: 100%; @@ -157,74 +155,6 @@ const useStyles = createStyles(({ token, css }) => ({ `, })); -interface AppListItemProps { - app: AppDetail; - isActive: boolean; - isPinned: boolean; - onItemClick: (app: AppDetail) => void; - onPinToggle: (e: React.MouseEvent, appId: string) => void; - styles: ReturnType["styles"]; - t: (key: string) => string; -} - -const AppListItem: React.FC = ({ - app, - isActive, - isPinned, - onItemClick, - onPinToggle, - styles, - t, -}) => { - const [isHovered, setIsHovered] = React.useState(false); - - // Pin overlay is visible when: - // 1. Item is pinned (always show) - // 2. Item is hovered (show for unpinned items) - const showPinOverlay = isPinned || isHovered; - - return ( - onItemClick(app)} - onMouseEnter={() => setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - transition={{ type: "spring", stiffness: 500, damping: 30 }} - > -
-
- {/* Pin overlay - visible on hover for unpinned, always visible for pinned */} -
onPinToggle(e, app.appId)} - > - - - {isPinned ? ( - - ) : ( - - )} - - -
- {/* App icon - hidden when pin overlay is visible */} - {!showPinOverlay && } -
- - {app.name} - - - ID: {app.appId} - -
-
- ); -}; - const AppList: React.FC = () => { const { t } = useTranslation("app"); const { styles } = useStyles(); diff --git a/src/renderer/src/components/AppList/AppListItem.tsx b/src/renderer/src/components/AppList/AppListItem.tsx new file mode 100644 index 0000000..e1db581 --- /dev/null +++ b/src/renderer/src/components/AppList/AppListItem.tsx @@ -0,0 +1,166 @@ +/** + * AppListItem Component + * Individual app item in the app list with pinning support + */ + +import React from "react"; +import { motion } from "motion/react"; +import { Typography } from "antd"; +import { Tooltip } from "@lobehub/ui"; +import { Pin, LayoutGrid } from "lucide-react"; +import type { AppDetail } from "@shared/types/kintone"; + +const { Text } = Typography; + +// Styles for AppListItem - defined inline to be self-contained +const listItemStyles = { + listItemMotion: { + cursor: "pointer", + padding: "8px 16px !important", + borderBottom: + "1px solid var(--ant-color-border-secondary, #f0f0f0) !important", + position: "relative" as const, + }, + listItemActive: { + background: "var(--ant-color-primary-bg-hover, #e6f7ff) !important", + borderLeft: "3px solid var(--ant-color-primary, #1890ff) !important", + }, + listItemPinned: { + background: "var(--ant-color-warning-bg, #fffbe6) !important", + }, + appInfoWrapper: { + display: "flex", + alignItems: "center", + gap: "8px", + flex: 1, + minWidth: 0, + position: "relative" as const, + }, + iconWrapper: { + position: "relative" as const, + width: "20px", + height: "20px", + display: "flex", + alignItems: "center", + justifyContent: "center", + flexShrink: 0, + }, + pinOverlay: { + position: "absolute" as const, + top: 0, + left: 0, + width: "20px", + height: "20px", + display: "flex", + alignItems: "center", + justifyContent: "center", + opacity: 0, + transition: "opacity 0.15s ease", + zIndex: 1, + }, + appName: { + fontWeight: 500, + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + flex: 1, + }, + appId: { + fontFamily: "monospace", + fontSize: "12px", + color: "var(--ant-color-text-secondary, #8c8c8c)", + flexShrink: 0, + }, + pinButton: { + color: "var(--ant-color-text-tertiary, #bfbfbf)", + cursor: "pointer", + fontSize: "14px", + display: "flex", + alignItems: "center", + justifyContent: "center", + }, +}; + +export interface AppListItemProps { + app: AppDetail; + isActive: boolean; + isPinned: boolean; + onItemClick: (app: AppDetail) => void; + onPinToggle: (e: React.MouseEvent, appId: string) => void; + t: (key: string) => string; + styles: { + listItemMotion: string; + listItemActive: string; + listItemPinned: string; + appInfoWrapper: string; + iconWrapper: string; + pinOverlay: string; + pinOverlayVisible: string; + pinButton: string; + pinButtonPinned: string; + appName: string; + appId: string; + }; +} + +const AppListItem: React.FC = ({ + app, + isActive, + isPinned, + onItemClick, + onPinToggle, + styles, + t, +}) => { + const [isHovered, setIsHovered] = React.useState(false); + + // Pin overlay is visible when: + // 1. Item is pinned (always show) + // 2. Item is hovered (show for unpinned items) + const showPinOverlay = isPinned || isHovered; + + return ( + onItemClick(app)} + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + transition={{ type: "spring", stiffness: 500, damping: 30 }} + > +
+
+ {/* Pin overlay - visible on hover for unpinned, always visible for pinned */} +
onPinToggle(e, app.appId)} + > + + + {isPinned ? ( + + ) : ( + + )} + + +
+ {/* App icon - hidden when pin overlay is visible */} + {!showPinOverlay && ( + + )} +
+ + {app.name} + + + ID: {app.appId} + +
+
+ ); +}; + +export default AppListItem; diff --git a/src/renderer/src/components/CodeViewer/CodeViewer.tsx b/src/renderer/src/components/CodeViewer/CodeViewer.tsx index ff7b7a6..468b3c3 100644 --- a/src/renderer/src/components/CodeViewer/CodeViewer.tsx +++ b/src/renderer/src/components/CodeViewer/CodeViewer.tsx @@ -7,10 +7,7 @@ import React from "react"; import { useTranslation } from "react-i18next"; import { Spin, Alert, Space, message } from "antd"; import { Button, Empty } from "@lobehub/ui"; -import { - Copy, - Download, -} from "lucide-react"; +import { Copy, Download } from "lucide-react"; import { createStyles } from "antd-style"; import CodeMirror from "@uiw/react-codemirror"; import { javascript } from "@codemirror/lang-javascript"; @@ -72,6 +69,7 @@ const CodeViewer: React.FC = ({ const [error, setError] = React.useState(null); const [content, setContent] = React.useState(""); const [language, setLanguage] = React.useState<"js" | "css">(fileType); + const [downloading, setDownloading] = React.useState(false); // Load file content React.useEffect(() => { @@ -128,17 +126,62 @@ const CodeViewer: React.FC = ({ message.success(t("copiedToClipboard")); }; - const handleDownload = () => { - const blob = new Blob([content], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = fileName; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - message.success(t("downloadSuccess")); + const handleDownload = async () => { + if (!currentDomain || downloading) return; + + // Check if fileName already has extension + const hasExt = /\.(js|css)$/i.test(fileName); + const finalFileName = hasExt ? fileName : `${fileName}.${fileType}`; + + setDownloading(true); + + try { + // 1. Show save dialog + const dialogResult = await window.api.showSaveDialog({ + defaultPath: finalFileName, + }); + + if (!dialogResult.success || !dialogResult.data) { + // User cancelled + setDownloading(false); + return; + } + + const savePath = dialogResult.data; + + // 2. Get current file content (already decoded in state) + const encoder = new TextEncoder(); + const uint8Array = encoder.encode(content); + // Convert to base64 for IPC transfer + let base64 = ""; + const chunkSize = 8192; + for (let i = 0; i < uint8Array.length; i += chunkSize) { + base64 += String.fromCharCode.apply( + null, + Array.from(uint8Array.slice(i, i + chunkSize)), + ); + } + const base64Content = btoa(base64); + + // 3. Save to selected path + const saveResult = await window.api.saveFileContent({ + filePath: savePath, + content: base64Content, + }); + + if (saveResult.success) { + message.success(t("downloadSuccess")); + } else { + message.error( + saveResult.error || t("downloadFailed", { ns: "common" }), + ); + } + } catch (error) { + console.error("Download failed:", error); + message.error(t("downloadFailed", { ns: "common" })); + } finally { + setDownloading(false); + } }; if (loading) { @@ -170,9 +213,7 @@ const CodeViewer: React.FC = ({ if (!content) { return (
- +
); } @@ -194,6 +235,7 @@ const CodeViewer: React.FC = ({ type="text" size="small" icon={} + loading={downloading} onClick={handleDownload} > {t("download", { ns: "common" })} diff --git a/src/renderer/src/components/DomainManager/DomainList.tsx b/src/renderer/src/components/DomainManager/DomainList.tsx index 343e1f3..88db23c 100644 --- a/src/renderer/src/components/DomainManager/DomainList.tsx +++ b/src/renderer/src/components/DomainManager/DomainList.tsx @@ -100,6 +100,9 @@ const DomainList: React.FC = ({ onEdit }) => { useDomainStore(); const handleSelect = (domain: Domain) => { + if (currentDomain?.id === domain.id) { + return; + } switchDomain(domain); }; diff --git a/src/renderer/src/components/DomainManager/DomainManager.tsx b/src/renderer/src/components/DomainManager/DomainManager.tsx index 1295049..34b18c8 100644 --- a/src/renderer/src/components/DomainManager/DomainManager.tsx +++ b/src/renderer/src/components/DomainManager/DomainManager.tsx @@ -242,7 +242,6 @@ const DomainManager: React.FC = ({

{t("domainManagement")}

diff --git a/src/renderer/src/components/Settings/Settings.tsx b/src/renderer/src/components/Settings/Settings.tsx index 3011f73..a47cc1a 100644 --- a/src/renderer/src/components/Settings/Settings.tsx +++ b/src/renderer/src/components/Settings/Settings.tsx @@ -5,8 +5,9 @@ import React from "react"; import { useTranslation } from "react-i18next"; -import { Typography, Radio, Divider } from "antd"; -import { Globe } from "lucide-react"; +import { Typography, Radio, Divider, Space, message } from "antd"; +import { Globe, Info, ExternalLink } from "lucide-react"; +import { Button } from "@lobehub/ui"; import { createStyles } from "antd-style"; import { useLocaleStore } from "@renderer/stores/localeStore"; @@ -42,6 +43,28 @@ const useStyles = createStyles(({ token, css }) => ({ display: none; } `, + versionInfo: css` + display: flex; + align-items: center; + gap: ${token.marginSM}px; + color: ${token.colorTextSecondary}; + font-size: 14px; + `, + versionNumber: css` + font-family: monospace; + font-weight: 500; + `, + updateInfo: css` + margin-top: ${token.marginSM}px; + padding: ${token.paddingSM}px ${token.paddingMD}px; + background: ${token.colorInfoBg}; + border-radius: ${token.borderRadius}px; + font-size: 13px; + `, + updateAvailable: css` + background: ${token.colorSuccessBg}; + color: ${token.colorSuccessText}; + `, })); const Settings: React.FC = () => { @@ -50,6 +73,25 @@ const Settings: React.FC = () => { const { locale, setLocale } = useLocaleStore(); const i18n = useTranslation().i18n; + // Version and update state + const [appVersion, setAppVersion] = React.useState(""); + const [checkingUpdate, setCheckingUpdate] = React.useState(false); + const [updateInfo, setUpdateInfo] = React.useState<{ + hasUpdate: boolean; + version?: string; + } | null>(null); + + // Load app version on mount + React.useEffect(() => { + const loadVersion = async () => { + const result = await window.api.getAppVersion(); + if (result.success) { + setAppVersion(result.data); + } + }; + loadVersion(); + }, []); + const handleLocaleChange = async (newLocale: LocaleCode) => { setLocale(newLocale); i18n.changeLanguage(newLocale); @@ -57,6 +99,33 @@ const Settings: React.FC = () => { await window.api.setLocale({ locale: newLocale }); }; + const handleCheckUpdate = async () => { + setCheckingUpdate(true); + setUpdateInfo(null); + + try { + const result = await window.api.checkForUpdates(); + if (result.success) { + setUpdateInfo({ + hasUpdate: result.data.hasUpdate, + version: result.data.updateInfo?.version, + }); + } else { + message.error(t("checkUpdateFailed")); + } + } catch { + message.error(t("checkUpdateFailed")); + } finally { + setCheckingUpdate(false); + } + }; + + const handleOpenReleasePage = () => { + // Open GitHub releases page (or configured update URL) + const releaseUrl = "https://github.com/example/kintone-customize-manager/releases"; + window.electron?.shell?.openExternal?.(releaseUrl); + }; + return (
{/* Language Section */} @@ -84,7 +153,58 @@ const Settings: React.FC = () => { - {/* Future settings sections can be added here */} + {/* About Section */} +
+
+ + + {t("about")} + +
+ +
+ {t("version")}: + {appVersion || "-"} +
+ + + + + {updateInfo && ( +
+ {updateInfo.hasUpdate ? ( + + + {t("updateAvailable")}: v{updateInfo.version} + + + + ) : ( + {t("noUpdates")} + )} +
+ )} +
+
); }; diff --git a/src/renderer/src/locales/en-US/settings.json b/src/renderer/src/locales/en-US/settings.json index 86abeb9..404dd2d 100644 --- a/src/renderer/src/locales/en-US/settings.json +++ b/src/renderer/src/locales/en-US/settings.json @@ -21,6 +21,8 @@ "about": "About", "version": "Version", "checkUpdate": "Check for Updates", + "checkUpdateFailed": "Failed to check for updates", "noUpdates": "You're up to date", - "updateAvailable": "Update available" + "updateAvailable": "Update available", + "downloadUpdate": "Download Update" } diff --git a/src/renderer/src/locales/ja-JP/settings.json b/src/renderer/src/locales/ja-JP/settings.json index 63988e1..7f49675 100644 --- a/src/renderer/src/locales/ja-JP/settings.json +++ b/src/renderer/src/locales/ja-JP/settings.json @@ -21,6 +21,8 @@ "about": "アプリについて", "version": "バージョン", "checkUpdate": "更新を確認", + "checkUpdateFailed": "更新の確認に失敗しました", "noUpdates": "最新バージョンです", - "updateAvailable": "新しいバージョンが利用可能です" + "updateAvailable": "新しいバージョンが利用可能です", + "downloadUpdate": "更新をダウンロード" } diff --git a/src/renderer/src/locales/zh-CN/settings.json b/src/renderer/src/locales/zh-CN/settings.json index ee8aef6..7014779 100644 --- a/src/renderer/src/locales/zh-CN/settings.json +++ b/src/renderer/src/locales/zh-CN/settings.json @@ -21,6 +21,8 @@ "about": "关于", "version": "版本", "checkUpdate": "检查更新", + "checkUpdateFailed": "检查更新失败", "noUpdates": "已是最新版本", - "updateAvailable": "有新版本可用" + "updateAvailable": "有新版本可用", + "downloadUpdate": "下载更新" } diff --git a/src/renderer/src/stores/domainStore.ts b/src/renderer/src/stores/domainStore.ts index 72abe61..4fae523 100644 --- a/src/renderer/src/stores/domainStore.ts +++ b/src/renderer/src/stores/domainStore.ts @@ -4,6 +4,7 @@ */ import { create } from "zustand"; +import { useAppStore } from "./appStore"; import { persist } from "zustand/middleware"; import type { Domain, DomainWithStatus } from "@shared/types/domain"; import type { ConnectionStatus } from "@shared/types/domain"; @@ -217,8 +218,17 @@ export const useDomainStore = create()( }, switchDomain: async (domain: Domain) => { + const appStore = useAppStore.getState(); + + // 1. reset + appStore.setLoading(true); + appStore.setApps([]); + appStore.setSelectedAppId(null); + + // 2. Set current domain set({ currentDomain: domain }); - // Test connection after switching + + // 3. Test connection after switching const status = await get().testConnection(domain.id); if (status) { set({ @@ -228,6 +238,19 @@ export const useDomainStore = create()( }, }); } + + // 4. Auto-load apps for the new domain + try { + const result = await window.api.getApps({ domainId: domain.id }); + if (result.success) { + appStore.setApps(result.data); + } + } catch (error) { + // Silent fail - user can manually reload + console.error("Failed to auto-load apps:", error); + } finally { + appStore.setLoading(false); + } }, testConnection: async (id: string) => { diff --git a/src/shared/types/ipc.ts b/src/shared/types/ipc.ts index d39a56f..d453274 100644 --- a/src/shared/types/ipc.ts +++ b/src/shared/types/ipc.ts @@ -125,6 +125,20 @@ export interface ShowSaveDialogParams { export interface SaveFileContentParams { filePath: string; content: string; // Base64 encoded +// ==================== App Version & Update IPC Types ==================== + +export interface UpdateInfo { + version: string; + releaseDate?: string; + releaseNotes?: string; +} + +export interface CheckUpdateResult { + hasUpdate: boolean; + updateInfo?: UpdateInfo; +} + + } // ==================== IPC API Interface ====================