feat: add download all as zip feature
- Add adm-zip dependency for zip compression - Add DownloadAllZipParams/Result IPC types - Implement registerDownloadAllZip handler in main process - Update frontend download flow with save dialog - ZIP includes pc/, mobile/ folders and metadata.json
This commit is contained in:
20
package-lock.json
generated
20
package-lock.json
generated
@@ -18,7 +18,9 @@
|
|||||||
"@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.1",
|
"@lobehub/ui": "^5.5.1",
|
||||||
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@uiw/react-codemirror": "^4.23.0",
|
"@uiw/react-codemirror": "^4.23.0",
|
||||||
|
"adm-zip": "^0.5.16",
|
||||||
"antd": "^6.1.0",
|
"antd": "^6.1.0",
|
||||||
"antd-style": "^4.1.0",
|
"antd-style": "^4.1.0",
|
||||||
"electron-store": "^10.0.0",
|
"electron-store": "^10.0.0",
|
||||||
@@ -4727,6 +4729,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/adm-zip": {
|
||||||
|
"version": "0.5.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/adm-zip/-/adm-zip-0.5.7.tgz",
|
||||||
|
"integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -5798,6 +5809,15 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adm-zip": {
|
||||||
|
"version": "0.5.16",
|
||||||
|
"resolved": "https://registry.npmmirror.com/adm-zip/-/adm-zip-0.5.16.tgz",
|
||||||
|
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "7.1.4",
|
"version": "7.1.4",
|
||||||
"resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
|
"resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
"@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.1",
|
"@lobehub/ui": "^5.5.1",
|
||||||
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@uiw/react-codemirror": "^4.23.0",
|
"@uiw/react-codemirror": "^4.23.0",
|
||||||
|
"adm-zip": "^0.5.16",
|
||||||
"antd": "^6.1.0",
|
"antd": "^6.1.0",
|
||||||
"antd-style": "^4.1.0",
|
"antd-style": "^4.1.0",
|
||||||
"electron-store": "^10.0.0",
|
"electron-store": "^10.0.0",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { ipcMain, dialog, app } from "electron";
|
import { ipcMain, dialog, app } from "electron";
|
||||||
import { autoUpdater } from "electron-updater";
|
import { autoUpdater } from "electron-updater";
|
||||||
|
import AdmZip from "adm-zip";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import {
|
import {
|
||||||
saveDomain,
|
saveDomain,
|
||||||
@@ -31,6 +32,8 @@ import type {
|
|||||||
DeployParams,
|
DeployParams,
|
||||||
DeployResult,
|
DeployResult,
|
||||||
DownloadParams,
|
DownloadParams,
|
||||||
|
DownloadAllZipParams,
|
||||||
|
DownloadAllZipResult,
|
||||||
DownloadResult,
|
DownloadResult,
|
||||||
GetVersionsParams,
|
GetVersionsParams,
|
||||||
RollbackParams,
|
RollbackParams,
|
||||||
@@ -490,6 +493,94 @@ function registerDownload(): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download all files as ZIP
|
||||||
|
*/
|
||||||
|
function registerDownloadAllZip(): void {
|
||||||
|
handle<DownloadAllZipParams, DownloadAllZipResult>(
|
||||||
|
"downloadAllZip",
|
||||||
|
async (params) => {
|
||||||
|
const client = await getClient(params.domainId);
|
||||||
|
const domainWithPassword = await getDomain(params.domainId);
|
||||||
|
|
||||||
|
if (!domainWithPassword) {
|
||||||
|
throw new Error(getErrorMessage("domainNotFound"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const appDetail = await client.getAppDetail(params.appId);
|
||||||
|
const zip = new AdmZip();
|
||||||
|
const metadata = {
|
||||||
|
downloadedAt: new Date().toISOString(),
|
||||||
|
domain: domainWithPassword.domain,
|
||||||
|
appId: params.appId,
|
||||||
|
appName: appDetail.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Download and add PC files
|
||||||
|
const pcJsFiles = appDetail.customization?.desktop?.js || [];
|
||||||
|
const pcCssFiles = appDetail.customization?.desktop?.css || [];
|
||||||
|
|
||||||
|
for (const [index, file] of pcJsFiles.entries()) {
|
||||||
|
const fileKey = getFileKey(file);
|
||||||
|
if (fileKey) {
|
||||||
|
const content = await client.getFileContent(fileKey);
|
||||||
|
const buffer = Buffer.from(content.content || "", "base64");
|
||||||
|
const fileName = getDisplayName(file, "js", index);
|
||||||
|
zip.addFile(`desktop-js/${fileName}`, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [index, file] of pcCssFiles.entries()) {
|
||||||
|
const fileKey = getFileKey(file);
|
||||||
|
if (fileKey) {
|
||||||
|
const content = await client.getFileContent(fileKey);
|
||||||
|
const buffer = Buffer.from(content.content || "", "base64");
|
||||||
|
const fileName = getDisplayName(file, "css", index);
|
||||||
|
zip.addFile(`desktop-css/${fileName}`, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download and add Mobile files
|
||||||
|
const mobileJsFiles = appDetail.customization?.mobile?.js || [];
|
||||||
|
const mobileCssFiles = appDetail.customization?.mobile?.css || [];
|
||||||
|
|
||||||
|
for (const [index, file] of mobileJsFiles.entries()) {
|
||||||
|
const fileKey = getFileKey(file);
|
||||||
|
if (fileKey) {
|
||||||
|
const content = await client.getFileContent(fileKey);
|
||||||
|
const buffer = Buffer.from(content.content || "", "base64");
|
||||||
|
const fileName = getDisplayName(file, "js", index);
|
||||||
|
zip.addFile(`mobile-js/${fileName}`, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [index, file] of mobileCssFiles.entries()) {
|
||||||
|
const fileKey = getFileKey(file);
|
||||||
|
if (fileKey) {
|
||||||
|
const content = await client.getFileContent(fileKey);
|
||||||
|
const buffer = Buffer.from(content.content || "", "base64");
|
||||||
|
const fileName = getDisplayName(file, "css", index);
|
||||||
|
zip.addFile(`mobile-css/${fileName}`, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add metadata.json
|
||||||
|
zip.addFile(
|
||||||
|
"metadata.json",
|
||||||
|
Buffer.from(JSON.stringify(metadata, null, 2), "utf-8"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write ZIP to user-provided path
|
||||||
|
zip.writeZip(params.savePath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
path: params.savePath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Version IPC Handlers ====================
|
// ==================== Version IPC Handlers ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -628,6 +719,7 @@ export function registerIpcHandlers(): void {
|
|||||||
|
|
||||||
// Download
|
// Download
|
||||||
registerDownload();
|
registerDownload();
|
||||||
|
registerDownloadAllZip();
|
||||||
|
|
||||||
// Version
|
// Version
|
||||||
registerGetVersions();
|
registerGetVersions();
|
||||||
|
|||||||
4
src/preload/index.d.ts
vendored
4
src/preload/index.d.ts
vendored
@@ -11,7 +11,8 @@ import type {
|
|||||||
DeployResult,
|
DeployResult,
|
||||||
DownloadParams,
|
DownloadParams,
|
||||||
DownloadResult,
|
DownloadResult,
|
||||||
GetVersionsParams,
|
DownloadAllZipParams,
|
||||||
|
DownloadAllZipResult,
|
||||||
RollbackParams,
|
RollbackParams,
|
||||||
SetLocaleParams,
|
SetLocaleParams,
|
||||||
ShowSaveDialogParams,
|
ShowSaveDialogParams,
|
||||||
@@ -61,6 +62,7 @@ export interface SelfAPI {
|
|||||||
|
|
||||||
// ==================== Download ====================
|
// ==================== Download ====================
|
||||||
download: (params: DownloadParams) => Promise<DownloadResult>;
|
download: (params: DownloadParams) => Promise<DownloadResult>;
|
||||||
|
downloadAllZip: (params: DownloadAllZipParams) => Promise<Result<DownloadAllZipResult>>;
|
||||||
|
|
||||||
// ==================== Version Management ====================
|
// ==================== Version Management ====================
|
||||||
getVersions: (params: GetVersionsParams) => Promise<Result<Version[]>>;
|
getVersions: (params: GetVersionsParams) => Promise<Result<Version[]>>;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const api: SelfAPI = {
|
|||||||
|
|
||||||
// Download
|
// Download
|
||||||
download: (params) => ipcRenderer.invoke("download", params),
|
download: (params) => ipcRenderer.invoke("download", params),
|
||||||
|
downloadAllZip: (params) => ipcRenderer.invoke("downloadAllZip", params),
|
||||||
|
|
||||||
// Version management
|
// Version management
|
||||||
getVersions: (params) => ipcRenderer.invoke("getVersions", params),
|
getVersions: (params) => ipcRenderer.invoke("getVersions", params),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Spin, Tag, Space, message } from "antd";
|
import { Spin, Tag, Space, App as AntApp } from "antd";
|
||||||
import { Button, Empty, MaterialFileTypeIcon } from "@lobehub/ui";
|
import { Button, Empty, MaterialFileTypeIcon } from "@lobehub/ui";
|
||||||
import {
|
import {
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
@@ -148,13 +148,14 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const AppDetail: React.FC = () => {
|
const AppDetail: React.FC = () => {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation(["app", "common"]);
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
const token = useTheme();
|
const token = useTheme();
|
||||||
const { currentDomain } = useDomainStore();
|
const { currentDomain } = useDomainStore();
|
||||||
const { currentApp, selectedAppId, loading, setCurrentApp, setLoading } =
|
const { currentApp, selectedAppId, loading, setCurrentApp, setLoading } =
|
||||||
useAppStore();
|
useAppStore();
|
||||||
const { viewMode, selectedFile, setViewMode, setSelectedFile } = useSessionStore();
|
const { viewMode, selectedFile, setViewMode, setSelectedFile } = useSessionStore();
|
||||||
|
const { message } = AntApp.useApp();
|
||||||
|
|
||||||
// Download state: track which file is being downloaded
|
// Download state: track which file is being downloaded
|
||||||
const [downloadingKey, setDownloadingKey] = React.useState<string | null>(
|
const [downloadingKey, setDownloadingKey] = React.useState<string | null>(
|
||||||
@@ -306,18 +307,37 @@ const AppDetail: React.FC = () => {
|
|||||||
|
|
||||||
// Download all files
|
// Download all files
|
||||||
const handleDownloadAll = async () => {
|
const handleDownloadAll = async () => {
|
||||||
if (!currentDomain || !selectedAppId || downloadingAll) return;
|
if (!currentDomain || !selectedAppId || downloadingAll || !currentApp) return;
|
||||||
|
|
||||||
|
// Generate default filename: {appName}_{YYYY-MM-DD}.zip
|
||||||
|
const appName = currentApp.name || "app";
|
||||||
|
const sanitizedAppName = appName.replace(/[:*?"<>|]/g, "_");
|
||||||
|
const date = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||||
|
const defaultFilename = `${sanitizedAppName}_${date}.zip`;
|
||||||
|
|
||||||
|
// Show save dialog first
|
||||||
|
const dialogResult = await window.api.showSaveDialog({
|
||||||
|
defaultPath: defaultFilename,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!dialogResult.success || !dialogResult.data) {
|
||||||
|
// User cancelled - return without error message
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const savePath = dialogResult.data;
|
||||||
setDownloadingAll(true);
|
setDownloadingAll(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.api.download({
|
const result = await window.api.downloadAllZip({
|
||||||
domainId: currentDomain.id,
|
domainId: currentDomain.id,
|
||||||
appId: selectedAppId,
|
appId: selectedAppId,
|
||||||
|
savePath,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
message.success(t("downloadAllSuccess", { path: result.path }));
|
|
||||||
|
message.success(t("downloadAllSuccess", { path: result.data.path, ns: "common" }))
|
||||||
} else {
|
} else {
|
||||||
message.error(result.error || t("downloadFailed", { ns: "common" }));
|
message.error(result.error || t("downloadFailed", { ns: "common" }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,16 @@ export interface DownloadResult {
|
|||||||
metadata?: DownloadMetadata;
|
metadata?: DownloadMetadata;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
export interface DownloadAllZipResult {
|
||||||
|
success: boolean;
|
||||||
|
path?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
export interface DownloadAllZipParams {
|
||||||
|
domainId: string;
|
||||||
|
appId: string;
|
||||||
|
savePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== Version IPC Types ====================
|
// ==================== Version IPC Types ====================
|
||||||
|
|
||||||
@@ -125,6 +135,7 @@ 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 ====================
|
// ==================== App Version & Update IPC Types ====================
|
||||||
|
|
||||||
export interface UpdateInfo {
|
export interface UpdateInfo {
|
||||||
@@ -138,8 +149,6 @@ export interface CheckUpdateResult {
|
|||||||
updateInfo?: UpdateInfo;
|
updateInfo?: UpdateInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
// ==================== IPC API Interface ====================
|
// ==================== IPC API Interface ====================
|
||||||
|
|
||||||
export interface ElectronAPI {
|
export interface ElectronAPI {
|
||||||
@@ -165,6 +174,7 @@ export interface ElectronAPI {
|
|||||||
|
|
||||||
// Download
|
// Download
|
||||||
download: (params: DownloadParams) => Promise<DownloadResult>;
|
download: (params: DownloadParams) => Promise<DownloadResult>;
|
||||||
|
downloadAllZip: (params: DownloadAllZipParams) => Promise<DownloadAllZipResult>;
|
||||||
|
|
||||||
// Version management
|
// Version management
|
||||||
getVersions: (params: GetVersionsParams) => Promise<Result<Version[]>>;
|
getVersions: (params: GetVersionsParams) => Promise<Result<Version[]>>;
|
||||||
|
|||||||
Reference in New Issue
Block a user