Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(project): use a native fallback for image service #328

Merged
merged 5 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 1 addition & 24 deletions src/components/Card/Card.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Card from './Card';
import type { PlaylistItem } from '#types/playlist';

const item = { title: 'aa', duration: 120 } as PlaylistItem;
const itemWithImage = { title: 'This is a movie', duration: 120, shelfImage: { image: 'http://movie.jpg' } } as PlaylistItem;
const itemWithImage = { title: 'This is a movie', duration: 120, cardImage: 'http://movie.jpg' } as PlaylistItem;

describe('<Card>', () => {
it('renders card with video title', () => {
Expand Down Expand Up @@ -35,27 +35,4 @@ describe('<Card>', () => {

expect(getByAltText('This is a movie')).toHaveStyle({ opacity: 1 });
});

it('uses the fallback image when the image fails to load', () => {
const itemWithFallbackImage = {
title: 'This is a movie',
duration: 120,
shelfImage: {
image: 'http://movie.jpg',
fallbackImage: 'http://fallback.jpg',
},
} as PlaylistItem;

const { getByAltText } = render(<Card item={itemWithFallbackImage} onClick={() => ''} />);

fireEvent.error(getByAltText('This is a movie'));

expect(getByAltText('This is a movie')).toHaveAttribute('src', 'http://fallback.jpg?width=320');
expect(getByAltText('This is a movie')).toHaveStyle({ opacity: 0 });

fireEvent.load(getByAltText('This is a movie'));

expect(getByAltText('This is a movie')).toHaveAttribute('src', 'http://fallback.jpg?width=320');
expect(getByAltText('This is a movie')).toHaveStyle({ opacity: 1 });
});
});
2 changes: 1 addition & 1 deletion src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function Card({
isLocked = true,
currentLabel,
}: CardProps): JSX.Element {
const { title, duration, episodeNumber, seasonNumber, shelfImage: image, mediaStatus, scheduledStart } = item;
const { title, duration, episodeNumber, seasonNumber, cardImage: image, mediaStatus, scheduledStart } = item;
const {
t,
i18n: { language },
Expand Down
2 changes: 1 addition & 1 deletion src/components/CardGrid/CardGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function CardGrid({
onCardHover,
}: CardGridProps) {
const breakpoint: Breakpoint = useBreakpoint();
const posterAspect = parseAspectRatio(playlist.shelfImageAspectRatio);
const posterAspect = parseAspectRatio(playlist.cardImageAspectRatio || playlist.shelfImageAspectRatio);
const visibleTiles = cols[breakpoint] + parseTilesDelta(posterAspect);
const [rowCount, setRowCount] = useState(INITIAL_ROW_COUNT);

Expand Down
3 changes: 1 addition & 2 deletions src/components/Hero/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import React from 'react';
import styles from './Hero.module.scss';

import Image from '#components/Image/Image';
import type { ImageData } from '#types/playlist';

type Props = {
title: string;
description: string;
image?: ImageData;
image?: string;
};

const Hero = ({ title, description, image }: Props) => {
Expand Down
81 changes: 3 additions & 78 deletions src/components/Image/Image.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,97 +5,22 @@ import Image from './Image';

describe('<Image>', () => {
test('uses the src attribute when valid', () => {
const { getByAltText } = render(<Image image={{ image: 'http://image.jpg' }} alt="image" />);
const { getByAltText } = render(<Image image="http://image.jpg" alt="image" />);

expect(getByAltText('image')).toHaveAttribute('src', 'http://image.jpg?width=640');
});

test('tries the fallbackSrc when the image fails to load', () => {
const { getByAltText } = render(
<Image
image={{
image: 'http://image.jpg',
fallbackImage: 'http://fallback.jpg',
}}
alt="image"
/>,
);

fireEvent.error(getByAltText('image'));

expect(getByAltText('image')).toHaveAttribute('src', 'http://fallback.jpg?width=640');
});

test('updates the src attribute when changed', () => {
const { getByAltText, rerender } = render(<Image image={{ image: 'http://image.jpg' }} alt="image" />);

expect(getByAltText('image')).toHaveAttribute('src', 'http://image.jpg?width=640');

rerender(<Image image={{ image: 'http://otherimage.jpg' }} alt="image" />);

expect(getByAltText('image')).toHaveAttribute('src', 'http://otherimage.jpg?width=640');
});

test('updates the src attribute when changed with the fallback image', () => {
const { getByAltText, rerender } = render(<Image image={{ image: 'http://image.jpg' }} alt="image" />);

expect(getByAltText('image')).toHaveAttribute('src', 'http://image.jpg?width=640');

rerender(
<Image
image={{
image: 'http://otherimage.jpg',
fallbackImage: 'http://otherfallback.jpg',
}}
alt="image"
/>,
);

fireEvent.error(getByAltText('image'));

expect(getByAltText('image')).toHaveAttribute('src', 'http://otherfallback.jpg?width=640');
});

test('fires the onLoad callback when the image is loaded', () => {
const onLoad = vi.fn();
const { getByAltText } = render(<Image image={{ image: 'http://image.jpg' }} alt="image" onLoad={onLoad} />);

fireEvent.load(getByAltText('image'));

expect(onLoad).toHaveBeenCalledTimes(1);
});

test('fires the onLoad callback when the fallback image is loaded', () => {
const onLoad = vi.fn();
const { getByAltText } = render(
<Image
image={{
image: 'http://image.jpg',
fallbackImage: 'http://fallback.jpg',
}}
alt="image"
onLoad={onLoad}
/>,
);
const { getByAltText } = render(<Image image="http://image.jpg" alt="image" onLoad={onLoad} />);

fireEvent.error(getByAltText('image'));
fireEvent.load(getByAltText('image'));

expect(getByAltText('image')).toHaveAttribute('src', 'http://fallback.jpg?width=640');
expect(onLoad).toHaveBeenCalledTimes(1);
});

test('changes the image width based on the given width', () => {
const { getByAltText } = render(
<Image
image={{
image: 'http://image.jpg',
fallbackImage: 'http://fallback.jpg',
}}
alt="image"
width={1280}
/>,
);
const { getByAltText } = render(<Image image="http://image.jpg" alt="image" width={1280} />);

expect(getByAltText('image')).toHaveAttribute('src', 'http://image.jpg?width=1280');
});
Expand Down
21 changes: 4 additions & 17 deletions src/components/Image/Image.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import classNames from 'classnames';

import styles from './Image.module.scss';

import { addQueryParams } from '#src/utils/formatting';
import type { ImageData } from '#types/playlist';

type Props = {
className?: string;
image?: ImageData;
image?: string;
onLoad?: () => void;
alt?: string;
width?: number;
Expand All @@ -19,25 +18,13 @@ const setWidth = (url: string, width: number) => {
};

const Image = ({ className, image, onLoad, alt = '', width = 640 }: Props) => {
const [imgSrc, setImgSrc] = useState(image?.image);

const handleLoad = () => {
if (onLoad) onLoad();
};

const handleError = () => {
if (image?.fallbackImage && image.fallbackImage !== image.image) {
setImgSrc(image?.fallbackImage);
}
};

useEffect(() => {
setImgSrc(image?.image);
}, [image]);

if (!imgSrc) return null;
if (!image) return null;

return <img className={classNames(className, styles.image)} src={setWidth(imgSrc, width)} onLoad={handleLoad} onError={handleError} alt={alt} />;
return <img className={classNames(className, styles.image)} src={setWidth(image, width)} onLoad={handleLoad} alt={alt} />;
};

export default React.memo(Image);
28 changes: 3 additions & 25 deletions src/components/VideoDetails/VideoDetails.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { render } from '@testing-library/react';

import VideoDetails from './VideoDetails';

Expand All @@ -11,7 +11,7 @@ describe('<VideoDetails>', () => {
description="Video description"
primaryMetadata="Primary metadata string"
secondaryMetadata={<strong>Secondary metadata string</strong>}
image={{ image: 'http://image.jpg' }}
image="http://image.jpg"
startWatchingButton={<button>Start watching</button>}
shareButton={<button>share</button>}
favoriteButton={<button>favorite</button>}
Expand All @@ -29,7 +29,7 @@ describe('<VideoDetails>', () => {
description="Video description"
primaryMetadata="Primary metadata string"
secondaryMetadata={<strong>Secondary metadata string</strong>}
image={{ image: 'http://image.jpg' }}
image="http://image.jpg"
startWatchingButton={<button>Start watching</button>}
shareButton={<button>share</button>}
favoriteButton={<button>favorite</button>}
Expand All @@ -39,26 +39,4 @@ describe('<VideoDetails>', () => {

expect(getByAltText('Test video')).toHaveAttribute('src', 'http://image.jpg?width=1280');
});

test('renders the fallback image when the image fails to load', () => {
const { getByAltText } = render(
<VideoDetails
title="Test video"
description="Video description"
primaryMetadata="Primary metadata string"
secondaryMetadata={<strong>Secondary metadata string</strong>}
image={{ image: 'http://image.jpg', fallbackImage: 'http://fallback.jpg' }}
startWatchingButton={<button>Start watching</button>}
shareButton={<button>share</button>}
favoriteButton={<button>favorite</button>}
trailerButton={<button>play trailer</button>}
/>,
);

expect(getByAltText('Test video')).toHaveAttribute('src', 'http://image.jpg?width=1280');

fireEvent.error(getByAltText('Test video'));

expect(getByAltText('Test video')).toHaveAttribute('src', 'http://fallback.jpg?width=1280');
});
});
3 changes: 1 addition & 2 deletions src/components/VideoDetails/VideoDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import styles from './VideoDetails.module.scss';
import CollapsibleText from '#components/CollapsibleText/CollapsibleText';
import useBreakpoint, { Breakpoint } from '#src/hooks/useBreakpoint';
import Image from '#components/Image/Image';
import type { ImageData } from '#types/playlist';
import { testId } from '#src/utils/common';

type Props = {
title: string;
description: string;
primaryMetadata: React.ReactNode;
secondaryMetadata?: React.ReactNode;
image?: ImageData;
image?: string;
startWatchingButton: React.ReactNode;
shareButton: React.ReactNode;
favoriteButton: React.ReactNode;
Expand Down
4 changes: 2 additions & 2 deletions src/components/VideoLayout/VideoLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import VideoDetailsInline from '#components/VideoDetailsInline/VideoDetailsInlin
import VideoList from '#components/VideoList/VideoList';
import useBreakpoint, { Breakpoint } from '#src/hooks/useBreakpoint';
import { testId } from '#src/utils/common';
import type { ImageData, Playlist, PlaylistItem } from '#types/playlist';
import type { Playlist, PlaylistItem } from '#types/playlist';
import type { AccessModel } from '#types/Config';

type FilterProps = {
Expand All @@ -29,7 +29,7 @@ type LoadMoreProps = {
type VideoDetailsProps = {
title: string;
description: string;
image?: ImageData;
image?: string;
primaryMetadata: React.ReactNode;
secondaryMetadata?: React.ReactNode;
shareButton: React.ReactNode;
Expand Down
2 changes: 1 addition & 1 deletion src/components/VideoListItem/VideoListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type VideoListItemProps = {
};

function VideoListItem({ onClick, onHover, progress, activeLabel, item, loading = false, isActive = false, isLocked = true }: VideoListItemProps): JSX.Element {
const { title, duration, seasonNumber, episodeNumber, shelfImage: image, mediaStatus, scheduledStart } = item;
const { title, duration, seasonNumber, episodeNumber, cardImage: image, mediaStatus, scheduledStart } = item;

const {
t,
Expand Down
2 changes: 1 addition & 1 deletion src/containers/ShelfList/ShelfList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const ShelfList = ({ rows }: Props) => {
<PlaylistContainer type={row.type} playlistId={row.contentId} key={`${row.contentId || row.type}_${index}`}>
{({ playlist, error, isLoading, style }) => {
const title = row?.title || playlist.title;
const posterAspect = parseAspectRatio(playlist.shelfImageAspectRatio);
const posterAspect = parseAspectRatio(playlist.cardImageAspectRatio || playlist.shelfImageAspectRatio);
const visibleTilesDelta = parseTilesDelta(posterAspect);

return (
Expand Down
30 changes: 15 additions & 15 deletions src/hooks/usePlanByEpg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@ const isBaseTimeFormat = is12HourClock();
const usePlanByEpg = (channels: EpgChannel[], sidebarWidth: number, itemHeight: number, highlightColor?: string | null, backgroundColor?: string | null) => {
const [epgChannels, epgPrograms] = useMemo(() => {
return [
channels.map((channel) => ({
uuid: channel.id,
logo: channel.channelLogoImage?.image || '',
channelLogoImage: channel.channelLogoImage,
backgroundImage: channel.backgroundImage,
channels.map(({ id, channelLogoImage, backgroundImage }) => ({
uuid: id,
logo: channelLogoImage,
channelLogoImage: channelLogoImage,
backgroundImage: backgroundImage,
})),
channels.flatMap((channel) =>
channel.programs.map((program) => ({
channel.programs.map(({ id, title, cardImage, backgroundImage, description, endTime, startTime }) => ({
channelUuid: channel.id,
id: program.id,
title: program.title,
image: program.shelfImage?.image || '',
// programs have the same shelfImage/backgroundImage (different API)
shelfImage: program.shelfImage,
backgroundImage: program.backgroundImage,
description: program.description || '',
till: program.endTime,
since: program.startTime,
id: id,
title,
image: cardImage || '',
// programs have the same cardImage/backgroundImage (different API)
cardImage: cardImage || '',
backgroundImage: backgroundImage || '',
description: description || '',
till: endTime,
since: startTime,
})),
),
];
Expand Down
4 changes: 2 additions & 2 deletions src/pages/LegacySeries/LegacySeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import ShareButton from '#components/ShareButton/ShareButton';
import FavoriteButton from '#src/containers/FavoriteButton/FavoriteButton';
import Button from '#components/Button/Button';
import PlayTrailer from '#src/icons/PlayTrailer';
import type { PlaylistItem, ImageData } from '#types/playlist';
import type { PlaylistItem } from '#types/playlist';
import useQueryParam from '#src/hooks/useQueryParam';
import Loading from '#src/pages/Loading/Loading';
import usePlaylist from '#src/hooks/usePlaylist';
Expand Down Expand Up @@ -108,7 +108,7 @@ const LegacySeries = () => {
const pageTitle = `${selectedItem.title} - ${siteName}`;
const pageDescription = selectedItem?.description || '';
const canonicalUrl = `${window.location.origin}${legacySeriesURL({ episodeId: episode?.mediaid, seriesId })}`;
const backgroundImage = (selectedItem.backgroundImage as ImageData) || undefined;
const backgroundImage = (selectedItem.backgroundImage as string) || undefined;

const primaryMetadata = episode
? formatVideoMetaString(episode, t('video:total_episodes', { count: seriesPlaylist?.playlist?.length }))
Expand Down
Loading