waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
| 1 | /** |
| 2 | * Typisierter IPC-Vertrag zwischen Main- und Renderer-Prozess. |
| 3 | * |
| 4 | * Der Renderer erhält NIEMALS direkten Zugriff auf `ipcRenderer`. Stattdessen |
| 5 | * legt das Preload-Skript über `contextBridge` genau die hier beschriebene |
| 6 | * API frei. Jeder Kanal ist über {@link IpcVertrag} typisiert – Anfrage- und |
| 7 | * Antworttyp werden auf beiden Seiten aus derselben Quelle abgeleitet. |
| 8 | */ |
| 9 | |
| 10 | import { ANZEIGEGROESSE_STANDARD } from './ansicht'; |
| 11 | import type { HartnaeckigeFrage } from './hartnaeckig'; |
| 12 | import { SPRECHTEMPO_STANDARD } from './sprechtempo'; |
| 13 | import { TEXTABSTAND_STANDARD } from './textabstand'; |
| 14 | import type { Erklaerungstiefe } from './druck/fehlerprotokoll'; |
| 15 | import type { Schriftgroesse } from './druck/stil'; |
| 16 | import type { Erklaerungen } from './erklaerungen'; |
| 17 | import type { Glossar } from './glossar'; |
| 18 | import type { Normtexte } from './normtexte'; |
| 19 | import type { Themen } from './themen'; |
| 20 | import type { Katalog } from './katalog'; |
| 21 | import type { Lernplan } from './lernplan'; |
| 22 | import type { |
| 23 | Antwortprotokoll, |
| 24 | FrageStand, |
| 25 | Lernuebersicht, |
| 26 | Profil, |
| 27 | SitzungsFilter, |
| 28 | SitzungsFrage, |
| 29 | } from './lernstand'; |
| 30 | import type { Datenschutzangaben } from './datenschutz'; |
| 31 | import type { Lizenzangaben } from './lizenzen'; |
| 32 | import type { |
| 33 | Einspielergebnis, |
| 34 | Pruefergebnis, |
| 35 | Sicherungsergebnis, |
| 36 | Uebernahmeergebnis, |
| 37 | } from './sicherung'; |
| 38 | import type { |
| 39 | OffenerLauf, |
| 40 | Pruefungsantwort, |
| 41 | Pruefungsauftrag, |
| 42 | Pruefungsbogen, |
| 43 | Pruefungsergebnis, |
| 44 | Pruefungsverlauf, |
| 45 | Zwischenstand, |
| 46 | } from './pruefung'; |
| 47 | import type { ThemeAuswahl } from './theme'; |
| 48 | |
| 49 | /** |
| 50 | * Ergebnis eines Exports. |
| 51 | * |
| 52 | * Ein Abbruch im Speicherdialog ist kein Fehler, sondern eine Entscheidung |
| 53 | * des Nutzers – deshalb `gespeichert: false` statt einer Ausnahme. Nur was |
| 54 | * wirklich schiefgeht (kein Schreibrecht, voller Datenträger), wirft. |
| 55 | */ |
| 56 | export interface Druckergebnis { |
| 57 | readonly gespeichert: boolean; |
| 58 | /** Wohin geschrieben wurde, oder `null` bei Abbruch. */ |
| 59 | readonly pfad: string | null; |
| 60 | /** Größe der geschriebenen Datei in Byte; 0 bei Abbruch. */ |
| 61 | readonly bytes: number; |
| 62 | /** |
| 63 | * Kennung, unter der sich die Datei öffnen oder im Ordner zeigen lässt. |
| 64 | * |
| 65 | * **Eine Kennung und kein Pfad**: Wer einen Pfad mitbringen darf, darf auch |
| 66 | * einen anderen mitbringen. Der Hauptprozess merkt sich, was er selbst |
| 67 | * geschrieben hat, und öffnet ausschließlich das (`main/dateizugriff.ts`) – |
| 68 | * dieselbe Überlegung wie beim Einspielweg der Sicherung. |
| 69 | * |
| 70 | * Fehlt bei Abbruch und in älteren Fassungen des Anwendungskerns; dann |
| 71 | * entfallen die beiden Schaltflächen. |
| 72 | */ |
| 73 | readonly dateiKennung?: string; |
| 74 | } |
| 75 | |
| 76 | /** Ergebnis des better-sqlite3-Rauchtests im Main-Prozess. */ |
| 77 | export interface DatenbankStatus { |
| 78 | /** `true`, wenn das native Modul geladen und eine Abfrage ausgeführt wurde. */ |
| 79 | readonly verfuegbar: boolean; |
| 80 | /** Von SQLite gemeldete Version, z. B. "3.50.2". */ |
| 81 | readonly sqliteVersion: string | null; |
| 82 | /** Für Menschen lesbare Statusmeldung (deutsch). */ |
| 83 | readonly meldung: string; |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * Befund des Katalogstand-Abgleichs beim Öffnen des Lernstands. |
| 88 | * |
| 89 | * Die Lernstand-Datenbank vermerkt seit Schemafassung 9, gegen welchen |
| 90 | * amtlichen Katalogstand sie geführt wird. Weicht der geladene Katalog davon |
| 91 | * ab – etwa nach einer neuen BVA-Fassung mit geänderter Nummerierung –, |
| 92 | * entsteht dieser Befund, und zwar genau in der Sitzung, die den Wechsel |
| 93 | * zuerst sieht: Danach ist der neue Stand vermerkt, und der nächste Start |
| 94 | * findet Gleichstand vor. Gelöscht wird dabei nichts; die beiden Zählungen |
| 95 | * beziffern nur, was im geladenen Katalog kein Ziel mehr hat. |
| 96 | */ |
| 97 | export interface Katalogwechsel { |
| 98 | /** Katalogstand (ISO-Datum), unter dem der Lernstand bisher geführt wurde. */ |
| 99 | readonly vorher: string; |
| 100 | /** Stand des jetzt geladenen Katalogs. */ |
| 101 | readonly nachher: string; |
| 102 | /** `frage_stand`-Zeilen zu Frage-IDs, die es im geladenen Katalog nicht gibt. */ |
| 103 | readonly verwaisteStaende: number; |
| 104 | /** `antwort_log`-Zeilen zu Frage-IDs, die es im geladenen Katalog nicht gibt. */ |
| 105 | readonly verwaisteAntworten: number; |
| 106 | } |
| 107 | |
| 108 | /** Laufzeit-Informationen für die Info-Anzeige im Startbildschirm. */ |
| 109 | export interface AnwendungsInfo { |
| 110 | readonly anwendungsVersion: string; |
| 111 | /** |
| 112 | * Kurzes Commit-Kürzel dieses Baus, `+` bei ungesicherten Änderungen. |
| 113 | * |
| 114 | * Leer, wenn beim Bauen kein Git zur Verfügung stand. Die Versionsnummer |
| 115 | * allein benennt einen Stand nicht eindeutig – zwischen zwei |
| 116 | * Veröffentlichungen entstehen viele Bauten mit derselben Nummer. |
| 117 | */ |
| 118 | readonly baukennung: string; |
| 119 | /** Commit-Datum als ISO-Datum, oder leer. */ |
| 120 | readonly baustand: string; |
| 121 | readonly electronVersion: string; |
| 122 | readonly chromeVersion: string; |
| 123 | readonly nodeVersion: string; |
| 124 | readonly plattform: NodeJS.Platform; |
| 125 | readonly datenbank: DatenbankStatus; |
| 126 | /** |
| 127 | * Befund eines Katalogwechsels, oder `null`, wenn nichts zu melden ist – |
| 128 | * auch dann, wenn der Lernstand nicht zu öffnen war: Dieser Fehler hat |
| 129 | * seine eigene Meldung an anderer Stelle. Optional geführt wie die übrigen |
| 130 | * jüngeren Erweiterungen, damit die Attrappen der Oberflächentests mit dem |
| 131 | * bisherigen Zuschnitt gültig bleiben. |
| 132 | */ |
| 133 | readonly katalogwechsel?: Katalogwechsel | null; |
| 134 | /** |
| 135 | * Ablageort des Fehlerprotokolls, oder `null`. |
| 136 | * |
| 137 | * Steht im Systemzustand, damit die Datei auffindbar ist, wenn jemand einen |
| 138 | * Absturz melden will. Sie geht nie von selbst irgendwohin – der Satz |
| 139 | * daneben sagt das ausdrücklich. |
| 140 | * |
| 141 | * Optional geführt wie die übrigen jüngeren Erweiterungen, damit die |
| 142 | * Attrappen der Oberflächentests mit dem bisherigen Zuschnitt gültig |
| 143 | * bleiben. |
| 144 | */ |
| 145 | readonly protokollPfad?: string | null; |
| 146 | } |
| 147 | |
| 148 | /** Dauerhaft gespeicherte Nutzereinstellungen. */ |
| 149 | export interface Einstellungen { |
| 150 | readonly thema: ThemeAuswahl; |
| 151 | /** Zuletzt benutztes Lernprofil. */ |
| 152 | readonly profilId?: number; |
| 153 | /** |
| 154 | * Wie viele Fragen eine Lernsitzung umfasst. |
| 155 | * |
| 156 | * Fehlt der Wert, gilt {@link SITZUNGSUMFANG}. Bis 0.26.6 war die Zahl |
| 157 | * eine Konstante im Quelltext: Wer täglich nur zehn Minuten hat, bekam |
| 158 | * dieselben zwanzig Fragen vorgelegt wie jemand mit einer Stunde – und |
| 159 | * ließ die Sitzung entweder halb liegen oder lernte sie unter Zeitdruck |
| 160 | * zu Ende. Der Anwendungskern nimmt seit jeher jede Zahl von 1 bis 1000 |
| 161 | * entgegen; es fehlte allein der Weg dorthin. |
| 162 | */ |
| 163 | readonly sitzungsumfang?: number; |
| 164 | /** |
| 165 | * Antwortoptionen in zufälliger Reihenfolge zeigen. |
| 166 | * |
| 167 | * Vorgabe ist **aus**. Ein Lernvorteil des Mischens ist nicht belegt – es |
| 168 | * existiert keine kontrollierte Studie, die eine gemischte gegen eine feste |
| 169 | * Übungsreihenfolge stellt und danach das Behalten misst. Belegt ist nur, |
| 170 | * dass Umsortieren die Itemschwierigkeit kaum verändert; damit trägt auch |
| 171 | * die Gegenbegründung „macht schwerer, also lernwirksamer" nicht. Dem |
| 172 | * unbelegten Nutzen steht ein messbarer Aufwand gegenüber: 83 % der |
| 173 | * Auswahlfragen erscheinen dann in anderer Reihenfolge als im amtlichen |
| 174 | * Katalog, in dem der Lernende nachschlägt. Die Begründung im Einzelnen |
| 175 | * steht in `docs/entscheidung-antwortreihenfolge.md`. |
| 176 | * |
| 177 | * Wer die Funktion will, schaltet sie unter „Schwierigkeit" ein. |
| 178 | */ |
| 179 | /** |
| 180 | * Die Frage nach dem abwählbaren Kapitel wurde beim Erststart gestellt. |
| 181 | * |
| 182 | * Eine Einstellung und keine Spalte am Profil: Es ist eine Frage der |
| 183 | * Bedienung, kein Lernstand. Sie steht deshalb auch nicht in der |
| 184 | * Sicherung – wer auf einem neuen Rechner anfängt, bekommt sie noch |
| 185 | * einmal, und das ist richtig so: Dort ist es wieder ein erster Start. |
| 186 | */ |
| 187 | readonly zuschnittGefragt?: boolean; |
| 188 | readonly optionenMischen?: boolean; |
| 189 | /** |
| 190 | * Verschweigen, wie viele Antworten richtig sind. |
| 191 | * |
| 192 | * Vorgabe ist **aus**, also wie bisher: Die Lernsitzung verrät über Radios |
| 193 | * gegenüber Kästchen und über den Hinweistext, ob eine oder mehrere |
| 194 | * Antworten richtig sind. Eingeschaltet entfällt dieser Hinweis – so, wie |
| 195 | * die Prüfungssimulation es ohnehin hält. |
| 196 | * |
| 197 | * Das ist der belegbarere der beiden Schwierigkeitsregler: Er entfernt |
| 198 | * einen Hinweis, den die Prüfung nicht gibt, statt einen hinzuzufügen, |
| 199 | * den sie auch nicht gibt. |
| 200 | */ |
| 201 | readonly antwortzahlVerbergen?: boolean; |
| 202 | /** |
| 203 | * Bietet in Lernsitzung und Prüfungslauf eine Schaltfläche zum Vorlesen an |
| 204 | * (nutzt Systemstimmen). |
| 205 | * |
| 206 | * Gesprochen wird **nur auf Knopfdruck**. Bis 0.11.1 begann die Ansage der |
| 207 | * Rückmeldung von selbst – was weder der Schalter noch das Handbuch zusagte. |
| 208 | */ |
| 209 | readonly vorlesen?: boolean; |
| 210 | /** |
| 211 | * In der Lernsitzung von selbst vorlesen, ohne Knopfdruck. |
| 212 | * |
| 213 | * Ab Werk **aus**, und nur wirksam, wenn {@link vorlesen} an ist. Bis 0.11.1 |
| 214 | * gab es diese Wahl nicht: Die Rückmeldung wurde immer von selbst |
| 215 | * angesagt, sobald die Sprachausgabe eingeschaltet war. Gemeldet von einem |
| 216 | * Nutzer, und die eigenen Texte gaben ihm recht – Schalter wie Handbuch |
| 217 | * sagten nur eine Schaltfläche zu. |
| 218 | * |
| 219 | * Wer es einschaltet, bekommt beides angesagt: die neue Frage, sobald sie |
| 220 | * erscheint, und die Rückmeldung nach dem Bestätigen. Gedacht für Menschen |
| 221 | * mit Lese-Rechtschreib-Störung oder ermüdeten Augen, denen ein Knopfdruck |
| 222 | * je Frage im Weg steht. |
| 223 | * |
| 224 | * **Nicht** im Prüfungslauf: Dort bildet die Anwendung eine schriftliche |
| 225 | * Prüfung nach, und eine Stimme, die von selbst losredet, gehört nicht dazu. |
| 226 | * Die Schaltfläche gibt es dort weiterhin. |
| 227 | */ |
| 228 | readonly vorlesenAutomatisch?: boolean; |
| 229 | /** |
| 230 | * Bei falscher Antwort die ausführliche Begründung vorlesen. |
| 231 | * |
| 232 | * Ab Werk **aus**, unabhängig von {@link vorlesenAutomatisch} und nur |
| 233 | * wirksam, wenn {@link vorlesen} an ist. |
| 234 | * |
| 235 | * Der ausführliche Text bleibt sonst überall außen vor – auch die |
| 236 | * Schaltfläche liest ihn nicht: Er dauert gesprochen über eine Minute, und |
| 237 | * die nächste Frage wartet. Genau deshalb ist er eine eigene Wahl und nicht |
| 238 | * Teil der Ansage: Wer eine Frage falsch hatte, hat die Minute gerade übrig; |
| 239 | * bei jeder richtigen Antwort wäre sie eine Zumutung. |
| 240 | * |
| 241 | * Unabhängig, weil die nützlichste Kombination die ist, die man sonst nicht |
| 242 | * einstellen könnte: sonst selbst lesen, aber bei einem Fehler die |
| 243 | * Begründung hören. |
| 244 | */ |
| 245 | readonly vorlesenErklaerungBeiFehler?: boolean; |
| 246 | /** |
| 247 | * Sprechgeschwindigkeit als `rate` der Sprachausgabe; 1 ist die |
| 248 | * Systemvorgabe. |
| 249 | * |
| 250 | * Bis 0.22.0 wurde sie nie gesetzt, das Tempo war also nicht einstellbar. |
| 251 | * Für Menschen mit Lese-Rechtschreib-Störung – die Zielgruppe, die der |
| 252 | * Schalter „Vorlesen“ selbst benennt – ist ein festes Tempo bei |
| 253 | * minutenlangen Rechtstexten eine echte Hürde. Stufen in |
| 254 | * `renderer/src/lernen/sprachausgabe.ts`. |
| 255 | */ |
| 256 | readonly sprechtempo?: number; |
| 257 | /** |
| 258 | * Anzeigegröße der ganzen Anwendung in Prozent. |
| 259 | * |
| 260 | * Gültige Stufen stehen in `shared/ansicht.ts`. Gespeichert wird die |
| 261 | * Prozentzahl, nicht Chromiums Zoomfaktor – sie ist die Zahl, die auch |
| 262 | * angezeigt und angesagt wird. |
| 263 | */ |
| 264 | readonly anzeigegroesse?: number; |
| 265 | /** |
| 266 | * Zeilen-, Wort- und Zeichenabstand in Stufen (WCAG 1.4.12). |
| 267 | * |
| 268 | * Stufen und Werte stehen in `shared/textabstand.ts`. Getrennt von der |
| 269 | * Anzeigegröße, weil es etwas anderes ist: Die Größe skaliert alles |
| 270 | * gemeinsam, der Abstand gibt dem Text bei gleicher Größe mehr Luft. Wer |
| 271 | * Legasthenie hat, braucht oft das Zweite und nicht das Erste. |
| 272 | */ |
| 273 | readonly textabstand?: string; |
| 274 | /** |
| 275 | * Zeichentasten-Kürzel in der Lernsitzung (WCAG 2.1.4). |
| 276 | * |
| 277 | * **Dreiwertig, und das mit Absicht.** Fehlt das Feld, hat niemand |
| 278 | * entschieden – dann richtet sich die Voreinstellung danach, ob ein |
| 279 | * Hilfsmittel läuft: Im Lesemodus von NVDA und JAWS sind die Ziffern für |
| 280 | * die Navigation nach Überschriftenebenen belegt. `true` und `false` sind |
| 281 | * ausdrückliche Entscheidungen und haben immer Vorrang, auch wenn später |
| 282 | * ein Screenreader startet. |
| 283 | * |
| 284 | * Deshalb steht das Feld **nicht** in {@link EINSTELLUNGEN_STANDARD} und |
| 285 | * wird von `einstellungenBereinigen` nicht aufgefüllt: Ein Rückfallwert |
| 286 | * wäre hier eine erfundene Entscheidung und löschte den Unterschied |
| 287 | * zwischen „aus, weil ein Hilfsmittel läuft“ und „aus, weil ich das so |
| 288 | * will“ – genau den Unterschied, den der Hinweistext am Schalter erklärt. |
| 289 | * |
| 290 | * Bis 0.21.0 lag diese Wahl allein im `localStorage` des Renderers und |
| 291 | * überlebte damit weder einen Gerätewechsel noch eine Sicherung. |
| 292 | */ |
| 293 | readonly tastenkuerzel?: boolean; |
| 294 | /** |
| 295 | * Zeitpunkt der letzten selbst angelegten Sicherung, als ISO-Zeit. |
| 296 | * |
| 297 | * Fehlt das Feld, ist noch nie eine angelegt worden – und genau das steht |
| 298 | * dann auch auf dem Bildschirm. Ein Rückfall auf „heute“ wäre die |
| 299 | * gefährlichste Lüge, die hier möglich ist. |
| 300 | * |
| 301 | * **Eine Einstellung und kein Lernstand**, obwohl es um den Lernstand |
| 302 | * geht: Der Vermerk beschreibt, was auf *diesem* Rechner geschehen ist. |
| 303 | * Wanderte er in der Sicherung mit, behauptete er auf dem neuen Rechner |
| 304 | * eine Sicherung, die dort niemand angelegt hat. |
| 305 | */ |
| 306 | readonly letzteSicherung?: string; |
| 307 | /** |
| 308 | * Zuletzt benutzte Fenstergröße und -lage. |
| 309 | * |
| 310 | * Bis 0.22.0 startete das Fenster immer mit 1180 × 820 an der |
| 311 | * Systemposition. Wer maximiert arbeitet oder es – wie der Kommentar in |
| 312 | * `main/fenster.ts` selbst als Anwendungsfall nennt – schmal neben eine |
| 313 | * Bildschirmlupe stellt, richtete das bei jedem Start neu ein. Das trifft |
| 314 | * genau die Zielgruppe des Barrierefreiheitsanspruchs. |
| 315 | * |
| 316 | * Die Lage wird beim Start gegen die vorhandenen Bildschirme geprüft: Ein |
| 317 | * Fenster, das auf einem abgesteckten zweiten Monitor lag, wäre sonst |
| 318 | * unsichtbar und praktisch unerreichbar. |
| 319 | */ |
| 320 | readonly fenster?: { |
| 321 | readonly breite: number; |
| 322 | readonly hoehe: number; |
| 323 | readonly x?: number; |
| 324 | readonly y?: number; |
| 325 | readonly maximiert?: boolean; |
| 326 | }; |
| 327 | } |
| 328 | |
| 329 | export const EINSTELLUNGEN_STANDARD: Einstellungen = Object.freeze({ |
| 330 | thema: 'system', |
| 331 | optionenMischen: false, |
| 332 | antwortzahlVerbergen: false, |
| 333 | vorlesen: false, |
| 334 | zuschnittGefragt: false, |
| 335 | vorlesenAutomatisch: false, |
| 336 | vorlesenErklaerungBeiFehler: false, |
| 337 | anzeigegroesse: ANZEIGEGROESSE_STANDARD, |
| 338 | textabstand: TEXTABSTAND_STANDARD, |
| 339 | sprechtempo: SPRECHTEMPO_STANDARD, |
| 340 | }); |
| 341 | |
| 342 | /** |
| 343 | * Die einzige Stelle, an der IPC-Kanäle definiert werden. |
| 344 | * `anfrage` = Nutzlast vom Renderer, `antwort` = Rückgabe des Main-Prozesses. |
| 345 | * |
| 346 | * Kanäle ohne Nutzlast verwenden `undefined` (nicht `void`): der Wert wird |
| 347 | * tatsächlich über die Bridge gereicht, ist also ein Wert und kein |
| 348 | * Rückgabetyp. |
| 349 | */ |
| 350 | export interface IpcVertrag { |
| 351 | 'anwendung:info': { anfrage: undefined; antwort: AnwendungsInfo }; |
| 352 | 'einstellungen:lesen': { anfrage: undefined; antwort: Einstellungen }; |
| 353 | /** |
| 354 | * Ändert einzelne Einstellungen. Die Nutzlast ist eine Teilmenge – sie |
| 355 | * wird über den gespeicherten Stand gelegt, nicht an seine Stelle |
| 356 | * gesetzt. Antwort ist der vollständige neue Stand. |
| 357 | */ |
| 358 | 'einstellungen:schreiben': { anfrage: Partial<Einstellungen>; antwort: Einstellungen }; |
| 359 | |
| 360 | /** Liefert den vollständigen Fragenkatalog (einmalig beim Start). */ |
| 361 | 'katalog:laden': { anfrage: undefined; antwort: Katalog }; |
| 362 | /** Bild eines Prüfzeichens als Data-URL – die CSP verbietet Dateizugriffe. */ |
| 363 | 'katalog:bild': { anfrage: { bildId: string }; antwort: string }; |
| 364 | /** |
| 365 | * Erklärungen zu den Fragen – eigener redaktioneller Inhalt. |
| 366 | * |
| 367 | * Wird wie der Katalog einmalig beim Start geladen. Der Bestand ist |
| 368 | * kleiner als der Katalog und wächst mit ihm; ein Nachladen je Frage |
| 369 | * wäre viel Verkehr für wenig Nutzen. |
| 370 | */ |
| 371 | 'erklaerungen:laden': { anfrage: undefined; antwort: Erklaerungen }; |
| 372 | /** |
| 373 | * Glossar der Fachbegriffe – erfüllt WCAG 3.1.3 und 3.1.4. |
| 374 | * |
| 375 | * Wie die Erklärungen einmalig geladen: Die Einträge ändern sich zur |
| 376 | * Laufzeit nicht, und die Begriffssuche braucht ohnehin alle auf einmal. |
| 377 | */ |
| 378 | 'glossar:laden': { anfrage: undefined; antwort: Glossar }; |
| 379 | |
| 380 | /** |
| 381 | * Die mitgelieferten Normtexte. |
| 382 | * |
| 383 | * Wie Erklärungen und Glossar einmalig geladen. Der Umfang ist bekannt und |
| 384 | * fest: nur die Normen, die irgendwo zitiert werden – rund 500 KiB. Ein |
| 385 | * Kanal je Fundstelle wäre bei 2123 Zitaten mehr Verkehr als die ganze |
| 386 | * Datei und brächte dem Lesenden nichts, weil er ohnehin blättert. |
| 387 | */ |
| 388 | 'gesetz:normtexte': { anfrage: undefined; antwort: Normtexte }; |
| 389 | |
| 390 | /** |
| 391 | * Die redaktionelle Feingliederung der Kapitel II bis IV. |
| 392 | * |
| 393 | * Wie Erklärungen und Glossar einmalig geladen: 29 Gruppen über 230 Fragen, |
| 394 | * und sie ändern sich zur Laufzeit nicht. |
| 395 | */ |
| 396 | 'themen:laden': { anfrage: undefined; antwort: Themen }; |
| 397 | |
| 398 | 'profil:liste': { anfrage: undefined; antwort: Profil[] }; |
| 399 | 'profil:anlegen': { anfrage: { name: string }; antwort: Profil }; |
| 400 | 'profil:aktualisieren': { |
| 401 | anfrage: { |
| 402 | id: number; |
| 403 | name?: string; |
| 404 | pruefungstermin?: string | null; |
| 405 | /** Kapitel, die dieses Profil dauerhaft nicht lernt – Kennungen wie „IV“. */ |
| 406 | kapitelAusschluss?: readonly string[]; |
| 407 | }; |
| 408 | antwort: Profil; |
| 409 | }; |
| 410 | /** |
| 411 | * Löscht ein Profil samt Lernstand, Antworten und Prüfungsverlauf. |
| 412 | * |
| 413 | * Antwort ist die verbleibende Liste – die Oberfläche muss danach ohnehin |
| 414 | * ein anderes Profil wählen und braucht die Auswahl sofort. |
| 415 | */ |
| 416 | 'profil:loeschen': { anfrage: { id: number }; antwort: Profil[] }; |
| 417 | |
| 418 | /** Stellt die Fragen einer Lernsitzung nach Filter zusammen. */ |
| 419 | 'lernen:sitzung': { |
| 420 | anfrage: { profilId: number; filter: SitzungsFilter }; |
| 421 | antwort: SitzungsFrage[]; |
| 422 | }; |
| 423 | /** Protokolliert eine Antwort und liefert den neuen Stand der Frage. */ |
| 424 | 'lernen:antworten': { |
| 425 | anfrage: { profilId: number; protokoll: Antwortprotokoll }; |
| 426 | antwort: FrageStand; |
| 427 | }; |
| 428 | /** Merkliste umschalten. */ |
| 429 | 'lernen:merken': { |
| 430 | anfrage: { profilId: number; frageId: string; gemerkt: boolean }; |
| 431 | antwort: FrageStand; |
| 432 | }; |
| 433 | /** |
| 434 | * Der gespeicherte Stand einer einzelnen Frage; `null`, wenn sie noch nie |
| 435 | * beantwortet wurde. |
| 436 | * |
| 437 | * **Wozu.** `FrageStand` transportierte `versuche`, `richtige`, |
| 438 | * `zuletztBeantwortet` und `faelligAb` schon immer über die Brücke – die |
| 439 | * Oberfläche benutzte davon bis 0.22.0 ausschließlich `gemerkt`. Der |
| 440 | * Lernende konnte nirgends beantworten, wie oft er diese Frage schon hatte |
| 441 | * und wie oft davon richtig; „Warum kommt die schon wieder?“ blieb ohne |
| 442 | * Antwort, obwohl die Antwort in der Datenbank stand. |
| 443 | * |
| 444 | * Ein eigener Kanal und nicht die Rückgabe von `lernen:antworten`: Der |
| 445 | * Steckbrief soll die Historie **vor** der heutigen Antwort zeigen, und bei |
| 446 | * offenen Fragen wird die Buchung ohnehin bis zum Weiterblättern |
| 447 | * zurückgehalten (`useSitzung`, `bewertungAendern`). |
| 448 | */ |
| 449 | 'lernen:fragestand': { |
| 450 | anfrage: { profilId: number; frageId: string }; |
| 451 | antwort: FrageStand; |
| 452 | }; |
| 453 | /** |
| 454 | * Die hartnäckigen Fragen dieses Profils – wiederholt danebengegangen. |
| 455 | * |
| 456 | * Rein deskriptiv: gezählte Fehlschläge aus dem Protokoll, keine |
| 457 | * Kennzahl. Einzelheiten in `shared/hartnaeckig.ts`. |
| 458 | */ |
| 459 | 'lernen:hartnaeckige': { |
| 460 | anfrage: { profilId: number }; |
| 461 | antwort: HartnaeckigeFrage[]; |
| 462 | }; |
| 463 | 'lernen:uebersicht': { anfrage: { profilId: number }; antwort: Lernuebersicht }; |
| 464 | /** |
| 465 | * Lernplan: Prognose, Tagespensum und Machbarkeit zum Prüfungstermin. |
| 466 | * |
| 467 | * Eigener Kanal statt einer Erweiterung von `lernen:uebersicht`: Der Plan |
| 468 | * rechnet über den gesamten Katalog und wird nur dort gebraucht, wo er |
| 469 | * auch angezeigt wird. |
| 470 | */ |
| 471 | 'lernen:plan': { anfrage: { profilId: number }; antwort: Lernplan }; |
| 472 | /** Lernstand des Profils vollständig zurücksetzen (ohne Begrenzung). */ |
| 473 | 'lernen:zuruecksetzen': { |
| 474 | anfrage: { profilId: number; kapitel?: string | null }; |
| 475 | antwort: Lernuebersicht; |
| 476 | }; |
| 477 | |
| 478 | /** |
| 479 | * Stellt einen Prüfungsbogen nach dem gewählten Profil zusammen. |
| 480 | * |
| 481 | * Die Antwort ist ein {@link Pruefungsbogen} und keine reine Fragenliste: |
| 482 | * Weicht das Ziehen von den Vorgaben ab, gehört das an den Bildschirm und |
| 483 | * nicht nur ins Protokoll des Hauptprozesses. Die Sätze in `warnungen` |
| 484 | * kommen fertig formuliert von dort. |
| 485 | */ |
| 486 | 'pruefung:starten': { |
| 487 | anfrage: { profilId: number; auftrag: Pruefungsauftrag }; |
| 488 | antwort: Pruefungsbogen; |
| 489 | }; |
| 490 | /** Wertet einen Simulationslauf aus und speichert ihn im Verlauf. */ |
| 491 | 'pruefung:auswerten': { |
| 492 | anfrage: { |
| 493 | profilId: number; |
| 494 | auftrag: Pruefungsauftrag; |
| 495 | antworten: Pruefungsantwort[]; |
| 496 | dauerMs: number; |
| 497 | zeitAbgelaufen: boolean; |
| 498 | }; |
| 499 | antwort: Pruefungsergebnis; |
| 500 | }; |
| 501 | 'pruefung:verlauf': { anfrage: { profilId: number }; antwort: Pruefungsverlauf[] }; |
| 502 | |
| 503 | /** |
| 504 | * Sichert den Zwischenstand eines laufenden Bogens. |
| 505 | * |
| 506 | * Bewusst OHNE den Bogen selbst: Der Kern hat ihn beim Starten abgelegt. |
| 507 | * Wer den Bogen mitbringen dürfte, könnte ihn auch erfinden – dann wäre |
| 508 | * die Prüfung der eingereichten Antwortliste wertlos. Gesichert wird nur, |
| 509 | * was dem Renderer gehört. |
| 510 | * |
| 511 | * Die Antwort sagt, ob es noch eine Zeile zu sichern gab. `false` heißt |
| 512 | * nicht Fehler, sondern „dieser Lauf ist vorbei" – etwa weil die Abgabe |
| 513 | * schneller war als ein entprellter Nachzügler. |
| 514 | */ |
| 515 | 'pruefung:sichern': { |
| 516 | anfrage: { profilId: number; stand: Zwischenstand }; |
| 517 | antwort: boolean; |
| 518 | }; |
| 519 | |
| 520 | /** Der unterbrochene Lauf eines Profils, oder `null`. */ |
| 521 | 'pruefung:offen': { anfrage: { profilId: number }; antwort: OffenerLauf | null }; |
| 522 | |
| 523 | /** Verwirft den unterbrochenen Lauf – nur auf ausdrücklichen Wunsch. */ |
| 524 | 'pruefung:verwerfen': { anfrage: { profilId: number }; antwort: undefined }; |
| 525 | |
| 526 | /** |
| 527 | * Meldet, ob gerade eine Prüfung bearbeitet wird. |
| 528 | * |
| 529 | * Der Hauptprozess fragt beim Schließen des Fensters nach, wenn eine läuft. |
| 530 | * Bewusst ein Abgleich und keine Kantenmeldung: Der Renderer schickt seinen |
| 531 | * Zustand auch unverändert, damit ein Merker kein Neuladen überlebt. |
| 532 | */ |
| 533 | 'pruefung:laeuft': { anfrage: { laeuft: boolean }; antwort: undefined }; |
| 534 | |
| 535 | /** |
| 536 | * Erzeugt den Lernbericht als PDF und fragt, wohin er gespeichert werden soll. |
| 537 | * |
| 538 | * Der Anwendungskern holt die Zahlen selbst – Übersicht, Lernplan und |
| 539 | * Prüfungsverlauf liegen dort ohnehin. Der Renderer schickt nur, für wen |
| 540 | * und wie groß; so kann keine Oberfläche Zahlen in ein Dokument bringen, |
| 541 | * die der Kern nicht bestätigt hat. |
| 542 | */ |
| 543 | 'druck:lernbericht': { |
| 544 | anfrage: { profilId: number; schriftgroesse: Schriftgroesse }; |
| 545 | antwort: Druckergebnis; |
| 546 | }; |
| 547 | |
| 548 | /** |
| 549 | * Setzt die Anzeigegröße und merkt sie sich. |
| 550 | * |
| 551 | * Der Zoom gehört in den Hauptprozess: Er wirkt auf das Fenster, nicht auf |
| 552 | * das Dokument, und muss beim nächsten Start wieder anliegen, bevor der |
| 553 | * erste Bildpunkt gezeichnet wird. |
| 554 | */ |
| 555 | 'ansicht:groesse': { anfrage: { prozent: number }; antwort: number }; |
| 556 | |
| 557 | /** |
| 558 | * Das Fehlerprotokoll als PDF – die zuletzt falsch beantworteten Fragen. |
| 559 | * |
| 560 | * `tiefe` steuert, ob nur die Kurzerklärung oder die vollständige |
| 561 | * Begründung mitgedruckt wird. Gemessen ist der Unterschied erheblich: 150 |
| 562 | * Fragen ergeben 51 gegen 161 Seiten. |
| 563 | */ |
| 564 | 'druck:fehlerprotokoll': { |
| 565 | anfrage: { profilId: number; schriftgroesse: Schriftgroesse; tiefe: Erklaerungstiefe }; |
| 566 | antwort: Druckergebnis; |
| 567 | }; |
| 568 | |
| 569 | /** |
| 570 | * Die Fragenliste als PDF – ein Bogen zum Bearbeiten auf Papier. |
| 571 | * |
| 572 | * `bereiche` sind Kapitel- oder Abschnittskennungen; eine leere Liste |
| 573 | * bedeutet den ganzen Katalog. **Ohne Lernprofil**: Die Liste hängt am |
| 574 | * Katalog und nicht am Lernstand, es gibt also nichts zu personalisieren. |
| 575 | */ |
| 576 | 'druck:fragenliste': { |
| 577 | anfrage: { |
| 578 | bereiche: readonly string[]; |
| 579 | schriftgroesse: Schriftgroesse; |
| 580 | mitLoesungen: boolean; |
| 581 | }; |
| 582 | antwort: Druckergebnis; |
| 583 | }; |
| 584 | |
| 585 | /** |
| 586 | * Ein einzelnes Profil aus der geprüften Datei dazunehmen. |
| 587 | * |
| 588 | * `profilIndex` zeigt in `Pruefergebnis.ausDatei.jeProfil` – nicht auf eine |
| 589 | * Profilnummer. Die Nummern der fremden Datei kennt nur der Anwendungskern, |
| 590 | * und er behält sie für sich. |
| 591 | */ |
| 592 | 'sicherung:uebernehmen': { |
| 593 | anfrage: { vorgang: string; profilIndex: number }; |
| 594 | antwort: Uebernahmeergebnis; |
| 595 | }; |
| 596 | |
| 597 | /** Lizenzangaben für den Bereich „Über diese Software“. */ |
| 598 | 'lizenzen:lesen': { anfrage: undefined; antwort: Lizenzangaben }; |
| 599 | |
| 600 | /** |
| 601 | * Die mitgelieferte Datenschutzerklärung. |
| 602 | * |
| 603 | * Derselbe Weg wie bei den Lizenztexten: Der Hauptprozess liest die Datei, |
| 604 | * der Renderer bekommt fertige Blöcke. Kein Dateizugriff im Renderer. |
| 605 | */ |
| 606 | 'datenschutz:lesen': { anfrage: undefined; antwort: Datenschutzangaben }; |
| 607 | |
| 608 | /** |
| 609 | * Meldet, ob gerade ein Hilfsmittel läuft (Screenreader, Bildschirmlupe …). |
| 610 | * |
| 611 | * Grundlage ist `app.accessibilitySupportEnabled` von Electron. Der Wert |
| 612 | * steuert die Voreinstellung der Zeichenkürzel: Diese kollidieren im |
| 613 | * Lesemodus von NVDA und JAWS mit der Schnellnavigation – dort sind die |
| 614 | * Ziffern für Überschriftenebenen belegt. |
| 615 | */ |
| 616 | 'system:hilfsmittel': { anfrage: undefined; antwort: boolean }; |
| 617 | /** |
| 618 | * Legt Text in die Zwischenablage. |
| 619 | * |
| 620 | * Es gibt diesen Kanal, weil der Renderer es selbst **nicht darf**: |
| 621 | * `sicherheit.ts` lehnt mit `setPermissionCheckHandler(() => false)` |
| 622 | * sämtliche Berechtigungen ab, und Blink fragt für |
| 623 | * `navigator.clipboard.writeText()` die Berechtigung |
| 624 | * `clipboard-sanitized-write` ab. Nachgemessen im gebauten Fenster: |
| 625 | * `isSecureContext` ist `true`, `navigator.clipboard.writeText` existiert – |
| 626 | * und der Aufruf scheitert mit `NotAllowedError: Write permission denied`. |
| 627 | * |
| 628 | * Den Berechtigungswächter dafür zu öffnen wäre der schlechtere Handel: Das |
| 629 | * gäbe die Zwischenablage jedem Renderer-Code frei statt einem Kanal mit |
| 630 | * fester Nutzlast. |
| 631 | */ |
| 632 | 'system:kopieren': { anfrage: { text: string }; antwort: boolean }; |
| 633 | |
| 634 | /** |
| 635 | * Öffnet die Unterstützungsseite im Standardbrowser des Systems. |
| 636 | * |
| 637 | * Der einzige Kanal dieser Anwendung, der nach außen führt – und bewusst |
| 638 | * kein allgemeines „öffne diese Adresse“. Der Hauptprozess vergleicht die |
| 639 | * Nutzlast mit `UNTERSTUETZUNG_URL` aus `shared/unterstuetzung.ts` und |
| 640 | * öffnet ausschließlich diese eine Adresse; jede andere wird abgewiesen. |
| 641 | * Wer eine Adresse mitbringen darf, darf sonst auch eine andere mitbringen |
| 642 | * – dieselbe Überlegung wie bei `sicherung:einspielen`. |
| 643 | * |
| 644 | * Dass die Adresse trotzdem in der Nutzlast steht und nicht weggelassen |
| 645 | * wird, ist Absicht: So lässt sich prüfen, dass die Oberfläche genau das |
| 646 | * anfordert, was sie anzeigt. |
| 647 | * |
| 648 | * Antwort ist `false`, wenn keine Adresse eingetragen ist, die Nutzlast |
| 649 | * nicht passt oder das Betriebssystem den Browser nicht öffnen konnte. Die |
| 650 | * Oberfläche zeigt dann die Adresse zum Abschreiben – sie behauptet nicht, |
| 651 | * es sei etwas geschehen. |
| 652 | */ |
| 653 | 'system:unterstuetzung': { anfrage: { url: string }; antwort: boolean }; |
| 654 | |
| 655 | /** |
| 656 | * Öffnet eine soeben geschriebene Datei oder zeigt sie im Dateimanager. |
| 657 | * |
| 658 | * Die Nutzlast ist eine **Kennung**, nie ein Pfad: Wer einen Pfad |
| 659 | * mitbringen darf, darf auch einen anderen mitbringen. Der Hauptprozess |
| 660 | * merkt sich, was er in dieser Sitzung selbst geschrieben hat, und öffnet |
| 661 | * ausschließlich das (`main/dateizugriff.ts`). |
| 662 | * |
| 663 | * Antwortet `false`, wenn die Kennung unbekannt ist oder das |
| 664 | * Betriebssystem die Datei nicht öffnen konnte – etwa weil sie inzwischen |
| 665 | * verschoben wurde. Die Oberfläche sagt dann, dass es nicht geklappt hat, |
| 666 | * statt so zu tun, als sei etwas geschehen. |
| 667 | */ |
| 668 | 'system:datei-zeigen': { |
| 669 | anfrage: { kennung: string; wunsch: 'oeffnen' | 'ordner' }; |
| 670 | antwort: boolean; |
| 671 | }; |
| 672 | |
| 673 | /** |
| 674 | * Schreibt den ganzen Lernstand in eine Datei. |
| 675 | * |
| 676 | * Der Nutzer wählt den Ort in einem Systemdialog. Übertragen wird nichts – |
| 677 | * die Datei geht dorthin, wohin er sie legt, und sonst nirgendwohin. |
| 678 | */ |
| 679 | 'sicherung:anlegen': { anfrage: undefined; antwort: Sicherungsergebnis }; |
| 680 | /** |
| 681 | * Öffnet den Ordner der selbsttätigen Sicherheitskopien. |
| 682 | * |
| 683 | * **Ohne Argument, mit Absicht.** Welcher Ordner das ist, entscheidet der |
| 684 | * Hauptprozess; aus der Oberfläche kommt kein Pfad und keine Kennung. Ein |
| 685 | * Kanal, der einen Pfad entgegennähme, wäre ein Weg, beliebige Ordner des |
| 686 | * Rechners zu öffnen – und der Renderer hat in diesem Projekt keinen |
| 687 | * Node-Zugriff, gerade damit es solche Wege nicht gibt. |
| 688 | * |
| 689 | * Antwortet `false`, wenn es den Ordner noch nicht gibt. Er entsteht mit |
| 690 | * der ersten Kopie; bis dahin gibt es nichts zu zeigen, und die Oberfläche |
| 691 | * sagt das, statt so zu tun, als sei etwas geschehen. |
| 692 | */ |
| 693 | 'sicherung:ordner-zeigen': { anfrage: undefined; antwort: boolean }; |
| 694 | |
| 695 | /** |
| 696 | * Prüft eine gewählte Datei und beziffert sie, **ohne etwas zu verändern**. |
| 697 | * |
| 698 | * Getrennt vom Einspielen, weil zwischen „das steht darin“ und „ja, ersetze |
| 699 | * meinen Lernstand“ eine Entscheidung des Nutzers liegt. Bis |
| 700 | * {@link IpcVertrag['sicherung:einspielen']} gerufen wird, ist nichts |
| 701 | * angefasst. |
| 702 | */ |
| 703 | 'sicherung:pruefen': { anfrage: undefined; antwort: Pruefergebnis }; |
| 704 | |
| 705 | /** |
| 706 | * Ersetzt den Lernstand durch die zuvor geprüfte Datei. |
| 707 | * |
| 708 | * Die Nutzlast ist ausschliesslich die Vorgangskennung aus |
| 709 | * `sicherung:pruefen`; einen Pfad nimmt dieser Kanal nicht entgegen. Wer |
| 710 | * einen Pfad mitbringen darf, darf auch einen anderen mitbringen. |
| 711 | */ |
| 712 | 'sicherung:einspielen': { anfrage: { vorgang: string }; antwort: Einspielergebnis }; |
| 713 | } |
| 714 | |
| 715 | /** |
| 716 | * Kanal, über den der Main-Prozess von sich aus meldet, dass sich der |
| 717 | * Hilfsmittel-Zustand geändert hat. Nötig, weil ein Screenreader auch |
| 718 | * mitten in der Sitzung gestartet werden kann – gerade dann, wenn jemand |
| 719 | * merkt, dass er ihn braucht. |
| 720 | */ |
| 721 | export const KANAL_HILFSMITTEL_GEAENDERT = 'system:hilfsmittel-geaendert' as const; |
| 722 | |
| 723 | /** Kanal, über den der Hauptprozess eine geänderte Anzeigegröße meldet. */ |
| 724 | export const KANAL_ANZEIGEGROESSE_GEAENDERT = 'ansicht:groesse-geaendert' as const; |
| 725 | |
| 726 | /** |
| 727 | * Befehle, die aus der Menüleiste kommen und in der Oberfläche wirken. |
| 728 | * |
| 729 | * Die Menüleiste liegt im Hauptprozess, die Ansicht im Renderer – ohne einen |
| 730 | * solchen Kanal wäre ein Menüeintrag, der etwas anzeigt, gar nicht baubar. |
| 731 | * Absichtlich eine geschlossene Liste und keine freie Zeichenkette: Was hier |
| 732 | * nicht steht, kommt am anderen Ende nicht an. |
| 733 | */ |
| 734 | export const MENUEBEFEHLE = [ |
| 735 | 'hilfe', |
| 736 | /* |
| 737 | Die Ziele des Menüs „Gehe zu“, seit Fassung 0.26.0. |
| 738 | |
| 739 | Bis dahin führte das Anwendungsmenü zu keiner einzigen der zehn |
| 740 | Ansichten. Es ist die einzige Fläche des Fensters, die nicht wegrollt — |
| 741 | und der Startbildschirm ist bei 1265 Bildpunkten Breite 8530 hoch. |
| 742 | Wer daran vorbeigerollt war, hatte keinen Weg mehr in eine andere |
| 743 | Ansicht als zurück an den Anfang. |
| 744 | |
| 745 | Die Namen sind die Ansichtsarten aus `renderer/src/lernen/typen.ts`, mit |
| 746 | „gehe-zu-“ davor: Ein Befehl ist keine Ansicht, und beide Listen sollen |
| 747 | sich unabhängig ändern dürfen. |
| 748 | */ |
| 749 | 'gehe-zu-start', |
| 750 | 'gehe-zu-kapitelwahl', |
| 751 | 'gehe-zu-pruefungswahl', |
| 752 | 'gehe-zu-suche', |
| 753 | 'gehe-zu-glossar', |
| 754 | 'gehe-zu-ueber', |
| 755 | ] as const; |
| 756 | |
| 757 | export type Menuebefehl = (typeof MENUEBEFEHLE)[number]; |
| 758 | |
| 759 | export function istMenuebefehl(wert: unknown): wert is Menuebefehl { |
| 760 | return typeof wert === 'string' && (MENUEBEFEHLE as readonly string[]).includes(wert); |
| 761 | } |
| 762 | |
| 763 | /** Kanal, über den der Hauptprozess einen Menübefehl an die Ansicht meldet. */ |
| 764 | export const KANAL_MENUE_BEFEHL = 'menue:befehl' as const; |
| 765 | |
| 766 | /** |
| 767 | * Kanal, über den der Hauptprozess meldet, wie lange der Rechner geschlafen |
| 768 | * hat. Die Nutzlast ist die Schlafdauer in Millisekunden. |
| 769 | * |
| 770 | * **Warum das nicht der Renderer selbst messen kann.** Naheliegend wäre, im |
| 771 | * Sekundentakt zu prüfen, ob zwischen zwei Takten mehr als eine Sekunde |
| 772 | * vergangen ist. Chromium drosselt Zeitgeber in verdeckten Fenstern aber auf |
| 773 | * einen Takt pro Minute – ein bloß **minimiertes** Fenster sähe damit aus wie |
| 774 | * ein schlafender Rechner. Die Prüfungsuhr ließe sich durch Minimieren |
| 775 | * anhalten, und das wäre ein Schummelweg statt eines Nachteilsausgleichs. |
| 776 | * `powerMonitor` meldet ausschließlich echten Energiesparmodus und ist von |
| 777 | * der Drosselung unberührt. |
| 778 | */ |
| 779 | export const KANAL_ENERGIESPAREN_ENDE = 'system:energiesparen-ende' as const; |
| 780 | |
| 781 | export type IpcKanal = keyof IpcVertrag; |
| 782 | export type IpcAnfrage<K extends IpcKanal> = IpcVertrag[K]['anfrage']; |
| 783 | export type IpcAntwort<K extends IpcKanal> = IpcVertrag[K]['antwort']; |
| 784 | |
| 785 | /** |
| 786 | * Allowlist aller erlaubten Kanäle. Main und Preload prüfen dagegen, damit |
| 787 | * keine unbeabsichtigten Kanäle über die Bridge erreichbar werden. |
| 788 | */ |
| 789 | export const IPC_KANAELE = [ |
| 790 | 'anwendung:info', |
| 791 | 'einstellungen:lesen', |
| 792 | 'einstellungen:schreiben', |
| 793 | 'katalog:laden', |
| 794 | 'katalog:bild', |
| 795 | 'erklaerungen:laden', |
| 796 | 'glossar:laden', |
| 797 | 'gesetz:normtexte', |
| 798 | 'themen:laden', |
| 799 | 'profil:liste', |
| 800 | 'profil:anlegen', |
| 801 | 'profil:aktualisieren', |
| 802 | 'profil:loeschen', |
| 803 | 'lernen:sitzung', |
| 804 | 'lernen:antworten', |
| 805 | 'lernen:merken', |
| 806 | 'lernen:fragestand', |
| 807 | 'lernen:hartnaeckige', |
| 808 | 'lernen:uebersicht', |
| 809 | 'lernen:plan', |
| 810 | 'lernen:zuruecksetzen', |
| 811 | 'pruefung:starten', |
| 812 | 'pruefung:auswerten', |
| 813 | 'pruefung:verlauf', |
| 814 | 'pruefung:sichern', |
| 815 | 'pruefung:offen', |
| 816 | 'pruefung:verwerfen', |
| 817 | 'pruefung:laeuft', |
| 818 | 'druck:lernbericht', |
| 819 | 'druck:fehlerprotokoll', |
| 820 | 'druck:fragenliste', |
| 821 | 'ansicht:groesse', |
| 822 | 'lizenzen:lesen', |
| 823 | 'datenschutz:lesen', |
| 824 | 'system:hilfsmittel', |
| 825 | 'system:kopieren', |
| 826 | 'system:unterstuetzung', |
| 827 | 'system:datei-zeigen', |
| 828 | 'sicherung:ordner-zeigen', |
| 829 | 'sicherung:anlegen', |
| 830 | 'sicherung:pruefen', |
| 831 | 'sicherung:einspielen', |
| 832 | 'sicherung:uebernehmen', |
| 833 | ] as const satisfies readonly IpcKanal[]; |
| 834 | |
| 835 | export function istIpcKanal(wert: unknown): wert is IpcKanal { |
| 836 | return typeof wert === 'string' && (IPC_KANAELE as readonly string[]).includes(wert); |
| 837 | } |
| 838 | |
| 839 | /** Name des Objekts, das im Renderer unter `window` liegt. */ |
| 840 | export const BRIDGE_NAME = 'lernApp' as const; |
| 841 | |
| 842 | /** Die im Renderer sichtbare, vollständig typisierte API. */ |
| 843 | export interface LernAppBridge { |
| 844 | readonly anwendungsInfoLesen: () => Promise<AnwendungsInfo>; |
| 845 | readonly einstellungenLesen: () => Promise<Einstellungen>; |
| 846 | readonly einstellungenSchreiben: (aenderung: Partial<Einstellungen>) => Promise<Einstellungen>; |
| 847 | |
| 848 | readonly katalogLaden: () => Promise<Katalog>; |
| 849 | readonly katalogBild: (bildId: string) => Promise<string>; |
| 850 | readonly erklaerungenLaden: () => Promise<Erklaerungen>; |
| 851 | readonly glossarLaden: () => Promise<Glossar>; |
| 852 | readonly normtexteLaden: () => Promise<Normtexte>; |
| 853 | readonly themenLaden: () => Promise<Themen>; |
| 854 | |
| 855 | readonly profilListe: () => Promise<Profil[]>; |
| 856 | readonly profilAnlegen: (name: string) => Promise<Profil>; |
| 857 | readonly profilAktualisieren: ( |
| 858 | id: number, |
| 859 | aenderung: { |
| 860 | name?: string; |
| 861 | pruefungstermin?: string | null; |
| 862 | /** Kapitel, die dieses Profil dauerhaft nicht lernt. */ |
| 863 | kapitelAusschluss?: readonly string[]; |
| 864 | }, |
| 865 | ) => Promise<Profil>; |
| 866 | /** Löscht ein Profil; liefert die verbleibenden zurück. */ |
| 867 | readonly profilLoeschen: (id: number) => Promise<Profil[]>; |
| 868 | |
| 869 | readonly lernSitzung: (profilId: number, filter: SitzungsFilter) => Promise<SitzungsFrage[]>; |
| 870 | readonly lernAntworten: (profilId: number, protokoll: Antwortprotokoll) => Promise<FrageStand>; |
| 871 | readonly lernMerken: (profilId: number, frageId: string, gemerkt: boolean) => Promise<FrageStand>; |
| 872 | /** |
| 873 | * Der gespeicherte Stand einer einzelnen Frage – für den Steckbrief. |
| 874 | * |
| 875 | * Optional geführt wie die übrigen jüngeren Kanäle: Fehlt er, entfällt der |
| 876 | * Steckbrief, und die Sitzung läuft wie bisher. |
| 877 | */ |
| 878 | readonly lernFragestand?: (profilId: number, frageId: string) => Promise<FrageStand>; |
| 879 | /** Die hartnäckigen Fragen – wiederholt danebengegangen. */ |
| 880 | readonly lernHartnaeckige?: (profilId: number) => Promise<HartnaeckigeFrage[]>; |
| 881 | readonly lernUebersicht: (profilId: number) => Promise<Lernuebersicht>; |
| 882 | readonly lernPlan: (profilId: number) => Promise<Lernplan>; |
| 883 | readonly lernZuruecksetzen: ( |
| 884 | profilId: number, |
| 885 | kapitel?: string | null, |
| 886 | ) => Promise<Lernuebersicht>; |
| 887 | |
| 888 | /** Liefert den Bogen samt der Abweichungen, die beim Ziehen nötig waren. */ |
| 889 | readonly pruefungStarten: ( |
| 890 | profilId: number, |
| 891 | auftrag: Pruefungsauftrag, |
| 892 | ) => Promise<Pruefungsbogen>; |
| 893 | readonly pruefungAuswerten: ( |
| 894 | profilId: number, |
| 895 | auftrag: Pruefungsauftrag, |
| 896 | antworten: Pruefungsantwort[], |
| 897 | dauerMs: number, |
| 898 | zeitAbgelaufen: boolean, |
| 899 | ) => Promise<Pruefungsergebnis>; |
| 900 | readonly pruefungVerlauf: (profilId: number) => Promise<Pruefungsverlauf[]>; |
| 901 | /** Sichert den Zwischenstand; `false` heißt „dieser Lauf ist vorbei". */ |
| 902 | readonly pruefungSichern: (profilId: number, stand: Zwischenstand) => Promise<boolean>; |
| 903 | /** Der unterbrochene Lauf eines Profils, oder `null`. */ |
| 904 | readonly pruefungOffen: (profilId: number) => Promise<OffenerLauf | null>; |
| 905 | readonly pruefungVerwerfen: (profilId: number) => Promise<void>; |
| 906 | /** Meldet dem Hauptprozess, ob gerade geprüft wird. */ |
| 907 | readonly pruefungLaeuft: (laeuft: boolean) => Promise<void>; |
| 908 | |
| 909 | /** |
| 910 | * Setzt die Anzeigegröße in Prozent; liefert die tatsächlich gesetzte |
| 911 | * Stufe zurück (der Kern begrenzt auf gültige Werte). |
| 912 | */ |
| 913 | readonly anzeigegroesseSetzen: (prozent: number) => Promise<number>; |
| 914 | /** |
| 915 | * Meldet, wenn die Anzeigegröße anderswo geändert wurde – über das Menü |
| 916 | * oder Strg+Plus. Ohne diese Meldung zeigte die Einstellung eine Zahl an, |
| 917 | * die nicht mehr stimmt. |
| 918 | */ |
| 919 | readonly anzeigegroesseBeobachten: (melden: (prozent: number) => void) => () => void; |
| 920 | |
| 921 | /** |
| 922 | * Nimmt ein Profil aus der geprüften Datei dazu, ohne etwas zu ersetzen. |
| 923 | * |
| 924 | * `profilIndex` zeigt in die Liste, die `sicherungPruefen` geliefert hat. |
| 925 | */ |
| 926 | readonly sicherungUebernehmen: ( |
| 927 | vorgang: string, |
| 928 | profilIndex: number, |
| 929 | ) => Promise<Uebernahmeergebnis>; |
| 930 | |
| 931 | /** Erzeugt das Fehlerprotokoll als PDF und fragt nach dem Speicherort. */ |
| 932 | readonly fehlerprotokollDrucken: ( |
| 933 | profilId: number, |
| 934 | schriftgroesse: Schriftgroesse, |
| 935 | tiefe: Erklaerungstiefe, |
| 936 | ) => Promise<Druckergebnis>; |
| 937 | |
| 938 | /** Erzeugt die Fragenliste als PDF und fragt nach dem Speicherort. */ |
| 939 | readonly fragenlisteDrucken: ( |
| 940 | bereiche: readonly string[], |
| 941 | schriftgroesse: Schriftgroesse, |
| 942 | mitLoesungen: boolean, |
| 943 | ) => Promise<Druckergebnis>; |
| 944 | |
| 945 | /** Erzeugt den Lernbericht als PDF und fragt nach dem Speicherort. */ |
| 946 | readonly lernberichtDrucken: ( |
| 947 | profilId: number, |
| 948 | schriftgroesse: Schriftgroesse, |
| 949 | ) => Promise<Druckergebnis>; |
| 950 | |
| 951 | readonly lizenzenLesen: () => Promise<Lizenzangaben>; |
| 952 | |
| 953 | readonly datenschutzLesen: () => Promise<Datenschutzangaben>; |
| 954 | |
| 955 | readonly hilfsmittelAktiv: () => Promise<boolean>; |
| 956 | /** |
| 957 | * Legt Text in die Zwischenablage. Siehe Kanal `system:kopieren`. |
| 958 | * |
| 959 | * Optional geführt wie die übrigen jüngeren Kanäle: Die Oberfläche muss |
| 960 | * ohne ihn auskommen, und die Attrappen der Oberflächentests tragen das |
| 961 | * volle Interface. Fehlt er, entfällt der Kopierknopf – der Meldetext bleibt |
| 962 | * sichtbar und markierbar. |
| 963 | */ |
| 964 | readonly zwischenablageSchreiben?: (text: string) => Promise<boolean>; |
| 965 | |
| 966 | /** |
| 967 | * Öffnet die Unterstützungsseite im Standardbrowser. Siehe Kanal |
| 968 | * `system:unterstuetzung`. |
| 969 | * |
| 970 | * Optional geführt wie die übrigen jüngeren Kanäle. Fehlt er, lässt die |
| 971 | * Ansicht „Über diese Software“ das Angebot **vollständig** weg – ein Knopf, |
| 972 | * der nichts öffnen kann, wäre eine Zusage ohne Deckung. |
| 973 | */ |
| 974 | readonly unterstuetzungOeffnen?: (url: string) => Promise<boolean>; |
| 975 | |
| 976 | /** |
| 977 | * Öffnet eine soeben geschriebene Datei oder zeigt sie im Ordner. |
| 978 | * |
| 979 | * Optional geführt wie die übrigen jüngeren Kanäle: Fehlt er, entfallen |
| 980 | * die beiden Schaltflächen nach einem Export – gespeichert ist die Datei |
| 981 | * dann trotzdem, und ihr Pfad steht auf dem Bildschirm. |
| 982 | */ |
| 983 | readonly dateiZeigen?: (kennung: string, wunsch: 'oeffnen' | 'ordner') => Promise<boolean>; |
| 984 | /** |
| 985 | * Öffnet den Ordner der selbsttätigen Sicherheitskopien. |
| 986 | * |
| 987 | * Ohne Argument: Welcher Ordner das ist, weiß allein der Hauptprozess. |
| 988 | */ |
| 989 | readonly sicherungsordnerZeigen?: () => Promise<boolean>; |
| 990 | |
| 991 | /* Optional geführt wie die übrigen jüngeren Kanäle: Die Oberfläche muss |
| 992 | ohne sie auskommen, und die Attrappen der Tests tragen das volle |
| 993 | Interface. Fehlen sie, entfällt die Karte zur Sicherung. */ |
| 994 | readonly sicherungAnlegen?: () => Promise<Sicherungsergebnis>; |
| 995 | readonly sicherungPruefen?: () => Promise<Pruefergebnis>; |
| 996 | readonly sicherungEinspielen?: (vorgang: string) => Promise<Einspielergebnis>; |
| 997 | /** |
| 998 | * Meldet Änderungen des Hilfsmittel-Zustands. Liefert eine Funktion zum |
| 999 | * Abmelden zurück, damit React beim Abbau aufräumen kann. |
| 1000 | */ |
| 1001 | readonly hilfsmittelBeobachten: (melden: (aktiv: boolean) => void) => () => void; |
| 1002 | |
| 1003 | /** |
| 1004 | * Meldet Befehle aus der Menüleiste. Liefert eine Funktion zum Abmelden |
| 1005 | * zurück, damit React beim Abbau aufräumen kann. |
| 1006 | */ |
| 1007 | readonly menuebefehlBeobachten: (melden: (befehl: Menuebefehl) => void) => () => void; |
| 1008 | |
| 1009 | /** |
| 1010 | * Meldet nach dem Aufwachen, wie lange der Rechner geschlafen hat. |
| 1011 | * |
| 1012 | * Optional geführt wie die übrigen jüngeren Kanäle: Fehlt er, läuft die |
| 1013 | * Simulation wie bisher – nur ohne Gutschrift. |
| 1014 | */ |
| 1015 | readonly energiesparenBeobachten?: (melden: (schlafMs: number) => void) => () => void; |
| 1016 | } |