import { useRef, useState } from "react"; import { IcUpload, IcInfo } from "./Icons"; export function Toggle({ on, onChange }) { return (
onChange(!on)} role="switch" aria-checked={on}>
); } export function ProcessRail({ title = "Quy trình xử lý", steps, tip }) { return (
{title}
{steps.map((s, i) => (
{i + 1}
{s.t}
{s.d}
))} {tip &&
Mẹo: {tip}
}
); } // Duyệt ĐỆ QUY một thư mục được kéo-thả (DataTransferItem -> FileSystemEntry). // Trình duyệt KHÔNG đưa file con của thư mục vào dataTransfer.files, nên phải tự // đọc qua webkitGetAsEntry để lấy hết file trong (các) thư mục. async function readDropEntries(dataTransfer) { const items = Array.from(dataTransfer.items || []); const entries = items .map((it) => (it.webkitGetAsEntry ? it.webkitGetAsEntry() : null)) .filter(Boolean); if (!entries.length) return Array.from(dataTransfer.files || []); const out = []; const walk = (entry) => new Promise((resolve) => { if (entry.isFile) { entry.file((f) => { out.push(f); resolve(); }, () => resolve()); } else if (entry.isDirectory) { const reader = entry.createReader(); const readBatch = () => reader.readEntries(async (batch) => { if (!batch.length) return resolve(); await Promise.all(batch.map(walk)); readBatch(); // readEntries trả theo lô -> đọc tới khi hết }, () => resolve()); readBatch(); } else resolve(); }); await Promise.all(entries.map(walk)); return out; } export function FileDrop({ accept = "video/*", multiple = true, onFiles, title, hint, compact, folder = false }) { const ref = useRef(); const folderRef = useRef(); const [drag, setDrag] = useState(false); const pick = (list) => list && list.length && onFiles(Array.from(list)); const onDrop = async (e) => { e.preventDefault(); setDrag(false); // Hỗ trợ kéo CẢ THƯ MỤC: nếu có entries thì duyệt đệ quy, lọc theo accept. let files = await readDropEntries(e.dataTransfer); if (accept && accept !== "*" && files.length) { const base = accept.replace("/*", ""); // "video" | "audio" const filtered = files.filter((f) => (f.type || "").startsWith(base)); if (filtered.length) files = filtered; // chỉ lọc khi còn file hợp lệ } pick(files); }; return (
{ e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={onDrop} onClick={() => ref.current.click()} >
{title || "Kéo & thả file vào đây"}
{hint || "Hỗ trợ MP4, MOV, AVI, MKV"}
{folder && ( )} pick(e.target.files)} /> {folder && ( pick(e.target.files)} /> )}
); } // Hiển thị tiến độ + log + danh sách item của một job đang chạy. export function JobView({ job }) { if (!job) return null; const pct = Math.round((job.progress || 0) * 100); return (
Tiến độ xử lý {pct}% · {job.status}
{job.items?.length > 0 && ( {job.items.map((it) => ( ))}
{it.label} {{ queued: "chờ", running: "đang xử lý", done: "xong", error: "lỗi" }[it.status] || it.status} {it.detail || it.error || ""}
)} {job.logs?.length > 0 && (
{job.logs.map((l, i) => (
{l.msg}
))}
)}
); } // Lớp phủ "ĐANG XỬ LÝ": hiện NGAY khi busy=true (kể cả trước khi có job), phủ kín // màn hình -> chặn mọi thao tác cho tới khi xong. Có spinner + tiến độ + bước hiện tại. export function ProcessingOverlay({ busy, job, title = "Đang xử lý…" }) { if (!busy) return null; const pct = Math.round((job?.progress || 0) * 100); const items = job?.items || []; const running = items.find((i) => i.status === "running"); const doneCount = items.filter((i) => i.status === "done").length; const lastLog = job?.logs?.length ? job.logs[job.logs.length - 1].msg : ""; // Đang xếp hàng chờ tài nguyên máy chủ (nhiều người dùng cùng lúc). const queued = job?.status === "queued" && (job?.queue_total || 0) > 0; const queueMsg = queued ? `Đang xếp hàng — vị trí ${(job.queue_pos || 0) + 1}/${job.queue_total}. Máy chủ đang bận, job của bạn sẽ tự chạy khi tới lượt.` : ""; const sub = queueMsg || running?.detail || lastLog || "Đang xử lý trên máy chủ, vui lòng đợi…"; return (
{queued ? "Đang chờ tới lượt…" : title}
{sub}
{job && ( <>
{pct}%{items.length ? ` · ${doneCount}/${items.length} mục` : ""} · {job.status}
)}
Thao tác bị khoá tới khi xử lý xong — đừng đóng tab.
); } // Thông báo cho user biết dữ liệu được giữ tạm theo phiên (cache), không lưu lâu dài. export function CacheNote({ onClose }) { return (
Dữ liệu được lưu tạm theo phiên làm việc (cache trên trình duyệt + vùng tạm trên máy chủ), giúp bạn chuyển qua lại giữa các tool mà không mất tiến trình. Sau khi xử lý xong, hãy tải kết quả về máy — dữ liệu tạm sẽ tự xoá, không lưu trữ lâu dài. {onClose && }
); }