import { Text, useTheme } from "@streamplace/components"; import { AlertCircle, ArrowUp, CheckCircle2, X } from "lucide-react-native"; import { useSyncExternalStore } from "react"; import { Pressable, View } from "react-native"; import { cancelUpload, dismissUpload, getUploads, subscribeUploads, UploadJob, } from "utils/upload-manager"; // Floating upload status card, pinned to the lower-right corner of the app // shell. Lives outside the navigators so it keeps rendering progress while // the user moves between screens; the uploads themselves live in // utils/upload-manager. function humanBytes(n: number): string { if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`; return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`; } function UploadRow({ job }: { job: UploadJob }) { const { theme } = useTheme(); const pct = job.bytesTotal > 0 ? (job.bytesSent / job.bytesTotal) * 100 : 0; return ( {job.status === "uploading" && ( )} {job.status === "done" && ( )} {job.status === "error" && ( )} {job.filename} job.status === "uploading" ? cancelUpload(job.id) : dismissUpload(job.id) } hitSlop={8} > {job.status === "uploading" && ( <> {pct.toFixed(1)}% — {humanBytes(job.bytesSent)} /{" "} {humanBytes(job.bytesTotal)} )} {job.status === "done" && ( Upload complete — processing continues on the server )} {job.status === "error" && ( {job.error || "Upload failed"} )} ); } export default function UploadProgressIndicator() { const { theme } = useTheme(); const jobs = useSyncExternalStore(subscribeUploads, getUploads, getUploads); if (jobs.length === 0) return null; return ( {jobs.map((job) => ( ))} ); }