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",
|
||||
"@kintone/rest-api-client": "^6.1.2",
|
||||
"@lobehub/ui": "^5.5.1",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"antd": "^6.1.0",
|
||||
"antd-style": "^4.1.0",
|
||||
"electron-store": "^10.0.0",
|
||||
@@ -4727,6 +4729,15 @@
|
||||
"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": {
|
||||
"version": "7.20.5",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@kintone/rest-api-client": "^6.1.2",
|
||||
"@lobehub/ui": "^5.5.1",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@uiw/react-codemirror": "^4.23.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"antd": "^6.1.0",
|
||||
"antd-style": "^4.1.0",
|
||||
"electron-store": "^10.0.0",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { ipcMain, dialog, app } from "electron";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import AdmZip from "adm-zip";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
saveDomain,
|
||||
@@ -31,6 +32,8 @@ import type {
|
||||
DeployParams,
|
||||
DeployResult,
|
||||
DownloadParams,
|
||||
DownloadAllZipParams,
|
||||
DownloadAllZipResult,
|
||||
DownloadResult,
|
||||
GetVersionsParams,
|
||||
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 ====================
|
||||
|
||||
/**
|
||||
@@ -628,6 +719,7 @@ export function registerIpcHandlers(): void {
|
||||
|
||||
// Download
|
||||
registerDownload();
|
||||
registerDownloadAllZip();
|
||||
|
||||
// Version
|
||||
registerGetVersions();
|
||||
|
||||
4
src/preload/index.d.ts
vendored
4
src/preload/index.d.ts
vendored
@@ -11,7 +11,8 @@ import type {
|
||||
DeployResult,
|
||||
DownloadParams,
|
||||
DownloadResult,
|
||||
GetVersionsParams,
|
||||
DownloadAllZipParams,
|
||||
DownloadAllZipResult,
|
||||
RollbackParams,
|
||||
SetLocaleParams,
|
||||
ShowSaveDialogParams,
|
||||
@@ -61,6 +62,7 @@ export interface SelfAPI {
|
||||
|
||||
// ==================== Download ====================
|
||||
download: (params: DownloadParams) => Promise<DownloadResult>;
|
||||
downloadAllZip: (params: DownloadAllZipParams) => Promise<Result<DownloadAllZipResult>>;
|
||||
|
||||
// ==================== Version Management ====================
|
||||
getVersions: (params: GetVersionsParams) => Promise<Result<Version[]>>;
|
||||
|
||||
@@ -24,6 +24,7 @@ const api: SelfAPI = {
|
||||
|
||||
// Download
|
||||
download: (params) => ipcRenderer.invoke("download", params),
|
||||
downloadAllZip: (params) => ipcRenderer.invoke("downloadAllZip", params),
|
||||
|
||||
// Version management
|
||||
getVersions: (params) => ipcRenderer.invoke("getVersions", params),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import React from "react";
|
||||
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 {
|
||||
LayoutGrid,
|
||||
@@ -148,13 +148,14 @@ const useStyles = createStyles(({ token, css }) => ({
|
||||
}));
|
||||
|
||||
const AppDetail: React.FC = () => {
|
||||
const { t } = useTranslation("app");
|
||||
const { t } = useTranslation(["app", "common"]);
|
||||
const { styles } = useStyles();
|
||||
const token = useTheme();
|
||||
const { currentDomain } = useDomainStore();
|
||||
const { currentApp, selectedAppId, loading, setCurrentApp, setLoading } =
|
||||
useAppStore();
|
||||
const { viewMode, selectedFile, setViewMode, setSelectedFile } = useSessionStore();
|
||||
const { message } = AntApp.useApp();
|
||||
|
||||
// Download state: track which file is being downloaded
|
||||
const [downloadingKey, setDownloadingKey] = React.useState<string | null>(
|
||||
@@ -306,18 +307,37 @@ const AppDetail: React.FC = () => {
|
||||
|
||||
// Download all files
|
||||
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);
|
||||
|
||||
try {
|
||||
const result = await window.api.download({
|
||||
const result = await window.api.downloadAllZip({
|
||||
domainId: currentDomain.id,
|
||||
appId: selectedAppId,
|
||||
savePath,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
message.success(t("downloadAllSuccess", { path: result.path }));
|
||||
|
||||
message.success(t("downloadAllSuccess", { path: result.data.path, ns: "common" }))
|
||||
} else {
|
||||
message.error(result.error || t("downloadFailed", { ns: "common" }));
|
||||
}
|
||||
|
||||
@@ -97,6 +97,16 @@ export interface DownloadResult {
|
||||
metadata?: DownloadMetadata;
|
||||
error?: string;
|
||||
}
|
||||
export interface DownloadAllZipResult {
|
||||
success: boolean;
|
||||
path?: string;
|
||||
error?: string;
|
||||
}
|
||||
export interface DownloadAllZipParams {
|
||||
domainId: string;
|
||||
appId: string;
|
||||
savePath: string;
|
||||
}
|
||||
|
||||
// ==================== Version IPC Types ====================
|
||||
|
||||
@@ -125,6 +135,7 @@ export interface ShowSaveDialogParams {
|
||||
export interface SaveFileContentParams {
|
||||
filePath: string;
|
||||
content: string; // Base64 encoded
|
||||
}
|
||||
// ==================== App Version & Update IPC Types ====================
|
||||
|
||||
export interface UpdateInfo {
|
||||
@@ -138,8 +149,6 @@ export interface CheckUpdateResult {
|
||||
updateInfo?: UpdateInfo;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
// ==================== IPC API Interface ====================
|
||||
|
||||
export interface ElectronAPI {
|
||||
@@ -165,6 +174,7 @@ export interface ElectronAPI {
|
||||
|
||||
// Download
|
||||
download: (params: DownloadParams) => Promise<DownloadResult>;
|
||||
downloadAllZip: (params: DownloadAllZipParams) => Promise<DownloadAllZipResult>;
|
||||
|
||||
// Version management
|
||||
getVersions: (params: GetVersionsParams) => Promise<Result<Version[]>>;
|
||||
|
||||
Reference in New Issue
Block a user