mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-07-09 17:28:28 +00:00
feat(ui): Implement plays refresh
This commit is contained in:
parent
fb452eb40b
commit
c4b1d72f3f
4 changed files with 106 additions and 27 deletions
|
|
@ -18,20 +18,27 @@ import {
|
|||
LuPowerOff,
|
||||
LuEye,
|
||||
LuEyeClosed,
|
||||
LuCalendar
|
||||
LuCalendar,
|
||||
LuRefreshCw
|
||||
} from "react-icons/lu"
|
||||
import { VscDebugRestart } from 'react-icons/vsc';
|
||||
import { RiZzzFill } from "react-icons/ri";
|
||||
import { SiGoogledocs } from "react-icons/si";
|
||||
import { IconButton, Clipboard, useClipboard } from "@chakra-ui/react"
|
||||
import { IconButton, Clipboard, useClipboard, Spinner } from "@chakra-ui/react"
|
||||
import { ComponentProps, PropsWithChildren } from 'react';
|
||||
import { IconBaseProps, IconType } from "react-icons/lib";
|
||||
|
||||
export const makeIconButton = (Icon: IconType) => (props: PropsWithChildren<ComponentProps<typeof IconButton>> & { iconProps?: IconBaseProps }) => {
|
||||
const { iconProps = {}, children, ...rest } = props;
|
||||
export const makeIconButton = (Icon: IconType) => (props: PropsWithChildren<ComponentProps<typeof IconButton>> & { iconProps?: IconBaseProps, loading?: boolean }) => {
|
||||
const {
|
||||
iconProps = {},
|
||||
children,
|
||||
loading = false,
|
||||
size = 'xs',
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<IconButton variant="surface" size="xs" {...rest}>
|
||||
<Icon {...iconProps} />{children}
|
||||
<IconButton variant="surface" disabled={loading} size={size} {...rest}>
|
||||
{loading ? <Spinner/> : <Icon {...iconProps} />}{children}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
|
@ -142,4 +149,7 @@ export const EyeClosedIcon = LuEyeClosed;
|
|||
export const EyeClosedButton = makeIconButton(LuEyeClosed);
|
||||
|
||||
export const CalendarIcon = LuCalendar;
|
||||
export const CalendarButton = makeIconButton(CalendarIcon);
|
||||
export const CalendarButton = makeIconButton(CalendarIcon);
|
||||
|
||||
export const RefreshIcon = LuRefreshCw;
|
||||
export const RefreshButton = makeIconButton(RefreshIcon);
|
||||
|
|
@ -8,7 +8,7 @@ import "./PlayList.scss";
|
|||
import { MsSseEvent, MsSseEventPayload, PlayApiCommon, PlayApiCommonDetailed } from '../../../core/Api.js';
|
||||
import { useQuery, useInfiniteQuery, UseInfiniteQueryResult, InfiniteData, useQueryClient } from '@tanstack/react-query';
|
||||
import { ErrorAlert } from '../ErrorAlert.js';
|
||||
import { tanQueries } from '../../queries/index.js';
|
||||
import { QueryPlaysOptsJsonRefreshable, tanQueries } from '../../queries/index.js';
|
||||
import { VirtualizedListNormal } from './VirtualListNormal.js';
|
||||
import { NoPlayResults, VirtualizedListDynamic } from './VirtualListDynamic.js';
|
||||
import { VirtualizedListExp } from './VirtualListExperimental.js';
|
||||
|
|
@ -67,7 +67,7 @@ const PlainAccordian = (props: ActivityLogProps) => {
|
|||
</Accordion.ItemTrigger>
|
||||
<Accordion.ItemContent>
|
||||
<Accordion.ItemBody borderTopColor="gray.border" >
|
||||
{live ? <ActivityDetailFetchable componentId={props.componentId} componentType={props.componentType} query={props.query} uid={activity.uid} /> : <ActivityDetails activity={activity as PlayApiCommonDetailed} {...props} />}
|
||||
{live ? <ActivityDetailFetchable componentId={props.componentId} componentType={props.componentType} uid={activity.uid} /> : <ActivityDetails activity={activity as PlayApiCommonDetailed} {...props} />}
|
||||
</Accordion.ItemBody>
|
||||
</Accordion.ItemContent>
|
||||
</Accordion.Item>
|
||||
|
|
@ -86,12 +86,12 @@ export const ListContainer = (props?: ComponentProps<typeof ActivityList>) => {
|
|||
return <Container maxWidth="3xl"><ActivityList {...props} /></Container>
|
||||
}
|
||||
|
||||
export const ListContainerFetchable = (props: { componentId: number, componentType: ComponentType, filters?: QueryPlaysOptsJson } & Pick<ComponentProps<typeof ActivityList>, 'render'>) => {
|
||||
export const ListContainerFetchable = (props: { componentId: number, componentType: ComponentType, filters?: QueryPlaysOptsJsonRefreshable } & Pick<ComponentProps<typeof ActivityList>, 'render'>) => {
|
||||
const {
|
||||
componentId,
|
||||
filters = {}
|
||||
} = props;
|
||||
const query: QueryPlaysOptsJson = { ...filters, order: 'desc', sort: 'playedAt' };
|
||||
const query: QueryPlaysOptsJsonRefreshable = filters; // { ...filters, order: 'desc', sort: 'playedAt' };
|
||||
const {
|
||||
isPending,
|
||||
isError,
|
||||
|
|
@ -133,8 +133,8 @@ export const ListContainerFetchable = (props: { componentId: number, componentTy
|
|||
});
|
||||
|
||||
let rendered;
|
||||
if (status === 'pending' && data === undefined) {
|
||||
rendered = <Stack><ActivitySummarySkeleton /><ActivitySummarySkeleton /><ActivitySummarySkeleton /></Stack>;
|
||||
if (isPending) {
|
||||
rendered = <Stack width="100%"><ActivitySummarySkeleton /><ActivitySummarySkeleton /><ActivitySummarySkeleton /></Stack>;
|
||||
} else if (isError || status === 'error') {
|
||||
rendered = <ErrorAlert error={error} />
|
||||
} else if(!isFetching && allPlays.length === 0) {
|
||||
|
|
@ -263,16 +263,18 @@ const playInWindow = (data: PlayApiCommonDetailed, query: QueryPlaysOptsJson): b
|
|||
}
|
||||
|
||||
export const ListContainerFilterable = (props: { componentId: number, componentType: ComponentType } & Pick<ComponentProps<typeof ActivityList>, 'render'>) => {
|
||||
const [filters, setFilter] = useState<QueryPlaysOptsJson>({
|
||||
const [filters, setFilter] = useState<QueryPlaysOptsJsonRefreshable>({
|
||||
playedAt: {
|
||||
type: 'between',
|
||||
range: todayRange,
|
||||
inclusive: true
|
||||
}
|
||||
},
|
||||
order: 'desc',
|
||||
sort: 'playedAt'
|
||||
});
|
||||
return (
|
||||
<Stack width="100%" gap="4">
|
||||
<ListFilters componentType={props.componentType} filters={filters} onChange={setFilter}/>
|
||||
<ListFilters componentType={props.componentType} componentId={props.componentId} filters={filters} onChange={setFilter}/>
|
||||
<ListContainerFetchable {...props} filters={filters} />
|
||||
</Stack>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Span, Stack, Text, Box, HStack, Wrap, Flex, Container, Select, Portal, createListCollection, useSelectContext, DatePicker, VStack, Button, Spacer, TagsInput, Card } from '@chakra-ui/react';
|
||||
import { Span, Stack, Text, Box, Spinner, HStack, Wrap, Flex, Container, Select, Portal, createListCollection, useSelectContext, DatePicker, VStack, Button, Spacer, TagsInput, Card } from '@chakra-ui/react';
|
||||
import { ComponentType, isComponentTypeSource, PLAY_CLIENT_STATE, PLAY_SOURCE_STATE, PlayState } from '../../../core/Atomic.js';
|
||||
import React, { ComponentProps, Fragment, useMemo, useCallback, useState } from "react"
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
|
|
@ -24,7 +24,10 @@ import {
|
|||
import { QueryPlaysOptsJson } from '../../../backend/common/database/drizzle/repositories/PlayRepository.js';
|
||||
import { cardHeaderSeparator } from '../../utils/ComponentUtils.js';
|
||||
import { CompareDateBetween } from '../../../backend/common/database/drizzle/repositories/BaseRepository.js';
|
||||
import { CalendarButton } from '../icons/ChakraIcons.js';
|
||||
import { CalendarButton, RefreshButton } from '../icons/ChakraIcons.js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { QueryPlaysOptsJsonRefreshable, tanQueries, useQueryWatcher } from '../../queries/index.js';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
const noop = (_) => null;
|
||||
|
||||
|
|
@ -256,15 +259,20 @@ export const PhraseFilter = (props: PhraseFilterProps) => {
|
|||
}
|
||||
|
||||
export const ListFilters = (props: {
|
||||
onChange: (e: QueryPlaysOptsJson) => void
|
||||
filters: QueryPlaysOptsJson
|
||||
onChange: (e: QueryPlaysOptsJsonRefreshable) => void,
|
||||
loading?: boolean
|
||||
filters: QueryPlaysOptsJsonRefreshable
|
||||
componentType: ComponentType,
|
||||
componentId: number
|
||||
}) => {
|
||||
const {
|
||||
filters,
|
||||
onChange,
|
||||
componentId,
|
||||
} = props;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const setState = useCallback((val: PlayState[]) => {
|
||||
const {
|
||||
state,
|
||||
|
|
@ -299,10 +307,24 @@ export const ListFilters = (props: {
|
|||
onChange({...rest, text: val});
|
||||
}, [onChange, filters]);
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: tanQueries.activities.list(componentId, filters).queryKey
|
||||
});
|
||||
// const nonce = nanoid();
|
||||
// const {
|
||||
// ...rest
|
||||
// } = filters;
|
||||
// console.log(`Adding nonce for refresh ${nonce}`);
|
||||
// onChange({...rest, nonce});
|
||||
}, [componentId, filters]);
|
||||
|
||||
const { isFetching } = useQueryWatcher(tanQueries.activities.list(componentId, filters).queryKey)
|
||||
|
||||
return (
|
||||
<Card.Root size="sm" variant="outline">
|
||||
<Card.Header {...cardHeaderSeparator}>
|
||||
Filters
|
||||
<HStack>Filters <RefreshButton variant="ghost" size="sm" loading={isFetching} onClick={(e) => onRefresh()}/></HStack>
|
||||
</Card.Header>
|
||||
<Card.Body px="3" py="4">
|
||||
<Wrap gap="5">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { createQueryKeys, mergeQueryKeys } from "@lukemorales/query-key-factory";
|
||||
import { useQueryClient, hashKey, QueryObserver } from '@tanstack/react-query'
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import ky from 'ky';
|
||||
import { QueryPlaysOpts, QueryPlaysOptsJson } from "../../backend/common/database/drizzle/repositories/PlayRepository";
|
||||
import qs from 'qs';
|
||||
|
|
@ -7,6 +9,8 @@ import { PaginatedResponse } from "../../backend/common/database/drizzle/reposit
|
|||
import { ComponentsApiJson, PlayApiCommonDetailed } from "../../core/Api";
|
||||
import { SourcePlayerJson } from "../../core/Atomic";
|
||||
|
||||
export type QueryPlaysOptsJsonRefreshable = QueryPlaysOptsJson & {nonce?: string};
|
||||
|
||||
const components = createQueryKeys('components', {
|
||||
list: () => ({
|
||||
queryKey: ['components'],
|
||||
|
|
@ -23,14 +27,18 @@ const components = createQueryKeys('components', {
|
|||
})
|
||||
|
||||
const activities = createQueryKeys('activities', {
|
||||
list: (componentId: number, filters: QueryPlaysOptsJson) => ({
|
||||
list: (componentId: number, filters: QueryPlaysOptsJsonRefreshable) => ({
|
||||
queryKey: ['components', componentId, 'plays', filters],
|
||||
queryFn: (ctx) => {
|
||||
const {
|
||||
nonce,
|
||||
...rest
|
||||
} = filters;
|
||||
return ky.get(`components/${componentId}/plays`, {
|
||||
baseUrl: baseUrl,
|
||||
searchParams: qs.stringify({...filters, offset: ctx.pageParam})
|
||||
}).json<PaginatedResponse<PlayApiCommonDetailed>>()
|
||||
}
|
||||
baseUrl: baseUrl,
|
||||
searchParams: qs.stringify({...rest, offset: ctx.pageParam})
|
||||
}).json<PaginatedResponse<PlayApiCommonDetailed>>()
|
||||
}
|
||||
}),
|
||||
single: (componentId: number, activityUid: string) => ({
|
||||
queryKey: ['components', componentId, 'play', activityUid],
|
||||
|
|
@ -53,4 +61,41 @@ const players = createQueryKeys('players', {
|
|||
})
|
||||
})
|
||||
|
||||
export const tanQueries = mergeQueryKeys(components, activities, players);
|
||||
export const tanQueries = mergeQueryKeys(components, activities, players);
|
||||
|
||||
export const useQueryState = (queryKey: Readonly<unknown[]>) => {
|
||||
const queryClient = useQueryClient()
|
||||
const [state, setState] = useState(() => queryClient.getQueryState(queryKey))
|
||||
|
||||
useEffect(() => {
|
||||
const targetHash = hashKey(queryKey)
|
||||
return queryClient.getQueryCache().subscribe((event) => {
|
||||
if (event.query.queryHash === targetHash) {
|
||||
setState(event.query.state)
|
||||
}
|
||||
})
|
||||
}, [queryClient, queryKey])
|
||||
|
||||
return state // { status, data, error, fetchStatus, ... }
|
||||
}
|
||||
|
||||
export const useQueryWatcher = <T>(queryKey: Readonly<unknown[]>) => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const observer = useMemo(
|
||||
() =>
|
||||
new QueryObserver<T>(queryClient, {
|
||||
queryKey,
|
||||
enabled: false, // never triggers its own fetch
|
||||
}),
|
||||
[queryClient, queryKey]
|
||||
)
|
||||
|
||||
const [result, setResult] = useState(() => observer.getCurrentResult())
|
||||
|
||||
useEffect(() => {
|
||||
return observer.subscribe(setResult)
|
||||
}, [observer])
|
||||
|
||||
return result // { status, data, error, isPending, isSuccess, ... }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue