import
{
FocusContext,
useFocusable,
} from "@noriginmedia/norigin-spatial-navigation";
import classNames from "classnames";
import
{
BatteryCharging,
BatteryFull,
BatteryLow,
BatteryMedium,
BatteryWarning,
Bell,
Bluetooth,
CircleFadingArrowUp,
Clock,
Settings,
Wifi,
WifiHigh,
WifiLow,
WifiZero,
} from "lucide-react";
import { RoundButton } from "./RoundButton";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { JSX, RefObject, useContext, useEffect, useRef, useState } from "react";
import { useStickyDataAttr } from "../scripts/utils";
import { twMerge } from "tailwind-merge";
import { TwitchIcon } from "../scripts/brandIcons";
import { rommLoggedInQuery } from "../scripts/queries/romm";
import { twitchLoginVerificationQuery } from "../scripts/queries/settings";
import { AppContext, SystemInfoContext } from "../scripts/contexts";
import { useNavigate, useRouter } from "@tanstack/react-router";
import { oneShot } from "../scripts/audio/audio";
import { hasUpdateQuery } from "../scripts/queries/system";
import { GamePadButtonCode, useShortcuts } from "../scripts/shortcuts";
function HeaderAvatar (data: {
id: string;
preview?: string | JSX.Element;
className?: string;
active?: boolean;
locked?: boolean;
onSelect?: () => void;
})
{
return (
{typeof data.preview === 'string' ? (
) : data.preview}
);
}
export interface HeaderButton
{
id: string;
icon: JSX.Element;
external?: boolean;
action?: () => void;
className?: string;
shortcutLabel?: string;
}
export interface HeaderAccount
{
id: string;
preview?: string | JSX.Element;
className?: string;
type?: "base" | "primary" | "secondary" | "accent";
locked?: boolean;
action?: () => void;
}
function UpdateStatus ()
{
const handleSelect = () =>
{
navigate({ to: '/settings/update' });
};
const hasUnread = false;
const navigate = useNavigate();
const { ref } = useFocusable({
focusKey: 'update-bt', onEnterPress: handleSelect
});
return
;
}
function NotificationStatus ()
{
const hasUnread = false;
return
;
}
function ClockStatus ()
{
const navigate = useNavigate();
const app = useContext(AppContext);
const refClock = useRef(null);
const activeTaskProgress = app.activeTaskProgress;
const handleTaskClick = () =>
{
navigate({ to: '/settings/tasks' });
};
const { ref, focusKey } = useFocusable({ focusKey: 'tasks-indicator', focusable: !!activeTaskProgress, onEnterPress: handleTaskClick });
useEffect(() =>
{
function update ()
{
if (refClock.current)
{
refClock.current.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
}
// Update immediately
update();
// Wait until next minute boundary
const now = new Date();
const msUntilNextMinute =
(60 - now.getSeconds()) * 1000 - now.getMilliseconds();
const timeout = setTimeout(() =>
{
update();
// Then update every minute
const interval = setInterval(update, 60_000);
return () => clearInterval(interval);
}, msUntilNextMinute);
return () => clearTimeout(timeout);
}, []);
useShortcuts(focusKey, () => [{
label: "Downloads", button: GamePadButtonCode.A, action (e)
{
handleTaskClick();
},
}]);
return
{activeTaskProgress ?
:
}
;
}
function BluetoothStatus ()
{
const systemContext = useContext(SystemInfoContext);
return systemContext?.bluetoothDevices.find(b => b.connected) &&
;
}
function WiFiStatus ()
{
const systemContext = useContext(SystemInfoContext);
return systemContext && systemContext.wifiConnections.length > 0 ?
{systemContext.wifiConnections.map(w =>
{
const className = "w-10 h-10";
let icon =
;
if (w.signalLevel >= -60)
icon =
;
else if (w.signalLevel >= -70)
icon =
;
else if (w.signalLevel >= -80)
icon =
;
else if (w.signalLevel >= -90)
icon =
;
return
{icon}
;
})}
: undefined;
}
function BatteryStatus ()
{
const systemContext = useContext(SystemInfoContext);
const batteryClassName = "md:size-10 sm:size-6";
let batteryIcon = ;
if (systemContext)
{
if (systemContext.battery.isCharging || systemContext.battery.acConnected)
{
batteryIcon = ;
} else if (systemContext.battery.percent)
{
if (systemContext.battery.percent < 5)
{
batteryIcon = ;
}
else if (systemContext.battery.percent < 15)
{
batteryIcon = ;
} else if (systemContext.battery.percent < 50)
{
batteryIcon = ;
}
}
}
return !!systemContext?.battery.hasBattery &&
{batteryIcon}
{systemContext.battery?.percent} %
;
}
export function HeaderAccounts (data: { accounts?: HeaderAccount[]; })
{
const rommUser = useQuery({ ...rommLoggedInQuery, placeholderData: keepPreviousData });
const twitchStatus = useQuery({
...twitchLoginVerificationQuery,
refetchOnWindowFocus: false,
retry: 1,
placeholderData: keepPreviousData
});
const handleSelect = () =>
{
router.navigate({ to: '/settings/accounts' });
oneShot('click');
};
const accounts: HeaderAccount[] = [];
if (data.accounts) accounts.push(...data.accounts);
if (rommUser.data?.hasLogin || rommUser.isError)
{
accounts.push({
id: 'romm', preview: `https://romm.app/_ipx/q_80/images/blocks/logos/romm.svg`,
className: rommUser.data?.hasLogin && !rommUser.isError ? undefined : "border-error",
type: 'secondary'
});
}
if (twitchStatus.data)
{
accounts.push({
id: 'twitch', preview: TwitchIcon,
type: 'secondary'
});
}
const hasAccounts = accounts.length > 0;
const router = useRouter();
const { ref } = useFocusable({
focusKey: 'accounts',
onEnterPress: handleSelect,
focusable: hasAccounts
});
return
{accounts?.map(a => )}
;
}
export function HeaderStatusBar (data: { buttons?: HeaderButton[]; buttonElements?: JSX.Element[] | JSX.Element; })
{
const { ref, focusKey } = useFocusable({ focusKey: 'header-status-bar' });
const { data: update } = useQuery(hasUpdateQuery);
return
{!!update && update.hasUpdate >= 1 && }
{!!data.buttons && }
{data.buttonElements}
{data.buttons?.map(b => {b.icon})}
;
}
interface HeaderUIParams
{
buttons?: HeaderButton[];
accounts?: HeaderAccount[];
buttonElements?: JSX.Element[] | JSX.Element;
title?: JSX.Element;
preferredChildFocusKey?: string;
focusable?: boolean;
}
export function HeaderUI (data: HeaderUIParams)
{
const { ref, focusKey } = useFocusable({ focusKey: "header-elements", focusable: data.focusable, preferredChildFocusKey: data.preferredChildFocusKey });
const router = useRouter();
const goToSettings = () =>
{
router.navigate({ to: '/settings/accounts' });
};
return (
{data.title}
,
id: "header-settings-btn",
action: goToSettings,
external: true,
shortcutLabel: "Settings"
}
]} />
);
}
export function StickyHeaderUI (data: { ref: RefObject; className?: string; children?: any; } & HeaderUIParams)
{
const [isStuck, setIsStuck] = useState(false);
const headerRef = useRef(null);
const sentinelRef = useRef(null);
useStickyDataAttr(headerRef, sentinelRef, data.ref, setIsStuck);
return <>
{data.children}
>;
}