import React, { useEffect, useState, useRef } from "react"; import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid"; import { motion, AnimatePresence } from "framer-motion"; import { LuPlay } from "react-icons/lu"; import { BsMouseFill } from "react-icons/bs"; import { m } from "@localizations/messages.js"; import { Button, LinkButton } from "@components/Button"; import LoadingSpinner from "@components/LoadingSpinner"; import Card, { GridCard } from "@components/Card"; import { useRTCStore, PostRebootAction } from "@/hooks/stores"; import LogoBlue from "@/assets/logo-blue.svg"; import LogoWhite from "@/assets/logo-white.svg"; import { isOnDevice } from "@/main"; interface OverlayContentProps { readonly children: React.ReactNode; } function OverlayContent({ children }: OverlayContentProps) { return (
{children}
); } interface LoadingOverlayProps { readonly show: boolean; } export function LoadingVideoOverlay({ show }: LoadingOverlayProps) { return ( {show && (

{m.video_overlay_loading_stream()}

)}
); } interface LoadingConnectionOverlayProps { readonly show: boolean; readonly text: string; } export function LoadingConnectionOverlay({ show, text }: LoadingConnectionOverlayProps) { return ( {show && (

{text}

)}
); } interface ConnectionErrorOverlayProps { readonly show: boolean; readonly setupPeerConnection: () => Promise; } export function ConnectionFailedOverlay({ show, setupPeerConnection, }: ConnectionErrorOverlayProps) { return ( {show && (

{m.video_overlay_connection_issue_title()}

  • {m.video_overlay_conn_verify_power()}
  • {m.video_overlay_conn_check_cables()}
  • {m.video_overlay_conn_ensure_network()}
  • {m.video_overlay_conn_restart()}
)}
); } interface PeerConnectionDisconnectedOverlay { readonly show: boolean; } export function PeerConnectionDisconnectedOverlay({ show, }: PeerConnectionDisconnectedOverlay) { return ( {show && (

{m.video_overlay_connection_issue_title()}

  • {m.video_overlay_conn_verify_power()}
  • {m.video_overlay_conn_check_cables()}
  • {m.video_overlay_conn_ensure_network()}
  • {m.video_overlay_conn_restart()}

{m.video_overlay_retrying_connection()}

)}
); } interface HDMIErrorOverlayProps { readonly show: boolean; readonly hdmiState: string; } export function HDMIErrorOverlay({ show, hdmiState }: HDMIErrorOverlayProps) { const isNoSignal = hdmiState === "no_signal"; const isOtherError = hdmiState === "no_lock" || hdmiState === "out_of_range"; return ( <> {show && isNoSignal && (

{m.video_overlay_no_hdmi_signal()}

  • {m.video_overlay_no_hdmi_ensure_cable()}
  • {m.video_overlay_no_hdmi_ensure_power()}
  • {m.video_overlay_no_hdmi_adapter_compat()}
)}
{show && isOtherError && (

{m.video_overlay_hdmi_error_title()}

  • {m.video_overlay_hdmi_loose_faulty()}
  • {m.video_overlay_hdmi_incompatible_resolution()}
  • {m.video_overlay_hdmi_source_issue()}
)}
); } interface NoAutoplayPermissionsOverlayProps { readonly show: boolean; readonly onPlayClick: () => void; } export function NoAutoplayPermissionsOverlay({ show, onPlayClick, }: NoAutoplayPermissionsOverlayProps) { return ( {show && (

{m.video_overlay_autoplay_permissions_required()}

{m.video_overlay_enable_autoplay_settings()}
)}
); } interface PointerLockBarProps { readonly show: boolean; } export function PointerLockBar({ show }: PointerLockBarProps) { return ( {show ? (
{m.video_overlay_pointerlock_click_to_enable()}
) : null}
); } interface RebootingOverlayProps { readonly show: boolean; readonly postRebootAction: PostRebootAction; } export function RebootingOverlay({ show, postRebootAction }: RebootingOverlayProps) { const { peerConnectionState } = useRTCStore(); // Check if we've already seen the connection drop (confirms reboot actually started) const [hasSeenDisconnect, setHasSeenDisconnect] = useState( ['disconnected', 'closed', 'failed'].includes(peerConnectionState ?? '') ); // Track if we've timed out const [hasTimedOut, setHasTimedOut] = useState(false); // Monitor for disconnect after reboot is initiated useEffect(() => { if (!show) return; if (hasSeenDisconnect) return; if (['disconnected', 'closed', 'failed'].includes(peerConnectionState ?? '')) { console.log('hasSeenDisconnect', hasSeenDisconnect); setHasSeenDisconnect(true); } }, [show, peerConnectionState, hasSeenDisconnect]); // Set timeout after 30 seconds useEffect(() => { if (!show) { setHasTimedOut(false); return; } const timeoutId = setTimeout(() => { setHasTimedOut(true); }, 30 * 1000); return () => { clearTimeout(timeoutId); }; }, [show]); // Poll suggested IP in device mode to detect when it's available const abortControllerRef = useRef(null); const isFetchingRef = useRef(false); useEffect(() => { // Only run in device mode with a postRebootAction if (!isOnDevice || !postRebootAction || !show || !hasSeenDisconnect) { return; } const checkPostRebootHealth = async () => { // Don't start a new fetch if one is already in progress if (isFetchingRef.current) { return; } // Cancel any pending fetch if (abortControllerRef.current) { abortControllerRef.current.abort(); } // Create new abort controller for this fetch const abortController = new AbortController(); abortControllerRef.current = abortController; isFetchingRef.current = true; console.log('Checking post-reboot health endpoint:', postRebootAction.healthCheck); const timeoutId = window.setTimeout(() => abortController.abort(), 2000); try { const response = await fetch( postRebootAction.healthCheck, { signal: abortController.signal, } ); if (response.ok) { // Device is available, redirect to the specified URL console.log('Device is available, redirecting to:', postRebootAction.redirectUrl); window.location.href = postRebootAction.redirectUrl; window.location.reload(); } } catch (err) { // Ignore errors - they're expected while device is rebooting // Only log if it's not an abort error if (err instanceof Error && err.name !== 'AbortError') { console.debug('Error checking post-reboot health:', err); } } finally { clearTimeout(timeoutId); isFetchingRef.current = false; } }; // Start interval (check every 2 seconds) const intervalId = setInterval(checkPostRebootHealth, 2000); // Also check immediately checkPostRebootHealth(); // Cleanup on unmount or when dependencies change return () => { clearInterval(intervalId); if (abortControllerRef.current) { abortControllerRef.current.abort(); } isFetchingRef.current = false; }; }, [show, postRebootAction, hasTimedOut, hasSeenDisconnect]); return ( {show && (

{hasTimedOut ? "Unable to Reconnect" : "Device is Rebooting"}

{hasTimedOut ? ( <> The device may have restarted with a different IP address. Check the JetKVM's physical display to find the current IP address and reconnect. ) : ( <> Please wait while the device restarts. This usually takes 20-30 seconds. )}

{!hasTimedOut ? ( <>

Waiting for device to restart...

) : (

Automatic Reconnection Timed Out

)}
)}
); }