mirror of
https://github.com/FoxxMD/multi-scrobbler.git
synced 2026-09-03 05:10:00 +03:00
feat(ui): Add more info to play data
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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}/>)
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user