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