Initial commit: InkFlow — EPUB vers livre audio local (MLX/Kokoro)
This commit is contained in:
80
frontend/src/Library.jsx
Normal file
80
frontend/src/Library.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { api } from "./api.js";
|
||||
import { Spinner } from "./ui.jsx";
|
||||
|
||||
export default function Library({ onOpen }) {
|
||||
const [books, setBooks] = useState(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const fileRef = useRef();
|
||||
|
||||
const refresh = () => api.listBooks().then(setBooks).catch((e) => setError(String(e)));
|
||||
useEffect(() => { refresh(); }, []);
|
||||
|
||||
const upload = async (file) => {
|
||||
if (!file) return;
|
||||
setUploading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const { slug } = await api.uploadBook(file);
|
||||
await refresh();
|
||||
onOpen(slug);
|
||||
} catch (e) {
|
||||
setError("Échec de l'import : " + e);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<section
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => { e.preventDefault(); upload(e.dataTransfer.files[0]); }}
|
||||
className="card flex flex-col items-center justify-center gap-3 border-dashed py-12 text-center"
|
||||
>
|
||||
<div className="text-4xl">📖</div>
|
||||
<p className="font-serif text-lg">Déposez un fichier EPUB</p>
|
||||
<p className="text-sm text-ink-muted">ou</p>
|
||||
<button className="btn-primary" disabled={uploading}
|
||||
onClick={() => fileRef.current?.click()}>
|
||||
{uploading ? <Spinner /> : null}
|
||||
{uploading ? "Import en cours…" : "Choisir un fichier"}
|
||||
</button>
|
||||
<input ref={fileRef} type="file" accept=".epub" className="hidden"
|
||||
onChange={(e) => upload(e.target.files[0])} />
|
||||
</section>
|
||||
|
||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 font-serif text-lg text-ink-muted">Bibliothèque</h2>
|
||||
{books === null ? (
|
||||
<p className="text-ink-muted"><Spinner /> chargement…</p>
|
||||
) : books.length === 0 ? (
|
||||
<p className="text-ink-muted">Aucun livre pour l'instant.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
|
||||
{books.map((b) => (
|
||||
<button key={b.slug} onClick={() => onOpen(b.slug)}
|
||||
className="card group overflow-hidden text-left transition-transform hover:-translate-y-1">
|
||||
<div className="aspect-[2/3] w-full bg-ink-edge">
|
||||
{b.cover && (
|
||||
<img src={b.cover} alt="" className="h-full w-full object-cover" />
|
||||
)}
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<p className="line-clamp-2 font-serif text-sm">{b.title}</p>
|
||||
<p className="mt-1 text-xs text-ink-muted">{b.author}</p>
|
||||
<p className="mt-2 text-xs text-ink-accent">
|
||||
{b.rendered}/{b.chapters} chapitres rendus
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user