lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
| 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); |