fix load app, download, upgrade
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -17,7 +17,7 @@
|
|||||||
"@electron-toolkit/preload": "^3.0.1",
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@kintone/rest-api-client": "^6.1.2",
|
"@kintone/rest-api-client": "^6.1.2",
|
||||||
"@lobehub/ui": "^5.5.0",
|
"@lobehub/ui": "^5.5.1",
|
||||||
"@uiw/react-codemirror": "^4.23.0",
|
"@uiw/react-codemirror": "^4.23.0",
|
||||||
"antd": "^6.1.0",
|
"antd": "^6.1.0",
|
||||||
"antd-style": "^4.1.0",
|
"antd-style": "^4.1.0",
|
||||||
@@ -2581,9 +2581,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@lobehub/ui": {
|
"node_modules/@lobehub/ui": {
|
||||||
"version": "5.5.0",
|
"version": "5.5.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@lobehub/ui/-/ui-5.5.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@lobehub/ui/-/ui-5.5.1.tgz",
|
||||||
"integrity": "sha512-wVAAl13cYxtFSiJcKPxmQuiKA6ZqHenXDPV7h1vadMjlhTp71PV4ZhtAJffAv7Mni4EjyJL4B6+pEX+nqlxeoQ==",
|
"integrity": "sha512-rAZ46Fc633EDgY0eBDEIifmLpSNUr5hHJIL6ra6q7nL0KiCSuIbiQmszA5QyzETKMvF1WW6EbQk4JOyq2E4kyg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/cssinjs": "^2.0.3",
|
"@ant-design/cssinjs": "^2.0.3",
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"@electron-toolkit/preload": "^3.0.1",
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
"@electron-toolkit/utils": "^3.0.0",
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@kintone/rest-api-client": "^6.1.2",
|
"@kintone/rest-api-client": "^6.1.2",
|
||||||
"@lobehub/ui": "^5.5.0",
|
"@lobehub/ui": "^5.5.1",
|
||||||
"@uiw/react-codemirror": "^4.23.0",
|
"@uiw/react-codemirror": "^4.23.0",
|
||||||
"antd": "^6.1.0",
|
"antd": "^6.1.0",
|
||||||
"antd-style": "^4.1.0",
|
"antd-style": "^4.1.0",
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
* Based on REQUIREMENTS.md:228-268
|
* 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 { v4 as uuidv4 } from "uuid";
|
||||||
import {
|
import {
|
||||||
saveDomain,
|
saveDomain,
|
||||||
@@ -34,6 +35,7 @@ import type {
|
|||||||
GetVersionsParams,
|
GetVersionsParams,
|
||||||
RollbackParams,
|
RollbackParams,
|
||||||
SetLocaleParams,
|
SetLocaleParams,
|
||||||
|
CheckUpdateResult,
|
||||||
} from "@shared/types/ipc";
|
} from "@shared/types/ipc";
|
||||||
import type { LocaleCode } from "@shared/types/locale";
|
import type { LocaleCode } from "@shared/types/locale";
|
||||||
import type {
|
import type {
|
||||||
@@ -539,6 +541,69 @@ function registerSetLocale(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== App Version & Update IPC Handlers ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current app version
|
||||||
|
*/
|
||||||
|
function registerGetAppVersion(): void {
|
||||||
|
handle<void, string>("getAppVersion", async () => {
|
||||||
|
return app.getVersion();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for app updates
|
||||||
|
*/
|
||||||
|
function registerCheckForUpdates(): void {
|
||||||
|
handle<void, CheckUpdateResult>("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 ====================
|
// ==================== Register All Handlers ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -573,6 +638,10 @@ export function registerIpcHandlers(): void {
|
|||||||
registerGetLocale();
|
registerGetLocale();
|
||||||
registerSetLocale();
|
registerSetLocale();
|
||||||
|
|
||||||
|
// App Version & Update
|
||||||
|
registerGetAppVersion();
|
||||||
|
registerCheckForUpdates();
|
||||||
|
|
||||||
// Dialog
|
// Dialog
|
||||||
registerShowSaveDialog();
|
registerShowSaveDialog();
|
||||||
registerSaveFileContent();
|
registerSaveFileContent();
|
||||||
|
|||||||
5
src/preload/index.d.ts
vendored
5
src/preload/index.d.ts
vendored
@@ -16,6 +16,7 @@ import type {
|
|||||||
SetLocaleParams,
|
SetLocaleParams,
|
||||||
ShowSaveDialogParams,
|
ShowSaveDialogParams,
|
||||||
SaveFileContentParams,
|
SaveFileContentParams,
|
||||||
|
CheckUpdateResult,
|
||||||
} from "@shared/types/ipc";
|
} from "@shared/types/ipc";
|
||||||
import type { Domain, DomainWithStatus } from "@shared/types/domain";
|
import type { Domain, DomainWithStatus } from "@shared/types/domain";
|
||||||
import type {
|
import type {
|
||||||
@@ -70,6 +71,10 @@ export interface SelfAPI {
|
|||||||
getLocale: () => Promise<Result<LocaleCode>>;
|
getLocale: () => Promise<Result<LocaleCode>>;
|
||||||
setLocale: (params: SetLocaleParams) => Promise<Result<void>>;
|
setLocale: (params: SetLocaleParams) => Promise<Result<void>>;
|
||||||
|
|
||||||
|
// ==================== App Version & Update ====================
|
||||||
|
getAppVersion: () => Promise<Result<string>>;
|
||||||
|
checkForUpdates: () => Promise<Result<CheckUpdateResult>>;
|
||||||
|
|
||||||
// ==================== Dialog ====================
|
// ==================== Dialog ====================
|
||||||
showSaveDialog: (params: ShowSaveDialogParams) => Promise<Result<string | null>>;
|
showSaveDialog: (params: ShowSaveDialogParams) => Promise<Result<string | null>>;
|
||||||
saveFileContent: (params: SaveFileContentParams) => Promise<Result<void>>;
|
saveFileContent: (params: SaveFileContentParams) => Promise<Result<void>>;
|
||||||
|
|||||||
@@ -34,10 +34,13 @@ const api: SelfAPI = {
|
|||||||
getLocale: () => ipcRenderer.invoke("getLocale"),
|
getLocale: () => ipcRenderer.invoke("getLocale"),
|
||||||
setLocale: (params) => ipcRenderer.invoke("setLocale", params),
|
setLocale: (params) => ipcRenderer.invoke("setLocale", params),
|
||||||
|
|
||||||
|
// App Version & Update
|
||||||
|
getAppVersion: () => ipcRenderer.invoke("getAppVersion"),
|
||||||
|
checkForUpdates: () => ipcRenderer.invoke("checkForUpdates"),
|
||||||
|
|
||||||
// Dialog
|
// Dialog
|
||||||
showSaveDialog: (params) => ipcRenderer.invoke("showSaveDialog", params),
|
showSaveDialog: (params) => ipcRenderer.invoke("showSaveDialog", params),
|
||||||
saveFileContent: (params) => ipcRenderer.invoke("saveFileContent", params),
|
saveFileContent: (params) => ipcRenderer.invoke("saveFileContent", params),
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
|||||||
@@ -4,15 +4,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { motion } from "motion/react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Input, Spin, Typography, Space } from "antd";
|
import { Spin, Typography, Space } from "antd";
|
||||||
import { Button, Tooltip, Empty, Select } from "@lobehub/ui";
|
import { Button, Tooltip, Empty, Select, Input } from "@lobehub/ui";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Search,
|
Search,
|
||||||
LayoutGrid,
|
|
||||||
Pin,
|
|
||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
ArrowDownUp,
|
ArrowDownUp,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -21,9 +19,9 @@ import { useAppStore } from "@renderer/stores";
|
|||||||
import { useDomainStore } from "@renderer/stores";
|
import { useDomainStore } from "@renderer/stores";
|
||||||
import { useUIStore } from "@renderer/stores";
|
import { useUIStore } from "@renderer/stores";
|
||||||
import type { AppDetail } from "@shared/types/kintone";
|
import type { AppDetail } from "@shared/types/kintone";
|
||||||
|
import AppListItem from "./AppListItem";
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
const useStyles = createStyles(({ token, css }) => ({
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
container: css`
|
container: css`
|
||||||
height: 100%;
|
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<typeof useStyles>["styles"];
|
|
||||||
t: (key: string) => string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppListItem: React.FC<AppListItemProps> = ({
|
|
||||||
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 (
|
|
||||||
<motion.div
|
|
||||||
layout
|
|
||||||
className={`${styles.listItemMotion} ${isActive ? styles.listItemActive : ""} ${isPinned ? styles.listItemPinned : ""}`}
|
|
||||||
onClick={() => onItemClick(app)}
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
|
||||||
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
|
||||||
>
|
|
||||||
<div className={styles.appInfoWrapper}>
|
|
||||||
<div className={styles.iconWrapper}>
|
|
||||||
{/* Pin overlay - visible on hover for unpinned, always visible for pinned */}
|
|
||||||
<div
|
|
||||||
className={`${styles.pinOverlay} ${showPinOverlay ? styles.pinOverlayVisible : ""}`}
|
|
||||||
onClick={(e) => onPinToggle(e, app.appId)}
|
|
||||||
>
|
|
||||||
<Tooltip title={isPinned ? t("unpin") : t("pinApp")}>
|
|
||||||
<span
|
|
||||||
className={`${styles.pinButton} ${isPinned ? styles.pinButtonPinned : ""}`}
|
|
||||||
>
|
|
||||||
{isPinned ? (
|
|
||||||
<Pin size={16} className="fill-current" />
|
|
||||||
) : (
|
|
||||||
<Pin size={16} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
{/* App icon - hidden when pin overlay is visible */}
|
|
||||||
{!showPinOverlay && <LayoutGrid size={16} style={{ color: "#1890ff" }} />}
|
|
||||||
</div>
|
|
||||||
<Tooltip title={app.name}>
|
|
||||||
<span className={styles.appName}>{app.name}</span>
|
|
||||||
</Tooltip>
|
|
||||||
<Text code className={styles.appId}>
|
|
||||||
ID: {app.appId}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppList: React.FC = () => {
|
const AppList: React.FC = () => {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
|
|||||||
166
src/renderer/src/components/AppList/AppListItem.tsx
Normal file
166
src/renderer/src/components/AppList/AppListItem.tsx
Normal file
@@ -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<AppListItemProps> = ({
|
||||||
|
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 (
|
||||||
|
<motion.div
|
||||||
|
layout
|
||||||
|
className={`${styles.listItemMotion} ${isActive ? styles.listItemActive : ""} ${isPinned ? styles.listItemPinned : ""}`}
|
||||||
|
onClick={() => onItemClick(app)}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||||
|
>
|
||||||
|
<div className={styles.appInfoWrapper}>
|
||||||
|
<div className={styles.iconWrapper}>
|
||||||
|
{/* Pin overlay - visible on hover for unpinned, always visible for pinned */}
|
||||||
|
<div
|
||||||
|
className={`${styles.pinOverlay} ${showPinOverlay ? styles.pinOverlayVisible : ""}`}
|
||||||
|
onClick={(e) => onPinToggle(e, app.appId)}
|
||||||
|
>
|
||||||
|
<Tooltip title={isPinned ? t("unpin") : t("pinApp")}>
|
||||||
|
<span
|
||||||
|
className={`${styles.pinButton} ${isPinned ? styles.pinButtonPinned : ""}`}
|
||||||
|
>
|
||||||
|
{isPinned ? (
|
||||||
|
<Pin size={16} className="fill-current" />
|
||||||
|
) : (
|
||||||
|
<Pin size={16} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{/* App icon - hidden when pin overlay is visible */}
|
||||||
|
{!showPinOverlay && (
|
||||||
|
<LayoutGrid size={16} style={{ color: "#1890ff" }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Tooltip title={app.name}>
|
||||||
|
<span className={styles.appName}>{app.name}</span>
|
||||||
|
</Tooltip>
|
||||||
|
<Text code className={styles.appId}>
|
||||||
|
ID: {app.appId}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppListItem;
|
||||||
@@ -7,10 +7,7 @@ import React from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Spin, Alert, Space, message } from "antd";
|
import { Spin, Alert, Space, message } from "antd";
|
||||||
import { Button, Empty } from "@lobehub/ui";
|
import { Button, Empty } from "@lobehub/ui";
|
||||||
import {
|
import { Copy, Download } from "lucide-react";
|
||||||
Copy,
|
|
||||||
Download,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { createStyles } from "antd-style";
|
import { createStyles } from "antd-style";
|
||||||
import CodeMirror from "@uiw/react-codemirror";
|
import CodeMirror from "@uiw/react-codemirror";
|
||||||
import { javascript } from "@codemirror/lang-javascript";
|
import { javascript } from "@codemirror/lang-javascript";
|
||||||
@@ -72,6 +69,7 @@ const CodeViewer: React.FC<CodeViewerProps> = ({
|
|||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const [content, setContent] = React.useState<string>("");
|
const [content, setContent] = React.useState<string>("");
|
||||||
const [language, setLanguage] = React.useState<"js" | "css">(fileType);
|
const [language, setLanguage] = React.useState<"js" | "css">(fileType);
|
||||||
|
const [downloading, setDownloading] = React.useState(false);
|
||||||
|
|
||||||
// Load file content
|
// Load file content
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -128,17 +126,62 @@ const CodeViewer: React.FC<CodeViewerProps> = ({
|
|||||||
message.success(t("copiedToClipboard"));
|
message.success(t("copiedToClipboard"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownload = () => {
|
const handleDownload = async () => {
|
||||||
const blob = new Blob([content], { type: "text/plain" });
|
if (!currentDomain || downloading) return;
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement("a");
|
// Check if fileName already has extension
|
||||||
a.href = url;
|
const hasExt = /\.(js|css)$/i.test(fileName);
|
||||||
a.download = fileName;
|
const finalFileName = hasExt ? fileName : `${fileName}.${fileType}`;
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
setDownloading(true);
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
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"));
|
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) {
|
if (loading) {
|
||||||
@@ -170,9 +213,7 @@ const CodeViewer: React.FC<CodeViewerProps> = ({
|
|||||||
if (!content) {
|
if (!content) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Empty
|
<Empty description={t("fileEmpty")} />
|
||||||
description={t("fileEmpty")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -194,6 +235,7 @@ const CodeViewer: React.FC<CodeViewerProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<Download size={16} />}
|
icon={<Download size={16} />}
|
||||||
|
loading={downloading}
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
>
|
>
|
||||||
{t("download", { ns: "common" })}
|
{t("download", { ns: "common" })}
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ const DomainList: React.FC<DomainListProps> = ({ onEdit }) => {
|
|||||||
useDomainStore();
|
useDomainStore();
|
||||||
|
|
||||||
const handleSelect = (domain: Domain) => {
|
const handleSelect = (domain: Domain) => {
|
||||||
|
if (currentDomain?.id === domain.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
switchDomain(domain);
|
switchDomain(domain);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -242,7 +242,6 @@ const DomainManager: React.FC<DomainManagerProps> = ({
|
|||||||
<h2 className={styles.title}>{t("domainManagement")}</h2>
|
<h2 className={styles.title}>{t("domainManagement")}</h2>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<Button type="primary" icon={<Plus size={16} />} onClick={handleAdd}>
|
<Button type="primary" icon={<Plus size={16} />} onClick={handleAdd}>
|
||||||
{t("add")}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Typography, Radio, Divider } from "antd";
|
import { Typography, Radio, Divider, Space, message } from "antd";
|
||||||
import { Globe } from "lucide-react";
|
import { Globe, Info, ExternalLink } from "lucide-react";
|
||||||
|
import { Button } from "@lobehub/ui";
|
||||||
|
|
||||||
import { createStyles } from "antd-style";
|
import { createStyles } from "antd-style";
|
||||||
import { useLocaleStore } from "@renderer/stores/localeStore";
|
import { useLocaleStore } from "@renderer/stores/localeStore";
|
||||||
@@ -42,6 +43,28 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
display: none;
|
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 = () => {
|
const Settings: React.FC = () => {
|
||||||
@@ -50,6 +73,25 @@ const Settings: React.FC = () => {
|
|||||||
const { locale, setLocale } = useLocaleStore();
|
const { locale, setLocale } = useLocaleStore();
|
||||||
const i18n = useTranslation().i18n;
|
const i18n = useTranslation().i18n;
|
||||||
|
|
||||||
|
// Version and update state
|
||||||
|
const [appVersion, setAppVersion] = React.useState<string>("");
|
||||||
|
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) => {
|
const handleLocaleChange = async (newLocale: LocaleCode) => {
|
||||||
setLocale(newLocale);
|
setLocale(newLocale);
|
||||||
i18n.changeLanguage(newLocale);
|
i18n.changeLanguage(newLocale);
|
||||||
@@ -57,6 +99,33 @@ const Settings: React.FC = () => {
|
|||||||
await window.api.setLocale({ locale: newLocale });
|
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 (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{/* Language Section */}
|
{/* Language Section */}
|
||||||
@@ -84,7 +153,58 @@ const Settings: React.FC = () => {
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{/* Future settings sections can be added here */}
|
{/* About Section */}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>
|
||||||
|
<Info size={16} />
|
||||||
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
|
{t("about")}
|
||||||
|
</Title>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.versionInfo}>
|
||||||
|
<span>{t("version")}:</span>
|
||||||
|
<span className={styles.versionNumber}>{appVersion || "-"}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space direction="vertical" style={{ width: "100%", marginTop: 12 }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<ExternalLink size={14} />}
|
||||||
|
onClick={handleCheckUpdate}
|
||||||
|
loading={checkingUpdate}
|
||||||
|
>
|
||||||
|
{t("checkUpdate")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{updateInfo && (
|
||||||
|
<div
|
||||||
|
className={`${styles.updateInfo} ${
|
||||||
|
updateInfo.hasUpdate ? styles.updateAvailable : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{updateInfo.hasUpdate ? (
|
||||||
|
<Space direction="vertical" size={4}>
|
||||||
|
<span>
|
||||||
|
{t("updateAvailable")}: v{updateInfo.version}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
icon={<ExternalLink size={12} />}
|
||||||
|
onClick={handleOpenReleasePage}
|
||||||
|
style={{ padding: 0, height: "auto" }}
|
||||||
|
>
|
||||||
|
{t("downloadUpdate")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<span>{t("noUpdates")}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
"about": "About",
|
"about": "About",
|
||||||
"version": "Version",
|
"version": "Version",
|
||||||
"checkUpdate": "Check for Updates",
|
"checkUpdate": "Check for Updates",
|
||||||
|
"checkUpdateFailed": "Failed to check for updates",
|
||||||
"noUpdates": "You're up to date",
|
"noUpdates": "You're up to date",
|
||||||
"updateAvailable": "Update available"
|
"updateAvailable": "Update available",
|
||||||
|
"downloadUpdate": "Download Update"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
"about": "アプリについて",
|
"about": "アプリについて",
|
||||||
"version": "バージョン",
|
"version": "バージョン",
|
||||||
"checkUpdate": "更新を確認",
|
"checkUpdate": "更新を確認",
|
||||||
|
"checkUpdateFailed": "更新の確認に失敗しました",
|
||||||
"noUpdates": "最新バージョンです",
|
"noUpdates": "最新バージョンです",
|
||||||
"updateAvailable": "新しいバージョンが利用可能です"
|
"updateAvailable": "新しいバージョンが利用可能です",
|
||||||
|
"downloadUpdate": "更新をダウンロード"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
"about": "关于",
|
"about": "关于",
|
||||||
"version": "版本",
|
"version": "版本",
|
||||||
"checkUpdate": "检查更新",
|
"checkUpdate": "检查更新",
|
||||||
|
"checkUpdateFailed": "检查更新失败",
|
||||||
"noUpdates": "已是最新版本",
|
"noUpdates": "已是最新版本",
|
||||||
"updateAvailable": "有新版本可用"
|
"updateAvailable": "有新版本可用",
|
||||||
|
"downloadUpdate": "下载更新"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import { useAppStore } from "./appStore";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import type { Domain, DomainWithStatus } from "@shared/types/domain";
|
import type { Domain, DomainWithStatus } from "@shared/types/domain";
|
||||||
import type { ConnectionStatus } from "@shared/types/domain";
|
import type { ConnectionStatus } from "@shared/types/domain";
|
||||||
@@ -217,8 +218,17 @@ export const useDomainStore = create<DomainState>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
switchDomain: async (domain: Domain) => {
|
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 });
|
set({ currentDomain: domain });
|
||||||
// Test connection after switching
|
|
||||||
|
// 3. Test connection after switching
|
||||||
const status = await get().testConnection(domain.id);
|
const status = await get().testConnection(domain.id);
|
||||||
if (status) {
|
if (status) {
|
||||||
set({
|
set({
|
||||||
@@ -228,6 +238,19 @@ export const useDomainStore = create<DomainState>()(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) => {
|
testConnection: async (id: string) => {
|
||||||
|
|||||||
@@ -125,6 +125,20 @@ export interface ShowSaveDialogParams {
|
|||||||
export interface SaveFileContentParams {
|
export interface SaveFileContentParams {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
content: string; // Base64 encoded
|
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 ====================
|
// ==================== IPC API Interface ====================
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user