feat(v5): Etappe W-5 — Oberflaeche: Tray, Toasts, Taskleiste, Einstellungen, Bibliothek
Ampel / ampel (push) Successful in 1m23s
Ampel / ampel (push) Successful in 1m23s
haupt/: tray.ts (Symbol mit Zustand, Kontextmenue, Fenster zu = weiter- laufen, Beenden nur noch gewollt), Windows-Toasts bei Disc erkannt / fertig / Fehler (Klick holt das Fenster), Taskleisten-Fortschritt (setProgressBar, Fehlermodus), echter Windows-Ordnerdialog per IPC. Eigenes Icon (bau/icon.png + tray.png, Disc-Optik). Kern: Einstellungen lesen/schreiben NUR ueber die eine Quelle (§ 6.7, Whitelist der Schluessel), Werkzeug-Auskunft (Katalog + gemessene HandBrake-Presets/Backends), Bibliothek in db.ts (Schema v2 — fingerprint, Titel, Groesse, Ort, Dauer), Duplikat-Erkennung beim Einlegen (§ 6.6: 'schon einmal gerippt am …' als disc-info + Toast), Pipeline traegt fertige Vorgaenge ein. Fenster: drei Bereiche (Uebersicht / Einstellungen / Bibliothek). Einstellungen: Ablage mit Durchsuchen-Dialog, Kompression je Disc-Typ (Preset-Liste VOM eigenen HandBrake gemessen, 'NICHT komprimieren' waehlbar), Sprachlisten, TMDb/OMDb-Keys, Medienserver, Werkzeug-Stand mit Klartext (gefunden/fehlt + Folgen). Bibliothek als Tabelle. Der Smoke-Screenshot zeigt nebenbei den Ernstfall in schoen: Das Laufwerk ist nach dem rc11-Vorfall inzwischen auch am Storage-Stack in 'unknown' gekippt — und die Kachel zeigt den ZUGRIFFS_GRUENDE-Klartext samt Abhilfe statt eines stummen Zustands. 165 Tests gruen, Smoke gruen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a9fa8fa2d8
commit
c78ff22ab7
@@ -1,18 +1,21 @@
|
||||
// Rippy v5 — Haupt-Prozess (KONZEPT-WINDOWS.md § 4.1).
|
||||
// Ablauf: Einzelinstanz-Sperre → Prozess-Leine → Kern starten → Fenster
|
||||
// öffnen → Fenster und Kern über einen MessagePort verdrahten.
|
||||
// öffnen → Fenster und Kern über einen MessagePort verdrahten. Seit W-5
|
||||
// mit Tray: Fenster zu heißt weiterlaufen (§ 4.1 Punkt 3), beendet wird
|
||||
// über das Tray-Menü; dazu Windows-Toasts und Taskleisten-Fortschritt (§ 7).
|
||||
//
|
||||
// Smoke-Modus (`electron . --smoke`): beweist das W-0-Fertigkriterium
|
||||
// („Ein Fenster geht auf, der Kern läuft") maschinell — alle drei Prozesse,
|
||||
// beide IPC-Wege und die Datenbank — und endet mit Code 0 (grün) oder 2.
|
||||
// RIPPY_SMOKE_BILD=<pfad.png> legt zusätzlich einen Screenshot ab.
|
||||
import { app, ipcMain, type BrowserWindow } from 'electron'
|
||||
import { app, dialog, ipcMain, Notification, type BrowserWindow } from 'electron'
|
||||
import { writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import type { HauptStatus, KernNachricht } from '../gemeinsam/nachrichten'
|
||||
import { fensterErstellen } from './fenster'
|
||||
import { KernVerwaltung } from './kernstart'
|
||||
import { leineSetzen, type LeineStatus } from './leine'
|
||||
import { trayErstellen, trayHinweis } from './tray'
|
||||
|
||||
const smokeModus = process.argv.includes('--smoke')
|
||||
|
||||
@@ -40,6 +43,7 @@ function start(): void {
|
||||
|
||||
let fenster: BrowserWindow | null = null
|
||||
let kernBereit: Extract<KernNachricht, { art: 'kern-bereit' }> | null = null
|
||||
let beendenGewollt = smokeModus
|
||||
|
||||
const smoke = {
|
||||
fenster: false,
|
||||
@@ -61,11 +65,60 @@ function start(): void {
|
||||
}
|
||||
statusSenden()
|
||||
smokePruefen()
|
||||
} else if (nachricht.art === 'rip-status') {
|
||||
ripStatusVerarbeiten(nachricht.status)
|
||||
} else if (nachricht.art === 'disc-info' && nachricht.info.titel.length > 0 && !smokeModus) {
|
||||
// Windows-Toast beim Erkennen (§ 7) — Klick holt das Fenster hoch.
|
||||
toast(
|
||||
'Disc erkannt',
|
||||
`${nachricht.info.titel}${nachricht.info.jahr !== null ? ` (${nachricht.info.jahr})` : ''} · ${Math.round(nachricht.info.confidence * 100)} %` +
|
||||
(nachricht.info.schonGerippt !== null
|
||||
? ` — schon einmal gerippt (${nachricht.info.schonGerippt.slice(0, 10)})`
|
||||
: ''),
|
||||
)
|
||||
}
|
||||
},
|
||||
() => statusSenden(),
|
||||
)
|
||||
|
||||
// § 7: Toasts, Taskleisten-Fortschritt und Tray-Zustand aus dem
|
||||
// Rip-Status — die Information, die man will, ohne das Fenster zu öffnen.
|
||||
function ripStatusVerarbeiten(status: Extract<KernNachricht, { art: 'rip-status' }>['status']): void {
|
||||
if (fenster !== null && !fenster.isDestroyed()) {
|
||||
if ((status.phase === 'rippt' || status.phase === 'komprimiert') && status.prozent >= 0) {
|
||||
fenster.setProgressBar(status.prozent / 100)
|
||||
} else if (status.phase === 'fehler') {
|
||||
fenster.setProgressBar(1, { mode: 'error' })
|
||||
} else {
|
||||
fenster.setProgressBar(-1)
|
||||
}
|
||||
}
|
||||
if (status.phase === 'rippt') {
|
||||
trayHinweis(`rippt Laufwerk ${status.id}${status.prozent >= 0 ? ` (${status.prozent} %)` : ''}`)
|
||||
} else if (status.phase === 'komprimiert') {
|
||||
trayHinweis(`komprimiert Laufwerk ${status.id}${status.prozent >= 0 ? ` (${status.prozent} %)` : ''}`)
|
||||
} else {
|
||||
trayHinweis('')
|
||||
}
|
||||
if (smokeModus) return
|
||||
if (status.phase === 'fertig') {
|
||||
toast('Rippy ist fertig', status.text)
|
||||
} else if (status.phase === 'fehler') {
|
||||
toast('Rippy: Fehler', status.text)
|
||||
}
|
||||
}
|
||||
|
||||
function toast(titel: string, text: string): void {
|
||||
try {
|
||||
if (!Notification.isSupported()) return
|
||||
const meldung = new Notification({ title: titel, body: text.slice(0, 200) })
|
||||
meldung.on('click', () => fensterZeigen())
|
||||
meldung.show()
|
||||
} catch (fehler) {
|
||||
console.error(`[haupt] Toast fehlgeschlagen: ${String(fehler)}`)
|
||||
}
|
||||
}
|
||||
|
||||
function statusSenden(): void {
|
||||
if (fenster === null || fenster.isDestroyed()) return
|
||||
const status: HauptStatus = {
|
||||
@@ -76,6 +129,39 @@ function start(): void {
|
||||
fenster.webContents.send('haupt-status', status)
|
||||
}
|
||||
|
||||
function fensterVerdrahten(): void {
|
||||
if (fenster === null) return
|
||||
fenster.webContents.on('did-finish-load', () => {
|
||||
smoke.fenster = true
|
||||
// Verdrahtung NACH dem Laden: Die Seite lauscht erst dann auf den Port.
|
||||
kernVerwaltung.fensterVerbinden(fenster!.webContents)
|
||||
statusSenden()
|
||||
smokePruefen()
|
||||
})
|
||||
// § 4.1 Punkt 3: Fenster zu, Rip läuft weiter — Rippy lebt im Tray.
|
||||
fenster.on('close', (ereignis) => {
|
||||
if (beendenGewollt) return
|
||||
ereignis.preventDefault()
|
||||
fenster?.hide()
|
||||
})
|
||||
}
|
||||
|
||||
function fensterZeigen(): void {
|
||||
if (fenster === null || fenster.isDestroyed()) {
|
||||
fenster = fensterErstellen()
|
||||
fensterVerdrahten()
|
||||
return
|
||||
}
|
||||
if (fenster.isMinimized()) fenster.restore()
|
||||
fenster.show()
|
||||
fenster.focus()
|
||||
}
|
||||
|
||||
function beenden(): void {
|
||||
beendenGewollt = true
|
||||
app.quit()
|
||||
}
|
||||
|
||||
// Kurze Rückmeldungen aus dem Fenster (Brücke: rippy.melden). Im
|
||||
// Smoke-Lauf ist 'kern-pong' der Beweis, dass der MessagePort
|
||||
// Fenster ↔ Kern in BEIDE Richtungen trägt.
|
||||
@@ -86,33 +172,39 @@ function start(): void {
|
||||
}
|
||||
})
|
||||
|
||||
app.on('second-instance', () => {
|
||||
if (fenster !== null && !fenster.isDestroyed()) {
|
||||
if (fenster.isMinimized()) fenster.restore()
|
||||
fenster.focus()
|
||||
}
|
||||
// Echter Windows-Ordnerdialog (§ 7) — der Nutzer sieht seine
|
||||
// Netzlaufwerke und Schnellzugriffe statt eines nachgebauten Browsers.
|
||||
ipcMain.handle('ordner-waehlen', async (_ereignis, titel: unknown) => {
|
||||
if (fenster === null) return null
|
||||
const ergebnis = await dialog.showOpenDialog(fenster, {
|
||||
title: typeof titel === 'string' && titel.length > 0 ? titel : 'Ordner wählen',
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
})
|
||||
return ergebnis.canceled || ergebnis.filePaths.length === 0 ? null : ergebnis.filePaths[0]
|
||||
})
|
||||
|
||||
// W-0: Fenster zu = Rippy zu. Erst W-5 bringt das Tray-Symbol — dann lebt
|
||||
// Rippy nach dem Schließen im Hintergrund weiter (§ 4.1, Punkt 3).
|
||||
app.on('second-instance', () => {
|
||||
fensterZeigen()
|
||||
})
|
||||
|
||||
// Mit Tray beendet ein geschlossenes Fenster Rippy NICHT mehr — es läuft
|
||||
// im Hintergrund weiter. Beendet wird über das Tray-Menü.
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit()
|
||||
if (beendenGewollt) app.quit()
|
||||
})
|
||||
|
||||
app.on('before-quit', () => {
|
||||
beendenGewollt = true
|
||||
kernVerwaltung.beenden()
|
||||
})
|
||||
|
||||
void app.whenReady().then(() => {
|
||||
kernVerwaltung.starten()
|
||||
if (!smokeModus) {
|
||||
trayErstellen(fensterZeigen, beenden)
|
||||
}
|
||||
fenster = fensterErstellen()
|
||||
fenster.webContents.on('did-finish-load', () => {
|
||||
smoke.fenster = true
|
||||
// Verdrahtung NACH dem Laden: Die Seite lauscht erst dann auf den Port.
|
||||
kernVerwaltung.fensterVerbinden(fenster!.webContents)
|
||||
statusSenden()
|
||||
smokePruefen()
|
||||
})
|
||||
fensterVerdrahten()
|
||||
})
|
||||
|
||||
let smokeAbgeschlossen = false
|
||||
|
||||
Reference in New Issue
Block a user