const { app, BrowserWindow, dialog, ipcMain, Menu, shell } = require("electron"); const fs = require("node:fs/promises"); const path = require("node:path"); const isDev = !app.isPackaged; const applicationModes = { word: { title: "QWord", windowTitle: "Документ1 - QWord", route: "/qword" }, sheets: { title: "QExcell", windowTitle: "Книга1 - QExcell", route: "/qexcell" }, slides: { title: "QPowerPoint", windowTitle: "Презентация1 - QPowerPoint", route: "/qpowerpoint" }, mail: { title: "QOutlook", windowTitle: "Почта - QOutlook", route: "/qoutlook" } }; const applicationAliases = { word: "word", qword: "word", docx: "word", sheets: "sheets", sheet: "sheets", excel: "sheets", excell: "sheets", qexcell: "sheets", xlsx: "sheets", slides: "slides", slide: "slides", powerpoint: "slides", qpowerpoint: "slides", pptx: "slides", mail: "mail", outlook: "mail", qoutlook: "mail", eml: "mail", msg: "mail" }; const fileKinds = { docx: { title: "Открыть документ Word", saveTitle: "Сохранить документ Word", defaultName: "Документ QWord.docx", filters: [{ name: "Документы Word (.docx)", extensions: ["docx"] }] }, xlsx: { title: "Открыть книгу Excel", saveTitle: "Сохранить книгу Excel", defaultName: "Книга QExcell.xlsx", filters: [{ name: "Книги Excel (.xlsx)", extensions: ["xlsx"] }] }, pptx: { title: "Открыть презентацию PowerPoint", saveTitle: "Сохранить презентацию PowerPoint", defaultName: "Презентация QPowerPoint.pptx", filters: [{ name: "Презентации PowerPoint (.pptx)", extensions: ["pptx"] }] }, eml: { title: "Открыть письмо EML", saveTitle: "Сохранить письмо EML", defaultName: "Письмо.eml", filters: [{ name: "Письма EML (.eml)", extensions: ["eml"] }] }, mail: { title: "Открыть письмо Outlook", saveTitle: "Сохранить письмо EML", defaultName: "Письмо.eml", filters: [{ name: "Письма Outlook (.eml, .msg)", extensions: ["eml", "msg"] }] } }; function validatedKind(kind) { if (typeof kind !== "string" || !Object.hasOwn(fileKinds, kind)) { throw new Error("Неподдерживаемый тип файла QOffice."); } return fileKinds[kind]; } function safeDefaultName(kind, defaultFileName) { const config = validatedKind(kind); const extension = config.filters[0].extensions[0]; return safeFileNameWithExtension(defaultFileName, extension, config.defaultName); } function safeFileNameWithExtension(defaultFileName, extension, fallbackName) { const candidate = typeof defaultFileName === "string" && defaultFileName.trim() ? path.basename(defaultFileName.trim()) : fallbackName; return candidate.toLowerCase().endsWith(`.${extension}`) ? candidate : `${candidate}.${extension}`; } function ownerWindow(event) { return BrowserWindow.fromWebContents(event.sender) ?? BrowserWindow.getFocusedWindow() ?? undefined; } function sendFocusedCommand(command) { const focusedWindow = BrowserWindow.getFocusedWindow(); if (focusedWindow && !focusedWindow.isDestroyed()) { focusedWindow.webContents.send("qoffice:command", command); } } function applicationModeFromArgs(argv = process.argv, env = process.env) { const explicitArg = argv.find((argument) => argument.startsWith("--qoffice-app=") || argument.startsWith("--app=")); const rawMode = explicitArg?.split("=")[1] ?? env.QOFFICE_APP ?? ""; const mode = applicationAliases[String(rawMode).trim().toLowerCase()] ?? "word"; return applicationModes[mode]; } function createApplicationMenu(applicationMode) { const isMac = process.platform === "darwin"; const template = [ ...(isMac ? [ { label: applicationMode.title, submenu: [ { role: "about", label: `О ${applicationMode.title}` }, { type: "separator" }, { role: "hide", label: `Скрыть ${applicationMode.title}` }, { role: "hideOthers", label: "Скрыть остальные" }, { role: "unhide", label: "Показать все" }, { type: "separator" }, { role: "quit", label: `Выйти из ${applicationMode.title}` } ] } ] : []), { label: "Файл", submenu: [ { label: "Открыть...", accelerator: "CmdOrCtrl+O", click: () => sendFocusedCommand("open") }, { label: "Сохранить как...", accelerator: "CmdOrCtrl+S", click: () => sendFocusedCommand("save") }, { type: "separator" }, { label: "Экспорт в PDF...", accelerator: "CmdOrCtrl+Shift+E", click: () => sendFocusedCommand("export-pdf") }, { type: "separator" }, { label: "Печать...", accelerator: "CmdOrCtrl+P", click: () => sendFocusedCommand("print") }, { type: "separator" }, isMac ? { role: "close", label: "Закрыть окно" } : { role: "quit", label: `Выход из ${applicationMode.title}` } ] }, { label: "Правка", submenu: [ { role: "undo", label: "Отменить" }, { role: "redo", label: "Повторить" }, { type: "separator" }, { role: "cut", label: "Вырезать" }, { role: "copy", label: "Копировать" }, { role: "paste", label: "Вставить" }, { role: "selectAll", label: "Выделить все" } ] }, { label: "Вид", submenu: [ { role: "resetZoom", label: "Сбросить масштаб" }, { role: "zoomIn", label: "Увеличить" }, { role: "zoomOut", label: "Уменьшить" }, { type: "separator" }, { role: "togglefullscreen", label: "Во весь экран" } ] }, { label: "Окно", submenu: [ { role: "minimize", label: "Свернуть" }, { role: "reload", label: "Перезагрузить" }, ...(isDev ? [{ role: "toggleDevTools", label: "Инструменты разработчика" }] : []) ] } ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } ipcMain.handle("qoffice:open-office-file", async (event, kind) => { const config = validatedKind(kind); const result = await dialog.showOpenDialog(ownerWindow(event), { title: config.title, properties: ["openFile"], filters: config.filters }); if (result.canceled || result.filePaths.length === 0) { return { canceled: true }; } const filePath = result.filePaths[0]; const bytes = await fs.readFile(filePath); return { canceled: false, fileName: path.basename(filePath), filePath, base64Data: bytes.toString("base64") }; }); ipcMain.handle("qoffice:save-office-file", async (event, payload) => { if (!payload || typeof payload !== "object") { throw new Error("Некорректный запрос сохранения файла QOffice."); } const { kind, defaultFileName, base64Data } = payload; const config = validatedKind(kind); if (typeof base64Data !== "string") { throw new Error("Некорректные данные файла QOffice."); } const result = await dialog.showSaveDialog(ownerWindow(event), { title: config.saveTitle, defaultPath: safeDefaultName(kind, defaultFileName), filters: config.filters }); if (result.canceled || !result.filePath) { return { canceled: true }; } await fs.writeFile(result.filePath, Buffer.from(base64Data, "base64")); return { canceled: false, fileName: path.basename(result.filePath), filePath: result.filePath }; }); ipcMain.handle("qoffice:export-pdf", async (event, defaultFileName) => { const sourceWindow = ownerWindow(event); if (!sourceWindow) { throw new Error("Не удалось найти окно QOffice для экспорта PDF."); } const result = await dialog.showSaveDialog(sourceWindow, { title: "Экспорт PDF", defaultPath: safeFileNameWithExtension(defaultFileName, "pdf", "QOffice.pdf"), filters: [{ name: "PDF (.pdf)", extensions: ["pdf"] }] }); if (result.canceled || !result.filePath) { return { canceled: true }; } const pdf = await event.sender.printToPDF({ printBackground: true, preferCSSPageSize: true }); await fs.writeFile(result.filePath, pdf); return { canceled: false, fileName: path.basename(result.filePath), filePath: result.filePath }; }); function createWindow(applicationMode) { const mainWindow = new BrowserWindow({ width: 1440, height: 920, minWidth: 1080, minHeight: 720, title: applicationMode.windowTitle, backgroundColor: "#f6f8fb", webPreferences: { preload: path.join(__dirname, "preload.cjs"), contextIsolation: true, nodeIntegration: false } }); mainWindow.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: "deny" }; }); if (isDev) { mainWindow.loadURL(new URL(applicationMode.route, "http://127.0.0.1:5173").toString()); } else { mainWindow.loadFile(path.join(__dirname, "../dist/index.html"), { hash: applicationMode.route }); } } const launchApplicationMode = applicationModeFromArgs(); app.setName(launchApplicationMode.title); app.whenReady().then(() => { createApplicationMenu(launchApplicationMode); createWindow(launchApplicationMode); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(launchApplicationMode); } }); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") { app.quit(); } });