260 lines
11 KiB
TypeScript
260 lines
11 KiB
TypeScript
import React, { useCallback, useMemo, useState } from 'react';
|
|
import type { SessionInfo } from '@jellyfin/sdk/lib/generated-client/models/session-info';
|
|
import Typography from '@mui/material/Typography';
|
|
import Card from '@mui/material/Card';
|
|
import CardMedia from '@mui/material/CardMedia';
|
|
import { getDeviceIcon } from 'utils/image';
|
|
import Stack from '@mui/material/Stack';
|
|
import getNowPlayingName from '../../sessions/utils/getNowPlayingName';
|
|
import getSessionNowPlayingTime from '../../sessions/utils/getSessionNowPlayingTime';
|
|
import getNowPlayingImageUrl from '../../sessions/utils/getNowPlayingImageUrl';
|
|
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
|
import Comment from '@mui/icons-material/Comment';
|
|
import PlayArrow from '@mui/icons-material/PlayArrow';
|
|
import Pause from '@mui/icons-material/Pause';
|
|
import Stop from '@mui/icons-material/Stop';
|
|
import Info from '@mui/icons-material/Info';
|
|
import LinearProgress from '@mui/material/LinearProgress';
|
|
import CardActions from '@mui/material/CardActions';
|
|
import IconButton from '@mui/material/IconButton';
|
|
import SimpleAlert from 'components/SimpleAlert';
|
|
import playmethodhelper from 'components/playback/playmethodhelper';
|
|
import globalize from 'lib/globalize';
|
|
import getSessionNowPlayingStreamInfo from '../../sessions/utils/getSessionNowPlayingStreamInfo';
|
|
import { useSendPlayStateCommand } from '../../sessions/api/usePlayPauseSession';
|
|
import { PlaystateCommand } from '@jellyfin/sdk/lib/generated-client/models/playstate-command';
|
|
import InputDialog from 'components/InputDialog';
|
|
import { useSendMessage } from '../../sessions/api/useSendMessage';
|
|
|
|
type DeviceCardProps = {
|
|
device: SessionInfo;
|
|
};
|
|
|
|
const DeviceCard = ({ device }: DeviceCardProps) => {
|
|
const [ playbackInfoTitle, setPlaybackInfoTitle ] = useState('');
|
|
const [ playbackInfoDesc, setPlaybackInfoDesc ] = useState('');
|
|
const [ isPlaybackInfoOpen, setIsPlaybackInfoOpen ] = useState(false);
|
|
const [ isMessageDialogOpen, setIsMessageDialogOpen ] = useState(false);
|
|
const sendMessage = useSendMessage();
|
|
const playStateCommand = useSendPlayStateCommand();
|
|
|
|
const onPlayPauseSession = useCallback(() => {
|
|
if (device.Id) {
|
|
playStateCommand.mutate({
|
|
sessionId: device.Id,
|
|
command: PlaystateCommand.PlayPause
|
|
});
|
|
}
|
|
}, [ device, playStateCommand ]);
|
|
|
|
const onStopSession = useCallback(() => {
|
|
if (device.Id) {
|
|
playStateCommand.mutate({
|
|
sessionId: device.Id,
|
|
command: PlaystateCommand.Stop
|
|
});
|
|
}
|
|
}, [ device, playStateCommand ]);
|
|
|
|
const onMessageSend = useCallback((message: string) => {
|
|
if (device.Id) {
|
|
sendMessage.mutate({
|
|
sessionId: device.Id,
|
|
messageCommand: {
|
|
Text: message,
|
|
TimeoutMs: 5000
|
|
}
|
|
});
|
|
setIsMessageDialogOpen(false);
|
|
}
|
|
}, [ sendMessage, device ]);
|
|
|
|
const showMessageDialog = useCallback(() => {
|
|
setIsMessageDialogOpen(true);
|
|
}, []);
|
|
|
|
const onMessageDialogClose = useCallback(() => {
|
|
setIsMessageDialogOpen(false);
|
|
}, []);
|
|
|
|
const closePlaybackInfo = useCallback(() => {
|
|
setIsPlaybackInfoOpen(false);
|
|
}, []);
|
|
|
|
const showPlaybackInfo = useCallback(() => {
|
|
const displayPlayMethod = playmethodhelper.getDisplayPlayMethod(device);
|
|
|
|
switch (displayPlayMethod) {
|
|
case 'Remux':
|
|
setPlaybackInfoTitle(globalize.translate('Remuxing'));
|
|
setPlaybackInfoDesc(globalize.translate('RemuxHelp1') + '\n' + globalize.translate('RemuxHelp2'));
|
|
break;
|
|
case 'DirectStream':
|
|
setPlaybackInfoTitle(globalize.translate('DirectStreaming'));
|
|
setPlaybackInfoDesc(globalize.translate('DirectStreamHelp1') + '\n' + globalize.translate('DirectStreamHelp2'));
|
|
break;
|
|
case 'DirectPlay':
|
|
setPlaybackInfoTitle(globalize.translate('DirectPlaying'));
|
|
setPlaybackInfoDesc(globalize.translate('DirectPlayHelp'));
|
|
break;
|
|
case 'Transcode': {
|
|
const transcodeReasons = device.TranscodingInfo?.TranscodeReasons as string[] | undefined;
|
|
const localizedTranscodeReasons = transcodeReasons?.map(transcodeReason => globalize.translate(transcodeReason)) || [];
|
|
setPlaybackInfoTitle(globalize.translate('Transcoding'));
|
|
setPlaybackInfoDesc(
|
|
globalize.translate('MediaIsBeingConverted')
|
|
+ '\n\n' + getSessionNowPlayingStreamInfo(device)
|
|
+ '\n\n' + globalize.translate('LabelReasonForTranscoding')
|
|
+ '\n' + localizedTranscodeReasons.join('\n')
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
setIsPlaybackInfoOpen(true);
|
|
}, [ device ]);
|
|
|
|
const nowPlayingName = useMemo(() => (
|
|
getNowPlayingName(device)
|
|
), [ device ]);
|
|
|
|
const nowPlayingImage = useMemo(() => (
|
|
device.NowPlayingItem && getNowPlayingImageUrl(device.NowPlayingItem)
|
|
), [device]);
|
|
|
|
const runningTime = useMemo(() => (
|
|
getSessionNowPlayingTime(device)
|
|
), [ device ]);
|
|
|
|
const deviceIcon = useMemo(() => (
|
|
getDeviceIcon(device)
|
|
), [ device ]);
|
|
|
|
const canControl = device.ServerId && device.SupportsRemoteControl;
|
|
const isPlayingMedia = !!device.NowPlayingItem;
|
|
|
|
return (
|
|
<Card sx={{ width: { xs: '100%', sm: '360px' } }}>
|
|
<InputDialog
|
|
open={isMessageDialogOpen}
|
|
onClose={onMessageDialogClose}
|
|
title={globalize.translate('HeaderSendMessage')}
|
|
label={globalize.translate('LabelMessageText')}
|
|
confirmButtonText={globalize.translate('ButtonSend')}
|
|
onConfirm={onMessageSend}
|
|
/>
|
|
<SimpleAlert
|
|
open={isPlaybackInfoOpen}
|
|
title={playbackInfoTitle}
|
|
text={playbackInfoDesc}
|
|
onClose={closePlaybackInfo}
|
|
/>
|
|
<CardMedia
|
|
sx={{
|
|
height: 200,
|
|
display: 'flex'
|
|
}}
|
|
className={getDefaultBackgroundClass(device.Id)}
|
|
image={nowPlayingImage || undefined}
|
|
>
|
|
<Stack
|
|
justifyContent={'space-between'}
|
|
flexGrow={1}
|
|
sx={{
|
|
backgroundColor: nowPlayingImage ? 'rgba(0, 0, 0, 0.7)' : null,
|
|
padding: 2
|
|
}}>
|
|
<Stack direction='row' alignItems='center' spacing={1}>
|
|
<img
|
|
src={deviceIcon}
|
|
style={{
|
|
maxWidth: '2.5em',
|
|
maxHeight: '2.5em'
|
|
}}
|
|
alt={device.DeviceName || ''}
|
|
/>
|
|
<Stack>
|
|
<Typography>{device.DeviceName}</Typography>
|
|
<Typography>{device.Client + ' ' + device.ApplicationVersion}</Typography>
|
|
</Stack>
|
|
</Stack>
|
|
<Stack direction='row' alignItems={'end'}>
|
|
<Stack flexGrow={1}>
|
|
{nowPlayingName.image ? (
|
|
<img
|
|
src={nowPlayingName.image}
|
|
style={{
|
|
maxHeight: '24px',
|
|
maxWidth: '130px',
|
|
alignSelf: 'flex-start'
|
|
}}
|
|
alt='Media Icon'
|
|
/>
|
|
) : (
|
|
<Typography>{nowPlayingName.topText}</Typography>
|
|
)}
|
|
<Typography>{nowPlayingName.bottomText}</Typography>
|
|
</Stack>
|
|
{device.NowPlayingItem && (
|
|
<Typography>{runningTime.start} / {runningTime.end}</Typography>
|
|
)}
|
|
</Stack>
|
|
</Stack>
|
|
</CardMedia>
|
|
{(device.PlayState?.PositionTicks != null && device.NowPlayingItem?.RunTimeTicks != null) && (
|
|
<LinearProgress
|
|
variant='buffer'
|
|
value={(device.PlayState.PositionTicks / device.NowPlayingItem.RunTimeTicks) * 100}
|
|
valueBuffer={device.TranscodingInfo?.CompletionPercentage || 0}
|
|
sx={{
|
|
'& .MuiLinearProgress-dashed': {
|
|
animation: 'none',
|
|
backgroundImage: 'none',
|
|
backgroundColor: 'background.paper'
|
|
},
|
|
'& .MuiLinearProgress-bar2': {
|
|
backgroundColor: '#dd4919'
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
<CardActions disableSpacing>
|
|
<Stack direction='row' flexGrow={1} justifyContent='center'>
|
|
{canControl && isPlayingMedia && (
|
|
<>
|
|
<IconButton onClick={onPlayPauseSession}>
|
|
{device.PlayState?.IsPaused ? <PlayArrow /> : <Pause />}
|
|
</IconButton>
|
|
<IconButton onClick={onStopSession}>
|
|
<Stop />
|
|
</IconButton>
|
|
</>
|
|
)}
|
|
{isPlayingMedia && (
|
|
<IconButton onClick={showPlaybackInfo}>
|
|
<Info />
|
|
</IconButton>
|
|
)}
|
|
{canControl && (
|
|
<IconButton onClick={showMessageDialog}>
|
|
<Comment />
|
|
</IconButton>
|
|
)}
|
|
</Stack>
|
|
</CardActions>
|
|
{device.UserName && (
|
|
<Stack
|
|
direction='row'
|
|
flexGrow={1}
|
|
justifyContent='center'
|
|
sx={{ paddingBottom: 2 }}
|
|
>
|
|
<Typography>{device.UserName}</Typography>
|
|
</Stack>
|
|
)}
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default DeviceCard;
|