Initial QOffice implementation

This commit is contained in:
Курнат Андрей
2026-06-01 05:31:05 +03:00
commit 9bf4dd3672
44 changed files with 16854 additions and 0 deletions
+258
View File
@@ -0,0 +1,258 @@
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 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 createApplicationMenu() {
const isMac = process.platform === "darwin";
const template = [
...(isMac
? [
{
label: "QOffice",
submenu: [
{ role: "about", label: "О QOffice" },
{ type: "separator" },
{ role: "hide", label: "Скрыть QOffice" },
{ role: "hideOthers", label: "Скрыть остальные" },
{ role: "unhide", label: "Показать все" },
{ type: "separator" },
{ role: "quit", label: "Выйти из QOffice" }
]
}
]
: []),
{
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: "Выход" }
]
},
{
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() {
const mainWindow = new BrowserWindow({
width: 1440,
height: 920,
minWidth: 1080,
minHeight: 720,
title: "QOffice",
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("http://127.0.0.1:5173");
} else {
mainWindow.loadFile(path.join(__dirname, "../dist/index.html"));
}
}
app.whenReady().then(() => {
createApplicationMenu();
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});