feishin/src/remote/components/remote-container.tsx

225 lines
8.6 KiB
TypeScript
Raw Normal View History

import formatDuration from 'format-duration';
import debounce from 'lodash/debounce';
2025-05-20 19:23:36 -07:00
import { useCallback } from 'react';
2025-06-24 14:36:14 -07:00
import { RiPauseFill, RiPlayFill, RiVolumeUpFill } from 'react-icons/ri';
2025-05-20 19:23:36 -07:00
2025-06-24 14:36:14 -07:00
import { PlayerImage } from '/@/remote/components/player-image';
import { WrappedSlider } from '/@/remote/components/wrapped-slider';
2025-05-20 19:23:36 -07:00
import { useInfo, useSend, useShowImage } from '/@/remote/store';
2025-06-24 14:36:14 -07:00
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group';
import { Rating } from '/@/shared/components/rating/rating';
2025-06-24 14:36:14 -07:00
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
2025-05-20 19:23:36 -07:00
import { PlayerRepeat, PlayerStatus } from '/@/shared/types/types';
export const RemoteContainer = () => {
const { position, repeat, shuffle, song, status, volume } = useInfo();
const send = useSend();
const showImage = useShowImage();
const id = song?.id;
const setRating = useCallback(
(rating: number) => {
send({ event: 'rating', id: id!, rating });
},
[send, id],
);
const debouncedSetRating = debounce(setRating, 400);
return (
2025-07-12 11:17:54 -07:00
<Stack gap="md" h="100dvh" w="100%">
2025-06-24 14:36:14 -07:00
{showImage && (
2025-07-12 11:17:54 -07:00
<Flex align="center" justify="center" w="100%">
2025-06-24 14:36:14 -07:00
<PlayerImage src={song?.imageUrl} />
</Flex>
)}
2024-07-03 08:47:26 +00:00
{id && (
2025-06-24 14:36:14 -07:00
<Stack gap="xs">
<Text
fw={700}
size="xl"
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{song.name}
</Text>
<Text
isMuted
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{song.album}
</Text>
<Text
isMuted
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{song.artistName}
</Text>
<Group justify="space-between">
{song.releaseDate && (
2025-06-24 14:36:14 -07:00
<Text isMuted>{new Date(song.releaseDate).toLocaleDateString()}</Text>
)}
2025-06-24 14:36:14 -07:00
<Text isMuted>Plays: {song.playCount}</Text>
</Group>
2025-06-24 14:36:14 -07:00
</Stack>
)}
2025-07-12 11:17:54 -07:00
<Group gap={0} grow>
2025-06-24 14:36:14 -07:00
<ActionIcon
2024-07-03 08:47:26 +00:00
disabled={!id}
2025-06-24 14:36:14 -07:00
icon="favorite"
iconProps={{
fill: song?.userFavorite ? 'primary' : 'default',
}}
onClick={() => {
if (!id) return;
send({ event: 'favorite', favorite: !song.userFavorite, id });
}}
tooltip={{
label: song?.userFavorite ? 'Unfavorite' : 'Favorite',
}}
variant="transparent"
/>
{(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && (
<div style={{ margin: 'auto' }}>
2025-07-12 11:17:54 -07:00
<Tooltip label="Double click to clear" openDelay={1000}>
2025-06-24 14:36:14 -07:00
<Rating
onChange={debouncedSetRating}
onDoubleClick={() => debouncedSetRating(0)}
style={{ margin: 'auto' }}
value={song.userRating ?? 0}
/>
</Tooltip>
</div>
)}
</Group>
2025-07-12 11:17:54 -07:00
<Group gap="xs" grow>
2025-06-24 14:36:14 -07:00
<ActionIcon
disabled={!id}
icon="mediaPrevious"
iconProps={{
fill: 'default',
size: 'lg',
}}
2025-05-20 19:23:36 -07:00
onClick={() => send({ event: 'previous' })}
tooltip={{
label: 'Previous track',
}}
variant="default"
2025-06-24 14:36:14 -07:00
/>
<ActionIcon
2024-07-03 08:47:26 +00:00
disabled={!id}
onClick={() => {
if (status === PlayerStatus.PLAYING) {
send({ event: 'pause' });
} else if (status === PlayerStatus.PAUSED) {
send({ event: 'play' });
}
}}
tooltip={{
label: id && status === PlayerStatus.PLAYING ? 'Pause' : 'Play',
}}
2025-05-20 19:23:36 -07:00
variant="default"
>
2024-07-03 08:47:26 +00:00
{id && status === PlayerStatus.PLAYING ? (
<RiPauseFill size={25} />
) : (
<RiPlayFill size={25} />
)}
2025-06-24 14:36:14 -07:00
</ActionIcon>
<ActionIcon
2024-07-03 08:47:26 +00:00
disabled={!id}
2025-06-24 14:36:14 -07:00
icon="mediaNext"
iconProps={{
fill: 'default',
size: 'lg',
}}
2025-05-20 19:23:36 -07:00
onClick={() => send({ event: 'next' })}
tooltip={{
label: 'Next track',
}}
variant="default"
2025-06-24 14:36:14 -07:00
/>
</Group>
2025-07-12 11:17:54 -07:00
<Group gap="xs" grow>
2025-06-24 14:36:14 -07:00
<ActionIcon
icon="mediaShuffle"
iconProps={{
fill: shuffle ? 'primary' : 'default',
size: 'lg',
}}
2025-05-20 19:23:36 -07:00
onClick={() => send({ event: 'shuffle' })}
tooltip={{
label: shuffle ? 'Shuffle tracks' : 'Shuffle disabled',
}}
variant="default"
2025-06-24 14:36:14 -07:00
/>
<ActionIcon
icon={
repeat === undefined || repeat === PlayerRepeat.ONE
? 'mediaRepeatOne'
: 'mediaRepeat'
}
iconProps={{
fill:
repeat !== undefined && repeat !== PlayerRepeat.NONE
? 'primary'
: 'default',
size: 'lg',
}}
2025-05-20 19:23:36 -07:00
onClick={() => send({ event: 'repeat' })}
tooltip={{
label: `Repeat ${
repeat === PlayerRepeat.ONE
? 'One'
: repeat === PlayerRepeat.ALL
? 'all'
: 'none'
}`,
}}
variant="default"
/>
2025-06-24 14:36:14 -07:00
</Group>
<Stack gap="lg">
{id && position !== undefined && (
<WrappedSlider
label={(value) => formatDuration(value * 1e3)}
leftLabel={formatDuration(position * 1e3)}
max={song.duration / 1e3}
onChangeEnd={(e) => send({ event: 'position', position: e })}
rightLabel={formatDuration(song.duration)}
value={position}
/>
)}
<WrappedSlider
leftLabel={<RiVolumeUpFill size={20} />}
max={100}
onChangeEnd={(e) => send({ event: 'volume', volume: e })}
rightLabel={
2025-07-12 11:17:54 -07:00
<Text fw={600} size="xs">
2025-06-24 14:36:14 -07:00
{volume ?? 0}
</Text>
}
value={volume ?? 0}
/>
2025-06-24 14:36:14 -07:00
</Stack>
</Stack>
);
};