diff --git a/web/src/components/ProgramDetailsDialog.tsx b/web/src/components/ProgramDetailsDialog.tsx index 83b6c02c7..58e007b0b 100644 --- a/web/src/components/ProgramDetailsDialog.tsx +++ b/web/src/components/ProgramDetailsDialog.tsx @@ -1,6 +1,7 @@ -import { OpenInNew } from '@mui/icons-material'; +import { Close as CloseIcon, OpenInNew } from '@mui/icons-material'; import { Box, + Button, Chip, Dialog, DialogContent, @@ -9,10 +10,13 @@ import { Skeleton, Stack, Typography, + useMediaQuery, + useTheme, } from '@mui/material'; import { createExternalId } from '@tunarr/shared'; import { forProgramType } from '@tunarr/shared/util'; -import { ChannelProgram } from '@tunarr/types'; +import { ChannelProgram, TvGuideProgram } from '@tunarr/types'; +import dayjs, { Dayjs } from 'dayjs'; import { isUndefined } from 'lodash-es'; import { ReactEventHandler, @@ -28,7 +32,9 @@ import { useSettings } from '../store/settings/selectors'; type Props = { open: boolean; onClose: () => void; - program: ChannelProgram | undefined; + program: TvGuideProgram | ChannelProgram | undefined; + start?: Dayjs; + stop?: Dayjs; }; const formattedTitle = forProgramType({ @@ -43,12 +49,16 @@ type ThumbLoadState = 'loading' | 'error' | 'success'; export default function ProgramDetailsDialog({ open, onClose, + start, + stop, program, }: Props) { const settings = useSettings(); const [thumbLoadState, setThumbLoadState] = useState('loading'); const imageRef = useRef(null); + const theme = useTheme(); + const smallViewport = useMediaQuery(theme.breakpoints.down('sm')); const rating = useMemo( () => @@ -69,12 +79,22 @@ export default function ProgramDetailsDialog({ [], ); + const episodeTitle = useMemo( + () => + forProgramType({ + custom: (p) => p.program?.episodeTitle ?? '', + content: (p) => p.episodeTitle, + default: '', + }), + [], + ); + const durationChip = useMemo( () => forProgramType({ content: (program) => ( @@ -87,7 +107,7 @@ export default function ProgramDetailsDialog({ (program: ChannelProgram) => { const ratingString = rating(program); return ratingString ? ( - + ) : null; }, [rating], @@ -139,6 +159,7 @@ export default function ProgramDetailsDialog({ const thumbUrl = program ? thumbnailImage(program) : null; const externalUrl = program ? externalLink(program) : null; const programSummary = program ? summary(program) : null; + const programEpisodeTitle = program ? episodeTitle(program) : null; useEffect(() => { setThumbLoadState('loading'); @@ -153,39 +174,61 @@ export default function ProgramDetailsDialog({ setThumbLoadState('error'); }, []); + const isEpisode = + program && program.type === 'content' && program.subtype === 'episode'; + const imageWidth = smallViewport ? (isEpisode ? '100%' : '55%') : 240; + const programStart = dayjs(start); + const programEnd = dayjs(stop); + return ( program && ( - - + + {formattedTitle(program)}{' '} - {externalUrl && ( - - - - )} + onClose()} + aria-label="close" + sx={{ position: 'absolute', top: 10, right: 10 }} + size="large" + > + + {durationChip(program)} {ratingChip(program)} + - - + + @@ -193,15 +236,20 @@ export default function ProgramDetailsDialog({ thumbLoadState === 'error') && ( )} + {programEpisodeTitle ? ( + + {programEpisodeTitle} + + ) : null} {programSummary ? ( - + {programSummary} ) : ( @@ -212,9 +260,21 @@ export default function ProgramDetailsDialog({ backgroundColor: (theme) => theme.palette.background.default, }} - width={240} + width={imageWidth} /> )} + {externalUrl && ( + + )} diff --git a/web/src/components/channel_config/ChannelProgrammingList.tsx b/web/src/components/channel_config/ChannelProgrammingList.tsx index 52239a0ab..126fcb2cf 100644 --- a/web/src/components/channel_config/ChannelProgrammingList.tsx +++ b/web/src/components/channel_config/ChannelProgrammingList.tsx @@ -19,7 +19,7 @@ import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; import { forProgramType } from '@tunarr/shared/util'; import { ChannelProgram } from '@tunarr/types'; -import dayjs from 'dayjs'; +import dayjs, { Dayjs } from 'dayjs'; import { findIndex, isUndefined, join, map, negate, reject } from 'lodash-es'; import { CSSProperties, useCallback, useState } from 'react'; import { useDrag, useDrop } from 'react-dnd'; @@ -309,6 +309,11 @@ const ProgramListItem = ({ ); }; +type GuideTime = { + start?: Dayjs; + stop?: Dayjs; +}; + export default function ChannelProgrammingList({ programList: passedProgramList, programListSelector = defaultProps.programListSelector, @@ -322,6 +327,7 @@ export default function ChannelProgrammingList({ const [focusedProgramDetails, setFocusedProgramDetails] = useState< ChannelProgram | undefined >(); + const [startStop, setStartStop] = useState({}); const [editProgram, setEditProgram] = useState< ((UIFlexProgram | UIRedirectProgram) & { index: number }) | undefined >(); @@ -349,9 +355,15 @@ export default function ChannelProgrammingList({ [passedProgramList], ); - const openDetailsDialog = useCallback((program: ChannelProgram) => { - setFocusedProgramDetails(program); - }, []); + const openDetailsDialog = useCallback( + (program: ChannelProgram, startTimeDate: Date) => { + setFocusedProgramDetails(program); + const start = dayjs(startTimeDate); + const stop = start.add(program.duration); + setStartStop({ start, stop }); + }, + [], + ); const openEditDialog = useCallback( (program: (UIFlexProgram | UIRedirectProgram) & { index: number }) => { @@ -376,7 +388,7 @@ export default function ChannelProgrammingList({ moveProgram={moveProgram} findProgram={findProgram} enableDrag={!!enableDnd} - onInfoClicked={openDetailsDialog} + onInfoClicked={() => openDetailsDialog(program, startTimeDate)} onEditClicked={openEditDialog} /> ); @@ -449,6 +461,8 @@ export default function ChannelProgrammingList({ open={!isUndefined(focusedProgramDetails)} onClose={() => setFocusedProgramDetails(undefined)} program={focusedProgramDetails} + start={startStop.start} + stop={startStop.stop} /> {episodeTitle} - - {`${programStart.format('h:mm')} - ${programEnd.format('h:mma')}`} - {isPlaying ? ` (${remainingTime}m remaining)` : null} - + {((smallViewport && pct > 20) || (!smallViewport && pct > 8)) && ( + <> + + {`${programStart.format('h:mm')} - ${programEnd.format( + 'h:mma', + )}`} + + + {isPlaying ? ` (${remainingTime}m left)` : null} + + + )} {endOfAvailableProgramming ? renderUnavailableProgramming(finalBlockWidth, index) @@ -459,6 +467,8 @@ export function TvGuide({ channelId, start, end }: Props) { open={!isUndefined(modalProgram)} onClose={() => handleModalClose()} program={modalProgram} + start={dayjs(modalProgram?.start)} + stop={dayjs(modalProgram?.stop)} />