feat: implement presentation export functionality via IPC in Electron app
This commit is contained in:
parent
ac59114208
commit
a7d00fc1a3
6 changed files with 1211 additions and 82 deletions
|
|
@ -1,10 +1,11 @@
|
|||
import { BrowserWindow, ipcMain, } from "electron";
|
||||
import { baseDir, downloadsDir } from "../utils/constants";
|
||||
import { ipcMain } from "electron";
|
||||
import { appDataDir, baseDir, downloadsDir, tempDir } from "../utils/constants";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { showFileDownloadedDialog } from "../utils/dialog";
|
||||
import { sanitizeFilename } from "../utils";
|
||||
|
||||
import { showFileDownloadedDialog } from "../utils/dialog";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { spawn } from "child_process";
|
||||
|
||||
export function setupExportHandlers() {
|
||||
ipcMain.handle("file-downloaded", async (_, filePath: string): Promise<IPCStatus> => {
|
||||
|
|
@ -16,43 +17,108 @@ export function setupExportHandlers() {
|
|||
return { success };
|
||||
});
|
||||
|
||||
ipcMain.handle("export-as-pdf", async (_, id: string, title: string) => {
|
||||
const ppt_url = `${process.env.NEXT_PUBLIC_URL}/pdf-maker?id=${id}`;
|
||||
const browser = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 720,
|
||||
icon: path.join(baseDir, "resources/ui/assets/images/presenton_short_filled.png"),
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
ipcMain.handle("export-presentation", async (_, id: string, title: string, exportAs: "pptx" | "pdf" | "png") => {
|
||||
try {
|
||||
const pptUrl = `${process.env.NEXT_PUBLIC_URL}/pdf-maker?id=${id}`;
|
||||
|
||||
preload: path.join(__dirname, '../preloads/index.js'),
|
||||
},
|
||||
show: false,
|
||||
});
|
||||
browser.loadURL(ppt_url);
|
||||
let exportTask = {
|
||||
type: "export",
|
||||
url: pptUrl,
|
||||
format: exportAs,
|
||||
title: title,
|
||||
}
|
||||
|
||||
const success = await new Promise((resolve, _) => {
|
||||
browser.webContents.on('did-finish-load', async () => {
|
||||
// Wait for 1 second to make sure the page is loaded
|
||||
await new Promise((resolve, _) => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
const randomUuid = uuidv4();
|
||||
const exportTempDir = path.join(tempDir, randomUuid);
|
||||
await fs.promises.mkdir(exportTempDir, { recursive: true });
|
||||
|
||||
const pdfBuffer = await browser.webContents.printToPDF({
|
||||
printBackground: true,
|
||||
pageSize: { width: 1280 / 96, height: 720 / 96 },
|
||||
margins: { top: 0, right: 0, bottom: 0, left: 0 }
|
||||
});
|
||||
browser.close();
|
||||
const sanitizedTitle = sanitizeFilename(title);
|
||||
const destinationPath = path.join(downloadsDir, `${sanitizedTitle}.pdf`);
|
||||
await fs.promises.writeFile(destinationPath, pdfBuffer);
|
||||
const exportTaskPath = path.join(exportTempDir, "export_task.json");
|
||||
await fs.promises.writeFile(exportTaskPath, JSON.stringify(exportTask));
|
||||
|
||||
const success = await showFileDownloadedDialog(destinationPath);
|
||||
resolve(success);
|
||||
const exportScriptPath = path.join(baseDir, "resources", "export", "index.js");
|
||||
const pythonModulePath = path.join(baseDir, "resources", "export", "py", "convert");
|
||||
const exportTaskProcess = spawn("node", [exportScriptPath, exportTaskPath], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: {
|
||||
...process.env,
|
||||
TEMP_DIRECTORY: tempDir,
|
||||
APP_DATA_DIRECTORY: appDataDir,
|
||||
BUILT_PYTHON_MODULE_PATH: pythonModulePath,
|
||||
},
|
||||
});
|
||||
});
|
||||
return { success };
|
||||
|
||||
exportTaskProcess.stdout.on("data", (data: Buffer) => {
|
||||
console.log(`[Export] ${data.toString()}`);
|
||||
});
|
||||
exportTaskProcess.stderr.on("data", (data: Buffer) => {
|
||||
console.error(`[Export] ${data.toString()}`);
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
exportTaskProcess.on("error", reject);
|
||||
exportTaskProcess.on("exit", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Export process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const responsePath = exportTaskPath.replace(".json", ".response.json");
|
||||
const responseRaw = await fs.promises.readFile(responsePath, "utf8");
|
||||
const responseData = JSON.parse(responseRaw);
|
||||
const exportFilePath = resolveExportedFilePath(responseData);
|
||||
|
||||
if (!exportFilePath) {
|
||||
return { success: false, message: "Export finished but output file was not found." };
|
||||
}
|
||||
|
||||
const destinationPath = path.join(downloadsDir, path.basename(exportFilePath));
|
||||
await moveFile(exportFilePath, destinationPath);
|
||||
const success = await showFileDownloadedDialog(destinationPath);
|
||||
return { success, message: success ? "Export completed." : "Export completed but dialog failed." };
|
||||
} catch (error: any) {
|
||||
console.error("[Export] Error exporting presentation:", error);
|
||||
return { success: false, message: error?.message ?? "Export failed." };
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function resolveExportedFilePath(responseData: any): string | null {
|
||||
if (responseData?.path && typeof responseData.path === "string") {
|
||||
return path.isAbsolute(responseData.path)
|
||||
? responseData.path
|
||||
: path.join(appDataDir, responseData.path);
|
||||
}
|
||||
|
||||
if (responseData?.url && typeof responseData.url === "string") {
|
||||
try {
|
||||
const parsed = new URL(responseData.url);
|
||||
if (parsed.protocol === "file:") {
|
||||
const filePath = decodeURIComponent(parsed.pathname);
|
||||
if (process.platform === "win32" && filePath.startsWith("/")) {
|
||||
return filePath.slice(1);
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function moveFile(sourcePath: string, destinationPath: string) {
|
||||
try {
|
||||
await fs.promises.rename(sourcePath, destinationPath);
|
||||
} catch (error: any) {
|
||||
if (error?.code !== "EXDEV") {
|
||||
throw error;
|
||||
}
|
||||
await fs.promises.copyFile(sourcePath, destinationPath);
|
||||
await fs.promises.unlink(sourcePath);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,8 @@ contextBridge.exposeInMainWorld('env', {
|
|||
contextBridge.exposeInMainWorld('electron', {
|
||||
fileDownloaded: (filePath: string) => ipcRenderer.invoke("file-downloaded", filePath),
|
||||
exportAsPDF: (id: string, title: string) => ipcRenderer.invoke("export-as-pdf", id, title),
|
||||
exportPresentation: (id: string, title: string, format: "pptx" | "pdf" | "png") =>
|
||||
ipcRenderer.invoke("export-presentation", id, title, format),
|
||||
getUserConfig: () => ipcRenderer.invoke("get-user-config"),
|
||||
setUserConfig: (userConfig: UserConfig) => ipcRenderer.invoke("set-user-config", userConfig),
|
||||
getCanChangeKeys: () => ipcRenderer.invoke("get-can-change-keys"),
|
||||
|
|
|
|||
1035
electron/resources/export/index.js
Normal file
1035
electron/resources/export/index.js
Normal file
File diff suppressed because one or more lines are too long
BIN
electron/resources/export/py/convert
Executable file
BIN
electron/resources/export/py/convert
Executable file
Binary file not shown.
|
|
@ -75,6 +75,25 @@ const Header = ({
|
|||
return pptx_model;
|
||||
};
|
||||
|
||||
const exportViaIpc = async (format: "pptx" | "pdf"): Promise<boolean> => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
if (!(window as any).electron?.exportPresentation) return false;
|
||||
trackEvent(
|
||||
format === "pptx"
|
||||
? MixpanelEvent.Header_ExportAsPPTX_API_Call
|
||||
: MixpanelEvent.Header_ExportAsPDF_API_Call
|
||||
);
|
||||
const result = await (window as any).electron.exportPresentation(
|
||||
presentation_id,
|
||||
presentationData?.title || 'presentation',
|
||||
format
|
||||
);
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.message || 'Export failed');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleExportPptx = async () => {
|
||||
if (isStreaming) return;
|
||||
|
||||
|
|
@ -84,28 +103,12 @@ const Header = ({
|
|||
// Save the presentation data before exporting
|
||||
trackEvent(MixpanelEvent.Header_UpdatePresentationContent_API_Call);
|
||||
await PresentationGenerationApi.updatePresentationContent(presentationData);
|
||||
trackEvent(MixpanelEvent.Header_GetPptxModel_API_Call);
|
||||
const pptx_model = await get_presentation_pptx_model(presentation_id);
|
||||
if (!pptx_model) {
|
||||
throw new Error("Failed to get presentation PPTX model");
|
||||
}
|
||||
trackEvent(MixpanelEvent.Header_ExportAsPPTX_API_Call);
|
||||
const pptx_path = await PresentationGenerationApi.exportAsPPTX(pptx_model);
|
||||
if (pptx_path) {
|
||||
// Check if we're in Electron mode
|
||||
if (typeof window !== 'undefined' && (window as any).electron?.fileDownloaded) {
|
||||
// Use Electron's file download dialog
|
||||
const result = await (window as any).electron.fileDownloaded(pptx_path);
|
||||
if (!result.success) {
|
||||
throw new Error('Export cancelled or failed');
|
||||
}
|
||||
} else {
|
||||
// Fallback to direct download for web mode
|
||||
downloadLink(pptx_path);
|
||||
}
|
||||
} else {
|
||||
throw new Error("No path returned from export");
|
||||
if (await exportViaIpc("pptx")) {
|
||||
toast.success("PPTX exported successfully!");
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Export is only supported in the desktop app.");
|
||||
} catch (error) {
|
||||
console.error("Export failed:", error);
|
||||
setShowLoader(false);
|
||||
|
|
@ -130,32 +133,26 @@ const Header = ({
|
|||
|
||||
trackEvent(MixpanelEvent.Header_ExportAsPDF_API_Call);
|
||||
|
||||
// Check if running in Electron environment
|
||||
if (typeof window !== 'undefined' && window.electron?.exportAsPDF) {
|
||||
// Use Electron IPC handler
|
||||
const result = await window.electron.exportAsPDF(presentation_id, presentationData?.title || 'presentation');
|
||||
if (result.success) {
|
||||
toast.success("PDF exported successfully!");
|
||||
} else {
|
||||
throw new Error("Failed to export PDF");
|
||||
}
|
||||
} else {
|
||||
// Fallback to API route for web-based deployments
|
||||
const response = await fetch('/api/export-as-pdf', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id: presentation_id,
|
||||
title: presentationData?.title,
|
||||
})
|
||||
});
|
||||
if (await exportViaIpc("pdf")) {
|
||||
toast.success("PDF exported successfully!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const { path: pdfPath } = await response.json();
|
||||
// window.open(pdfPath, '_blank');
|
||||
downloadLink(pdfPath);
|
||||
} else {
|
||||
throw new Error("Failed to export PDF");
|
||||
}
|
||||
// Fallback to API route for web-based deployments
|
||||
const response = await fetch('/api/export-as-pdf', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
id: presentation_id,
|
||||
title: presentationData?.title,
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const { path: pdfPath } = await response.json();
|
||||
// window.open(pdfPath, '_blank');
|
||||
downloadLink(pdfPath);
|
||||
} else {
|
||||
throw new Error("Failed to export PDF");
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -85,6 +85,25 @@ const PresentationHeader = ({
|
|||
return pptx_model;
|
||||
};
|
||||
|
||||
const exportViaIpc = async (format: "pptx" | "pdf"): Promise<boolean> => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
if (!(window as any).electron?.exportPresentation) return false;
|
||||
trackEvent(
|
||||
format === "pptx"
|
||||
? MixpanelEvent.Header_ExportAsPPTX_API_Call
|
||||
: MixpanelEvent.Header_ExportAsPDF_API_Call
|
||||
);
|
||||
const result = await (window as any).electron.exportPresentation(
|
||||
presentation_id,
|
||||
presentationData?.title || 'presentation',
|
||||
format
|
||||
);
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.message || 'Export failed');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleExportPptx = async () => {
|
||||
if (isStreaming) return;
|
||||
|
||||
|
|
@ -93,6 +112,12 @@ const PresentationHeader = ({
|
|||
// Save the presentation data before exporting
|
||||
trackEvent(MixpanelEvent.Header_UpdatePresentationContent_API_Call);
|
||||
await PresentationGenerationApi.updatePresentationContent(presentationData);
|
||||
|
||||
if (await exportViaIpc("pptx")) {
|
||||
toast.success("PPTX exported successfully!");
|
||||
return;
|
||||
}
|
||||
|
||||
trackEvent(MixpanelEvent.Header_GetPptxModel_API_Call);
|
||||
const pptx_model = await get_presentation_pptx_model(presentation_id);
|
||||
if (!pptx_model) {
|
||||
|
|
@ -127,6 +152,10 @@ const PresentationHeader = ({
|
|||
await PresentationGenerationApi.updatePresentationContent(presentationData);
|
||||
|
||||
trackEvent(MixpanelEvent.Header_ExportAsPDF_API_Call);
|
||||
if (await exportViaIpc("pdf")) {
|
||||
toast.success("PDF exported successfully!");
|
||||
return;
|
||||
}
|
||||
const response = await fetch('/api/export-as-pdf', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue