"use client"; import { motion } from "motion/react"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { FaDownload } from "react-icons/fa"; import { FiWifi } from "react-icons/fi"; import { GoGear, GoKebabHorizontal } from "react-icons/go"; import { LuCloud, LuInfo, LuKeyboard, LuPlug, LuPuzzle, LuUser, LuUsers, } from "react-icons/lu"; import { launchDonutClone } from "@/lib/donut-physics"; import { MOTION_SPRING_POSITION } from "@/lib/motion"; import { cn } from "@/lib/utils"; import { Logo } from "./icons/logo"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; export type AppPage = | "profiles" | "proxies" | "extensions" | "groups" | "vpns" | "settings" | "integrations" | "account" | "import" | "shortcuts"; const CLICK_THRESHOLD = 5; const CLICK_WINDOW_MS = 2000; const LOGO_HIDDEN_KEY = "donut-logo-hidden"; function useLogoEasterEgg({ currentPage, onNavigate, }: { currentPage: AppPage; onNavigate: (page: AppPage) => void; }) { const clickTimestamps = useRef([]); const [isPressed, setIsPressed] = useState(false); const [wobbleKey, setWobbleKey] = useState(0); const [isFalling, setIsFalling] = useState(false); /** * Click count toward the bounce trigger while the user is on the profiles * page. Capped at 4: each click here grows the logo by 25%, so step 4 has * doubled the original size. Click 5 fires `triggerFall` and resets. */ const [growStep, setGrowStep] = useState(0); const resetTimeoutRef = useRef(null); const [isHidden, setIsHidden] = useState(() => { try { return sessionStorage.getItem(LOGO_HIDDEN_KEY) === "1"; } catch { return false; } }); const logoRef = useRef(null); const cancelFallRef = useRef<(() => void) | null>(null); const triggerFall = useCallback(() => { const el = logoRef.current; if (!el || isFalling) return; setIsFalling(true); cancelFallRef.current = launchDonutClone(el, { onExit: () => { try { sessionStorage.setItem(LOGO_HIDDEN_KEY, "1"); } catch { // ignore — sessionStorage unavailable in some Tauri WebViews } setIsHidden(true); setIsFalling(false); }, }); }, [isFalling]); useEffect(() => { return () => { cancelFallRef.current?.(); }; }, []); const handleClick = useCallback(() => { if (isFalling || isHidden) return; // First behaviour: any click from elsewhere in the app just routes the // user back to the profiles list. Growing the donut requires the user // to already be home — that keeps the easter egg from accidentally // firing during normal navigation. if (currentPage !== "profiles") { onNavigate("profiles"); clickTimestamps.current = []; setGrowStep(0); if (resetTimeoutRef.current !== null) { window.clearTimeout(resetTimeoutRef.current); resetTimeoutRef.current = null; } return; } const now = Date.now(); clickTimestamps.current = clickTimestamps.current.filter( (t) => now - t < CLICK_WINDOW_MS, ); clickTimestamps.current.push(now); if (clickTimestamps.current.length >= CLICK_THRESHOLD) { clickTimestamps.current = []; setGrowStep(0); if (resetTimeoutRef.current !== null) { window.clearTimeout(resetTimeoutRef.current); resetTimeoutRef.current = null; } triggerFall(); } else { setGrowStep( Math.min(clickTimestamps.current.length, CLICK_THRESHOLD - 1), ); setWobbleKey((k) => k + 1); if (resetTimeoutRef.current !== null) { window.clearTimeout(resetTimeoutRef.current); } resetTimeoutRef.current = window.setTimeout(() => { clickTimestamps.current = []; setGrowStep(0); resetTimeoutRef.current = null; }, CLICK_WINDOW_MS); } }, [currentPage, isFalling, isHidden, onNavigate, triggerFall]); // Leaving the profiles page mid-streak cancels growth so we never end up // with an outsized logo when the user returns later. useEffect(() => { if (currentPage !== "profiles") { clickTimestamps.current = []; setGrowStep(0); if (resetTimeoutRef.current !== null) { window.clearTimeout(resetTimeoutRef.current); resetTimeoutRef.current = null; } } }, [currentPage]); useEffect(() => { return () => { if (resetTimeoutRef.current !== null) { window.clearTimeout(resetTimeoutRef.current); } }; }, []); return { logoRef, isPressed, setIsPressed, wobbleKey, isFalling, isHidden, growStep, handleClick, }; } interface RailNavProps { currentPage: AppPage; onNavigate: (page: AppPage) => void; onOpenAbout: () => void; } /** Shared-element indicator that slides between the active rail items. */ function ActiveIndicator() { return (