import { type ComponentProps, useState, useCallback, type ReactNode, type PropsWithChildren } from "react" import { Badge, Stat, HStack, type BadgeProps } from '@chakra-ui/react'; import type {ComponentClientApiJson, ComponentCommonApiJson, MsSseEvent} from "../../../core/Api.js"; import { TextMuted } from "../TextMuted.js"; import type { IconBaseProps } from "react-icons/lib"; import { DownArrowIcon, UpArrowIcon } from "../icons/ChakraIcons.js"; import { useTimeout } from 'react-use-timeout'; import { useSSEContext, useSSEAnyEvent } from "@flamefrontend/sse-runtime-react"; import dayjs from "dayjs"; import { shortTodayAwareFormat } from "../../../core/TimeUtils.js"; import { TextTip } from "../ToggleTip.js"; type Color = BadgeProps['colorPalette'] & IconBaseProps['color']; export const StaticStat = (props: PropsWithChildren<{ label: string | ReactNode helpText?: string | ReactNode }> & ComponentProps) => { const { children, label, helpText, ...rest } = props; const presentationText = helpText !== undefined ? {helpText} : label return ( {presentationText} {children} ); } export const Indicator = (props: { current?: number currentText: string | ReactNode recentTimeout?: number total?: number totalText?: string | ReactNode helpText?: string | ReactNode directionColors?: [Color, Color] as?: 'text' | 'stat' } & ComponentProps) => { const { as = 'stat', current = 0, currentText, recentTimeout: timeoutProp = 10000, directionColors = ['green', 'red'], total, totalText, helpText, ...rest } = props; const [lastCurrent, setLastCurrent] = useState(current); const [recent, setRecent] = useState(0); const [recentDirection, setRecentDirection] = useState<'up' | 'down'>('up'); const resetRecent = useCallback(() => { setRecent(0); }, [setRecent]); const recentTimeout = useTimeout(resetRecent, timeoutProp ?? 10000); if (lastCurrent !== current) { recentTimeout.stop(); if (lastCurrent > current) { if (recentDirection === 'up') { setRecent(1); setRecentDirection('down'); } else { setRecent(recent + 1); } } else { if (recentDirection === 'down') { setRecent(1); setRecentDirection('up'); } else { setRecent(recent + 1); } } setLastCurrent(current); recentTimeout.start(); } let contextText: string | ReactNode; if(helpText !== undefined) { contextText = helpText; } if(totalText !== undefined) { contextText = <>{currentText} and ({totalText}); } const presentationText = contextText !== undefined ? {contextText} : currentText if (as === 'stat') { return ( {presentationText} {current} {total !== undefined && <>({total})} {recent !== 0 ? {recentDirection === 'up' ? : } {recent} : null} ); } return ( {current} {total !== undefined && <>({total})} {recent !== 0 ? {recentDirection === 'up' ? : } {recent} : null} {currentText} ); }; export const CountIndicatorStatic = (props: Omit, 'totalText' | 'helpText' | 'directionColors' | 'currentText'> & Pick) => ( ) export const CountIndicatorStreamable = (props: { data: Pick & { tracksDiscovered?: number, tracksScrobbled?: number } } & Omit, 'mode'>) => { const { data, ...rest } = props; const [statsData, setStatsData] = useState({current: data.tracksDiscovered ?? data.tracksScrobbled, total: data.countLive}); const client = useSSEContext(); useSSEAnyEvent(client, (payload) => { if ('componentId' in (payload.data as object) && (payload.data as Record).componentId === data.id) { switch (payload.type) { case 'scrobble': case 'discovered': setStatsData({current: statsData.current + 1, total: statsData.total + 1}); break; } } }); return } export const QueuedIndicatorStatic = (props: Omit, 'currentText' | 'totalText' | 'helpText' | 'directionColors'>) => ( ); export const QueuedIndicatorStreamable = (props: { data: Pick } & ComponentProps) => { const { data, ...rest } = props; const [statsData, setStatsData] = useState({current: data.queued}); const client = useSSEContext(); useSSEAnyEvent(client, (payload) => { if ('componentId' in (payload.data as object) && (payload.data as Record).componentId === data.id) { switch (payload.type) { case 'playQueued': setStatsData({current: statsData.current + 1}); break; case 'playDequeued': setStatsData({current: statsData.current - 1}); break; } } }); return } export const DeadLetterIndicatorStatic = (props: Omit, 'currentText' | 'totalText' | 'helpText' | 'directionColors'>) => ( ); export const DeadLetterIndicatorStreamable = (props: { data: Pick } & ComponentProps) => { const { data, ...rest } = props; const [statsData, setStatsData] = useState({current: data.deadLetterPlays, total: data.deadLetterPlaysTotal}); const client = useSSEContext(); useSSEAnyEvent(client, (payload) => { if ('componentId' in (payload.data as object) && (payload.data as Record).componentId === data.id) { switch (payload.type) { case 'deadLetter': setStatsData({current: statsData.current, total: statsData.total + 1}); break; case 'deadLetterRemoved': case 'removeDeadLetter': setStatsData({current: statsData.current, total: statsData.total - 1}); break; case 'deadLetterDequeued': setStatsData({current: statsData.current - 1, total: statsData.total}); break; case 'deadQueued': setStatsData({current: statsData.current + 1, total: statsData.total}); break; } } }); return } export const DateIndicatorStreamable = (props: Omit, 'streamedDate'>) => { const { data: { id, lastActiveAt, lastReadyAt, state } = {}, } = props; const useActive = state < 5 const [current, setCurrent] = useState(useActive ? lastActiveAt : lastReadyAt); const client = useSSEContext(); useSSEAnyEvent(client, (payload) => { if ('componentId' in (payload.data as object) && (payload.data as Record).componentId === id) { // TODO update state from event setCurrent(dayjs().toISOString()); } }); return } export const DateIndicator = (props: { data: Pick, streamable?: boolean as?: 'text' | 'stat' streamedDate?: string } & ComponentProps) => { const { data: { lastActiveAt, lastReadyAt, state } = {}, streamable, as = 'stat', ...rest } = props; const useActive = state < 5; let usedDate: string = props.streamedDate; if(usedDate === undefined) { usedDate = useActive ? lastActiveAt : lastReadyAt; } if(as === 'stat') { return ( {useActive? 'Last Active At' : 'Last Ready At'} {shortTodayAwareFormat(dayjs(usedDate))} ); } return ( {shortTodayAwareFormat(dayjs(usedDate))} {useActive? 'Last Active At' : 'Last Ready At'} ); }