"use client"; import { useEffect, useRef, useState, useCallback } from "react"; import { createPortal } from "react-dom"; const S = { CHECKING: "checking", NOT_INSTALLED: "not_installed", NOT_RUNNING: "not_running", INSTALLING: "installing", STARTING: "starting", RUNNING: "running", ERROR: "error", }; const POLL_INTERVAL_MS = 2000; const POLL_TIMEOUT_MS = 60000; const FEATURES = [ { icon: "terminal", label: "Terminal", desc: "Full shell access" }, { icon: "cast", label: "Desktop", desc: "Screen sharing" }, { icon: "folder_open", label: "Files", desc: "Browse & edit files" }, ]; const BULLETS = [ { icon: "qr_code_scanner", text: "Scan QR to connect instantly" }, { icon: "wifi_off", text: "No port forwarding needed" }, { icon: "devices", text: "Works on any device" }, ]; export default function NineRemoteModal({ isOpen, onClose, onInstalled }) { const [state, setState] = useState(S.CHECKING); const [errorMsg, setErrorMsg] = useState(""); const pollRef = useRef(null); const stopPolling = () => { if (pollRef.current) { clearTimeout(pollRef.current); pollRef.current = null; } }; const pollUntilRunning = useCallback(() => { stopPolling(); const startedAt = Date.now(); const poll = async () => { if (Date.now() - startedAt > POLL_TIMEOUT_MS) { setState(S.ERROR); setErrorMsg("9Remote is taking too long to start. Try running `9remote ui` manually."); return; } try { const res = await fetch("/api/9remote/status"); const data = await res.json(); if (data.running) { setState(S.RUNNING); return; } } catch {} pollRef.current = setTimeout(poll, POLL_INTERVAL_MS); }; poll(); }, []); const checkAndInit = useCallback(async () => { setState(S.CHECKING); setErrorMsg(""); try { const res = await fetch("/api/9remote/status"); const data = await res.json(); if (data.running) { setState(S.RUNNING); return; } if (!data.installed) { setState(S.NOT_INSTALLED); return; } setState(S.NOT_RUNNING); } catch (err) { setState(S.ERROR); setErrorMsg(err.message); } }, []); const handleStart = useCallback(async () => { setState(S.STARTING); try { const res = await fetch("/api/9remote/start", { method: "POST" }); if (!res.ok) throw new Error("Failed to start 9Remote"); pollUntilRunning(); } catch (err) { setState(S.ERROR); setErrorMsg(err.message); } }, [pollUntilRunning]); const handleInstall = async () => { setState(S.INSTALLING); setErrorMsg(""); try { const res = await fetch("/api/9remote/install", { method: "POST" }); if (!res.ok) { const d = await res.json(); throw new Error(d.error || "Install failed"); } onInstalled?.(); await handleStart(); } catch (err) { setState(S.ERROR); setErrorMsg(err.message); } }; useEffect(() => { if (isOpen) checkAndInit(); else stopPolling(); return stopPolling; }, [isOpen, checkAndInit]); useEffect(() => { if (!isOpen) return; document.body.style.overflow = "hidden"; const onEsc = (e) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onEsc); return () => { document.body.style.overflow = ""; document.removeEventListener("keydown", onEsc); }; }, [isOpen, onClose]); if (!isOpen) return null; const btnCfg = { [S.NOT_INSTALLED]: { label: "Install 9Remote", icon: "download", onClick: handleInstall, loading: false }, [S.INSTALLING]: { label: "Installing...", icon: "hourglass_top", onClick: null, loading: true }, [S.NOT_RUNNING]: { label: "Start 9Remote", icon: "play_arrow", onClick: handleStart, loading: false }, [S.STARTING]: { label: "Starting...", icon: "hourglass_top", onClick: null, loading: true }, [S.CHECKING]: { label: "Checking...", icon: "hourglass_top", onClick: null, loading: true }, }[state]; // Running — iframe only if (state === S.RUNNING) { return createPortal(
Something went wrong
{errorMsg}
Access your terminal, desktop & files from anywhere
{label}
{desc}