"use client"; import { invoke } from "@tauri-apps/api/core"; import { openUrl } from "@tauri-apps/plugin-opener"; import { useReducedMotion } from "motion/react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { LuArrowLeft, LuExternalLink, LuSearch } from "react-icons/lu"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { FadingScrollArea } from "@/components/ui/fading-scroll-area"; import { Input } from "@/components/ui/input"; import { StepTransition } from "@/components/ui/step-transition"; import licenses from "@/generated/licenses.json"; import xraySource from "@/generated/xray-source.json"; import { launchDonutClone } from "@/lib/donut-physics"; import { Logo } from "./icons/logo"; import { RippleButton } from "./ui/ripple"; interface AboutDialogProps { isOpen: boolean; onClose: () => void; } interface SystemInfo { app_version: string; os: string; arch: string; portable: boolean; } type AboutView = "about" | "licenses"; // Flywheel: each click adds spin; past this speed the donut escapes the // dialog and bounces around the window (shared physics with the rail egg). const SPIN_PER_CLICK = 540; // deg/s const ESCAPE_VELOCITY = 2200; // deg/s const SPIN_FRICTION = 1.1; // fraction of velocity lost per second export function AboutDialog({ isOpen, onClose }: AboutDialogProps) { const { t } = useTranslation(); const reducedMotion = useReducedMotion(); const [systemInfo, setSystemInfo] = useState(null); const [logoFlown, setLogoFlown] = useState(false); const [view, setView] = useState("about"); const [licenseQuery, setLicenseQuery] = useState(""); const logoRef = useRef(null); const rotationRef = useRef(0); const velocityRef = useRef(0); const rafRef = useRef(0); const lastTimeRef = useRef(0); const cancelLaunchRef = useRef<(() => void) | null>(null); const restoreLicensesButtonFocusRef = useRef(false); useEffect(() => { if (!isOpen) return; invoke("get_system_info") .then(setSystemInfo) .catch(() => { setSystemInfo(null); }); }, [isOpen]); const stopSpin = useCallback(() => { if (rafRef.current) cancelAnimationFrame(rafRef.current); rafRef.current = 0; velocityRef.current = 0; }, []); const spinFrame = useCallback( (time: number) => { const el = logoRef.current; if (!el) { rafRef.current = 0; return; } const dt = Math.min((time - lastTimeRef.current) / 1000, 0.05); lastTimeRef.current = time; rotationRef.current += velocityRef.current * dt; velocityRef.current *= Math.max(0, 1 - SPIN_FRICTION * dt); el.style.transform = `rotate(${rotationRef.current}deg)`; if (velocityRef.current >= ESCAPE_VELOCITY) { // The flywheel wins: the donut tears loose and joins the bounce sim, // keeping its spin. stopSpin(); setLogoFlown(true); cancelLaunchRef.current = launchDonutClone(el, { initialVX: Math.random() > 0.5 ? 420 : -420, initialVY: -750, spinSpeed: ESCAPE_VELOCITY, onExit: () => { cancelLaunchRef.current = null; }, }); return; } if (velocityRef.current > 5) { rafRef.current = requestAnimationFrame(spinFrame); } else { rafRef.current = 0; velocityRef.current = 0; } }, [stopSpin], ); const handleLogoClick = useCallback(() => { if (reducedMotion || logoFlown) return; velocityRef.current += SPIN_PER_CLICK; if (!rafRef.current) { lastTimeRef.current = performance.now(); rafRef.current = requestAnimationFrame(spinFrame); } }, [reducedMotion, logoFlown, spinFrame]); const resetLogo = useCallback(() => { stopSpin(); cancelLaunchRef.current?.(); cancelLaunchRef.current = null; rotationRef.current = 0; if (logoRef.current) { logoRef.current.style.transform = ""; logoRef.current.style.visibility = ""; } setLogoFlown(false); }, [stopSpin]); const handleClose = useCallback(() => { resetLogo(); setView("about"); setLicenseQuery(""); restoreLicensesButtonFocusRef.current = false; onClose(); }, [onClose, resetLogo]); const handleLicensesButtonRef = useCallback( (node: HTMLButtonElement | null) => { if (node && restoreLicensesButtonFocusRef.current) { restoreLicensesButtonFocusRef.current = false; node.focus(); } }, [], ); const handleOpenLicenses = useCallback(() => { resetLogo(); setLicenseQuery(""); setView("licenses"); }, [resetLogo]); const handleBackToAbout = useCallback(() => { restoreLicensesButtonFocusRef.current = true; setLicenseQuery(""); setView("about"); }, []); const filteredLicenses = useMemo(() => { const query = licenseQuery.trim().toLocaleLowerCase(); if (!query) return licenses; return licenses.filter( ({ name, license }) => name.toLocaleLowerCase().includes(query) || license.toLocaleLowerCase().includes(query), ); }, [licenseQuery]); useEffect(() => { return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); cancelLaunchRef.current?.(); }; }, []); return ( { if (!open) handleClose(); }} > {view === "about" ? ( <> {t("about.title")}

Donut Browser

{systemInfo && ( <>

{t("about.version", { version: systemInfo.app_version, })} {systemInfo.portable && ( {t("about.portableBadge")} )}

{systemInfo.os} {systemInfo.arch}

)}

{t("about.licenseNotice")}

{t("common.buttons.close")}
) : ( <> {t("about.licenses")}
{filteredLicenses.length > 0 ? (
    {filteredLicenses.map(({ name, license }) => (
  • {name === "Xray-core" ? ( ) : ( {name} )} {license}
  • ))}
) : (

{t("about.noMatchingLicenses")}

)}
)}
); }