feat(ui): Add more info to play data

This commit is contained in:
FoxxMD
2026-03-24 01:22:39 +00:00
parent 2085df4406
commit 0daaec81ca
8 changed files with 118 additions and 50 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ import {
} from "../utils.js";
import { sortByNewestPlayDate } from '../../core/PlayUtils.js';
import { formatNumber } from '../../core/DataUtils.js';
import { timeToHumanTimestamp } from "../utils/TimeUtils.js";
import { timeToHumanTimestamp } from "../../core/TimeUtils.js";
import { todayAwareFormat } from "../../core/TimeUtils.js";
import { getRoot } from '../ioc.js';
import { componentFileLogger } from '../common/logging.js';
+2 -1
View File
@@ -21,7 +21,8 @@ import {
} from "../utils.js";
import { genGroupIdStr } from '../../core/PlayUtils.js';
import { formatNumber } from '../../core/DataUtils.js';
import { timePassesScrobbleThreshold, timeToHumanTimestamp } from "../utils/TimeUtils.js";
import { timePassesScrobbleThreshold } from "../utils/TimeUtils.js";
import { timeToHumanTimestamp } from "../../core/TimeUtils.js";
import { PromisePool } from "@supercharge/promise-pool";
import AbstractSource from "./AbstractSource.js";
import { AbstractPlayerState, createPlayerOptions, PlayerStateOptions } from "./PlayerState/AbstractPlayerState.js";
@@ -18,7 +18,8 @@ import { genGroupIdStr } from '../../../core/PlayUtils.js';
import { formatNumber } from '../../../core/DataUtils.js';
import { ListenProgress } from "./ListenProgress.js";
import { ListenRange, ListenRangePositional } from "./ListenRange.js";
import { closeToPlayEnd, closeToPlayStart, repeatDurationPlayed, timeToHumanTimestamp } from "../../utils/TimeUtils.js";
import { closeToPlayEnd, closeToPlayStart, repeatDurationPlayed } from "../../utils/TimeUtils.js";
import { timeToHumanTimestamp } from "../../../core/TimeUtils.js";
import { todayAwareFormat } from "../../../core/TimeUtils.js";
export interface PlayerStateIntervals {
-12
View File
@@ -318,18 +318,6 @@ export const parseDurationFromTimestamp = (timestamp: any) => {
export type Milliseconds = number;
export const timeToHumanTimestamp = (val: ReturnType<typeof dayjs.duration> | Milliseconds): string => {
const ms = dayjs.isDuration(val) ? Math.abs(val.asMilliseconds()) : val;
// less than one hour
if(ms < 3600000) {
// EX 14:07
return new Date(ms).toISOString().substring(14, 19)
}
// EX 01:15:45
return new Date(ms).toISOString().substring(11, 19);
}
/** Is Position earlier than X seconds or Y% percent of the start of a Play? */
export const closeToPlayStart = (play: PlayObject, position: number, thresholds: {absolute?: number, percent?: number, hintPrefix?: boolean} = {}): [boolean, string] => {
const {
+83 -30
View File
@@ -1,11 +1,13 @@
import React, { Fragment, useMemo, useState } from 'react';
import { EmptyState, DataList, HStack, Tag, Tabs, Wrap, Box, Flex, SegmentGroup, Stack, Text, Separator, IconButton, Container, SimpleGrid, Float } from "@chakra-ui/react"
import { LuCode, LuText } from "react-icons/lu"
import { EmptyState, DataList, HStack, Tag, Tabs, Wrap, Box, Flex, SegmentGroup, Stack, Text, Separator, IconButton, Container, SimpleGrid, Float, Spacer, Icon, Link, Span, Show } from "@chakra-ui/react"
import { LuCode, LuText, LuCheck, LuX } from "react-icons/lu"
import { JsonPlayObject, PlayObjectLifecycleless } from '../../core/Atomic.js';
import { shortTodayAwareFormat } from '../../core/TimeUtils.js';
import { shortTodayAwareFormat, timeToHumanTimestamp } from '../../core/TimeUtils.js';
import dayjs from 'dayjs';
import { ChakraCodeBlock } from './CodeBlock.js';
import { TextMuted } from './TextMuted.js';
import { formatNumber } from '../../core/DataUtils.js';
import { Muted } from './Typography.js';
const EmptyPlayData = () => {
return (
@@ -114,36 +116,87 @@ export const PlayDataDataList = (props: { play: JsonPlayObject, dates: DisplayDa
const {
data: {
artists = []
track,
artists = [],
listenedFor,
duration,
repeat,
meta: {
brainz = {}
} = {}
} = {},
meta: {
url: {
web: webUrl,
origin: originUrl
} = {}
} = {}
} = play;
let titleElm: JSX.Element;
if(webUrl !== undefined || originUrl !== undefined) {
titleElm = <Link variant="underline" target="_blank" href={webUrl ?? originUrl}>{track}</Link>
} else {
titleElm = <Span>{track}</Span>;
}
return (
<DataList.Root flexWrap="wrap" flexDirection="row">
<DataList.Item flexGrow="1">
<DataList.ItemLabel flexShrink="1">Title</DataList.ItemLabel>
<DataList.ItemValue>{play.data.track}</DataList.ItemValue>
</DataList.Item>
<DataList.Item flexGrow="1">
<DataList.ItemLabel>Artists</DataList.ItemLabel>
<DataList.ItemValue>
{artists.length === 0 ? <Text color="fg.muted">(No Artists)</Text> :
<HStack>{play.data.artists.map((x, index) => {
return (
<Tag.Root key={index}>
<Tag.Label>{x}</Tag.Label>
</Tag.Root>
);
})}</HStack>}
</DataList.ItemValue>
</DataList.Item>
{albumArtistElm}
<DataList.Item flexGrow="1">
<DataList.ItemLabel>Album</DataList.ItemLabel>
<DataList.ItemValue>{play.data.album}</DataList.ItemValue>
</DataList.Item>
<PlayDatesStack play={play} dates={dates} />
</DataList.Root>
<Flex flexDirection="column" gap="4">
<DataList.Root flexWrap="wrap" flexDirection="row">
<DataList.Item flexGrow="1">
<DataList.ItemLabel flexShrink="1">Title</DataList.ItemLabel>
<DataList.ItemValue>{titleElm}</DataList.ItemValue>
</DataList.Item>
<DataList.Item flexGrow="1">
<DataList.ItemLabel>Artists</DataList.ItemLabel>
<DataList.ItemValue>
{artists.length === 0 ? <Text color="fg.muted">(No Artists)</Text> :
<HStack>{play.data.artists.map((x, index) => {
return (
<Tag.Root key={index}>
<Tag.Label>{x}</Tag.Label>
</Tag.Root>
);
})}</HStack>}
</DataList.ItemValue>
</DataList.Item>
{albumArtistElm}
<DataList.Item flexGrow="1">
<DataList.ItemLabel>Album</DataList.ItemLabel>
<DataList.ItemValue>{play.data.album}</DataList.ItemValue>
</DataList.Item>
</DataList.Root>
<DataList.Root flexWrap="wrap" flexDirection="row">
<PlayDatesStack play={play} dates={dates} />
<DataList.Item flexGrow="1" hideBelow="sm">
<DataList.ItemLabel>Duration</DataList.ItemLabel>
<DataList.ItemValue>
<Stack gap="1">
<Text textStyle="xs">Track Length: {timeToHumanTimestamp(dayjs.duration(duration, 's'))}</Text>
{listenedFor !== undefined ? <Muted textStyle="xs">Listened For: {timeToHumanTimestamp(dayjs.duration(listenedFor, 's'))} ({formatNumber((listenedFor / duration) * 100)}%)</Muted> : null}
</Stack>
</DataList.ItemValue>
</DataList.Item>
<DataList.Item flexGrow="1" hideBelow="sm">
<DataList.ItemLabel>Repeat?</DataList.ItemLabel>
<DataList.ItemValue><Icon>{repeat ? <LuCheck/> : <LuX/>}</Icon></DataList.ItemValue>
</DataList.Item>
</DataList.Root>
<DataList.Root flexWrap="wrap" flexDirection="row" hideBelow="sm">
<Show when={Object.keys(brainz).length > 0}>
<DataList.Item flexGrow="1">
<DataList.ItemLabel>MBIDs</DataList.ItemLabel>
<DataList.ItemValue>
<Stack gap="1">
<Show when={brainz.track !== undefined}><Text textStyle="xs"><Muted>Track:</Muted> {brainz.track}</Text></Show>
<Show when={brainz.recording !== undefined}><Text textStyle="xs"><Muted>Recording</Muted>: {brainz.recording}</Text></Show>
<Show when={brainz.album !== undefined}><Text textStyle="xs"><Muted>Album</Muted>: {brainz.album}</Text></Show>
</Stack>
</DataList.ItemValue>
</DataList.Item>
</Show>
</DataList.Root>
</Flex>
)
}
@@ -163,7 +216,7 @@ export const PlayDatesStack = (props: { play: JsonPlayObject, dates: DisplayDate
} else {
const dateElements = [];
if (dates.includes('played') || dates.includes('all')) {
dateElements.push((<TextMuted key="playDate">{`Played ${shortTodayAwareFormat(dayjs(play.data.playDate))}`}</TextMuted>));
dateElements.push((<Text textStyle="xs" key="playDate">{`Played ${shortTodayAwareFormat(dayjs(play.data.playDate))}`}</Text>));
if (play.data.playDateCompleted !== undefined) {
dateElements.push((<TextMuted key="playDateCompleted">{`Played Until ${shortTodayAwareFormat(dayjs(play.data.playDateCompleted))}`}</TextMuted>));
}
+7 -2
View File
@@ -154,13 +154,15 @@ export const generatePlay = (data: ObjectPlayData = {}, meta: MarkOptional<PlayM
playDateCompleted = false
} = opts;
const duration = faker.number.int({min: 30, max: 300});
const play: PlayObject = {
data: {
track: faker.music.songName(),
artists: faker.helpers.multiple(faker.music.artist, {count: {min: 1, max: 3}}),
duration: faker.number.int({min: 30, max: 300}),
duration,
playDate: dayjs().subtract(faker.number.int({min: 1, max: 800})),
album: faker.music.album(),
listenedFor: faker.number.int({min: duration * 0.5, max: duration}),
...data
},
meta: {
@@ -172,7 +174,10 @@ export const generatePlay = (data: ObjectPlayData = {}, meta: MarkOptional<PlayM
meta: {}
},
steps: []
}
},
url: {
origin: 'https://example.com'
},
}
}
+13 -1
View File
@@ -1,6 +1,7 @@
import dayjs, { Dayjs } from "dayjs";
import isToday from 'dayjs/plugin/isToday.js';
import { SHORT_CALENDAR_NOTZ_FORMAT, SHORT_TODAY_NOTZ_FORMAT } from "./Atomic.js";
import { Milliseconds } from "../backend/utils/TimeUtils.js";
dayjs.extend(isToday);
@@ -13,4 +14,15 @@ export const todayAwareFormat = (date: Dayjs, opts: { fullFormat?: string; today
export const shortTodayAwareFormat = (date: Dayjs): string => {
return todayAwareFormat(date, {fullFormat: SHORT_CALENDAR_NOTZ_FORMAT, todayFormat: SHORT_TODAY_NOTZ_FORMAT});
}
};
export const timeToHumanTimestamp = (val: ReturnType<typeof dayjs.duration> | Milliseconds): string => {
const ms = dayjs.isDuration(val) ? Math.abs(val.asMilliseconds()) : val;
// less than one hour
if (ms < 3600000) {
// EX 14:07
return new Date(ms).toISOString().substring(14, 19);
}
// EX 01:15:45
return new Date(ms).toISOString().substring(11, 19);
};
+10 -2
View File
@@ -5,12 +5,14 @@ import { fn } from 'storybook/test';
import { PlayData } from "../client/components/PlayData.js";
import {Provider} from "../client/components/Provider";
import { Container } from '@chakra-ui/react';
import { generateArtists, generateJsonPlay, generatePlay } from "../core/PlayTestUtils.js"
import { generateArtists, generateJsonPlay, generatePlay, withBrainz } from "../core/PlayTestUtils.js"
import clone from "clone";
import { asJsonPlayObject } from "../core/tests/utils/fixtures.js";
type PropsAndCustomArgs = React.ComponentProps<typeof PlayData> & {
includeAlbumArtists?: boolean;
defaultFinal?: boolean
brainz?: boolean
};
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = preview.type<{args: PropsAndCustomArgs}>().meta({
@@ -29,7 +31,8 @@ args: {
play: generateJsonPlay(),
includeAlbumArtists: false,
showCodeToggle: true,
defaultFinal: true
defaultFinal: true,
brainz: false
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#story-args
});
@@ -51,6 +54,11 @@ export const PlayInfoStory = meta.story({
args.final.data.albumArtists = aa;
}
}
if(args.brainz) {
// @ts-ignore
args.play = asJsonPlayObject(withBrainz(args.play, {include: ['album','track','artist']}));
}
return (<PlayData {...args}/>)
}
});