feat(v5): 5.5.0 — Kern und Oberfläche aufgeteilt, Übersicht und Einstellungen neu, Erst-Einrichtung mit Poster-Kulisse
Commander-Wünsche vom 02.09.2026 vor der Freigabe von 5.4.0 (die deshalb nie veröffentlicht wurde): Kern: kern/index.ts ist nur noch Bootstrap und Router; Dienste in kern/dienste/ (Einstellungen, Werkzeuge, Laufwerke, Erkennung, Vorgänge, Speicher, Benachrichtigung, Automatik), Kontext in kern/kontext.ts, Protokoll-Datei in gemeinsam/protokoll.ts, Platz-Rechnung in gemeinsam/platz.ts. Oberfläche: fenster/App.tsx ist nur noch der Rahmen; bausteine.tsx, dialoge/, uebersicht/ (LaufwerkKachel, KompressionsKarte, SpeicherKarte, VerlaufKarte), einstellungen/ (Karten je Funktion, SprachAuswahl), einrichtung/ (ErstEinrichtung, PosterHintergrund). Neu: Bibliothek weg, dafür Verlauf; Speicher-und-Bilanz-Karte (statfs); LED-Fußleiste; MakeMKV-Kopf in die Einstellungen; Sprach-Chips mit Dropdown statt Allgemeinem Preset; Karten Automatik/Benachrichtigung/Allgemein/Update; Durchsuchen bei Werkzeugen; Arbeitsordner lokal (Encode dort, dann in die Ablage); Platz-Prüfung vor dem Rip (Dialog und Kern); Automatik mit Countdown; Serien-Merker über Discs; Rettungs-Abbild mit Nullen nur nach gesehenen Lesefehlern (kern/rip/rettung.ts, Phase rettet, iso:-Quelle); Warteschlange pausieren; Discord/Telegram-Benachrichtigung; Protokoll-Datei je Tag; Fensterlage 80 % mittig, gemerkt; Erst-Einrichtung in acht Schritten vor dem Poster-Grid. Beweise: 413 Tests (neu: speicher, benachrichtigung, automatik, rettung, fensterlage, protokoll, kleinkram-550), drei Typprüfungen, Klick-Beweis mit 29 Schritten inklusive Erst-Einrichtung (zweiter Start mit RIPPY_START_BEREICH=einrichtung), Bilder beweise/klick/01…09. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
6678c81592
commit
3dc6798c79
@@ -1,17 +1,29 @@
|
||||
// Fenster anlegen (KONZEPT § 4.2). Das Fenster zeigt an, sonst nichts —
|
||||
// kein Node-Zugriff, contextIsolation an, alles läuft über die Brücke
|
||||
// (src/bruecke) und den MessagePort zum Kern.
|
||||
// „Zustand merken" (Position/Größe) kommt mit den Einstellungen im UI (W-5).
|
||||
import { BrowserWindow } from 'electron'
|
||||
//
|
||||
// 5.5.0: Die Größe hängt am Bildschirm (fensterlage.ts) — 80 % des
|
||||
// Arbeitsbereichs beim ersten Start, danach die zuletzt gemerkte Lage,
|
||||
// sofern sie noch auf einem Bildschirm liegt.
|
||||
import { BrowserWindow, screen } from 'electron'
|
||||
import { join } from 'node:path'
|
||||
import { fensterrandEntfernen } from './fensterrand'
|
||||
import { anfangsLage, lageLesen, lageSchreiben, MINDEST_BREITE, MINDEST_HOEHE, type Lage } from './fensterlage'
|
||||
|
||||
export function fensterErstellen(): BrowserWindow {
|
||||
export function fensterErstellen(profilOrdner: string): BrowserWindow {
|
||||
const lageDatei = join(profilOrdner, 'fenster-lage.json')
|
||||
const lage = anfangsLage(
|
||||
lageLesen(lageDatei),
|
||||
screen.getPrimaryDisplay().workArea,
|
||||
screen.getAllDisplays().map((d) => d.workArea),
|
||||
)
|
||||
const fenster = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 820,
|
||||
minWidth: 860,
|
||||
minHeight: 560,
|
||||
x: lage.x,
|
||||
y: lage.y,
|
||||
width: lage.width,
|
||||
height: lage.height,
|
||||
minWidth: Math.min(MINDEST_BREITE, 860),
|
||||
minHeight: Math.min(MINDEST_HOEHE, 560),
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
// Eigene Titelleiste (Commander-Wunsch 01.09.2026): Der Windows-Rahmen
|
||||
@@ -35,17 +47,37 @@ export function fensterErstellen(): BrowserWindow {
|
||||
sandbox: true,
|
||||
},
|
||||
})
|
||||
if (lage.maximiert) fenster.maximize()
|
||||
|
||||
// Lage merken — verzögert, damit ein Ziehen nicht hundertmal schreibt.
|
||||
let zeiger: NodeJS.Timeout | null = null
|
||||
const merken = (): void => {
|
||||
if (zeiger !== null) clearTimeout(zeiger)
|
||||
zeiger = setTimeout(() => {
|
||||
if (fenster.isDestroyed()) return
|
||||
const maximiert = fenster.isMaximized()
|
||||
// Im maximierten Zustand die NORMALE Lage behalten (getNormalBounds),
|
||||
// sonst würde beim nächsten Start ein riesiges Fenster entmaximiert.
|
||||
const b = maximiert ? fenster.getNormalBounds() : fenster.getBounds()
|
||||
const neu: Lage = { x: b.x, y: b.y, width: b.width, height: b.height, maximiert }
|
||||
lageSchreiben(lageDatei, neu)
|
||||
}, 400)
|
||||
}
|
||||
fenster.on('resize', merken)
|
||||
fenster.on('move', merken)
|
||||
fenster.on('maximize', merken)
|
||||
fenster.on('unmaximize', merken)
|
||||
|
||||
// electron-vite: Im dev-Modus bedient ein Vite-Server das Fenster
|
||||
// (ELECTRON_RENDERER_URL), gebaut wird aus out/fenster geladen.
|
||||
// RIPPY_START_BEREICH öffnet einen bestimmten Bereich (#einstellungen /
|
||||
// #bibliothek) — der Smoke-Beweis fotografiert damit auch diese Seiten.
|
||||
// Der DWM-Rand von Windows 11 (die feine helle Linie ums Fenster) — er
|
||||
// gehört nicht zum Fensterinhalt, mit CSS ist er nicht zu erreichen.
|
||||
// Ein Fehlschlag ist kein Ausfall: Unter Windows 10 gibt es ihn nicht.
|
||||
const rand = fensterrandEntfernen(fenster.getNativeWindowHandle())
|
||||
if (!rand.ok) console.warn(`[haupt] Fensterrand bleibt: ${rand.fehler}`)
|
||||
|
||||
// electron-vite: Im dev-Modus bedient ein Vite-Server das Fenster
|
||||
// (ELECTRON_RENDERER_URL), gebaut wird aus out/fenster geladen.
|
||||
// RIPPY_START_BEREICH öffnet einen bestimmten Bereich (#einstellungen /
|
||||
// #einrichtung) — der Smoke-Beweis fotografiert damit auch diese Seiten.
|
||||
const devUrl = process.env['ELECTRON_RENDERER_URL']
|
||||
const startBereich = process.env['RIPPY_START_BEREICH'] ?? ''
|
||||
if (devUrl !== undefined && devUrl.length > 0) {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Die Fensterlage (5.5.0, Commander: „Das Programm sollte immer in einer
|
||||
// angenehmen Größe öffnen, abhängig von der Auflösung des Monitors") —
|
||||
// PUR, damit die Ampel die Rechnung prüft:
|
||||
// * Ohne gemerkte Lage: 80 % des nutzbaren Bildschirms (Arbeitsbereich
|
||||
// ohne Taskleiste), mittig, mindestens 1100 × 720, nie größer als der
|
||||
// Bildschirm.
|
||||
// * Mit gemerkter Lage: wiederherstellen — aber nur, wenn sie auf einem
|
||||
// heutigen Bildschirm liegt (ein abgestöpselter Zweitmonitor darf das
|
||||
// Fenster nicht ins Nirgendwo schieben).
|
||||
// Gemerkt wird in <Profil>\fenster-lage.json — keine Nutzer-Einstellung,
|
||||
// sondern Geometrie; die EINE Einstellungs-Quelle (§ 6.7) bleibt der Kern.
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
|
||||
export interface Lage {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
maximiert: boolean
|
||||
}
|
||||
|
||||
export interface Bereich {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export const MINDEST_BREITE = 1100
|
||||
export const MINDEST_HOEHE = 720
|
||||
export const ANTEIL = 0.8
|
||||
|
||||
/** Die Startlage aus dem Arbeitsbereich des Hauptbildschirms (pur). */
|
||||
export function startLage(arbeitsbereich: Bereich): Lage {
|
||||
const width = Math.max(Math.min(arbeitsbereich.width, MINDEST_BREITE), Math.round(arbeitsbereich.width * ANTEIL))
|
||||
const height = Math.max(Math.min(arbeitsbereich.height, MINDEST_HOEHE), Math.round(arbeitsbereich.height * ANTEIL))
|
||||
return {
|
||||
x: arbeitsbereich.x + Math.round((arbeitsbereich.width - width) / 2),
|
||||
y: arbeitsbereich.y + Math.round((arbeitsbereich.height - height) / 2),
|
||||
width: Math.min(width, arbeitsbereich.width),
|
||||
height: Math.min(height, arbeitsbereich.height),
|
||||
maximiert: false,
|
||||
}
|
||||
}
|
||||
|
||||
/** Liegt die gemerkte Lage sichtbar auf einem der Bildschirme? (pur) */
|
||||
export function liegtSichtbar(lage: Lage, bildschirme: readonly Bereich[]): boolean {
|
||||
if (lage.width < 400 || lage.height < 300) return false
|
||||
return bildschirme.some((b) => {
|
||||
const rechts = Math.min(lage.x + lage.width, b.x + b.width)
|
||||
const links = Math.max(lage.x, b.x)
|
||||
const unten = Math.min(lage.y + lage.height, b.y + b.height)
|
||||
const oben = Math.max(lage.y, b.y)
|
||||
// Mindestens 200 × 100 sichtbar — genug, um das Fenster anzufassen.
|
||||
return rechts - links >= 200 && unten - oben >= 100
|
||||
})
|
||||
}
|
||||
|
||||
/** Gemerkt oder Start — was das Fenster beim Öffnen bekommt (pur). */
|
||||
export function anfangsLage(gemerkt: Lage | null, arbeitsbereich: Bereich, bildschirme: readonly Bereich[]): Lage {
|
||||
if (gemerkt !== null && liegtSichtbar(gemerkt, bildschirme)) return gemerkt
|
||||
return startLage(arbeitsbereich)
|
||||
}
|
||||
|
||||
export function lageLesen(pfad: string): Lage | null {
|
||||
try {
|
||||
if (!existsSync(pfad)) return null
|
||||
const roh: unknown = JSON.parse(readFileSync(pfad, 'utf8'))
|
||||
if (typeof roh !== 'object' || roh === null) return null
|
||||
const l = roh as Record<string, unknown>
|
||||
if ([l.x, l.y, l.width, l.height].some((v) => typeof v !== 'number' || !Number.isFinite(v))) return null
|
||||
return { x: l.x as number, y: l.y as number, width: l.width as number, height: l.height as number, maximiert: l.maximiert === true }
|
||||
} catch {
|
||||
// Eine kaputte Datei heißt: Startlage. Sie wird beim nächsten Speichern ersetzt.
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function lageSchreiben(pfad: string, lage: Lage): void {
|
||||
try {
|
||||
writeFileSync(pfad, JSON.stringify(lage))
|
||||
} catch {
|
||||
// Nicht schreibbar: Das Fenster öffnet beim nächsten Mal in der Startlage — kein Grund für Lärm.
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { writeFile } from 'node:fs/promises'
|
||||
import { join, win32 } from 'node:path'
|
||||
import type { HauptStatus, KernNachricht } from '../gemeinsam/nachrichten'
|
||||
import { werkzeugOrdner } from '../kern/werkzeuge/katalog'
|
||||
import { Protokoll } from '../gemeinsam/protokoll'
|
||||
import { autostartAktiv, autostartSetzen } from './autostart'
|
||||
import { fensterErstellen } from './fenster'
|
||||
import { KernVerwaltung } from './kernstart'
|
||||
@@ -47,12 +48,19 @@ if (!app.requestSingleInstanceLock()) {
|
||||
}
|
||||
|
||||
function start(): void {
|
||||
// 5.5.0: Die Protokoll-Datei (aus ARM) — Haupt und Kern schreiben in
|
||||
// denselben Ordner, eine Datei je Tag, 14 Tage aufgehoben.
|
||||
const protokollOrdner = join(app.getPath('userData'), 'logs')
|
||||
const protokoll = new Protokoll(protokollOrdner, 'haupt')
|
||||
protokoll.aufraeumen()
|
||||
protokoll.schreiben(`Rippy ${app.getVersion()} startet (Electron ${process.versions.electron}, Profil ${app.getPath('userData')})`)
|
||||
// Die Leine kommt VOR dem ersten Kindprozess — Mitgliedschaft in der
|
||||
// Arbeitsgruppe wird nur beim Start eines Kindes vererbt (§ 3.3).
|
||||
const leine: LeineStatus = leineSetzen()
|
||||
if (!leine.ok) {
|
||||
// R4: laut, nicht still — zusätzlich steht es im Fenster-Status.
|
||||
console.error(`[haupt] Prozess-Leine NICHT gesetzt: ${leine.fehler}`)
|
||||
protokoll.schreiben(`Prozess-Leine NICHT gesetzt: ${leine.fehler}`)
|
||||
}
|
||||
|
||||
let fenster: BrowserWindow | null = null
|
||||
@@ -80,6 +88,7 @@ function start(): void {
|
||||
smoke.datenbank = nachricht.datenbank.ok
|
||||
if (!nachricht.datenbank.ok) {
|
||||
console.error(`[haupt] Kern meldet Datenbank-Fehler: ${nachricht.datenbank.fehler}`)
|
||||
protokoll.schreiben(`Kern meldet Datenbank-Fehler: ${nachricht.datenbank.fehler}`)
|
||||
}
|
||||
statusSenden()
|
||||
smokePruefen()
|
||||
@@ -93,7 +102,10 @@ function start(): void {
|
||||
updateEinrichten(
|
||||
nachricht.werte['updateUrl'] ?? '',
|
||||
() => aktiveVorgaenge.size > 0,
|
||||
(text) => toast('Rippy-Update', text),
|
||||
(text) => {
|
||||
protokoll.schreiben(`Update: ${text}`)
|
||||
toast('Rippy-Update', text)
|
||||
},
|
||||
// Jede Änderung am Update-Stand geht SOFORT ans Fenster —
|
||||
// vorher lag sie still im Haupt und war erst beim nächsten
|
||||
// Laden zu sehen (Commander-Fund 01.09.2026).
|
||||
@@ -122,7 +134,10 @@ function start(): void {
|
||||
)
|
||||
}
|
||||
},
|
||||
() => statusSenden(),
|
||||
(zustand) => {
|
||||
if (!zustand.laeuft) protokoll.schreiben(`Kern nicht aktiv (Neustarts: ${zustand.neustarts})`)
|
||||
statusSenden()
|
||||
},
|
||||
smokeModus,
|
||||
)
|
||||
|
||||
@@ -222,6 +237,7 @@ function start(): void {
|
||||
// Für die eigene Titelleiste: Das Knopf-Symbol muss wissen, ob das
|
||||
// Fenster gerade maximiert ist.
|
||||
fensterMaximiert: fenster !== null && !fenster.isDestroyed() && fenster.isMaximized(),
|
||||
protokollOrdner,
|
||||
}
|
||||
fenster.webContents.send('haupt-status', status)
|
||||
}
|
||||
@@ -250,7 +266,7 @@ function start(): void {
|
||||
|
||||
function fensterZeigen(): void {
|
||||
if (fenster === null || fenster.isDestroyed()) {
|
||||
fenster = fensterErstellen()
|
||||
fenster = fensterErstellen(app.getPath('userData'))
|
||||
fensterVerdrahten()
|
||||
return
|
||||
}
|
||||
@@ -327,6 +343,33 @@ function start(): void {
|
||||
return ergebnis.canceled || ergebnis.filePaths.length === 0 ? null : ergebnis.filePaths[0]
|
||||
})
|
||||
|
||||
// Echter Windows-Dateidialog (5.5.0: Werkzeuge → Durchsuchen) — nur
|
||||
// Programme, sonst nichts.
|
||||
ipcMain.handle('datei-waehlen', async (_ereignis, titel: unknown) => {
|
||||
if (fenster === null) return null
|
||||
const ergebnis = await dialog.showOpenDialog(fenster, {
|
||||
title: typeof titel === 'string' && titel.length > 0 ? titel : 'Programm wählen',
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{ name: 'Programme', extensions: ['exe'] },
|
||||
{ name: 'Alle Dateien', extensions: ['*'] },
|
||||
],
|
||||
})
|
||||
return ergebnis.canceled || ergebnis.filePaths.length === 0 ? null : ergebnis.filePaths[0]
|
||||
})
|
||||
|
||||
// Einen Ordner im Explorer zeigen (5.5.0, aus ARM) — nur absolute Pfade,
|
||||
// und shell.openPath sagt selbst, wenn es ihn nicht gibt.
|
||||
ipcMain.handle('ordner-oeffnen', async (_ereignis, pfad: unknown) => {
|
||||
if (typeof pfad !== 'string' || !win32.isAbsolute(pfad)) return 'Kein gültiger Pfad.'
|
||||
protokoll.schreiben(`Ordner geöffnet: ${pfad}`)
|
||||
return await shell.openPath(pfad)
|
||||
})
|
||||
|
||||
ipcMain.handle('protokoll-oeffnen', async () => {
|
||||
return await shell.openPath(protokollOrdner)
|
||||
})
|
||||
|
||||
app.on('second-instance', () => {
|
||||
fensterZeigen()
|
||||
})
|
||||
@@ -339,6 +382,7 @@ function start(): void {
|
||||
|
||||
app.on('before-quit', () => {
|
||||
beendenGewollt = true
|
||||
protokoll.schreiben('Rippy beendet sich')
|
||||
kernVerwaltung.beenden()
|
||||
})
|
||||
|
||||
@@ -347,7 +391,7 @@ function start(): void {
|
||||
if (!ohneNebenwirkungen) {
|
||||
trayErstellen(fensterZeigen, beenden)
|
||||
}
|
||||
fenster = fensterErstellen()
|
||||
fenster = fensterErstellen(app.getPath('userData'))
|
||||
fensterVerdrahten()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user