feat(v5): eigenes Fenster ohne Windows-Rahmen + Layout skaliert mit
Ampel / ampel (push) Successful in 1m34s

Zwei Befunde des Commanders (01.09.2026):

1. "Momentan ist alles nach links gerueckt, nichts skaliert mit der
   fenstergroesse und es ist alles gequetscht."

   Ursache gefunden: <main> hatte WEDER Zentrierung NOCH Maximalbreite,
   die Karten darin aber einen harten Deckel von max-w-2xl (672 px). In
   einem 1280er Fenster klebte damit alles am linken Rand und rechts
   stand nichts. Behoben:
   * <main> und Kopfzeile teilen sich mx-auto max-w-[1600px] mit
     mitwachsendem Innenabstand — der Inhalt ist mittig und nutzt die
     Breite.
   * Der 672-px-Deckel der Einstellungs-Spalte ist weg. Drei
     Encoder-Felder stehen jetzt nebeneinander statt untereinander.
   * Die Sektionswahl der Einstellungen wird im schmalen Fenster zur
     Zeile ueber den Karten (lg:flex-row), statt eine 176-px-Spalte vom
     ohnehin knappen Platz abzuziehen.
   * Info-Kacheln der Laufwerks-Karte: zwei Spalten schmal, vier breit.
   * Startgroesse 1000x700 -> 1280x820, Mindestmass 720x480 -> 860x560.

2. "waere sogar richtig cool wenn du den rahmen wegbekommst"

   frame:false, und die Kopfzeile IST jetzt die Titelleiste:
   Minimieren / Maximieren / Schliessen sitzen oben rechts in
   Windows-Massen (46x32), gezogen wird an der Kopfzeile, Doppelklick
   maximiert. Groesse aendern bleibt moeglich — Electron setzt fuer
   rahmenlose Fenster unter Windows weiter WS_THICKFRAME (thickFrame,
   Standard true), die Kanten ziehen also wie gewohnt.

   Schliessen nimmt GENAU den Weg des alten Fensterkreuzes
   (fenster.close() -> der bestehende close-Horcher versteckt nur):
   Rippy lebt im Tray weiter, ein laufender Rip merkt davon nichts
   (Paragraph 4.1 Punkt 3).

   Die Ziehflaeche ist .ziehbar in stil.css; alles Bedienbare darin
   traegt .nicht-ziehbar, sonst verschluckt der Griff den Klick.
   fensterMaximiert im HauptStatus entscheidet ueber das Knopf-Symbol
   und wird auch bei Doppelklick/Tastenkuerzel nachgefuehrt (maximize-
   und unmaximize-Horcher).

Nachgemessen mit dem Smoke-Beweis: beweise/ui-513-uebersicht.png (kein
Rahmen, eigene Knoepfe, Inhalt ueber die volle Breite) und
beweise/ui-513-einstellungen.png (drei Encoder nebeneinander).

230 Tests gruen, Typpruefung sauber. Version 5.1.3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-01 18:07:45 +02:00
co-authored by Claude Opus 5
parent 0c38754c09
commit 827910fd63
12 changed files with 182 additions and 32 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

+20 -12
View File
@@ -1,17 +1,25 @@
Version 5.1.2
Version 5.1.3
Neu: Rippy sagt Bescheid, wenn ein Update da ist.
Bisher hat es sich still im Hintergrund aktualisiert — jetzt steht eine
Karte oben in der Übersicht, mit Versionsnummer und dieser Liste hier.
In den Einstellungen gibt es dazu den Knopf "Jetzt auf Updates prüfen".
Wer nicht warten will, startet über die Karte sofort neu.
Das Fenster gehört jetzt Rippy.
Der graue Windows-Rahmen ist weg. Minimieren, Maximieren und Schließen
sitzen oben rechts im Programm selbst, gezogen wird an der Kopfzeile —
Doppelklick darauf maximiert. Die Kanten lassen sich weiter ziehen wie
gewohnt. Schließen versteckt Rippy weiterhin nur im Infobereich, ein
laufender Rip merkt davon nichts.
Nichts klebt mehr links.
Die Seite ist jetzt mittig und wächst mit dem Fenster mit, statt auf
halber Breite abzubrechen. Im schmalen Fenster rutschen die Kacheln
untereinander, statt sich zu quetschen; die Einstellungen legen ihre
Abschnittswahl dann als Zeile über die Karten.
Aus 5.1.2: Rippy sagt Bescheid, wenn ein Update da ist.
Diese Karte hier ist der Beweis — mit Versionsnummer und dieser Liste.
In den Einstellungen gibt es den Knopf "Jetzt auf Updates prüfen".
Aus 5.1.1: Serien-Discs werden richtig erkannt.
Disc 2 von "Spartacus: Gods of the Arena" wurde als der Film "Spartacus"
von 1960 erkannt. Rippy fragt jetzt die Disc selbst: Ihr
Inhaltsverzeichnis nennt die Folgen, und daran erkennt Rippy eine Serie,
statt sie zu raten. Disc-Nummer und Folgenzahl stehen neben dem Titel.
Und wenn Rippy sich unsicher ist, sagt es das jetzt, statt sich sicher
zu irren.
Disc 2 von "Spartacus: Gods of the Arena" galt als der Film "Spartacus"
von 1960. Rippy fragt jetzt die Disc selbst: Ihr Inhaltsverzeichnis
nennt die Folgen. Disc-Nummer und Folgenzahl stehen neben dem Titel.
Unverändert: Updates warten immer, bis kein Rip mehr läuft.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "rippy",
"version": "5.1.2",
"version": "5.1.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rippy",
"version": "5.1.2",
"version": "5.1.3",
"license": "UNLICENSED",
"dependencies": {
"electron-updater": "^6.8.9",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "rippy",
"productName": "Rippy",
"version": "5.1.2",
"version": "5.1.3",
"description": "Rippy v5 — Disc-Ripping als eigenständiges Windows-Programm (KONZEPT-WINDOWS.md)",
"main": "out/haupt/index.js",
"author": "KrBrZ",
+5
View File
@@ -56,6 +56,11 @@ contextBridge.exposeInMainWorld('rippy', {
autostartSetzen(an: boolean): Promise<boolean> {
return ipcRenderer.invoke('autostart-setzen', an === true) as Promise<boolean>
},
/** Eigene Titelleiste (§ 4.2): minimieren | maximieren | schliessen.
* „schliessen" versteckt nur — Rippy lebt im Tray weiter. */
fensterSteuern(was: 'minimieren' | 'maximieren' | 'schliessen'): Promise<boolean> {
return ipcRenderer.invoke('fenster-steuern', String(was)) as Promise<boolean>
},
/** „Jetzt prüfen" (§ 6.8) — das Ergebnis kommt über den Haupt-Status. */
updatePruefen(): Promise<unknown> {
return ipcRenderer.invoke('update-pruefen') as Promise<unknown>
+97 -13
View File
@@ -83,6 +83,60 @@ const PHASEN_MONO: Record<RipStatus['phase'], string> = {
const LAUFENDE_PHASEN = ['liest-info', 'rippt', 'komprimiert', 'legt-ab']
// ── Eigene Fensterknöpfe (§ 4.2, seit 5.1.3) ────────────────────────────
// Bewusst schlicht und in Windows-Maßen (46 × 32) — wer sie sucht, findet
// sie dort, wo er sie erwartet. Sie sitzen in der Ziehfläche und müssen
// deshalb `nicht-ziehbar` sein, sonst verschluckt der Fenstergriff den Klick.
const IconMinimieren = (): React.JSX.Element => (
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
<path d="M0 5h10" stroke="currentColor" strokeWidth="1.1" />
</svg>
)
const IconMaximieren = (): React.JSX.Element => (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">
<rect x="0.6" y="0.6" width="8.8" height="8.8" stroke="currentColor" strokeWidth="1.1" />
</svg>
)
const IconWiederherstellen = (): React.JSX.Element => (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">
<rect x="0.6" y="2.6" width="6.8" height="6.8" stroke="currentColor" strokeWidth="1.1" />
<path d="M2.7 2.6V0.6h6.7v6.7h-2" stroke="currentColor" strokeWidth="1.1" />
</svg>
)
const IconSchliessen = (): React.JSX.Element => (
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true">
<path d="M0.6 0.6l8.8 8.8M9.4 0.6l-8.8 8.8" stroke="currentColor" strokeWidth="1.1" />
</svg>
)
function FensterKnopf({
titel,
aufKlick,
rot = false,
children,
}: {
titel: string
aufKlick: () => void
rot?: boolean
children: React.ReactNode
}) {
return (
<button
onClick={aufKlick}
title={titel}
aria-label={titel}
className={`flex h-8 w-[46px] items-center justify-center text-dunst transition-colors ${
rot ? 'hover:bg-rot hover:text-white' : 'hover:bg-white/10 hover:text-schnee'
}`}
>
{children}
</button>
)
}
function sicherheitsWort(confidence: number): string {
if (confidence >= 0.9) return 'sehr wahrscheinlich'
if (confidence >= 0.8) return 'wahrscheinlich'
@@ -427,7 +481,9 @@ function LaufwerkKachel({
</p>
)}
<div className="mt-3 grid max-w-2xl grid-cols-4 gap-2.5">
{/* Zwei Kacheln nebeneinander im schmalen Fenster, vier im
breiten — vorher immer vier, also im schmalen gequetscht. */}
<div className="mt-3 grid max-w-3xl grid-cols-2 gap-2.5 sm:grid-cols-4">
<InfoKachel label="MEDIUM" wert={TYP_NAMEN[geraet.typ]} />
<InfoKachel label="GRÖSSE" wert={geraet.groesseBytes > 0 ? gb(geraet.groesseBytes) : '—'} />
<InfoKachel
@@ -807,9 +863,12 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
if (ordner !== null) setzen('ablage', ordner)
}
// Schmales Fenster: die Sektionswahl legt sich als Zeile über die Karten.
// Ab lg steht sie wieder als mitlaufende Spalte daneben (Commander-Fund
// 01.09.2026: „alles gequetscht").
return (
<div className="flex gap-8">
<nav className="sticky top-4 flex h-fit w-44 flex-none flex-col gap-1">
<div className="flex flex-col gap-5 lg:flex-row lg:gap-8">
<nav className="flex flex-row flex-wrap gap-1 lg:sticky lg:top-4 lg:h-fit lg:w-44 lg:flex-none lg:flex-col">
{SEKTIONEN.map(([id, name]) => (
<button
key={id}
@@ -825,7 +884,9 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
))}
</nav>
<div className="min-w-0 max-w-2xl flex-1 space-y-5">
{/* Kein max-w mehr: Der 672-px-Deckel war der Grund, warum in einem
breiten Fenster alles links klebte und rechts nichts stand. */}
<div className="min-w-0 flex-1 space-y-5">
<SektionsKarte
id="ablage"
titel="ABLAGE"
@@ -1395,30 +1456,53 @@ export default function App() {
<div className="buehnenlicht pointer-events-none fixed inset-0" />
<div className="filmkorn pointer-events-none fixed inset-0" />
<header className="relative border-b border-white/8 px-7 py-3.5">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-3.5">
<IconDisc groesse={30} farbe="oklch(0.83 0.11 75)" />
<h1 className="font-display text-[25px] font-bold tracking-tight">Rippy</h1>
<span className="rounded-md border border-white/12 px-2 py-0.5 font-mono text-[10px] text-nebel">
{/* Die Kopfzeile IST die Titelleiste: Sie ist die Ziehfläche des
rahmenlosen Fensters (§ 4.2). Alles Bedienbare darin trägt
`nicht-ziehbar`, sonst schluckt der Griff den Klick. */}
<header className="ziehbar relative flex-none border-b border-white/8">
<div className="mx-auto flex w-full max-w-[1600px] items-center gap-3 py-2 pl-5 pr-0 sm:pl-7">
<div className="flex min-w-0 items-center gap-3">
<IconDisc groesse={28} farbe="oklch(0.83 0.11 75)" />
<h1 className="font-display text-[22px] font-bold tracking-tight sm:text-[25px]">Rippy</h1>
<span className="hidden rounded-md border border-white/12 px-2 py-0.5 font-mono text-[10px] text-nebel md:inline">
v{haupt?.version ?? '…'}
</span>
{einRipLaeuft && (
<span className="inline-flex items-center gap-2 rounded-md border border-rec/45 bg-rec/12 px-2.5 py-0.5 font-mono text-[10px] tracking-[0.15em] text-[oklch(0.83_0.12_50)]">
<span className="inline-flex flex-none items-center gap-2 rounded-md border border-rec/45 bg-rec/12 px-2.5 py-0.5 font-mono text-[10px] tracking-[0.15em] text-[oklch(0.83_0.12_50)]">
<span className="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-rec shadow-[0_0_9px_oklch(0.72_0.17_45/0.9)]" />
REC
</span>
)}
</div>
<nav className="flex gap-1.5">
<nav className="nicht-ziehbar ml-auto flex flex-wrap justify-end gap-1.5">
{knopf('uebersicht', 'Übersicht')}
{knopf('einstellungen', 'Einstellungen')}
{knopf('bibliothek', `Bibliothek${verbindung.bibliothek.length > 0 ? ` (${verbindung.bibliothek.length})` : ''}`)}
</nav>
<div className="nicht-ziehbar ml-1 flex flex-none items-center self-start">
<FensterKnopf titel="Minimieren" aufKlick={() => void window.rippy.fensterSteuern('minimieren')}>
<IconMinimieren />
</FensterKnopf>
<FensterKnopf
titel={haupt?.fensterMaximiert === true ? 'Wiederherstellen' : 'Maximieren'}
aufKlick={() => void window.rippy.fensterSteuern('maximieren')}
>
{haupt?.fensterMaximiert === true ? <IconWiederherstellen /> : <IconMaximieren />}
</FensterKnopf>
{/* Schließen versteckt nur — Rippy lebt im Tray weiter, ein
laufender Rip merkt davon nichts (§ 4.1 Punkt 3). */}
<FensterKnopf
titel="In den Infobereich (Rippy läuft weiter)"
rot
aufKlick={() => void window.rippy.fensterSteuern('schliessen')}
>
<IconSchliessen />
</FensterKnopf>
</div>
</div>
</header>
<main className="relative flex-1 space-y-7 p-7">
<main className="relative mx-auto w-full max-w-[1600px] flex-1 space-y-6 px-5 py-6 sm:px-7 sm:py-7">
{setupOffen && <ErstEinrichtung verbindung={verbindung} />}
{!setupOffen && bereich === 'uebersicht' && (
<>
+1
View File
@@ -11,6 +11,7 @@ declare global {
autostartSetzen(an: boolean): Promise<boolean>
updatePruefen(): Promise<unknown>
updateInstallieren(): Promise<{ ok: boolean; grund: string }>
fensterSteuern(was: 'minimieren' | 'maximieren' | 'schliessen'): Promise<boolean>
}
}
}
+12
View File
@@ -59,6 +59,18 @@
box-shadow: 0 0 8px oklch(0.8 0.12 70 / 0.45);
}
/* Eigene Titelleiste (§ 4.2): Das Fenster hat seit 5.1.3 keinen
Windows-Rahmen mehr (frame:false) — gezogen wird an der Kopfzeile.
`drag` macht eine Fläche zum Fenstergriff; ALLES Bedienbare darin
braucht `nicht-ziehbar`, sonst verschluckt der Griff den Klick.
Doppelklick auf den Griff maximiert, das macht Windows von selbst. */
.ziehbar {
-webkit-app-region: drag;
}
.nicht-ziehbar {
-webkit-app-region: no-drag;
}
/* Dezente Scrollbalken — helle Standardbalken zerschneiden die Bühne. */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: oklch(1 0 0 / 0.12); border-radius: 999px; border: 2px solid transparent; background-clip: content-box; }
@@ -191,6 +191,9 @@ export interface HauptStatus {
autostart: boolean
/** Selbst-Update (§ 6.8). */
update: UpdateStand
/** Für die eigene Titelleiste (§ 4.2, seit 5.1.3): Ist das Fenster
* gerade maximiert? Entscheidet nur, welches Symbol der Knopf zeigt. */
fensterMaximiert: boolean
}
/** Der Stand des Selbst-Updates (§ 6.8) — EINE Definition, die sich Haupt
+13 -4
View File
@@ -7,12 +7,21 @@ import { join } from 'node:path'
export function fensterErstellen(): BrowserWindow {
const fenster = new BrowserWindow({
width: 1000,
height: 700,
minWidth: 720,
minHeight: 480,
width: 1280,
height: 820,
minWidth: 860,
minHeight: 560,
show: true,
autoHideMenuBar: true,
// Eigene Titelleiste (Commander-Wunsch 01.09.2026): Der Windows-Rahmen
// ist weg, Minimieren/Maximieren/Schließen sitzen im UI und gezogen
// wird an der Kopfzeile (.ziehbar in fenster/stil.css).
//
// Wichtig: GRÖSSE ÄNDERN bleibt möglich. Electron setzt für rahmenlose
// Fenster unter Windows weiterhin WS_THICKFRAME (Option `thickFrame`,
// Standard true) — die Kanten lassen sich also weiter ziehen, und
// Doppelklick auf die Ziehfläche maximiert wie gewohnt.
frame: false,
// Dunkler Grund SOFORT — sonst blitzt beim Start ein weißes Rechteck,
// bevor React lädt. Das warme Bühnen-Schwarz des Kino-Mixes
// (oklch(0.13 0.012 60), siehe fenster/stil.css).
+28
View File
@@ -176,6 +176,9 @@ function start(): void {
kern: kernVerwaltung.zustand(),
autostart: autostartAktiv(),
update: updateStand(),
// Für die eigene Titelleiste: Das Knopf-Symbol muss wissen, ob das
// Fenster gerade maximiert ist.
fensterMaximiert: fenster !== null && !fenster.isDestroyed() && fenster.isMaximized(),
}
fenster.webContents.send('haupt-status', status)
}
@@ -189,6 +192,11 @@ function start(): void {
statusSenden()
smokePruefen()
})
// Die eigene Titelleiste zeichnet ihr Maximieren-Symbol nach diesem
// Zustand — auch wenn per Doppelklick oder Windows-Tastenkürzel
// maximiert wird, nicht nur über unseren Knopf.
fenster.on('maximize', () => statusSenden())
fenster.on('unmaximize', () => statusSenden())
// § 4.1 Punkt 3: Fenster zu, Rip läuft weiter — Rippy lebt im Tray.
fenster.on('close', (ereignis) => {
if (beendenGewollt) return
@@ -223,6 +231,26 @@ function start(): void {
}
})
// Eigene Titelleiste (§ 4.2): Das Fenster hat keinen Windows-Rahmen
// mehr, die drei Knöpfe sitzen im UI und melden sich hier.
ipcMain.handle('fenster-steuern', (_ereignis, was: unknown) => {
if (fenster === null || fenster.isDestroyed()) return false
if (was === 'minimieren') {
fenster.minimize()
} else if (was === 'maximieren') {
if (fenster.isMaximized()) fenster.unmaximize()
else fenster.maximize()
} else if (was === 'schliessen') {
// Genau der Weg des alten Fensterkreuzes: Der close-Horcher weiter
// unten versteckt nur — Rippy lebt im Tray weiter (§ 4.1 Punkt 3),
// ein laufender Rip darf davon nichts merken.
fenster.close()
} else {
return false
}
return fenster.isDestroyed() ? false : fenster.isMaximized()
})
// Der Knopf „Jetzt prüfen" (§ 6.8). Der Stand meldet sich über
// beiAenderung von selbst ans Fenster — hier gibt es nichts zurück.
ipcMain.handle('update-pruefen', () => {