feat(v5): Etappe W-6 — Setup, Selbst-Update, Erst-Einrichtung, Lizenzen
Ampel / ampel (push) Successful in 1m22s
Ampel / ampel (push) Successful in 1m22s
electron-builder → NSIS: EIN RippySetup-<version>.exe, pro Benutzer, ohne Adminrechte, eigenes Icon (PNG-basiertes ICO selbst gebaut). extraResources: die MITGELIEFERTEN Werkzeuge (HandBrakeCLI GPL-2, flac.exe MIT libFLAC.dll — die Ohne-DLL-Falle aus § 9) samt Lizenztexten und Quellverweisen (bau/lizenzen, Entscheid 9); vendor/ liegt nicht im Git (74 MB), Befuellung steht in BAUEN.md. Der Kern findet die mitgelieferten Werkzeuge ueber RIPPY_VENDOR (resourcesvendor) VOR allen anderen Suchwegen. haupt/update.ts (§ 6.8): electron-updater gegen das oeffentliche Gitea (festes Tag 'aktuell' als Rueckfallebene, Entscheid 6) — NUR https (http wird ABGELEHNT; Regel-Tests), NIE waehrend eines Rips (der Haupt kennt die aktiven Vorgaenge), Fehlschlaege still aber ablesbar (letzte erfolgreiche Pruefung im Status), installiert beim naechsten Start. haupt/autostart.ts: HKCU-Run ueber app.setLoginItemSettings, Toggle in den Einstellungen. Erst-Einrichtung (§ 6.9) im Programm: 5 Schritte, NUR ein Fehler blockiert (kein Laufwerk = Warnung, MakeMKV-Pflicht mit Klartext-Weg). GEBAUT UND GEMESSEN (30.08.2026): - dist-setup/RippySetup-5.0.0-w0.exe — 130,8 MB, SHA-256 07D39D6BAB74A47C21AA53D847FB5A5DB6990AE9FA1CBC7BE814B51050883115, plus .blockmap. - MESSUNG: Die Update-Auskunft heisst bei Vorab-Versionen nach dem KANAL (w0.yml) — erst ein Release ohne Suffix erzeugt latest.yml (steht jetzt in BAUEN.md). - Das GEPACKTE Programm (win-unpacked, asar + resources) besteht den Smoke: SMOKE OK, und der Screenshot zeigt die Erst-Einrichtung — das Bild eines frischen Rechners. Bewusst offen (SAVEPOINT): die automatische MakeMKV-Beschaffung (beschaffen.ts — Download+Installer-Start braucht einen Live-Test); das Setup NENNT den Weg klar. 185 Tests gruen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
2d9cef22b3
commit
9fa3df500d
@@ -248,7 +248,7 @@ function PresetWahl({
|
||||
)
|
||||
}
|
||||
|
||||
function Einstellungen({ verbindung }: { verbindung: Verbindung }) {
|
||||
function Einstellungen({ verbindung, haupt }: { verbindung: Verbindung; haupt: HauptStatus | null }) {
|
||||
const werte = verbindung.einstellungen
|
||||
const presets = verbindung.werkzeuge?.presets ?? []
|
||||
const setzen = einstellungSetzen
|
||||
@@ -353,6 +353,31 @@ function Einstellungen({ verbindung }: { verbindung: Verbindung }) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-500">Programm</h2>
|
||||
<label className="flex items-center gap-2 text-xs text-slate-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={haupt?.autostart === true}
|
||||
onChange={(e) => void window.rippy.autostartSetzen(e.target.checked)}
|
||||
/>
|
||||
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)"
|
||||
wert={werte['updateUrl'] ?? ''}
|
||||
aufAendern={(w) => setzen('updateUrl', w)}
|
||||
/>
|
||||
{haupt !== null && (
|
||||
<p className="font-mono text-xs text-slate-400">
|
||||
Selbst-Update: {haupt.update.text}
|
||||
{haupt.update.letzteErfolgreichePruefung.length > 0 &&
|
||||
` · letzte erfolgreiche Prüfung: ${haupt.update.letzteErfolgreichePruefung.slice(0, 16).replace('T', ' ')}`}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-500">Werkzeuge</h2>
|
||||
<div className="space-y-1 rounded-xl border border-slate-700/60 bg-slate-800/40 p-4 font-mono text-xs">
|
||||
@@ -418,6 +443,169 @@ function Bibliothek({ verbindung }: { verbindung: Verbindung }) {
|
||||
)
|
||||
}
|
||||
|
||||
// ── Erst-Einrichtung (§ 6.9): nach der Installation, im Programm ────────
|
||||
// Zwei Regeln aus dem alten Setup: NUR ein Fehler blockiert (kein
|
||||
// Laufwerk ist eine WARNUNG — eine reine Komprimier-Maschine ist ein
|
||||
// vorgesehener Betriebsfall), und Erfolg wird am BESTAND gemessen.
|
||||
function ErstEinrichtung({ verbindung }: { verbindung: Verbindung }) {
|
||||
const [schritt, setSchritt] = useState(0)
|
||||
const werkzeuge = verbindung.werkzeuge
|
||||
const makemkvFehlt = werkzeuge !== null && werkzeuge.makemkvcon.length === 0
|
||||
|
||||
const pruefzeilen: Array<{ ok: boolean | null; pflicht: boolean; text: string }> = [
|
||||
{
|
||||
ok: werkzeuge === null ? null : werkzeuge.makemkvcon.length > 0,
|
||||
pflicht: true,
|
||||
text:
|
||||
werkzeuge === null
|
||||
? 'MakeMKV wird gesucht …'
|
||||
: werkzeuge.makemkvcon.length > 0
|
||||
? `MakeMKV gefunden: ${werkzeuge.makemkvcon}`
|
||||
: 'MakeMKV FEHLT — ohne makemkvcon kann Rippy keine Video-Disc lesen. Von makemkv.com/download installieren, dann findet Rippy es von selbst.',
|
||||
},
|
||||
{
|
||||
ok: werkzeuge === null ? null : werkzeuge.handbrake.length > 0,
|
||||
pflicht: false,
|
||||
text:
|
||||
werkzeuge === null
|
||||
? 'HandBrakeCLI wird gesucht …'
|
||||
: werkzeuge.handbrake.length > 0
|
||||
? `HandBrakeCLI dabei: ${werkzeuge.handbrake}`
|
||||
: 'HandBrakeCLI fehlt — Rippen geht, Komprimieren nicht.',
|
||||
},
|
||||
{
|
||||
ok: werkzeuge === null ? null : werkzeuge.flac.length > 0,
|
||||
pflicht: false,
|
||||
text:
|
||||
werkzeuge === null
|
||||
? 'FLAC wird gesucht …'
|
||||
: werkzeuge.flac.length > 0
|
||||
? `FLAC dabei: ${werkzeuge.flac}`
|
||||
: 'FLAC fehlt — nur für Audio-CDs nötig.',
|
||||
},
|
||||
{
|
||||
ok: verbindung.laufwerke.length > 0 ? true : null,
|
||||
pflicht: false,
|
||||
text:
|
||||
verbindung.laufwerke.length > 0
|
||||
? `${verbindung.laufwerke.length} optisches Laufwerk gefunden (${verbindung.laufwerke.map((l) => l.id + ':').join(', ')})`
|
||||
: 'Kein optisches Laufwerk — nur eine Warnung: Eine reine Komprimier-Maschine ist ein vorgesehener Betriebsfall.',
|
||||
},
|
||||
]
|
||||
|
||||
const schritte = ['Willkommen', 'Prüfung', 'Ordner', 'Automatik', 'Fertig']
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-xl space-y-6">
|
||||
<div className="flex gap-2">
|
||||
{schritte.map((name, i) => (
|
||||
<span
|
||||
key={name}
|
||||
className={`rounded-full px-3 py-1 text-xs ${i === schritt ? 'bg-sky-500/20 text-sky-300' : i < schritt ? 'text-emerald-400' : 'text-slate-500'}`}
|
||||
>
|
||||
{i + 1} {name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{schritt === 0 && (
|
||||
<div className="space-y-3 text-sm text-slate-300">
|
||||
<h2 className="text-xl font-bold text-slate-100">Willkommen bei Rippy</h2>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{schritt === 1 && (
|
||||
<div className="space-y-2">
|
||||
{pruefzeilen.map((zeile, i) => (
|
||||
<p
|
||||
key={i}
|
||||
className={`font-mono text-xs ${zeile.ok === true ? 'text-emerald-300' : zeile.ok === null ? 'text-slate-400' : zeile.pflicht ? 'text-red-300' : 'text-amber-300'}`}
|
||||
>
|
||||
{zeile.ok === true ? '✓' : zeile.ok === null ? '…' : zeile.pflicht ? '✗' : '⚠'} {zeile.text}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{schritt === 2 && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-slate-300">Wohin sollen fertige Filme, Musik und Abbilder?</p>
|
||||
<p className="break-all font-mono text-xs text-slate-400">{verbindung.einstellungen['ablage'] ?? ''}</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
void window.rippy.ordnerWaehlen('Ablage-Ordner wählen').then((ordner) => {
|
||||
if (ordner !== null) einstellungSetzen('ablage', ordner)
|
||||
})
|
||||
}
|
||||
className="rounded-lg border border-slate-600 px-3 py-1.5 text-xs text-slate-300 hover:bg-slate-700"
|
||||
>
|
||||
Ordner wählen … (auch Netzlaufwerke)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{schritt === 3 && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-slate-300">Wie viel soll Rippy allein entscheiden?</p>
|
||||
<label className="flex items-center gap-2 text-xs text-slate-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={verbindung.einstellungen['transcodeEnabled'] !== 'false'}
|
||||
onChange={(e) => einstellungSetzen('transcodeEnabled', e.target.checked ? 'true' : 'false')}
|
||||
/>
|
||||
Nach dem Rip automatisch komprimieren (je Disc-Typ später fein einstellbar)
|
||||
</label>
|
||||
<p className="text-xs text-slate-500">
|
||||
Alles Weitere — Presets je Disc-Typ, Sprachen, Medienserver, API-Schlüssel — steht unter
|
||||
„Einstellungen".
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{schritt === 4 && (
|
||||
<div className="space-y-3 text-sm text-slate-300">
|
||||
<p>Das war's. Disc einlegen — Rippy meldet sich.</p>
|
||||
{makemkvFehlt && (
|
||||
<p className="text-xs text-red-300">
|
||||
Denk an MakeMKV: Ohne makemkvcon bleibt der Video-Weg zu (Einstellungen → Werkzeuge zeigt
|
||||
den Stand, sobald es installiert ist).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
onClick={() => setSchritt((s) => Math.max(0, s - 1))}
|
||||
disabled={schritt === 0}
|
||||
className="rounded-lg border border-slate-600 px-4 py-1.5 text-xs text-slate-300 hover:bg-slate-700 disabled:opacity-40"
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
{schritt < schritte.length - 1 ? (
|
||||
<button
|
||||
onClick={() => setSchritt((s) => s + 1)}
|
||||
className="rounded-lg border border-sky-500/60 px-4 py-1.5 text-xs text-sky-300 hover:bg-sky-500/10"
|
||||
>
|
||||
Weiter
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => einstellungSetzen('setup.done', 'true')}
|
||||
className="rounded-lg border border-emerald-500/60 px-4 py-1.5 text-xs text-emerald-300 hover:bg-emerald-500/10"
|
||||
>
|
||||
Los geht's
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Bereich = 'uebersicht' | 'einstellungen' | 'bibliothek'
|
||||
|
||||
export default function App() {
|
||||
@@ -447,6 +635,11 @@ export default function App() {
|
||||
|
||||
const db = verbindung.kern?.datenbank ?? null
|
||||
|
||||
// § 6.9: Die Erst-Einrichtung läuft NACH der Installation, im Programm —
|
||||
// solange sie nicht abgeschlossen ist, ist sie die ganze Ansicht.
|
||||
const setupOffen =
|
||||
Object.keys(verbindung.einstellungen).length > 0 && verbindung.einstellungen['setup.done'] !== 'true'
|
||||
|
||||
const knopf = (ziel: Bereich, text: string): React.JSX.Element => (
|
||||
<button
|
||||
onClick={() => setBereich(ziel)}
|
||||
@@ -477,7 +670,8 @@ export default function App() {
|
||||
</header>
|
||||
|
||||
<main className="flex-1 space-y-6 p-6">
|
||||
{bereich === 'uebersicht' && (
|
||||
{setupOffen && <ErstEinrichtung verbindung={verbindung} />}
|
||||
{!setupOffen && bereich === 'uebersicht' && (
|
||||
<>
|
||||
<section>
|
||||
<h2 className="mb-3 text-xs font-semibold uppercase tracking-widest text-slate-500">
|
||||
@@ -581,8 +775,8 @@ export default function App() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{bereich === 'einstellungen' && <Einstellungen verbindung={verbindung} />}
|
||||
{bereich === 'bibliothek' && <Bibliothek verbindung={verbindung} />}
|
||||
{!setupOffen && bereich === 'einstellungen' && <Einstellungen verbindung={verbindung} haupt={haupt} />}
|
||||
{!setupOffen && bereich === 'bibliothek' && <Bibliothek verbindung={verbindung} />}
|
||||
</main>
|
||||
|
||||
<footer className="border-t border-slate-700/60 px-6 py-3 text-center text-xs text-slate-500">
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ declare global {
|
||||
aufStatus(horcher: (status: unknown) => void): void
|
||||
melden(art: string): void
|
||||
ordnerWaehlen(titel: string): Promise<string | null>
|
||||
autostartSetzen(an: boolean): Promise<boolean>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user