feat(v5): Einstellungen als erklärende Karten-Seite — Version 5.1.0
Ampel / ampel (push) Successful in 1m35s
Ampel / ampel (push) Successful in 1m35s
Der Einstellungs-Tab im Kino-Mix, statt magerer Feldliste: links eine Sektions-Navigation, rechts Karten mit je einem Satz Einordnung — Ablage (mit Struktur-Vorschau Filme/Musik/ISO/roh), Kompression (Schalter, gemessene Encoder als Badges, Empfehlungs-Erklärung), Metadaten (Key-Quellen im Klartext, hinterlegt/fehlt-Punkte), MakeMKV (eigene Karte: Live-Schlüsselstand, Update- und Prüf-Knopf, Dauerlizenz), Medienserver, Programm (Schalter, Update-Stand, Versionszeile) und Werkzeuge — NEU mit den drei „Eigener Pfad'-Feldern (werkzeug.*, vom Katalog seit je unterstützt, bisher ohne UI). Dazu: #einstellungen/#bibliothek als Startanker (RIPPY_START_BEREICH), RIPPY_SMOKE_WARTE_MS für Bild-Beweise, die auf späte Kern-Antworten warten (Einstellungen+Encoder-Messung kommen nach den 750 ms). Version 5.1.0 — erste Fassung ohne Vorab-Suffix, der Bau erzeugt damit erstmals latest.yml (der Update-Kanal aus § 6.8). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c12b1be448
commit
b64ddf5f06
Binary file not shown.
|
After Width: | Height: | Size: 282 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rippy",
|
||||
"productName": "Rippy",
|
||||
"version": "5.0.0-w0",
|
||||
"version": "5.1.0",
|
||||
"description": "Rippy v5 — Disc-Ripping als eigenständiges Windows-Programm (KONZEPT-WINDOWS.md)",
|
||||
"main": "out/haupt/index.js",
|
||||
"author": "KrBrZ",
|
||||
|
||||
@@ -609,6 +609,59 @@ function Kicker({ text }: { text: string }) {
|
||||
return <h2 className="font-mono text-[10px] font-semibold tracking-[0.22em] text-gold/80">{text}</h2>
|
||||
}
|
||||
|
||||
/** Ein richtiger Schalter statt einer nackten Checkbox. */
|
||||
function Schalter({ an, aufWechsel, label }: { an: boolean; aufWechsel: (an: boolean) => void; label: string }) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => aufWechsel(!an)}
|
||||
className="flex items-center gap-3 text-left"
|
||||
role="switch"
|
||||
aria-checked={an}
|
||||
>
|
||||
<span
|
||||
className={`relative inline-block h-5 w-9 flex-none rounded-full transition-colors ${
|
||||
an ? 'bg-gradient-to-b from-gold-hell to-gold-tief' : 'bg-white/12'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 h-4 w-4 rounded-full bg-schnee shadow transition-all ${an ? 'left-[18px]' : 'left-0.5'}`}
|
||||
/>
|
||||
</span>
|
||||
<span className="text-xs leading-5 text-dunst">{label}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
/** Eine Einstellungs-Karte: Kicker, ein Satz Einordnung, dann die Felder. */
|
||||
function SektionsKarte({
|
||||
id,
|
||||
titel,
|
||||
intro,
|
||||
children,
|
||||
}: {
|
||||
id: string
|
||||
titel: string
|
||||
intro: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<section id={id} className="scroll-mt-4 rounded-2xl border border-white/10 bg-karte/50 p-5">
|
||||
<Kicker text={titel} />
|
||||
<p className="mt-1.5 max-w-xl text-xs leading-5 text-nebel">{intro}</p>
|
||||
<div className="mt-4 space-y-3">{children}</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
/** Kleiner Status-Punkt für Schlüssel-Felder: hinterlegt oder leer. */
|
||||
function HinterlegtPunkt({ wert }: { wert: string }) {
|
||||
return (
|
||||
<span className={`ml-2 font-mono text-[10px] ${wert.length > 0 ? 'text-gruen' : 'text-nebel'}`}>
|
||||
{wert.length > 0 ? '● hinterlegt' : '○ fehlt'}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function Feld({
|
||||
name,
|
||||
wert,
|
||||
@@ -672,12 +725,64 @@ function PresetWahl({
|
||||
)
|
||||
}
|
||||
|
||||
/** Eine Werkzeug-Zeile: Fundort in Klartext plus der eigene Pfad, der
|
||||
* die automatische Suche übersteuert (Katalog-Schritt 1). */
|
||||
function WerkzeugZeile({
|
||||
name,
|
||||
pfad,
|
||||
fehlt,
|
||||
pflicht,
|
||||
schluessel,
|
||||
wert,
|
||||
aufAendern,
|
||||
}: {
|
||||
name: string
|
||||
pfad: string | null
|
||||
fehlt: string
|
||||
pflicht: boolean
|
||||
schluessel: string
|
||||
wert: string
|
||||
aufAendern: (schluessel: string, wert: string) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border border-white/8 bg-samt/50 p-3">
|
||||
<p className={`break-all font-mono text-xs ${pfad === null ? 'text-nebel' : pfad.length > 0 ? 'text-gruen' : pflicht ? 'text-rot' : 'text-warn'}`}>
|
||||
{name}: {pfad === null ? 'wird gesucht …' : pfad.length > 0 ? pfad : fehlt}
|
||||
</p>
|
||||
<Feld
|
||||
name="Eigener Pfad"
|
||||
hinweis="leer = automatische Suche; ein eingetragener Pfad gilt exakt (Tippfehler werden als FEHLT sichtbar)"
|
||||
wert={wert}
|
||||
aufAendern={(w) => aufAendern(schluessel, w)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: HauptStatus | null }) {
|
||||
const werte = verbindung.einstellungen
|
||||
const presets = verbindung.werkzeuge?.presets ?? []
|
||||
const backends = verbindung.werkzeuge?.backends ?? []
|
||||
const setzen = einstellungSetzen
|
||||
const empfehlungFuer = (typ: string): string =>
|
||||
presetEmpfehlung(typ, verbindung.werkzeuge?.backends ?? [], presets)
|
||||
const s = verbindung.schluessel
|
||||
const b = verbindung.beschaffung
|
||||
const empfehlungFuer = (typ: string): string => presetEmpfehlung(typ, backends, presets)
|
||||
const [aktiv, setAktiv] = useState('ablage')
|
||||
|
||||
const SEKTIONEN: Array<[string, string]> = [
|
||||
['ablage', 'Ablage'],
|
||||
['kompression', 'Kompression'],
|
||||
['metadaten', 'Metadaten'],
|
||||
['makemkv', 'MakeMKV'],
|
||||
['medienserver', 'Medienserver'],
|
||||
['programm', 'Programm'],
|
||||
['werkzeuge', 'Werkzeuge'],
|
||||
]
|
||||
|
||||
const springe = (id: string): void => {
|
||||
setAktiv(id)
|
||||
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
|
||||
const ordnerWaehlen = async (): Promise<void> => {
|
||||
const ordner = await window.rippy.ordnerWaehlen('Ablage-Ordner wählen')
|
||||
@@ -685,9 +790,29 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl space-y-7">
|
||||
<section className="space-y-3">
|
||||
<Kicker text="ABLAGE" />
|
||||
<div className="flex gap-8">
|
||||
<nav className="sticky top-4 flex h-fit w-44 flex-none flex-col gap-1">
|
||||
{SEKTIONEN.map(([id, name]) => (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => springe(id)}
|
||||
className={`rounded-lg px-3 py-1.5 text-left text-[13px] ${
|
||||
aktiv === id
|
||||
? 'border border-gold/40 bg-gold/12 font-semibold text-gold-hell'
|
||||
: 'border border-transparent text-dunst hover:bg-white/5 hover:text-schnee'
|
||||
}`}
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="min-w-0 max-w-2xl flex-1 space-y-5">
|
||||
<SektionsKarte
|
||||
id="ablage"
|
||||
titel="ABLAGE"
|
||||
intro="Ein Ordner genügt — auch ein Netzlaufwerk. Rippy baut darin die Struktur, die Jellyfin, Emby und Kodi ohne Zutun verstehen."
|
||||
>
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<Feld name="Ablage-Ordner" wert={werte['ablage'] ?? ''} aufAendern={(w) => setzen('ablage', w)} />
|
||||
@@ -699,26 +824,45 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
|
||||
Durchsuchen …
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<div className="rounded-xl border border-white/8 bg-buehne/60 p-3 font-mono text-[11px] leading-5 text-nebel">
|
||||
<p className="text-dunst">So legt Rippy ab:</p>
|
||||
<p>├─ Filme\Akira (1988)\ <span className="text-nebel/70">— Film, Poster und Infodatei beieinander</span></p>
|
||||
<p>├─ Musik\ <span className="text-nebel/70">— Audio-CDs als FLAC, benannt nach MusicBrainz</span></p>
|
||||
<p>├─ ISO\ <span className="text-nebel/70">— unerkannte Discs als 1:1-Abbild</span></p>
|
||||
<p>└─ roh\ <span className="text-nebel/70">— Arbeitsstand während des Rips, räumt sich selbst auf</span></p>
|
||||
</div>
|
||||
</SektionsKarte>
|
||||
|
||||
<section className="space-y-3">
|
||||
<Kicker
|
||||
text={`KOMPRESSION${verbindung.werkzeuge !== null && verbindung.werkzeuge.handbrake.length === 0 ? ' — HANDBRAKECLI FEHLT' : ''}`}
|
||||
<SektionsKarte
|
||||
id="kompression"
|
||||
titel="KOMPRESSION"
|
||||
intro="Nach dem verlustfreien Rip rechnet HandBrake die Filme auf Arbeitsgröße — der Rohling bleibt bis zum Erfolg liegen, es geht nie etwas verloren."
|
||||
>
|
||||
{verbindung.werkzeuge !== null && verbindung.werkzeuge.handbrake.length === 0 && (
|
||||
<p className="font-mono text-xs text-warn">⚠ HandBrakeCLI fehlt — Rippen geht, Komprimieren nicht (siehe Werkzeuge).</p>
|
||||
)}
|
||||
<Schalter
|
||||
an={werte['transcodeEnabled'] !== 'false'}
|
||||
aufWechsel={(an) => setzen('transcodeEnabled', an ? 'true' : 'false')}
|
||||
label="Nach dem Rip automatisch komprimieren"
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-xs text-dunst">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={werte['transcodeEnabled'] !== 'false'}
|
||||
onChange={(e) => setzen('transcodeEnabled', e.target.checked ? 'true' : 'false')}
|
||||
className="accent-[oklch(0.83_0.11_75)]"
|
||||
/>
|
||||
Nach dem Rip komprimieren (der verlustfreie Rip bleibt bis zum Erfolg erhalten)
|
||||
</label>
|
||||
{backends.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="font-mono text-[10px] tracking-wider text-nebel">GEMESSENE ENCODER:</span>
|
||||
{backends.map((backend) => (
|
||||
<Badge key={backend} text={backend} ton={backend.startsWith('cpu') ? 'grau' : 'blau'} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<PresetWahl name="DVD" schluessel="transcodePresetDvd" wert={werte['transcodePresetDvd'] ?? ''} presets={presets} empfehlung={empfehlungFuer('dvd')} aufAendern={setzen} />
|
||||
<PresetWahl name="Blu-ray" schluessel="transcodePresetBluray" wert={werte['transcodePresetBluray'] ?? ''} presets={presets} empfehlung={empfehlungFuer('bluray')} aufAendern={setzen} />
|
||||
<PresetWahl name="4K-UHD" schluessel="transcodePresetUhd" wert={werte['transcodePresetUhd'] ?? ''} presets={presets} empfehlung={empfehlungFuer('uhd')} aufAendern={setzen} />
|
||||
</div>
|
||||
<p className="text-[11px] leading-4 text-nebel">
|
||||
„automatisch" nimmt die Empfehlung aus der gemessenen Liste deines HandBrake — auf dieser Maschine
|
||||
die Grafikkarte, wo es sie gibt. Eine eigene Wahl schlägt die Empfehlung immer.
|
||||
</p>
|
||||
<Feld
|
||||
name="Allgemeines Preset"
|
||||
hinweis="gilt, wo oben nichts gewählt ist"
|
||||
@@ -739,25 +883,93 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
|
||||
aufAendern={(w) => setzen('untertitelSprachen', w)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</SektionsKarte>
|
||||
|
||||
<section className="space-y-3">
|
||||
<Kicker text="METADATEN" />
|
||||
<SektionsKarte
|
||||
id="metadaten"
|
||||
titel="METADATEN"
|
||||
intro="TMDb erkennt Titel, Jahr, Poster und Beschreibung; OMDb hilft bei Ausreißern nach. Beide Schlüssel sind kostenlos und in zwei Minuten geholt."
|
||||
>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<Feld name="TMDb-API-Key" geheim wert={werte['tmdbApiKey'] ?? ''} aufAendern={(w) => setzen('tmdbApiKey', w)} />
|
||||
<Feld name="OMDb-API-Key" geheim wert={werte['omdbApiKey'] ?? ''} aufAendern={(w) => setzen('omdbApiKey', w)} />
|
||||
<p className="mt-1 font-mono text-[10px] text-nebel">
|
||||
www.themoviedb.org → Einstellungen → API
|
||||
<HinterlegtPunkt wert={werte['tmdbApiKey'] ?? ''} />
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Feld name="OMDb-API-Key" geheim wert={werte['omdbApiKey'] ?? ''} aufAendern={(w) => setzen('omdbApiKey', w)} />
|
||||
<p className="mt-1 font-mono text-[10px] text-nebel">
|
||||
www.omdbapi.com/apikey.aspx
|
||||
<HinterlegtPunkt wert={werte['omdbApiKey'] ?? ''} />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[11px] leading-4 text-nebel">
|
||||
Ohne Schlüssel rippt Rippy trotzdem — die Filme heißen dann wie das Disc-Label, und „Ändern …"
|
||||
an der Laufwerks-Kachel bleibt ohne Suche.
|
||||
</p>
|
||||
</SektionsKarte>
|
||||
|
||||
<SektionsKarte
|
||||
id="makemkv"
|
||||
titel="MAKEMKV"
|
||||
intro="MakeMKV liest die Discs. Rippy holt den frei veröffentlichten Beta-Key automatisch aus dem Hersteller-Forum, hinterlegt ihn und fragt MakeMKV selbst, ob er angenommen wird — täglich."
|
||||
>
|
||||
{s !== null && (
|
||||
<div className="rounded-xl border border-white/8 bg-buehne/60 p-3">
|
||||
<p className="flex items-center gap-2 font-mono text-[10.5px] tracking-wider">
|
||||
<span className={`inline-block h-2 w-2 rounded-full ${s.angenommen === true ? 'bg-gruen' : s.weiterBetrieb ? 'bg-warn' : 'bg-rot'}`} />
|
||||
<span className={s.angenommen === true ? 'text-gruen' : s.weiterBetrieb ? 'text-warn' : 'text-rot'}>
|
||||
{s.angenommen === true ? 'SCHLÜSSEL ANGENOMMEN' : s.weiterBetrieb ? 'BETA-MODUS — RIPS LAUFEN' : 'AUSFALL'}
|
||||
</span>
|
||||
{s.version.length > 0 && <span className="ml-auto text-nebel">MakeMKV {s.version}</span>}
|
||||
</p>
|
||||
<p className="mt-1.5 text-xs leading-5 text-dunst">{s.text}</p>
|
||||
<p className="mt-1 font-mono text-[10px] text-nebel">
|
||||
zuletzt geprüft: {s.geholtAm.slice(0, 16).replace('T', ' ')}
|
||||
{s.gueltigBis.length > 0 && ` · Key gültig bis ${s.gueltigBis}`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{s !== null && s.angenommen === false && (
|
||||
<button
|
||||
onClick={() => makemkvUpdate()}
|
||||
disabled={b !== null && b.laeuft}
|
||||
className="rounded-lg bg-gradient-to-b from-warn to-[oklch(0.72_0.12_78)] px-3 py-1.5 text-xs font-bold text-buehne shadow-[inset_0_1px_0_oklch(1_0_0/0.35)] hover:brightness-105 disabled:opacity-40"
|
||||
>
|
||||
MakeMKV aktualisieren …
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => schluesselPruefen()}
|
||||
disabled={b !== null && b.laeuft}
|
||||
className="rounded-lg border border-white/15 px-3 py-1.5 text-xs text-dunst hover:text-schnee disabled:opacity-40"
|
||||
>
|
||||
Schlüssel neu prüfen
|
||||
</button>
|
||||
</div>
|
||||
{b !== null && (
|
||||
<p className={`font-mono text-[11px] leading-5 ${b.fehler ? 'text-rot' : b.laeuft ? 'text-gold-hell' : 'text-gruen'}`}>
|
||||
{b.text}
|
||||
</p>
|
||||
)}
|
||||
<Feld
|
||||
name="MakeMKV-Schlüssel (Dauerlizenz)"
|
||||
name="Eigener Schlüssel (Dauerlizenz)"
|
||||
hinweis="schlägt den automatischen Beta-Key — leer lassen, wenn keiner gekauft ist"
|
||||
geheim
|
||||
wert={werte['makemkvAppKey'] ?? ''}
|
||||
aufAendern={(w) => setzen('makemkvAppKey', w)}
|
||||
/>
|
||||
</section>
|
||||
</SektionsKarte>
|
||||
|
||||
<section className="space-y-3">
|
||||
<Kicker text="MEDIENSERVER" />
|
||||
<SektionsKarte
|
||||
id="medienserver"
|
||||
titel="MEDIENSERVER"
|
||||
intro="Nach jedem fertigen Rip sagt Rippy deinem Server Bescheid, damit der Film sofort auftaucht — ohne Warten auf den nächsten Bibliotheks-Scan."
|
||||
>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<label className="block">
|
||||
<span className="text-xs font-semibold text-dunst">System</span>
|
||||
@@ -772,51 +984,83 @@ function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: H
|
||||
<Feld name="Adresse" hinweis="http(s)://…" wert={werte['mediaServerUrl'] ?? ''} aufAendern={(w) => setzen('mediaServerUrl', w)} />
|
||||
<Feld name="API-Key" geheim hinweis="Kodi: benutzer:passwort" wert={werte['mediaServerApiKey'] ?? ''} aufAendern={(w) => setzen('mediaServerApiKey', w)} />
|
||||
</div>
|
||||
</section>
|
||||
<p className="text-[11px] leading-4 text-nebel">
|
||||
Bei Plex benennt Rippy nur nach dessen Regeln — angestoßen wird dort nichts. Die Ordnerstruktur
|
||||
aus „Ablage" passt für alle vier.
|
||||
</p>
|
||||
</SektionsKarte>
|
||||
|
||||
<section className="space-y-3">
|
||||
<Kicker text="PROGRAMM" />
|
||||
<label className="flex items-center gap-2 text-xs text-dunst">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={haupt?.autostart === true}
|
||||
onChange={(e) => void window.rippy.autostartSetzen(e.target.checked)}
|
||||
className="accent-[oklch(0.83_0.11_75)]"
|
||||
<SektionsKarte
|
||||
id="programm"
|
||||
titel="PROGRAMM"
|
||||
intro="Rippy lebt im Infobereich neben der Uhr: Fenster zu heißt weiterlaufen, beendet wird über das Tray-Menü."
|
||||
>
|
||||
<Schalter
|
||||
an={haupt?.autostart === true}
|
||||
aufWechsel={(an) => void window.rippy.autostartSetzen(an)}
|
||||
label="Rippy mit Windows starten (Autostart im Benutzerkonto — kein Dienst, keine Adminrechte)"
|
||||
/>
|
||||
Rippy mit Windows starten (HKCU\…\Run — kein Dienst, keine Adminrechte)
|
||||
</label>
|
||||
<Feld
|
||||
name="Update-Adresse"
|
||||
hinweis="leer = Gitea des Commanders; NUR https:// wird angenommen (§ 6.8)"
|
||||
hinweis="leer = Standard; NUR https:// wird angenommen"
|
||||
wert={werte['updateUrl'] ?? ''}
|
||||
aufAendern={(w) => setzen('updateUrl', w)}
|
||||
/>
|
||||
{haupt !== null && (
|
||||
<p className="font-mono text-xs text-nebel">
|
||||
Selbst-Update: {haupt.update.text}
|
||||
{haupt.update.letzteErfolgreichePruefung.length > 0 &&
|
||||
` · letzte erfolgreiche Prüfung: ${haupt.update.letzteErfolgreichePruefung.slice(0, 16).replace('T', ' ')}`}
|
||||
</p>
|
||||
<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>
|
||||
{haupt.update.letzteErfolgreichePruefung.length > 0 && (
|
||||
<p>letzte erfolgreiche Prüfung: {haupt.update.letzteErfolgreichePruefung.slice(0, 16).replace('T', ' ')}</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<Kicker text="WERKZEUGE" />
|
||||
<div className="space-y-1.5 rounded-2xl border border-white/10 bg-karte/50 p-4 font-mono text-xs">
|
||||
<p className={verbindung.werkzeuge === null ? 'text-nebel' : verbindung.werkzeuge.makemkvcon.length > 0 ? 'text-gruen' : 'text-rot'}>
|
||||
makemkvcon: {verbindung.werkzeuge === null ? 'wird gesucht …' : verbindung.werkzeuge.makemkvcon || 'NICHT GEFUNDEN — MakeMKV installieren'}
|
||||
<p>
|
||||
Rippy v{haupt.version}
|
||||
{verbindung.kern !== null && ` · Kern: Node ${verbindung.kern.nodeVersion} (PID ${verbindung.kern.pid})`}
|
||||
</p>
|
||||
<p className={verbindung.werkzeuge === null ? 'text-nebel' : verbindung.werkzeuge.handbrake.length > 0 ? 'text-gruen' : 'text-warn'}>
|
||||
HandBrakeCLI: {verbindung.werkzeuge === null ? 'wird gesucht …' : verbindung.werkzeuge.handbrake || 'nicht gefunden — Rippen geht, Kompression nicht'}
|
||||
</p>
|
||||
<p className={verbindung.werkzeuge === null ? 'text-nebel' : verbindung.werkzeuge.flac.length > 0 ? 'text-gruen' : 'text-warn'}>
|
||||
flac: {verbindung.werkzeuge === null ? 'wird gesucht …' : verbindung.werkzeuge.flac || 'nicht gefunden — nur für Audio-CDs nötig'}
|
||||
</p>
|
||||
{verbindung.werkzeuge !== null && verbindung.werkzeuge.backends.length > 0 && (
|
||||
<p className="text-nebel">Encoder: {verbindung.werkzeuge.backends.join(', ')}</p>
|
||||
)}
|
||||
<p className="text-nebel/70">Updates warten, solange ein Rip läuft — nie mitten im Vorgang.</p>
|
||||
</div>
|
||||
)}
|
||||
</SektionsKarte>
|
||||
|
||||
<SektionsKarte
|
||||
id="werkzeuge"
|
||||
titel="WERKZEUGE"
|
||||
intro="Rippy findet seine Werkzeuge von allein: mitgeliefert, selbst geholt, installiert oder im PATH — in dieser Reihenfolge. Wer einen eigenen Pfad einträgt, bekommt genau den."
|
||||
>
|
||||
<WerkzeugZeile
|
||||
name="makemkvcon"
|
||||
pfad={verbindung.werkzeuge === null ? null : verbindung.werkzeuge.makemkvcon}
|
||||
fehlt="NICHT GEFUNDEN — MakeMKV von makemkv.com installieren, Rippy findet es danach von allein"
|
||||
pflicht={true}
|
||||
schluessel="werkzeug.makemkv"
|
||||
wert={werte['werkzeug.makemkv'] ?? ''}
|
||||
aufAendern={setzen}
|
||||
/>
|
||||
<WerkzeugZeile
|
||||
name="HandBrakeCLI"
|
||||
pfad={verbindung.werkzeuge === null ? null : verbindung.werkzeuge.handbrake}
|
||||
fehlt="nicht gefunden — Rippen geht, Kompression nicht"
|
||||
pflicht={false}
|
||||
schluessel="werkzeug.handbrake"
|
||||
wert={werte['werkzeug.handbrake'] ?? ''}
|
||||
aufAendern={setzen}
|
||||
/>
|
||||
<WerkzeugZeile
|
||||
name="flac"
|
||||
pfad={verbindung.werkzeuge === null ? null : verbindung.werkzeuge.flac}
|
||||
fehlt="nicht gefunden — nur für Audio-CDs nötig"
|
||||
pflicht={false}
|
||||
schluessel="werkzeug.flac"
|
||||
wert={werte['werkzeug.flac'] ?? ''}
|
||||
aufAendern={setzen}
|
||||
/>
|
||||
{verbindung.werkzeuge !== null && (
|
||||
<p className="font-mono text-[10.5px] text-nebel">
|
||||
HandBrake gemessen: {verbindung.werkzeuge.presets.length} Presets · Encoder:{' '}
|
||||
{backends.length > 0 ? backends.join(', ') : '—'}
|
||||
</p>
|
||||
)}
|
||||
</SektionsKarte>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1037,7 +1281,12 @@ type Bereich = 'uebersicht' | 'einstellungen' | 'bibliothek'
|
||||
|
||||
export default function App() {
|
||||
const [haupt, setHaupt] = useState<HauptStatus | null>(null)
|
||||
const [bereich, setBereich] = useState<Bereich>('uebersicht')
|
||||
// #einstellungen / #bibliothek als Startanker — der Smoke-Beweis
|
||||
// fotografiert damit auch die anderen Bereiche.
|
||||
const [bereich, setBereich] = useState<Bereich>(() => {
|
||||
const anker = window.location.hash.replace('#', '')
|
||||
return anker === 'einstellungen' || anker === 'bibliothek' ? anker : 'uebersicht'
|
||||
})
|
||||
const [verbindung, setVerbindung] = useState<Verbindung>({
|
||||
verbunden: false,
|
||||
kern: null,
|
||||
|
||||
@@ -28,11 +28,17 @@ export function fensterErstellen(): BrowserWindow {
|
||||
|
||||
// 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.
|
||||
const devUrl = process.env['ELECTRON_RENDERER_URL']
|
||||
const startBereich = process.env['RIPPY_START_BEREICH'] ?? ''
|
||||
if (devUrl !== undefined && devUrl.length > 0) {
|
||||
void fenster.loadURL(devUrl)
|
||||
void fenster.loadURL(startBereich.length > 0 ? `${devUrl}#${startBereich}` : devUrl)
|
||||
} else {
|
||||
void fenster.loadFile(join(__dirname, '../fenster/index.html'))
|
||||
void fenster.loadFile(
|
||||
join(__dirname, '../fenster/index.html'),
|
||||
startBereich.length > 0 ? { hash: startBereich } : undefined,
|
||||
)
|
||||
}
|
||||
return fenster
|
||||
}
|
||||
|
||||
@@ -279,8 +279,11 @@ function start(): void {
|
||||
try {
|
||||
// Der Pong kommt schneller, als der Compositor das erste Bild malt —
|
||||
// ein sofortiges capturePage lieferte hier »UnknownVizError«
|
||||
// (gemessen 30.08.2026). Kurz warten, bis wirklich gemalt ist.
|
||||
await new Promise((fertig) => setTimeout(fertig, 750))
|
||||
// (gemessen 30.08.2026). Kurz warten, bis wirklich gemalt ist;
|
||||
// RIPPY_SMOKE_WARTE_MS verlängert das für Bild-Beweise, die auf
|
||||
// späte Kern-Antworten warten.
|
||||
const warteMs = Number(process.env['RIPPY_SMOKE_WARTE_MS'] ?? '750')
|
||||
await new Promise((fertig) => setTimeout(fertig, Number.isFinite(warteMs) ? warteMs : 750))
|
||||
const bild = await fenster.webContents.capturePage()
|
||||
await writeFile(bildPfad, bild.toPNG())
|
||||
console.log(`SMOKE: Bild abgelegt unter ${bildPfad}`)
|
||||
|
||||
Reference in New Issue
Block a user