lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ electron preload.ts

4,7 KB Rohdatei
electron/preload.ts — 126 Zeilen
1 import { contextBridge, ipcRenderer } from 'electron';
2 import {
3 IPC,
4 type ErrorReport,
5 type KartenAnfrage,
6 type KartenAntwort,
7 type FileDialogFilter,
8 type MenuCommand,
9 type OpenFileResult,
10 type SaveFileRequest,
11 type SaveFileResult,
12 type Verweisziel,
13 } from '../shared/ipc';
14
15 /**
16 * Bruecke zwischen Renderer und Hauptprozess.
17 *
18 * Es werden ausschliesslich genau umrissene Vorgaenge freigegeben - kein
19 * allgemeiner Datei- oder Prozesszugriff. Der Altbestand reichte hier
20 * `saveFile(pfad, daten)` und `readFile(pfad)` mit frei waehlbarem Pfad an den
21 * Renderer durch. Zusammen mit den ungefilterten innerHTML-Einsetzungen im
22 * Renderer liess sich damit ueber eine praeparierte Projektdatei eine beliebige
23 * Datei auf die Platte schreiben.
24 *
25 * Jetzt gilt: Schreiben und Lesen sind nur fuer Pfade moeglich, die der Anwender
26 * zuvor selbst in einem Dialog bestaetigt hat. Die Pruefung liegt im
27 * Hauptprozess, nicht hier.
28 *
29 * Das Preload-Skript laeuft unter aktivierter Sandbox. Ein solches Skript kann
30 * ausschliesslich `electron` einbinden - ein `require` auf eine Nachbardatei
31 * schlaegt zur Laufzeit fehl ("module not found"), und zwar stumm: das Fenster
32 * oeffnet sich, aber `window.lsaPlaner` fehlt und saemtliche Dateibefehle tun
33 * nichts. Deshalb wird diese Datei fuer die Auslieferung mit esbuild zu einer
34 * einzelnen, in sich geschlossenen Datei gebuendelt (siehe build:preload).
35 *
36 * KEIN `appVersion` IN DIESER BRUECKE - und das ist Absicht. Bis einschliesslich
37 * 5.7.0 stand hier `process.env['LSA_APP_VERSION'] ?? '5.0.0'`. Gesetzt hat die
38 * Umgebungsvariable nie jemand: kein Startbefehl, kein Bauschritt, kein
39 * electron-builder-Eintrag. Die Bruecke meldete deshalb acht Fassungen lang
40 * "5.0.0" - gemerkt hat es niemand, weil den Wert auch niemand gelesen hat. Die
41 * Versionsangabe der Statusleiste kommt aus `__APP_VERSION__`, das vite.config.ts
42 * aus package.json einsetzt; dort steht als Ueberschrift "Programmversion - eine
43 * Quelle, nicht zwei", und genau die zweite Quelle war dieses Feld.
44 *
45 * Wird die Version im Renderer doch einmal aus dem Hauptprozess gebraucht, dann
46 * aus `app.getVersion()` und ausdruecklich uebergeben - nicht aus einer
47 * Umgebungsvariablen, die niemand fuellt, und nicht mit einer festen Zahl als
48 * Rueckfall, die stillschweigend falsch wird. tests/electron/brueckeVersion.test.ts
49 * haelt das fest.
50 */
51
52 const bridge = {
53 platform: 'electron' as const,
54
55 saveFile(request: SaveFileRequest): Promise<SaveFileResult> {
56 return ipcRenderer.invoke(IPC.saveFile, {
57 defaultName: String(request.defaultName),
58 filters: request.filters,
59 data: request.data,
60 }) as Promise<SaveFileResult>;
61 },
62
63 openFile(filters: readonly FileDialogFilter[]): Promise<OpenFileResult> {
64 return ipcRenderer.invoke(IPC.openFile, filters) as Promise<OpenFileResult>;
65 },
66
67 writeFile(filePath: string, data: string | Uint8Array): Promise<SaveFileResult> {
68 return ipcRenderer.invoke(IPC.writeFile, {
69 filePath: String(filePath),
70 data,
71 }) as Promise<SaveFileResult>;
72 },
73
74 readFile(filePath: string): Promise<OpenFileResult> {
75 return ipcRenderer.invoke(IPC.readFile, String(filePath)) as Promise<OpenFileResult>;
76 },
77
78 setDirty(dirty: boolean): void {
79 ipcRenderer.send(IPC.setDirty, dirty === true);
80 },
81
82 reportError(bericht: ErrorReport): void {
83 ipcRenderer.send(IPC.logError, bericht);
84 },
85
86 logPath(): Promise<string> {
87 return ipcRenderer.invoke(IPC.logPath) as Promise<string>;
88 },
89
90 fetchMapImage(anfrage: KartenAnfrage): Promise<KartenAntwort> {
91 return ipcRenderer.invoke(IPC.fetchMapImage, anfrage) as Promise<KartenAntwort>;
92 },
93
94 oeffneVerweis(ziel: Verweisziel): void {
95 ipcRenderer.send(IPC.oeffneVerweis, ziel);
96 },
97
98 onMenuCommand(handler: (command: MenuCommand) => void): () => void {
99 const listener = (_event: unknown, command: MenuCommand): void => {
100 handler(command);
101 };
102 ipcRenderer.on(IPC.menuCommand, listener);
103 // Der Renderer muss den Empfaenger wieder abmelden koennen; sonst
104 // sammeln sich bei jedem Neuaufbau weitere an.
105 return () => {
106 ipcRenderer.removeListener(IPC.menuCommand, listener);
107 };
108 },
109
110 onBeforeClose(handler: () => Promise<boolean>): void {
111 // Nur ein Empfaenger, auch wenn die Oberflaeche neu aufgebaut wird.
112 ipcRenderer.removeAllListeners(IPC.requestClose);
113 ipcRenderer.on(IPC.requestClose, () => {
114 void handler()
115 .then((allow) => {
116 ipcRenderer.send(IPC.closeResponse, allow === true);
117 })
118 .catch(() => {
119 // Im Zweifel nicht schliessen - lieber nachfragen als Daten verlieren.
120 ipcRenderer.send(IPC.closeResponse, false);
121 });
122 });
123 },
124 };
125
126 contextBridge.exposeInMainWorld('lsaPlaner', bridge);