feat(ui): Indicate failed scrobble is dead queued

This commit is contained in:
FoxxMD 2026-07-07 02:32:35 +00:00
parent 5daba47ac7
commit b3c1ce6d8a
3 changed files with 24 additions and 15 deletions

View file

@ -1,6 +1,6 @@
import React, { ComponentProps, useState, Fragment, useEffect } from "react"
import { Accordion, For, Span, Stack, Text, Box, AbsoluteCenter, Button, Clipboard, Separator, HStack, Flex, Badge, IconButton, Container, Icon, useAccordionItemContext, Skeleton, SkeletonText, Collapsible, BadgeProps } from '@chakra-ui/react';
import { ComponentType, Second } from "../../core/Atomic";
import { CLIENT_DEAD_QUEUE, ComponentType, QUEUE_NAMES, Second } from "../../core/Atomic";
import { PlayData } from "./PlayData";
import { ErrorAlert } from "./ErrorAlert";
import { AiOutlineExclamationCircle } from "react-icons/ai";
@ -93,7 +93,7 @@ export interface ActivityDetailProps {
}
export interface ActivitySummaryProps extends SortPlaysByProps {
activity: PlayApiCommon & {isNew?: boolean | Second}
activity: PlayApiCommonDetailed & {isNew?: boolean | Second}
componentType: ComponentType
}
@ -109,6 +109,7 @@ export const ActivitySummary = (props: ActivitySummaryProps) => {
} = props;
const [updated, setUpdated] = useState<{lastUpdated: string, updated: boolean}>({lastUpdated: updatedAt, updated: false});
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setUpdated((old) => {
if(old.lastUpdated === activity.updatedAt) {
return {lastUpdated: activity.updatedAt, updated: false};
@ -237,20 +238,26 @@ export const ActivityDetailFetchable = (props: ActivityDetailFetchableProps) =>
return <ErrorAlert error={error}/>
}
return <ActivityDetails componentType={props.componentType} key={props.uid} activity={activity as PlayApiCommonDetailed}/>
return <ActivityDetails componentType={props.componentType} key={props.uid} activity={activity}/>
}
export const ActivityStateActions = (props: {activity: PlayApiCommon}) => {
export const ActivityStateActions = (props: {activity: PlayApiCommonDetailed}) => {
let suffix: React.JSX.Element | null;
let badgeProps: BadgeProps = {};
const badgeProps: BadgeProps = {};
const {
activity: {
queueStates = []
} = {}
} = props;
if(props.activity.state === 'failed') {
suffix = <RetryButton size="xs" margin="1px" variant="subtle"/>;
badgeProps.paddingRight = 0;
}
const hasDeadQueue = queueStates.some(x => x.queueName === CLIENT_DEAD_QUEUE && x.queueStatus === 'queued');
return (
<Stack>
<HStack>
<PlayStateBadge {...badgeProps} minH="32px" alignItems="anchor-center" size="lg" state={props.activity.state} suffix={suffix} />
<PlayStateBadge {...badgeProps} minH="32px" alignItems="anchor-center" size="lg" hasDeadQueue={hasDeadQueue} state={props.activity.state} suffix={suffix} />
</HStack>
<HStack justifyContent="flex-end">
<DebugCopy variant="ghost" value={JSON.stringify(props.activity)}/>

View file

@ -1,18 +1,17 @@
import { Badge, Separator, HStack } from "@chakra-ui/react";
import React, { ComponentProps, useState, useCallback, useEffect } from "react";
import { COMPONENT_STATE, ComponentCommonApiJson, componentStateToFriendly, MsSseEvent, MsSseEventPayload, PlayApiCommon } from "../../core/Api";
import { COMPONENT_STATE, ComponentCommonApiJson, componentStateToFriendly, MsSseEvent, PlayApiCommon } from "../../core/Api";
import { capitalize } from "../../core/StringUtils";
import { PlayerState } from "../../backend/common/infrastructure/config/source/mpd";
import {useSSE, useSSEContext, useSSEEvent} from "@flamefrontend/sse-runtime-react";
import {useSSEContext, useSSEEvent} from "@flamefrontend/sse-runtime-react";
import { Second } from "../../core/Atomic";
import { useTimeout } from 'react-use-timeout';
export const PlayStateBadge = (props: ComponentProps<typeof Badge> & { state: PlayApiCommon['state'], suffix?: React.JSX.Element }) => {
export const PlayStateBadge = (props: ComponentProps<typeof Badge> & { state: PlayApiCommon['state'], suffix?: React.JSX.Element, hasDeadQueue?: boolean }) => {
const { state, suffix, ...rest } = props;
let badgeColor = undefined,
badgeText = capitalize(state);
let badgeColor = undefined;
let badgeText = capitalize(state);
switch (state) {
case 'queued':
@ -24,6 +23,9 @@ export const PlayStateBadge = (props: ComponentProps<typeof Badge> & { state: Pl
break;
case 'failed':
badgeColor = 'red';
if(props.hasDeadQueue) {
badgeText = 'Dead Queued';
}
break;
case 'discarded':
badgeColor = 'grey';

View file

@ -17,12 +17,12 @@ export interface GroupInfo {
}
export interface GroupData {
plays: PlayApiCommon[]
plays: PlayApiCommonDetailed[]
date: Dayjs
}
export interface ActivityLogProps extends SortPlaysByProps {
data: PlayApiCommon[]
data: PlayApiCommonDetailed[]
componentId: number
componentType: ComponentType
render?: 'virtNormal' | 'virtDynamic' | 'virtExp' | 'accordian'
@ -106,7 +106,7 @@ export const generateGroupPlays = (data: PlayApiCommon[]): GroupData[] => {
return groupsReduced.groups;
}
export const generateFlatItems = (data: PlayApiCommon[]) => {
export const generateFlatItems = (data: PlayApiCommonDetailed[]) => {
// ensure there are no duplicates
// this may happen if a play is "bumped" from one "page" to another, based on offset,
// when new plays are inserted out of order (playedAt)