feat(v5): Windows-Rand weg, Hardware-Empfehlung sichtbar, Wizard geradegezogen
Ampel / ampel (push) Successful in 1m35s
Ampel / ampel (push) Successful in 1m35s
Vier Punkte des Commanders (01.09.2026):
1. "Ich hab noch den rand ich glaube der kommt aber von windows selbst"
Stimmt. Ein rahmenloses Fenster behaelt unter Windows 11 den DWM-Rand
— eine feine Linie, die der Fenstermanager zeichnet, nicht die Seite;
mit CSS ist da nichts zu holen. Neu fensterrand.ts:
DwmSetWindowAttribute mit DWMWA_BORDER_COLOR = DWMWA_COLOR_NONE.
Gemessen: HRESULT 0, Windows nimmt es an. Runde Ecken bleiben (sie
sind unter Windows 11 das Uebliche); ein Schalter fuer kantig ist da.
Aeltere Windows antworten mit Fehler — der wird als Wert
zurueckgegeben und protokolliert, nicht verschluckt (R4); dort gibt es
den Rand schlicht nicht.
Das ist der DRITTE erlaubte koffi-Ort. Waechter R1 kennt jetzt drei
statt zwei Dateien; die Regel selbst ("Win32 nur an benannten Orten")
bleibt und haelt alles andere weiter dicht.
2. "Die alte Version konnte automatisch anhand der gescannten hardware
encoder das beste preset waehlen."
Das KANN diese auch — presetEmpfehlung() waehlt nach den gemessenen
Backends (VCN -> NVENC -> QSV -> CPU) und laeuft seit dem Kino-Mix.
Man sah es nur nirgends ausser als Wort "automatisch:" in einem
Auswahlfeld. Die Erst-Einrichtung zeigt jetzt die gemessenen Encoder
als Badges und was daraus fuer DVD, Blu-ray und 4K wird — plus eine
ehrliche Warnung, wenn nur CPU-Encoder da sind.
3. Feld "Update-Adresse" ist raus. Der Schluessel updateUrl gilt
weiterhin (Datenbank), Paragraph 6.8 prueft ihn wie zuvor.
4. Erst-Einrichtung geprueft. Gefunden und behoben:
* "Dieses Setup stellt vier Dinge ein" — es stellte ZWEI ein.
* Der Ablage-Schritt zeigte eine LEERE Zeile, wenn nichts gewaehlt
war. Es gibt aber eine Vorgabe (Videos\Rippy im Benutzerprofil) —
die steht jetzt da.
* Kein Wort zum TMDb-Schluessel, obwohl Schritt 1 "Rippy erkennt, was
drauf ist" verspricht. Ohne Schluessel heissen Filme wie das
Disc-Label. Jetzt ein eigener Schritt mit Eingabefeld.
* Rohe HTML-Checkbox statt des Schalters aus dem Kino-Mix.
* Kein Hinweis, dass Rippy im Infobereich weiterlebt.
Neu sechs Schritte, drei Fragen — und die Zahl im Text stimmt.
Dazu: #einrichtung als Beweis-Anker (RIPPY_START_BEREICH), sonst ist
der Wizard nach dem ersten Start nicht mehr zu fotografieren und
damit nicht mehr pruefbar. Beweis: beweise/ui-515-wizard.png
237 Tests gruen, Typpruefung sauber. Version 5.1.5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
45402e887e
commit
663127f854
@@ -1079,12 +1079,12 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
|
||||
aufWechsel={(an) => void window.rippy.autostartSetzen(an)}
|
||||
label="Rippy mit Windows starten (Autostart im Benutzerkonto — kein Dienst, keine Adminrechte)"
|
||||
/>
|
||||
<Feld
|
||||
name="Update-Adresse"
|
||||
hinweis="leer = Standard; NUR https:// wird angenommen"
|
||||
wert={werte['updateUrl'] ?? ''}
|
||||
aufAendern={(w) => setzen('updateUrl', w)}
|
||||
/>
|
||||
{/* Das Feld „Update-Adresse" ist am 01.09.2026 auf Zuruf des
|
||||
Commanders verschwunden: Updates kommen immer vom Gitea, ein
|
||||
Eingabefeld dafür war nur Ballast. Der Einstellungs-Schlüssel
|
||||
`updateUrl` gilt weiterhin — wer ihn wirklich braucht, setzt
|
||||
ihn in der Datenbank, und § 6.8 prüft ihn wie zuvor (nur
|
||||
https://). */}
|
||||
{haupt !== null && (
|
||||
<div className="rounded-xl border border-white/8 bg-buehne/60 p-3 font-mono text-[11px] leading-5 text-nebel">
|
||||
<p>Selbst-Update: {haupt.update.text}</p>
|
||||
@@ -1261,10 +1261,24 @@ function ErstEinrichtung({ verbindung }: { verbindung: Verbindung }) {
|
||||
},
|
||||
]
|
||||
|
||||
const schritte = ['Willkommen', 'Prüfung', 'Ordner', 'Automatik', 'Fertig']
|
||||
// Was Rippy an DIESER Maschine gemessen hat — die Grundlage der
|
||||
// Preset-Empfehlung. Stand bisher nur in den Einstellungen und war
|
||||
// deshalb unsichtbar (Commander-Fund 01.09.2026: „die alte Version konnte
|
||||
// automatisch anhand der gescannten hardware encoder das beste preset
|
||||
// wählen"). Sie konnte es, diese auch — man sah es nur nirgends.
|
||||
const backends = werkzeuge?.backends ?? []
|
||||
const presets = werkzeuge?.presets ?? []
|
||||
const nurCpu = backends.length > 0 && backends.every((b) => b.startsWith('cpu'))
|
||||
const EMPFEHLUNGEN: ReadonlyArray<readonly [string, string]> = [
|
||||
['DVD', 'dvd'],
|
||||
['Blu-ray', 'bluray'],
|
||||
['4K-UHD', 'uhd'],
|
||||
]
|
||||
|
||||
const schritte = ['Willkommen', 'Prüfung', 'Ordner', 'Qualität', 'Titel', 'Fertig']
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-xl space-y-7 pt-6">
|
||||
<div className="mx-auto max-w-2xl space-y-7 pt-6">
|
||||
<div className="flex gap-2">
|
||||
{schritte.map((name, i) => (
|
||||
<span
|
||||
@@ -1282,7 +1296,10 @@ function ErstEinrichtung({ verbindung }: { verbindung: Verbindung }) {
|
||||
<p>Rippy sichert deine DVDs, Blu-rays und Audio-CDs — erst verlustfrei, dann komprimiert.</p>
|
||||
<p>Disc rein genügt: Rippy erkennt, was drauf ist, und sagt Bescheid.</p>
|
||||
<p>Fertige Filme landen so abgelegt, dass Jellyfin, Emby, Kodi und Plex sie sofort erkennen.</p>
|
||||
<p>Dieses Setup stellt vier Dinge ein und dauert eine Minute.</p>
|
||||
<p>
|
||||
Rippy prüft gleich seine Werkzeuge und fragt dich danach drei Dinge: wohin, wie schnell, und
|
||||
woher die Titel kommen. Dauert eine Minute — ändern kannst du alles später.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1302,7 +1319,16 @@ function ErstEinrichtung({ verbindung }: { verbindung: Verbindung }) {
|
||||
{schritt === 2 && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-dunst">Wohin sollen fertige Filme, Musik und Abbilder?</p>
|
||||
<p className="break-all font-mono text-xs text-nebel">{verbindung.einstellungen['ablage'] ?? ''}</p>
|
||||
{(verbindung.einstellungen['ablage'] ?? '').length > 0 ? (
|
||||
<p className="break-all font-mono text-xs text-gruen">{verbindung.einstellungen['ablage']}</p>
|
||||
) : (
|
||||
// Vorher stand hier eine LEERE Zeile — man sah nicht, wohin
|
||||
// Rippy legt, wenn man nichts wählt. Es gibt aber eine Vorgabe
|
||||
// (kern/index.ts: Videos\Rippy im Benutzerprofil).
|
||||
<p className="font-mono text-xs text-nebel">
|
||||
noch nichts gewählt — dann nimmt Rippy „Videos\Rippy" in deinem Benutzerordner
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
onClick={() =>
|
||||
void window.rippy.ordnerWaehlen('Ablage-Ordner wählen').then((ordner) => {
|
||||
@@ -1313,31 +1339,98 @@ function ErstEinrichtung({ verbindung }: { verbindung: Verbindung }) {
|
||||
>
|
||||
Ordner wählen … (auch Netzlaufwerke)
|
||||
</button>
|
||||
<p className="text-xs text-nebel">
|
||||
Rippy baut darin selbst die Struktur: Filme, Musik, ISO und roh. Ein Netzlaufwerk geht auch.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{schritt === 3 && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-dunst">Wie viel soll Rippy allein entscheiden?</p>
|
||||
<label className="flex items-center gap-2 text-xs text-dunst">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={verbindung.einstellungen['transcodeEnabled'] !== 'false'}
|
||||
onChange={(e) => einstellungSetzen('transcodeEnabled', e.target.checked ? 'true' : 'false')}
|
||||
className="accent-[oklch(0.83_0.11_75)]"
|
||||
/>
|
||||
Nach dem Rip automatisch komprimieren (je Disc-Typ später fein einstellbar)
|
||||
</label>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-dunst">
|
||||
Rippy sichert eine Disc immer erst verlustfrei. Danach kann es eine kleinere Fassung
|
||||
berechnen — die kostet Zeit, spart aber viel Platz.
|
||||
</p>
|
||||
<Schalter
|
||||
an={verbindung.einstellungen['transcodeEnabled'] !== 'false'}
|
||||
aufWechsel={(an) => einstellungSetzen('transcodeEnabled', an ? 'true' : 'false')}
|
||||
label="Nach dem Rip automatisch komprimieren"
|
||||
/>
|
||||
|
||||
{/* Die Hardware-Empfehlung — gemessen, nicht geraten. */}
|
||||
<div className="rounded-xl border border-white/8 bg-samt/50 p-3">
|
||||
{werkzeuge === null ? (
|
||||
<p className="font-mono text-xs text-nebel">Encoder werden gemessen …</p>
|
||||
) : backends.length === 0 ? (
|
||||
<p className="font-mono text-xs text-warn">
|
||||
Keine Encoder gemessen — ohne HandBrakeCLI bleibt es beim verlustfreien Rip.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="font-mono text-[10px] tracking-wider text-nebel">
|
||||
AUF DIESER MASCHINE GEMESSEN:
|
||||
</span>
|
||||
{backends.map((backend) => (
|
||||
<Badge key={backend} text={backend} ton={backend.startsWith('cpu') ? 'grau' : 'blau'} />
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-dunst">
|
||||
Danach wählt Rippy von selbst — Grafikkarte vor CPU, wo es eine gibt:
|
||||
</p>
|
||||
<div className="mt-2 grid gap-2 sm:grid-cols-3">
|
||||
{EMPFEHLUNGEN.map(([name, typ]) => (
|
||||
<div key={typ} className="rounded-lg border border-white/8 bg-buehne/60 p-2">
|
||||
<p className="font-mono text-[10px] tracking-wider text-nebel">{name}</p>
|
||||
<p className="mt-0.5 font-mono text-xs text-gold-hell">
|
||||
{presetEmpfehlung(typ, backends, presets) || DEFAULT_HB_PRESET}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{nurCpu && (
|
||||
<p className="mt-2 text-xs text-warn">
|
||||
Nur CPU-Encoder gefunden — das Komprimieren dauert dann deutlich länger. Der
|
||||
verlustfreie Rip ist davon nicht betroffen.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-nebel">
|
||||
Alles Weitere — Presets je Disc-Typ, Sprachen, Medienserver, API-Schlüssel — steht unter
|
||||
„Einstellungen".
|
||||
Du musst hier nichts wählen. Unter „Einstellungen → Kompression" kannst du je Disc-Typ ein
|
||||
eigenes Preset setzen — deine Wahl schlägt die Empfehlung immer.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{schritt === 4 && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-dunst">
|
||||
Woher sollen Titel, Jahr, Poster und Beschreibung kommen? Dafür braucht Rippy einen
|
||||
kostenlosen TMDb-Schlüssel.
|
||||
</p>
|
||||
<Feld
|
||||
name="TMDb-API-Key"
|
||||
hinweis="kostenlos auf themoviedb.org → Einstellungen → API; in zwei Minuten geholt"
|
||||
wert={verbindung.einstellungen['tmdbApiKey'] ?? ''}
|
||||
aufAendern={(w) => einstellungSetzen('tmdbApiKey', w)}
|
||||
/>
|
||||
<p className="text-xs text-nebel">
|
||||
Ohne Schlüssel rippt Rippy trotzdem — die Filme heißen dann so, wie die Disc sich selbst
|
||||
nennt, und es gibt keine Poster. Nachtragen geht jederzeit unter „Einstellungen →
|
||||
Metadaten".
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{schritt === 5 && (
|
||||
<div className="space-y-3 text-sm text-dunst">
|
||||
<p>Das war's. Disc einlegen — Rippy meldet sich.</p>
|
||||
<p className="text-xs text-nebel">
|
||||
Rippy lebt im Infobereich neben der Uhr: Fenster zu heißt weiterlaufen, beendet wird über
|
||||
das Tray-Menü.
|
||||
</p>
|
||||
{makemkvFehlt && (
|
||||
<p className="text-xs text-rot">
|
||||
Denk an MakeMKV: Ohne makemkvcon bleibt der Video-Weg zu (Einstellungen → Werkzeuge zeigt
|
||||
@@ -1426,8 +1519,13 @@ export default function App() {
|
||||
|
||||
// § 6.9: Die Erst-Einrichtung läuft NACH der Installation, im Programm —
|
||||
// solange sie nicht abgeschlossen ist, ist sie die ganze Ansicht.
|
||||
//
|
||||
// `#einrichtung` erzwingt sie (RIPPY_START_BEREICH) — wie #einstellungen
|
||||
// und #bibliothek. Ohne diesen Anker wäre der Wizard nach dem ersten
|
||||
// Start nicht mehr zu fotografieren und damit nicht mehr zu prüfen.
|
||||
const setupOffen =
|
||||
Object.keys(verbindung.einstellungen).length > 0 && verbindung.einstellungen['setup.done'] !== 'true'
|
||||
window.location.hash === '#einrichtung' ||
|
||||
(Object.keys(verbindung.einstellungen).length > 0 && verbindung.einstellungen['setup.done'] !== 'true')
|
||||
|
||||
const knopf = (ziel: Bereich, text: string): React.JSX.Element => (
|
||||
<button
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// „Zustand merken" (Position/Größe) kommt mit den Einstellungen im UI (W-5).
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { join } from 'node:path'
|
||||
import { fensterrandEntfernen } from './fensterrand'
|
||||
|
||||
export function fensterErstellen(): BrowserWindow {
|
||||
const fenster = new BrowserWindow({
|
||||
@@ -39,6 +40,12 @@ export function fensterErstellen(): BrowserWindow {
|
||||
// (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}`)
|
||||
|
||||
const devUrl = process.env['ELECTRON_RENDERER_URL']
|
||||
const startBereich = process.env['RIPPY_START_BEREICH'] ?? ''
|
||||
if (devUrl !== undefined && devUrl.length > 0) {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Den Windows-11-Fensterrand loswerden.
|
||||
//
|
||||
// Commander-Fund (01.09.2026): „Ich hab noch den rand ich glaube der kommt
|
||||
// aber von windows selbst oder?" — Genau so ist es. Ein rahmenloses Fenster
|
||||
// (`frame: false`) behält unter Windows 11 trotzdem den DWM-Rand: eine
|
||||
// hauchfeine Linie in der Systemfarbe, gezeichnet vom Fenstermanager, nicht
|
||||
// von der Seite. Mit CSS ist da nichts zu holen, das liegt außerhalb des
|
||||
// Fensterinhalts.
|
||||
//
|
||||
// DwmSetWindowAttribute kennt dafür zwei Schalter (dwmapi.h, beide erst ab
|
||||
// Windows 11 Build 22000):
|
||||
// DWMWA_BORDER_COLOR (34) = DWMWA_COLOR_NONE (0xFFFFFFFE)
|
||||
// DWMWA_WINDOW_CORNER_PREFERENCE (33) = DWMWCP_DONOTROUND (1)
|
||||
//
|
||||
// Ältere Windows antworten mit einem Fehler-HRESULT. Das ist KEIN Problem
|
||||
// und wird als Wert zurückgegeben, nicht verschluckt (R4): Unter Windows 10
|
||||
// gibt es diesen Rand schlicht nicht.
|
||||
//
|
||||
// Dies ist neben `leine.ts` und `kern/laufwerk/win32.ts` der DRITTE erlaubte
|
||||
// koffi-Ort; der Wächter-Test R1 kennt genau diese drei. Begründung wie
|
||||
// dort: ein Win32-Aufruf, der nirgendwo sonst hingehört — und er MUSS im
|
||||
// Haupt liegen, weil nur dort das Fensterhandle existiert.
|
||||
import koffi from 'koffi'
|
||||
|
||||
const DWMWA_WINDOW_CORNER_PREFERENCE = 33
|
||||
const DWMWA_BORDER_COLOR = 34
|
||||
/** „Zeichne gar keinen Rand" — nicht zu verwechseln mit Schwarz. */
|
||||
const DWMWA_COLOR_NONE = 0xfffffffe
|
||||
const DWMWCP_DONOTROUND = 1
|
||||
|
||||
export interface RandErgebnis {
|
||||
ok: boolean
|
||||
/** '' wenn alles saß; sonst der HRESULT-Grund im Klartext. */
|
||||
fehler: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Rand entfernen. `eckenKantig` schaltet zusätzlich die runden Ecken ab —
|
||||
* standardmäßig AUS, weil runde Ecken unter Windows 11 das Übliche sind
|
||||
* und Rippy sich nicht fremder anfühlen soll, als es muss.
|
||||
*
|
||||
* @param handle Das native Fensterhandle (`BrowserWindow.getNativeWindowHandle()`).
|
||||
*/
|
||||
export function fensterrandEntfernen(handle: Buffer, eckenKantig = false): RandErgebnis {
|
||||
if (handle.length < 8) return { ok: false, fehler: 'Fensterhandle unbrauchbar' }
|
||||
try {
|
||||
const dwmapi = koffi.load('dwmapi.dll')
|
||||
// Das Handle steckt als Zeiger IM Puffer. Auf x64 wandert ein Zeiger im
|
||||
// selben Register wie ein uint64 — deshalb ist das hier gefahrlos.
|
||||
const DwmSetWindowAttribute = dwmapi.func(
|
||||
'int32_t __stdcall DwmSetWindowAttribute(uint64_t hwnd, uint32_t dwAttribute, void *pvAttribute, uint32_t cbAttribute)',
|
||||
)
|
||||
const hwnd = handle.readBigUInt64LE(0)
|
||||
|
||||
const farbe = Buffer.alloc(4)
|
||||
farbe.writeUInt32LE(DWMWA_COLOR_NONE, 0)
|
||||
const randErgebnis = DwmSetWindowAttribute(hwnd, DWMWA_BORDER_COLOR, farbe, 4)
|
||||
|
||||
let eckenErgebnis = 0
|
||||
if (eckenKantig) {
|
||||
const ecken = Buffer.alloc(4)
|
||||
ecken.writeUInt32LE(DWMWCP_DONOTROUND, 0)
|
||||
eckenErgebnis = DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, ecken, 4)
|
||||
}
|
||||
|
||||
if (randErgebnis !== 0 || eckenErgebnis !== 0) {
|
||||
// Ein negatives HRESULT heißt hier fast immer: älteres Windows, das
|
||||
// diese Schalter nicht kennt. Rippy läuft weiter, nur mit Rand.
|
||||
return {
|
||||
ok: false,
|
||||
fehler: `DwmSetWindowAttribute: HRESULT 0x${(randErgebnis >>> 0).toString(16)} (Rand) / 0x${(eckenErgebnis >>> 0).toString(16)} (Ecken) — vermutlich Windows 10`,
|
||||
}
|
||||
}
|
||||
return { ok: true, fehler: '' }
|
||||
} catch (fehler) {
|
||||
return { ok: false, fehler: `koffi/dwmapi nicht verfügbar: ${String(fehler)}` }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user