Add standalone desktop launch modes
This commit is contained in:
@@ -24,6 +24,17 @@ npm run dev
|
|||||||
npm run electron: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
|
```bash
|
||||||
|
|||||||
+66
-13
@@ -3,6 +3,49 @@ const fs = require("node:fs/promises");
|
|||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
|
|
||||||
const isDev = !app.isPackaged;
|
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 = {
|
const fileKinds = {
|
||||||
docx: {
|
docx: {
|
||||||
title: "Открыть документ Word",
|
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 isMac = process.platform === "darwin";
|
||||||
const template = [
|
const template = [
|
||||||
...(isMac
|
...(isMac
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: "QOffice",
|
label: applicationMode.title,
|
||||||
submenu: [
|
submenu: [
|
||||||
{ role: "about", label: "О QOffice" },
|
{ role: "about", label: `О ${applicationMode.title}` },
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{ role: "hide", label: "Скрыть QOffice" },
|
{ role: "hide", label: `Скрыть ${applicationMode.title}` },
|
||||||
{ role: "hideOthers", label: "Скрыть остальные" },
|
{ role: "hideOthers", label: "Скрыть остальные" },
|
||||||
{ role: "unhide", label: "Показать все" },
|
{ role: "unhide", label: "Показать все" },
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{ role: "quit", label: "Выйти из QOffice" }
|
{ role: "quit", label: `Выйти из ${applicationMode.title}` }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -95,7 +145,7 @@ function createApplicationMenu() {
|
|||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{ label: "Печать...", accelerator: "CmdOrCtrl+P", click: () => sendFocusedCommand("print") },
|
{ label: "Печать...", accelerator: "CmdOrCtrl+P", click: () => sendFocusedCommand("print") },
|
||||||
{ type: "separator" },
|
{ 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({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 1440,
|
width: 1440,
|
||||||
height: 920,
|
height: 920,
|
||||||
minWidth: 1080,
|
minWidth: 1080,
|
||||||
minHeight: 720,
|
minHeight: 720,
|
||||||
title: "QOffice",
|
title: applicationMode.windowTitle,
|
||||||
backgroundColor: "#f6f8fb",
|
backgroundColor: "#f6f8fb",
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, "preload.cjs"),
|
preload: path.join(__dirname, "preload.cjs"),
|
||||||
@@ -234,19 +284,22 @@ function createWindow() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
mainWindow.loadURL("http://127.0.0.1:5173");
|
mainWindow.loadURL(new URL(applicationMode.route, "http://127.0.0.1:5173").toString());
|
||||||
} else {
|
} 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(() => {
|
app.whenReady().then(() => {
|
||||||
createApplicationMenu();
|
createApplicationMenu(launchApplicationMode);
|
||||||
createWindow();
|
createWindow(launchApplicationMode);
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow(launchApplicationMode);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
"preview": "vite preview --host 127.0.0.1",
|
"preview": "vite preview --host 127.0.0.1",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"electron:dev": "concurrently -k \"npm:dev\" \"wait-on http://127.0.0.1:5173 && electron .\"",
|
"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"
|
"dist:desktop": "npm run build && electron-builder"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+2
-13
@@ -6,21 +6,10 @@ import { QWord } from "./features/qword/QWord";
|
|||||||
import { QExcell } from "./features/qexcell/QExcell";
|
import { QExcell } from "./features/qexcell/QExcell";
|
||||||
import { initialState } from "./data/sampleData";
|
import { initialState } from "./data/sampleData";
|
||||||
import { usePersistentState } from "./hooks/usePersistentState";
|
import { usePersistentState } from "./hooks/usePersistentState";
|
||||||
|
import { activeAppFromLocation } from "./appRouting";
|
||||||
import type { MailMessage, QOfficeState, SlideDeck, WordDocument, Workbook } from "./types";
|
import type { MailMessage, QOfficeState, SlideDeck, WordDocument, Workbook } from "./types";
|
||||||
|
|
||||||
const STORAGE_KEY = "qoffice-state-v1";
|
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 {
|
function migrateQOfficeState(state: QOfficeState): QOfficeState {
|
||||||
const launchChecklistAttachments = initialState.mail.messages.find((message) => message.id === "mail-1")?.attachments;
|
const launchChecklistAttachments = initialState.mail.messages.find((message) => message.id === "mail-1")?.attachments;
|
||||||
@@ -103,7 +92,7 @@ function migrateQOfficeState(state: QOfficeState): QOfficeState {
|
|||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const [state, setState] = usePersistentState<QOfficeState>(STORAGE_KEY, initialState, migrateQOfficeState);
|
const [state, setState] = usePersistentState<QOfficeState>(STORAGE_KEY, initialState, migrateQOfficeState);
|
||||||
const activeApp = activeAppFromPath(window.location.pathname);
|
const activeApp = activeAppFromLocation(window.location);
|
||||||
|
|
||||||
const status = useMemo(() => {
|
const status = useMemo(() => {
|
||||||
const updatedAt =
|
const updatedAt =
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import type { AppId } from "./types";
|
||||||
|
|
||||||
|
const routeAppIds = {
|
||||||
|
"/": "word",
|
||||||
|
"/qword": "word",
|
||||||
|
"/qexcell": "sheets",
|
||||||
|
"/qpowerpoint": "slides",
|
||||||
|
"/qoutlook": "mail"
|
||||||
|
} as const satisfies Record<string, AppId>;
|
||||||
|
|
||||||
|
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<string, AppId>;
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user