mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
feat(ui): Implement play actions
This commit is contained in:
@@ -16,6 +16,7 @@ import { ComponentDetailedRoutable } from './components/msComponent/MSComponentD
|
||||
import { MSComponentListFetchable } from './components/msComponent/MSComponentList';
|
||||
import { Provider } from './components/Provider';
|
||||
import { SettingsContainer } from './components/settings/settings';
|
||||
import { Toaster } from './components/Toaster';
|
||||
|
||||
function NoMatch() {
|
||||
const location = useLocation();
|
||||
@@ -115,6 +116,7 @@ export const sseProviderOptions = {
|
||||
function App() {
|
||||
return (
|
||||
<Provider>
|
||||
<Toaster/>
|
||||
{/* <Box px="4" py="2" pb="4"><AppHeader fetchable/></Box> */}
|
||||
<SSEProvider<MsSseEvent> options={sseProviderOptions}>
|
||||
<RouterProvider router={router}/>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Accordion, Alert, Box, Code, Collapsible, Flex, HStack, Separator, Skeleton, SkeletonText, Span, Stack, useAccordionItemContext, type BadgeProps } from '@chakra-ui/react';
|
||||
import { Accordion, Alert, Box, Group, Portal, Code, Collapsible, Flex, HStack, Separator, Menu, Skeleton, SkeletonText, Span, Stack, useAccordionItemContext, type BadgeProps, type MenuItemProps, type MenuSelectionDetails, useClipboard } from '@chakra-ui/react';
|
||||
import { useSSEContext, useSSEEvent } from "@flamefrontend/sse-runtime-react";
|
||||
import { useQuery, useQueryClient, type InfiniteData } from '@tanstack/react-query';
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient, type InfiniteData } from '@tanstack/react-query';
|
||||
import React, { Fragment, useCallback, useEffect, useState, type ComponentProps } from "react";
|
||||
import { LuChevronRight } from "react-icons/lu";
|
||||
import type { MarkOptional } from "ts-essentials";
|
||||
import type { ComponentsApiJson, MsSseEvent, PaginatedResponse, PlayApiCommonDetailed, QueryPlaysOptsJson, SortPlaysByProps } from "../../core/Api";
|
||||
import { DEAD_QUEUE, type ComponentType, type Second } from "../../core/Atomic";
|
||||
import { INGRESS_QUEUE, type ComponentType, type Second } from "../../core/Atomic";
|
||||
import { tanQueries, useQueryWatcher } from "../queries";
|
||||
import { activityTimelineHasIssue } from "../utils/ComponentUtils";
|
||||
import { ActivityTimeline } from "./ActivityTimeline";
|
||||
@@ -13,10 +13,13 @@ import { EphemeralElement, PlayStateBadge } from "./Badges";
|
||||
import { ShortDateDisplay } from "./DateDisplay";
|
||||
import { ErrorAlert } from "./ErrorAlert";
|
||||
import { ExpandCollapse } from "./ExpandCollapse";
|
||||
import { DebugCopy, ExclamationCircleIcon, ExclamationTriangleIcon, InsertedIcon, RetryButton, UpdatedIcon } from "./icons/ChakraIcons";
|
||||
import { DebugIcon, EllipsisButton, ExclamationCircleIcon, ExclamationTriangleIcon, FinishIconRaw, InsertedIcon, type PowerOffButton, RetryButton, RetryIcon, StopButton, StopIconRaw, TrashIconRaw, UpdatedIcon } from "./icons/ChakraIcons";
|
||||
import { PlayData } from "./PlayData";
|
||||
import { TextMuted } from "./TextMuted";
|
||||
import { capitalize } from '../../core/StringUtils';
|
||||
import ky from 'ky';
|
||||
import type { IconType } from 'react-icons/lib';
|
||||
import { toaster } from "./Toaster"
|
||||
|
||||
type UseActivityQueryOptions = {
|
||||
msQuery?: QueryPlaysOptsJson
|
||||
@@ -306,26 +309,104 @@ export const ActivityDetailFetchable = (props: ActivityDetailFetchableProps) =>
|
||||
return <ActivityDetails componentType={props.componentType} componentName={data?.type} key={props.uid} activity={activity}/>
|
||||
}
|
||||
|
||||
const playStateMenuItem = (Icon: IconType, value: string, name?: string) => (props: Pick<MenuItemProps, 'disabled'> = {}) => {
|
||||
return (<Menu.Item key={value} value={value} {...props}><Box flex="1">{name ?? capitalize(value)}</Box><Icon/></Menu.Item>);
|
||||
}
|
||||
|
||||
const MenuItemRetry = playStateMenuItem(RetryIcon, 'retry');
|
||||
const MenuItemRetryWith = playStateMenuItem(RetryIcon, 'retry', 'Retry With...');
|
||||
const MenuItemCancel = playStateMenuItem(StopIconRaw, 'cancel');
|
||||
const MenuItemTrash = playStateMenuItem(TrashIconRaw, 'delete');
|
||||
const MenuItemDebug = playStateMenuItem(DebugIcon, 'debug');
|
||||
const MenuItemFinish = playStateMenuItem(FinishIconRaw, 'finish', 'Mark Completed');
|
||||
|
||||
const primaryActionProps: ComponentProps<typeof PowerOffButton> = {
|
||||
margin: "1px",
|
||||
variant: "subtle",
|
||||
size: 'xs'
|
||||
}
|
||||
|
||||
export const ActivityStateActions = (props: {activity: PlayApiCommonDetailed}) => {
|
||||
let suffix: React.JSX.Element | null;
|
||||
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 = {};
|
||||
|
||||
const clipboard = useClipboard({value: JSON.stringify(props.activity)});
|
||||
|
||||
const {mutate, isPending, variables, isSuccess} = useMutation({
|
||||
mutationKey: ['playAction', props.activity.uid],
|
||||
mutationFn: () => ky.post(`/api/components/${props.activity.componentId}/state`,{
|
||||
json: {reason: 'User initiated from UI'}
|
||||
})
|
||||
});
|
||||
|
||||
const menuCb = useCallback((select: MenuSelectionDetails) => {
|
||||
if(select.value === 'debug') {
|
||||
clipboard.copy();
|
||||
toaster.create({
|
||||
title: 'Copied debug data to clipboard',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
//mutate();
|
||||
},[mutate, props.activity]);
|
||||
|
||||
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 === INGRESS_QUEUE && x.retries > 0);
|
||||
//badgeProps.paddingRight = 0;
|
||||
switch(props.activity.state) {
|
||||
case 'queued':
|
||||
primaryAction = <StopButton size={{base: '2xs', smTo2xl: 'xs'}} color="red.400" margin="1px" variant="subtle"/>;
|
||||
menuItems = [<MenuItemTrash disabled={isPending}/>,<MenuItemDebug/>];
|
||||
break;
|
||||
case 'failed':
|
||||
primaryAction = <RetryButton size={{base: '2xs', smTo2xl: 'xs'}} margin="1px" variant="subtle"/>;
|
||||
menuItems = [<MenuItemRetryWith disabled />, <MenuItemTrash disabled={isPending}/>,<MenuItemDebug/>];
|
||||
if(!hasDeadQueue) {
|
||||
menuItems.unshift(<MenuItemFinish disabled={isPending}/>)
|
||||
}
|
||||
break
|
||||
default:
|
||||
primaryAction = <RetryButton size={{base: '2xs', smTo2xl: 'xs'}} margin="1px" variant="subtle"/>;
|
||||
menuItems = [<MenuItemRetryWith disabled />, <MenuItemTrash disabled={isPending}/>,<MenuItemDebug/>];
|
||||
break
|
||||
|
||||
}
|
||||
const hasDeadQueue = queueStates.some(x => x.queueName === DEAD_QUEUE && x.queueStatus === 'queued');
|
||||
if(menuItems.length > 0) {
|
||||
menuElm = (
|
||||
<Menu.Root positioning={{ placement: "bottom-end" }} onSelect={menuCb}>
|
||||
<Group attached>
|
||||
{primaryAction}
|
||||
<Menu.Trigger asChild>
|
||||
<EllipsisButton hideBelow="sm" disabled={isPending} {...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 (
|
||||
<Stack>
|
||||
<HStack>
|
||||
<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)}/>
|
||||
<PlayStateBadge {...badgeProps} minH="32px" alignItems="anchor-center" size={{base: 'xs', sm: 'lg', mdTo2xl: 'lg'}} hasDeadQueue={hasDeadQueue} state={props.activity.state} suffix={suffix} />
|
||||
</HStack>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Toaster as ChakraToaster,
|
||||
Portal,
|
||||
Spinner,
|
||||
Stack,
|
||||
Toast,
|
||||
createToaster,
|
||||
} from "@chakra-ui/react"
|
||||
|
||||
export const toaster = createToaster({
|
||||
placement: "bottom-end",
|
||||
pauseOnPageIdle: true,
|
||||
})
|
||||
|
||||
export const Toaster = () => (
|
||||
<Portal>
|
||||
<ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
|
||||
{(toast) => (
|
||||
<Toast.Root width={{ md: "sm" }}>
|
||||
{toast.type === "loading" ? (
|
||||
<Spinner size="sm" color="blue.solid" />
|
||||
) : (
|
||||
<Toast.Indicator />
|
||||
)}
|
||||
<Stack gap="1" flex="1" maxWidth="100%">
|
||||
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
|
||||
{toast.description && (
|
||||
<Toast.Description>{toast.description}</Toast.Description>
|
||||
)}
|
||||
</Stack>
|
||||
{toast.action && (
|
||||
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
|
||||
)}
|
||||
{toast.closable && <Toast.CloseTrigger />}
|
||||
</Toast.Root>
|
||||
)}
|
||||
</ChakraToaster>
|
||||
</Portal>
|
||||
)
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
LuLockOpen
|
||||
} from "react-icons/lu"
|
||||
import { VscDebugRestart } from 'react-icons/vsc';
|
||||
import { HiMiniStop } from "react-icons/hi2";
|
||||
import { FaTrashCan, FaFlagCheckered } from "react-icons/fa6";
|
||||
import { MdOutlineFiberNew } from "react-icons/md";
|
||||
import { RiZzzFill } from "react-icons/ri";
|
||||
import { SiGoogledocs } from "react-icons/si";
|
||||
@@ -243,4 +245,16 @@ export const getMusicServiceIconElement = (service: string): ReactNode => {
|
||||
export const getMusicServiceChakraIcon = (service: string) => {
|
||||
const ServiceIcon = getMusicServiceIcon(service);
|
||||
return (props: IconProps = {}) => <Icon {...props}><ServiceIcon/></Icon>;
|
||||
}
|
||||
}
|
||||
|
||||
export const StopIconRaw = HiMiniStop;
|
||||
export const StopIcon = makeChakraIcon(StopIconRaw);
|
||||
export const StopButton = makeIconButton(StopIconRaw);
|
||||
|
||||
export const TrashIconRaw = FaTrashCan;
|
||||
export const TrashIcon = makeChakraIcon(TrashIconRaw);
|
||||
export const TrashIconButton = makeIconButton(TrashIconRaw);
|
||||
|
||||
export const FinishIconRaw = FaFlagCheckered;
|
||||
export const FinishIcon = makeChakraIcon(FinishIconRaw);
|
||||
export const FinishButton = makeIconButton(FinishIconRaw);
|
||||
@@ -146,6 +146,7 @@ export const generateComponentCommonApiJson = (data: Partial<ComponentCommonApi>
|
||||
countNonLive: 0,
|
||||
state,
|
||||
players,
|
||||
errors: [],
|
||||
status: faker.helpers.arrayElement(statusSamples),
|
||||
monitoringStatus,
|
||||
queued,
|
||||
|
||||
Reference in New Issue
Block a user