import { useLingui } from "@lingui/react/macro"; import type { RakazoDesktopBrowserPreview, RakazoDesktopBrowserPreviewState, } from "@rakazo/contracts"; import { Button, cn, Input } from "@rakazo/ui-web"; import { ArrowLeft, ArrowRight, Code, ExternalLink, RotateCw, X } from "lucide-react"; import { type FormEvent, useCallback, useEffect, useRef, useState } from "react"; import { browserFrameSandbox, normalizeBrowserUrl } from "../../lib/browser-url"; import { desktopBridge } from "../../lib/desktop"; const LAST_URL_KEY = "rk:in-app-browser:url"; function readLastUrl(): string { try { return window.localStorage.getItem(LAST_URL_KEY) ?? ""; } catch { return ""; } } function rememberUrl(url: string) { try { window.localStorage.setItem(LAST_URL_KEY, url); } catch { /* A per-viewer convenience only. */ } } const EMPTY_STATE: RakazoDesktopBrowserPreviewState = { url: "", title: "", loading: false, canGoBack: false, canGoForward: false, error: null, }; /** DOM layers the native desktop view would otherwise paint over. */ const FLOATING_UI = '[role="dialog"],[role="alertdialog"],[role="menu"],[role="listbox"]'; /** * Address bar plus page. On desktop the page is a native Chromium view laid over the reserved * area, so dev servers keep hot reload, DevTools and full speed; in a browser it is an iframe. */ export function InAppBrowser({ onClose }: { onClose: () => void }) { const { t } = useLingui(); const native = desktopBridge()?.browserPreview; const [state, setState] = useState(() => ({ ...EMPTY_STATE, url: native ? "" : readLastUrl(), })); const [draft, setDraft] = useState(state.url); const [invalid, setInvalid] = useState(false); const [frameKey, setFrameKey] = useState(0); const editing = useRef(false); const inputRef = useRef(null); useEffect(() => { if (!native) return; let live = true; const apply = (next: RakazoDesktopBrowserPreviewState) => { if (!live) return; setState(next); if (!editing.current) setDraft(next.url); }; const unsubscribe = native.onState(apply); void native .state() .then((current) => { apply(current); const last = readLastUrl(); if (!current.url && last) void native.navigate(last).catch(() => undefined); }) .catch(() => undefined); return () => { live = false; unsubscribe(); }; }, [native]); useEffect(() => { if (!state.url) inputRef.current?.focus(); }, [state.url]); const go = useCallback( (input: string) => { const url = normalizeBrowserUrl(input); setInvalid(!url); if (!url) return; editing.current = false; setDraft(url); rememberUrl(url); if (native) { void native.navigate(url).catch(() => setInvalid(true)); } else { setState((current) => ({ ...current, url, loading: true, error: null })); setFrameKey((key) => key + 1); } }, [native], ); const reload = useCallback(() => { if (native) { void (state.loading ? native.stop() : native.reload()).catch(() => undefined); return; } if (!state.url) return; setState((current) => ({ ...current, loading: true })); setFrameKey((key) => key + 1); }, [native, state.loading, state.url]); const onSubmit = (event: FormEvent) => { event.preventDefault(); go(draft); inputRef.current?.blur(); }; return (
{native ? ( <> ) : null} { editing.current = true; event.currentTarget.select(); }} onBlur={() => { editing.current = false; }} onChange={(event) => { setDraft(event.target.value); setInvalid(false); }} onKeyDown={(event) => { if (event.key !== "Escape") return; setDraft(state.url); setInvalid(false); event.currentTarget.blur(); }} className="mx-1 h-control-sm flex-1 rounded-full bg-muted/60 px-3 text-ui-body-sm" /> {native ? ( ) : null}
{native ? ( ) : state.url ? (