import { readFileSync, writeFileSync } from 'node:fs'; /** * Schriftgroesse des Anwendungsfensters - Stufen, Pruefung und Sicherung. * * Fassung 5.7.0, Befund L11 (EN 301 549 Kap. 11.7, "Nutzereinstellungen"): * Vergroessern war bis hierher ausschliesslich ueber die Chromium-Vorgabe * Strg+Plus moeglich, und die eingestellte Stufe hielt genau eine Sitzung. Sie * war nach jedem Programmstart, nach jedem "Neu laden" und nach jedem Neustart * aus einem Absturz heraus (main.ts, `mainWindow.reload()`) wieder auf 100 %. * Wer 175 % braucht - in Behoerden die Kerngruppe -, stellte sie also mehrmals * taeglich neu ein. WCAG 1.4.4 ist davon nicht beruehrt: Die Vergroesserung war * moeglich. 11.7 verlangt darueber hinaus, dass eine getroffene Einstellung * erhalten bleibt. * * WARUM DIE STUFE IM HAUPTPROZESS LIEGT UND NICHT IN `AppSettings` * * Die Programmeinstellungen des Anzeigeprozesses (Erscheinungsbild, * Selbstsicherung, Sprache, Fundstellen) liegen im localStorage und werden ueber * `loadAppSettings()` gelesen - der Hauptprozess kommt dort nicht heran. Die * Vergroesserungsstufe muss aber schon feststehen, BEVOR das Fenster entsteht: * Sie geht als `webPreferences.zoomFactor` in `new BrowserWindow(...)` ein, * damit die erste Darstellung bereits in der richtigen Groesse erscheint statt * sichtbar von 100 % auf 175 % zu springen. Sie ist ausserdem keine Eigenschaft * des Dokuments, sondern des Fensters - wie Fenstergroesse und Fensterlage, die * kuenftig in dieselbe Datei gehoeren. * * Die Art der Sicherung folgt bewusst `loadAppSettings()`: fester Satz zulaessiger * Werte, jede Abweichung faellt auf die Vorgabe zurueck, ein Lesefehler bleibt * folgenlos. Fremde Schluessel der Datei bleiben beim Schreiben erhalten, damit * eine spaetere Fenstereinstellung nicht die hier gesicherte ueberschreibt. */ /** * Waehlbare Stufen in Prozent. * * 200 % ist die von WCAG 1.4.4 geforderte Vergroesserung, 250 % die Reserve fuer * Anwender mit staerkerer Sehbehinderung; darunter bleiben 75 % und 90 % * erhalten, weil Chromiums "Verkleinern" bisher unter 100 % gehen konnte und * dieser Weg nicht verloren gehen darf. */ export const SCHRIFT_STUFEN: readonly number[] = [75, 90, 100, 125, 150, 175, 200, 250]; export const SCHRIFT_STANDARD = 100; /** Name der Datei im Benutzerdatenverzeichnis. */ export const ANSICHT_DATEINAME = 'ansicht.json'; /** Schluessel in der Datei. */ const SCHLUESSEL = 'schriftgroesseProzent'; /** * Naechstgelegene zulaessige Stufe. * * Alles, was keine Zahl ist, wird zur Vorgabe. Ein von Hand eingetragener oder * ueber Strg+Mausrad entstandener Zwischenwert wird auf die naechste Stufe * gerundet - so bleibt es bei genau einer Buchfuehrung ueber die Groesse. */ export function gueltigeStufe(wert: unknown): number { if (typeof wert !== 'number' || !Number.isFinite(wert)) return SCHRIFT_STANDARD; return SCHRIFT_STUFEN[stufenIndex(wert)] ?? SCHRIFT_STANDARD; } /** Eine Stufe groesser (`1`) oder kleiner (`-1`); an den Enden bleibt es stehen. */ export function nachbarstufe(aktuell: number, richtung: 1 | -1): number { const ziel = stufenIndex(aktuell) + richtung; const begrenzt = Math.min(SCHRIFT_STUFEN.length - 1, Math.max(0, ziel)); return SCHRIFT_STUFEN[begrenzt] ?? SCHRIFT_STANDARD; } function stufenIndex(prozent: number): number { let beste = 0; let abstand = Number.POSITIVE_INFINITY; for (let i = 0; i < SCHRIFT_STUFEN.length; i += 1) { const stufe = SCHRIFT_STUFEN[i]; if (stufe === undefined) continue; const naehe = Math.abs(stufe - prozent); if (naehe < abstand) { abstand = naehe; beste = i; } } return beste; } /** Liest die gesicherte Stufe. Schlaegt niemals fehl. */ export function liesSchriftgroesse(datei: string): number { return gueltigeStufe(leseDatei(datei)[SCHLUESSEL]); } /** * Sichert die Stufe. Schlaegt niemals fehl - eine Ansichtseinstellung ist * nichts, wofuer die Anwendung stehenbleiben darf. * * Zurueck kommt, ob wirklich geschrieben wurde. Der Anwender erfaehrt von einem * Fehlschlag nichts, die Buchfuehrung in `schriftsicherung` aber schon: Ein * Fehlschlag, der als Erfolg verbucht wird, kostet die Einstellung fuer die * ganze Sitzung (Befund 11). */ export function schreibeSchriftgroesse(datei: string, prozent: number): boolean { const inhalt = { ...leseDatei(datei), [SCHLUESSEL]: gueltigeStufe(prozent) }; try { writeFileSync(datei, `${JSON.stringify(inhalt, null, 2)}\n`, 'utf8'); return true; } catch { /* Kein Schreibrecht, Datentraeger voll: Die Anwendung laeuft ohne Sicherung weiter. */ return false; } } /** * Verzoegerte Sicherung - eine Schreibung je Bedienvorgang, nicht je Raste. * * BEFUND (Fassung 5.7.0): `zoom-changed` feuert bei jeder Raste von * Strg+Mausrad, und jede Raste lief bis hierher in ein `writeFileSync` im * HAUPTPROZESS - also in genau den Prozess, der die Fenster bedient. Wer das * Rad einmal durchdreht, loeste ein Dutzend Schreibungen aus; auf einem * servergespeicherten Benutzerprofil, in Behoerden der Regelfall, blockiert * jede davon die Oberflaeche. Geschrieben wurde ausserdem auch dann, wenn sich * gar nichts geaendert hatte: An beiden Enden der Stufenliste gibt * `nachbarstufe` denselben Wert zurueck. * * Zwei Massnahmen, beide hier, damit jeder Weg zur Stufe sie bekommt: * * - `plane()` schreibt nur, wenn der Wert vom zuletzt gesicherten abweicht. * - Es wartet die Ruhezeit ab. Wer weiterdreht, verschiebt sie; erst wenn er * aufhoert, geht eine einzige Schreibung heraus. * * `jetzt()` fuer den Programmschluss: Eine Einstellung, die eine halbe Sekunde * vor dem Beenden getroffen wurde, darf nicht verlorengehen. * * Der zuletzt gesicherte Wert wird gemerkt und nicht aus der Datei gelesen - * ein Lesevorgang je Raste waere derselbe Fehler mit umgekehrtem Vorzeichen. * Gemerkt wird er erst, wenn das Schreiben wirklich gelungen ist: Sonst stuende * ein misslungener Versuch als gesichert in der Buchfuehrung, und `jetzt()` * haette beim Programmende nichts mehr nachzuholen (Befund 11). */ export interface Schriftsicherung { /** Merkt die Stufe vor; geschrieben wird nach der Ruhezeit. */ plane: (prozent: number) => void; /** Schreibt sofort, was noch aussteht - fuer das Programmende. */ jetzt: () => void; } export const SICHERUNG_RUHEZEIT_MS = 750; export function schriftsicherung( datei: string, gesichert: number, ruhezeitMs: number = SICHERUNG_RUHEZEIT_MS, ): Schriftsicherung { let letzteGesicherte = gueltigeStufe(gesichert); let offen: number | null = null; let uhr: ReturnType | null = null; const schreibe = (): void => { if (uhr !== null) { clearTimeout(uhr); uhr = null; } if (offen === null) return; const wert = offen; // Erst der Versuch, dann die Buchfuehrung. Misslingt das Schreiben - kein // Schreibrecht, Profilabgleich, Virenwaechter -, bleibt der Wert // vorgemerkt: `jetzt()` beim Programmende und jede weitere Raste versuchen // es dann noch einmal. Andersherum galt er als gesichert, obwohl nichts auf // dem Datentraeger stand, und die Einstellung war fuer die ganze Sitzung // verloren. if (!schreibeSchriftgroesse(datei, wert)) return; offen = null; letzteGesicherte = wert; }; return { plane: (prozent) => { const wert = gueltigeStufe(prozent); if (wert === letzteGesicherte) { // Auch eine bereits vorgemerkte Aenderung faellt weg, wenn der Anwender // wieder auf dem gesicherten Wert landet - hin und zurueck ist keine // Aenderung. offen = null; if (uhr !== null) { clearTimeout(uhr); uhr = null; } return; } offen = wert; if (uhr !== null) clearTimeout(uhr); uhr = setTimeout(schreibe, ruhezeitMs); // Ein Zeitgeber im Hauptprozess darf das Beenden nicht hinauszoegern. // Das Fragezeichen beanstandet der Linter, weil Node den Zeitgeber als // Timeout mit unref fuehrt. Es bleibt: Unter einer Testnachbildung oder // einem Browser-setTimeout kommt eine blosse Zahl zurueck, und dann // duerfte das Ablegen des Schriftgrads nicht daran scheitern. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- unref gibt es nur bei Node-Zeitgebern; siehe darueber uhr.unref?.(); }, jetzt: schreibe, }; } function leseDatei(datei: string): Record { try { const gelesen: unknown = JSON.parse(readFileSync(datei, 'utf8')); if (gelesen === null || typeof gelesen !== 'object' || Array.isArray(gelesen)) return {}; return gelesen as Record; } catch { return {}; } }