Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
Something went wrong. Try again.
12 kB · 264 lines
TSX
at debugStartup
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264import React, { type ComponentProps } from "react"import { Portal, Group, Span, Menu, Box, Heading, Skeleton, Wrap, HStack, Stack, Flex, Card, SkeletonText, type BadgeProps, type MenuItemProps } from '@chakra-ui/react';import { COMPONENT_STATE, type ComponentClientApiJson, type ComponentCommonApiJson, type ComponentState, isComponentClientApiJson, isComponentSourceApiJson, type MsSseEvent, type MsSseEventPayload } from "../../../core/Api.js";import { capitalize } from "../../../core/StringUtils.js";import { ChevronLeftButton, EllipsisButton, EyeButton, EyeClosedIcon, EyeIcon, IdleIcon, PowerButton, PowerIcon, PowerOffButton, PowerOffIcon, RetryIcon } from "../icons/ChakraIcons.js";import { PlayersContainer, PlayersContainerFetchable } from "../chakraPlayer/Player.js";import { Tooltip } from "../ToggleTip.js";import { useQuery, useQueryClient } from '@tanstack/react-query';import { ErrorAlert } from "../ErrorAlert";import { useSSEContext, useSSEAnyEvent} from "@flamefrontend/sse-runtime-react";import { Link } from "react-router";import { CountLiveIndicator, DateIndicator, DeadLetterIndicator, QueuedIndicator } from "./Stats.js";import { ListContainerFilterable } from "../playActivity/ActivityList.js";import { useParams } from "react-router-dom";import { ComponentStateBadge } from "../Badges.js";import { ActivitySummarySkeleton } from "../ActivityDetail.js";import dayjs from "dayjs";import { durationToHuman, shortTodayAwareFormat } from "../../../core/TimeUtils.js";import { tanQueries } from "../../queries/index.js";import { MSErrorBoundary } from "../ErrorBoundary.js";import type {IconType} from "react-icons/lib";import { useIsWrapped } from "../../utils/hooks/useIsWrapped.js";import { PlaybackReportingServer } from "../icons/PlaybackReporting.js";
export const ComponentBackButton = (props: ComponentProps<typeof ChevronLeftButton> = {}) => { return ( <Link to={`/next`}> <ChevronLeftButton variant="ghost" iconProps={{style: {width: 'unset', height: 'unset', fontSize: "2em"}}} {...props} /> </Link> );}
export const MSComponentName = (props: {data?: Pick<ComponentCommonApiJson, 'name'> & {playbackReporting?: boolean}}) => { if(props.data === undefined) { return <HStack><ComponentBackButton/><Skeleton width="5rem" height="5rem" /></HStack>; } let subsonicPlaybackReporting: React.JSX.Element; if('playbackReporting' in props.data) { subsonicPlaybackReporting = <PlaybackReportingServer playbackReporting={props.data.playbackReporting}/> } return <Heading truncate size="2xl"><ComponentBackButton/>{props.data.name}{subsonicPlaybackReporting}</Heading>;}
export const MSComponentType = (props: {data?: Pick<ComponentCommonApiJson, 'mode' | 'type'>}) => { if(props.data === undefined) { return <Skeleton width="3rem" height="1rem" />; } return <Heading color="fg.subtle" size="lg">({props.data.mode}) {capitalize(props.data.type)}</Heading>;}
export const MSComponentStats = (props: { data?: ComponentCommonApiJson, live?: boolean }) => { if (props.data === undefined) { return ( <Box> <SkeletonText noOfLines={6} /> </Box> ) } const isClient = isComponentClientApiJson(props.data); return ( <Wrap gap="6" rowGap="5" justify="flex-start" flexGrow="0"> <CountLiveIndicator data={props.data} streamable={props.live} flexGrow="0"/> {isClient ? <QueuedIndicator data={props.data as ComponentClientApiJson} streamable={props.live} flexGrow="0"/> : null} {isClient ? <DeadLetterIndicator data={props.data as ComponentClientApiJson} streamable={props.live} flexGrow="0"/> : null} <DateIndicator data={props.data} streamable={props.live} flexGrow="0"/> </Wrap> )}
const stateIsStarted = (state: ComponentState): boolean => state <= COMPONENT_STATE.MUTED;
const componentStateMenuItem = (Icon: IconType, value: string, name?: string) => (props: Pick<MenuItemProps, 'disabled'> = {}) => { return (<Menu.Item key={value} value={value} {...props}><Icon/><Box flex="1">{name ?? capitalize(value)}</Box></Menu.Item>);}const MenuItemRestart = componentStateMenuItem(RetryIcon, 'restart');const MenuItemStop = componentStateMenuItem(PowerOffIcon, 'stop');const MenuItemStart = componentStateMenuItem(PowerIcon, 'start');const MenuItemMute = componentStateMenuItem(EyeClosedIcon, 'mute', 'Ignore')const MenuItemUnmute = componentStateMenuItem(EyeIcon, 'unmute', 'Monitor');
const primaryActionProps: ComponentProps<typeof PowerOffButton> = { margin: "1px", variant: "subtle", size: 'xs'}
export const ComponentStateBadgeActionable = (props: Omit<ComponentProps<typeof ComponentStateBadge>, 'suffix'>) => { const { componentId, live, ...rest } = props; let suffix: React.JSX.Element | undefined; let primaryAction: React.JSX.Element | undefined; let menuElm: React.JSX.Element | undefined; let menuItems: React.JSX.Element[] = []; const badgeProps: BadgeProps = {}; switch(props.data.state) { case COMPONENT_STATE.RUNNING: primaryAction = <PowerOffButton {...primaryActionProps}/> menuItems = [<MenuItemStop/>,<MenuItemRestart/>,<MenuItemMute/>]; break; case COMPONENT_STATE.MUTED: primaryAction = <EyeButton {...primaryActionProps}/>; menuItems = [<MenuItemStop/>,<MenuItemRestart/>,<MenuItemUnmute/>]; break; case COMPONENT_STATE.INITIALIZING: // no actions while init is occurring break; default: // otherwise generic start action for all non-running states primaryAction = <PowerButton {...primaryActionProps}/>; menuItems = [<MenuItemStart/>]; } if(menuItems.length > 0) { menuElm = ( <Menu.Root positioning={{ placement: "bottom-end" }}> <Group attached> {primaryAction} <Menu.Trigger asChild> <EllipsisButton {...primaryActionProps}/> </Menu.Trigger> </Group> <Portal> <Menu.Positioner> <Menu.Content> {menuItems} </Menu.Content> </Menu.Positioner> </Portal> </Menu.Root> ); suffix = menuElm; } else if(primaryAction !== undefined) { suffix = primaryAction; } if(suffix !== undefined || primaryAction !== undefined) { badgeProps.paddingRight = 0; }
return <ComponentStateBadge size="lg" maxWidth="fit-content" {...badgeProps} separator suffix={suffix} {...rest}/>;}
export const ComponentDetailedDesktop = (props: {data?: ComponentCommonApiJson, live?: boolean}) => { let sleepingRender: React.JSX.Element = null; const { data, data: { warning, error } = {} } = props; const isSource = isComponentSourceApiJson(data) if(isSource) { const { sleeping, wakeAt } = data; if(sleeping) { if(sleeping && wakeAt !== undefined) { const wakeDay = dayjs(wakeAt); const now = dayjs(); sleepingRender = ( <Tooltip content={<Span>Will next poll Source for activity at {shortTodayAwareFormat(wakeDay)} (in {durationToHuman(dayjs.duration(wakeDay.diff(now, 'ms')))})</Span>}> <IdleIcon animated cursor="pointer"/> </Tooltip> ) } else { sleepingRender = <IdleIcon/>; } } } const target = React.useRef(null); const isWrapped = useIsWrapped(target); return ( <MSErrorBoundary> <Flex direction="row" wrap="wrap" style={{whiteSpace: 'break-spaces'}} truncate rowGap="4"> <Wrap width="100%" ref={target}> <Box marginEnd="auto" truncate> <MSComponentName data={data}/> <MSComponentType data={data}/> </Box> <Stack alignItems={isWrapped ? 'flex-start' : 'flex-end'}> <ComponentStateBadgeActionable size="lg" maxWidth="fit-content" data={data} /> <HStack style={{whiteSpace: 'break-spaces'}}>{sleepingRender}{data.status}</HStack> </Stack> </Wrap> <Flex justifyContent="flex-end" rowGap="6" flexDirection="row-reverse" wrap="wrap"> <Box marginEnd="auto"><MSComponentStats {...props}/></Box> </Flex> {error !== undefined && error !== null ? <ErrorAlert error={error}/> : undefined} {warning !== undefined && warning !== null ? <ErrorAlert error={warning} status="warning"/> : undefined} <MSErrorBoundary>{props.live ? <PlayersContainerFetchable nowPlaying={isSource ? undefined : true} data={data}/> : <PlayersContainer nowPlaying={isSource ? undefined : true} data={data} live={props.live}/>}</MSErrorBoundary> <MSErrorBoundary><ListContainerFilterable render="virtDynamic" componentType={data.mode} componentId={data.id}/></MSErrorBoundary> </Flex> </MSErrorBoundary> )}
const ComponentDetailedSkeleton = () => { return ( <Flex direction="column" gap="6"> <Flex justifyContent="flex-end" rowGap="6" wrap="wrap"> <Box marginEnd="auto"><SkeletonText noOfLines={2}/></Box> <Stack alignItems="flex-end"> <Skeleton height="2"/> </Stack> </Flex> <Flex justifyContent="flex-end" rowGap="6" flexDirection="row-reverse" wrap="wrap"> <Card.Root bgColor="bg.subtle" size="sm"> <Card.Header>Actions</Card.Header> <Card.Body> <Skeleton height="2"/> </Card.Body> </Card.Root> <Box marginEnd="auto"><SkeletonText noOfLines={2}/></Box> </Flex> <ActivitySummarySkeleton/> </Flex> )}
export const ComponentDetailedFetchable = (props: { componentId: number }) => { const { isPending, isError, data, error } = useQuery({ ...tanQueries.components.single(props.componentId), });
let rendered; if (data === undefined) { rendered = <ComponentDetailedSkeleton /> } else if (isError) { rendered = <ErrorAlert error={error} /> } else { rendered = <ComponentDetailedDesktop data={data} live />; }
const queryClient = useQueryClient(); const client = useSSEContext<MsSseEvent>(); useSSEAnyEvent(client, (payload) => { if ('componentId' in (payload.data as object) && (payload.data as Record<string, any>).componentId === props.componentId) { switch (payload.type) { case 'componentUpdate': queryClient.setQueryData(tanQueries.components.single(props.componentId).queryKey, (old: ComponentCommonApiJson) => { const componentData = payload.data as MsSseEventPayload<Partial<ComponentCommonApiJson>>; return { ...old, ...componentData.data }; }); } } });
return rendered;}
export const ComponentDetailedRoutable = () => { const params = useParams(); if(params.componentId === undefined) { return <ErrorAlert error={{message: 'Component is on a route with :componentId, cannot rendering anything!'}} /> }
return <ComponentDetailedFetchable componentId={Number.parseInt(params.componentId)}/>}