diff --git a/README.md b/README.md index 37f3763..4471a58 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,17 @@ npm run dev npm run electron:dev ``` +Отдельные desktop-запуски приложений: + +```bash +npm run electron:qword +npm run electron:qexcell +npm run electron:qpowerpoint +npm run electron:qoutlook +``` + +Electron также принимает аргумент `--qoffice-app=qword|qexcell|qpowerpoint|qoutlook`. + ## Проверка ```bash diff --git a/electron/main.cjs b/electron/main.cjs index c8a817e..047aba4 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -3,6 +3,49 @@ 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", @@ -66,21 +109,28 @@ function sendFocusedCommand(command) { } } -function createApplicationMenu() { +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: "QOffice", + label: applicationMode.title, submenu: [ - { role: "about", label: "О QOffice" }, + { role: "about", label: `О ${applicationMode.title}` }, { type: "separator" }, - { role: "hide", label: "Скрыть QOffice" }, + { role: "hide", label: `Скрыть ${applicationMode.title}` }, { role: "hideOthers", label: "Скрыть остальные" }, { role: "unhide", label: "Показать все" }, { type: "separator" }, - { role: "quit", label: "Выйти из QOffice" } + { role: "quit", label: `Выйти из ${applicationMode.title}` } ] } ] @@ -95,7 +145,7 @@ function createApplicationMenu() { { type: "separator" }, { label: "Печать...", accelerator: "CmdOrCtrl+P", click: () => sendFocusedCommand("print") }, { type: "separator" }, - isMac ? { role: "close", label: "Закрыть окно" } : { role: "quit", label: "Выход" } + isMac ? { role: "close", label: "Закрыть окно" } : { role: "quit", label: `Выход из ${applicationMode.title}` } ] }, { @@ -213,13 +263,13 @@ ipcMain.handle("qoffice:export-pdf", async (event, defaultFileName) => { }; }); -function createWindow() { +function createWindow(applicationMode) { const mainWindow = new BrowserWindow({ width: 1440, height: 920, minWidth: 1080, minHeight: 720, - title: "QOffice", + title: applicationMode.windowTitle, backgroundColor: "#f6f8fb", webPreferences: { preload: path.join(__dirname, "preload.cjs"), @@ -234,19 +284,22 @@ function createWindow() { }); if (isDev) { - mainWindow.loadURL("http://127.0.0.1:5173"); + mainWindow.loadURL(new URL(applicationMode.route, "http://127.0.0.1:5173").toString()); } else { - mainWindow.loadFile(path.join(__dirname, "../dist/index.html")); + mainWindow.loadFile(path.join(__dirname, "../dist/index.html"), { hash: applicationMode.route }); } } +const launchApplicationMode = applicationModeFromArgs(); +app.setName(launchApplicationMode.title); + app.whenReady().then(() => { - createApplicationMenu(); - createWindow(); + createApplicationMenu(launchApplicationMode); + createWindow(launchApplicationMode); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { - createWindow(); + createWindow(launchApplicationMode); } }); }); diff --git a/package.json b/package.json index c59d329..54a9d32 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,10 @@ "preview": "vite preview --host 127.0.0.1", "test": "vitest run", "electron:dev": "concurrently -k \"npm:dev\" \"wait-on http://127.0.0.1:5173 && electron .\"", + "electron:qword": "concurrently -k \"npm:dev\" \"wait-on http://127.0.0.1:5173 && electron . --qoffice-app=qword\"", + "electron:qexcell": "concurrently -k \"npm:dev\" \"wait-on http://127.0.0.1:5173 && electron . --qoffice-app=qexcell\"", + "electron:qpowerpoint": "concurrently -k \"npm:dev\" \"wait-on http://127.0.0.1:5173 && electron . --qoffice-app=qpowerpoint\"", + "electron:qoutlook": "concurrently -k \"npm:dev\" \"wait-on http://127.0.0.1:5173 && electron . --qoffice-app=qoutlook\"", "dist:desktop": "npm run build && electron-builder" }, "dependencies": { diff --git a/src/App.tsx b/src/App.tsx index db8afb9..b698635 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,21 +6,10 @@ import { QWord } from "./features/qword/QWord"; import { QExcell } from "./features/qexcell/QExcell"; import { initialState } from "./data/sampleData"; import { usePersistentState } from "./hooks/usePersistentState"; +import { activeAppFromLocation } from "./appRouting"; import type { MailMessage, QOfficeState, SlideDeck, WordDocument, Workbook } from "./types"; const STORAGE_KEY = "qoffice-state-v1"; -const appPaths = { - "/": "word", - "/qword": "word", - "/qexcell": "sheets", - "/qpowerpoint": "slides", - "/qoutlook": "mail" -} as const; - -function activeAppFromPath(pathname: string) { - const normalizedPath = pathname.toLowerCase().replace(/\/$/, "") || "/"; - return appPaths[normalizedPath as keyof typeof appPaths] ?? "word"; -} function migrateQOfficeState(state: QOfficeState): QOfficeState { const launchChecklistAttachments = initialState.mail.messages.find((message) => message.id === "mail-1")?.attachments; @@ -103,7 +92,7 @@ function migrateQOfficeState(state: QOfficeState): QOfficeState { export function App() { const [state, setState] = usePersistentState(STORAGE_KEY, initialState, migrateQOfficeState); - const activeApp = activeAppFromPath(window.location.pathname); + const activeApp = activeAppFromLocation(window.location); const status = useMemo(() => { const updatedAt = diff --git a/src/appRouting.test.ts b/src/appRouting.test.ts new file mode 100644 index 0000000..8c658e9 --- /dev/null +++ b/src/appRouting.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { activeAppFromLocation } from "./appRouting"; + +describe("activeAppFromLocation", () => { + it.each([ + ["/", "", "", "word"], + ["/qword", "", "", "word"], + ["/qexcell", "", "", "sheets"], + ["/qpowerpoint", "", "", "slides"], + ["/qoutlook", "", "", "mail"], + ["/index.html", "#/qexcell", "", "sheets"], + ["/index.html", "#qpowerpoint", "", "slides"], + ["/index.html", "", "?app=qoutlook", "mail"], + ["/index.html", "", "?module=pptx", "slides"], + ["/unknown", "", "", "word"] + ])("определяет приложение для pathname=%s hash=%s search=%s", (pathname, hash, search, expected) => { + expect(activeAppFromLocation({ pathname, hash, search })).toBe(expected); + }); +}); diff --git a/src/appRouting.ts b/src/appRouting.ts new file mode 100644 index 0000000..cf58d8c --- /dev/null +++ b/src/appRouting.ts @@ -0,0 +1,67 @@ +import type { AppId } from "./types"; + +const routeAppIds = { + "/": "word", + "/qword": "word", + "/qexcell": "sheets", + "/qpowerpoint": "slides", + "/qoutlook": "mail" +} as const satisfies Record; + +const appAliases = { + 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" +} as const satisfies Record; + +export interface AppLocationParts { + pathname: string; + hash?: string; + search?: string; +} + +export function activeAppFromLocation(location: AppLocationParts): AppId { + const queryApp = appFromSearch(location.search); + if (queryApp) { + return queryApp; + } + + const hashPath = normalizedPath(location.hash?.replace(/^#/, "") ?? ""); + if (hashPath && hashPath !== "/") { + return routeAppIds[hashPath as keyof typeof routeAppIds] ?? appFromAlias(hashPath); + } + + const path = normalizedPath(location.pathname); + return routeAppIds[path as keyof typeof routeAppIds] ?? appFromAlias(path) ?? "word"; +} + +function appFromSearch(search?: string) { + const params = new URLSearchParams(search ?? ""); + return appFromAlias(params.get("app") ?? params.get("module") ?? ""); +} + +function appFromAlias(value: string) { + const normalized = value.trim().toLowerCase().replace(/^\/+|\/+$/g, ""); + return appAliases[normalized as keyof typeof appAliases]; +} + +function normalizedPath(pathname: string) { + const normalized = pathname.trim().toLowerCase().replace(/\/+$/g, "") || "/"; + return normalized.startsWith("/") ? normalized : `/${normalized}`; +}