import { useCallback, useEffect, useMemo, useRef } from "react"; import { ActivityIndicator, Pressable, Text, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { Check, X, XCircle } from "lucide-react-native"; import { useDownloadStore, formatSpeed, formatEta, type Download } from "@/stores/download-store"; const AUTO_DISMISS_DELAY = 3000; function getDownloadStatusText(download: Download): string { if (download.status === "downloading") { if (download.progress) { return `${Math.round(download.progress.percent * 100)}% · ${formatSpeed(download.progress.speed)} · ${formatEta(download.progress.eta)}`; } return "Starting..."; } if (download.status === "complete") return "Download complete"; return download.message ?? "Download failed"; } export function DownloadToast() { const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); const downloads = useDownloadStore((state) => state.downloads); const activeDownloadId = useDownloadStore((state) => state.activeDownloadId); const dismissDownload = useDownloadStore((state) => state.dismissDownload); const dismissTimeoutRef = useRef | null>(null); const activeDownload = activeDownloadId ? downloads.get(activeDownloadId) : null; useEffect(() => { if (dismissTimeoutRef.current) { clearTimeout(dismissTimeoutRef.current); dismissTimeoutRef.current = null; } if (activeDownload && activeDownload.status !== "downloading") { dismissTimeoutRef.current = setTimeout(() => { dismissDownload(activeDownload.id); }, AUTO_DISMISS_DELAY); } return () => { if (dismissTimeoutRef.current) { clearTimeout(dismissTimeoutRef.current); } }; }, [activeDownload, dismissDownload]); const containerStyle = useMemo( () => [styles.container, { bottom: theme.spacing[4] + insets.bottom }], [theme.spacing, insets.bottom], ); const handleDismiss = useCallback(() => { if (activeDownload) { dismissDownload(activeDownload.id); } }, [activeDownload, dismissDownload]); if (!activeDownload) { return null; } return ( {activeDownload.status === "downloading" ? ( ) : null} {activeDownload.status === "complete" ? ( ) : null} {activeDownload.status !== "downloading" && activeDownload.status !== "complete" ? ( ) : null} {activeDownload.fileName} {getDownloadStatusText(activeDownload)} {activeDownload.status === "downloading" && activeDownload.progress && ( )} {activeDownload.status !== "downloading" && ( )} ); } function ProgressFill({ percent }: { percent: number }) { const width: `${number}%` = `${Math.round(percent * 100)}%`; const fillStyle = useMemo(() => [styles.progressFill, { width }], [width]); return ; } const styles = StyleSheet.create((theme) => ({ container: { position: "absolute", left: theme.spacing[4], right: theme.spacing[4], zIndex: 1000, }, toast: { flexDirection: "row", alignItems: "center", gap: theme.spacing[3], backgroundColor: theme.colors.surface2, borderRadius: theme.borderRadius.lg, borderWidth: theme.borderWidth[1], borderColor: theme.colors.border, paddingVertical: theme.spacing[3], paddingHorizontal: theme.spacing[4], ...theme.shadow.md, }, textContainer: { flex: 1, gap: theme.spacing[1], }, fileName: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.semibold, }, status: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, }, progressBar: { height: 3, backgroundColor: theme.colors.surface2, borderRadius: theme.borderRadius.full, marginTop: theme.spacing[1], overflow: "hidden", }, progressFill: { height: "100%", backgroundColor: theme.colors.primary, borderRadius: theme.borderRadius.full, }, dismiss: { padding: theme.spacing[1], }, }));