Skip to content

Commit

Permalink
feat(project): add support for custom screens
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristiaanScheermeijer committed Sep 30, 2022
1 parent 3141fe3 commit 77b264c
Show file tree
Hide file tree
Showing 63 changed files with 812 additions and 349 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"test-watch": "TZ=UTC vitest",
"test-coverage": "TZ=UTC vitest run --coverage",
"test-commit": "TZ=UTC vitest run --changed HEAD~1 --coverage",
"i18next": "i18next src/{components,containers,screens,services,stores}/**/{**/,/}*.{ts,tsx} && node ./scripts/i18next/generate.js",
"i18next": "i18next src/{components,containers,pages,services,stores}/**/{**/,/}*.{ts,tsx} && node ./scripts/i18next/generate.js",
"format": "run-s -c format:*",
"format:eslint": "eslint \"{**/*,*}.{js,ts,jsx,tsx}\" --fix",
"format:prettier": "prettier --write \"{**/*,*}.{js,ts,jsx,tsx}\"",
Expand Down
5 changes: 5 additions & 0 deletions public/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
"enableText": true,
"type": "playlist",
"contentId": "fWpLtzVh"
},
{
"enableText": true,
"type": "playlist",
"contentId": "RyFgF0PS"
}
],
"menu": [
Expand Down
3 changes: 2 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { Component } from 'react';
import { getI18n, I18nextProvider } from 'react-i18next';

import type { Config } from '#types/Config';
import Router from '#src/components/Router/Router';
import Router from '#src/containers/Router/Router';
import LoadingOverlay from '#src/components/LoadingOverlay/LoadingOverlay';
import QueryProvider from '#src/providers/QueryProvider';
import { restoreWatchHistory } from '#src/stores/WatchHistoryController';
Expand All @@ -14,6 +14,7 @@ import { clearStoredConfig } from '#src/utils/configOverride';
import { PersonalShelf } from '#src/enum/PersonalShelf';
import initI18n from '#src/i18n/config';

import '#src/screenMapping';
import '#src/styles/main.scss';

interface State {
Expand Down
10 changes: 3 additions & 7 deletions src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';

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

import { formatDurationTag } from '#src/utils/formatting';
import { formatDurationTag, formatSeriesMetaString } from '#src/utils/formatting';
import Lock from '#src/icons/Lock';
import Image from '#src/components/Image/Image';
import type { ImageData } from '#types/playlist';
Expand Down Expand Up @@ -66,12 +66,8 @@ function Card({

if (seriesId) {
return <div className={styles.tag}>Series</div>;
} else if (seasonNumber && episodeNumber) {
return (
<div className={styles.tag}>
S{seasonNumber}:E{episodeNumber}
</div>
);
} else if (episodeNumber) {
return <div className={styles.tag}>{formatSeriesMetaString(seasonNumber, episodeNumber)}</div>;
} else if (duration) {
return <div className={styles.tag}>{formatDurationTag(duration)}</div>;
} else if (duration === 0) {
Expand Down
49 changes: 49 additions & 0 deletions src/components/Hero/Hero.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@use '../../styles/variables';
@use '../../styles/theme';
@use '../../styles/mixins/responsive';
@use '../../styles/mixins/typography';

.hero {
height: 40vh;
}

.content {
max-width: 60vw;
padding: 37px 56px 0;

@include responsive.tablet-only() {
padding: 32px;
}

@include responsive.mobile-only() {
max-width: none;
padding: 16px;
}
}

.title {
@include typography.video-title-base;
}

.image {
position: absolute;
top: 0;
right: 0;
z-index: -1;
width: 80vw;
height: calc(85vw / 16 * 9);

mask-image: radial-gradient(farthest-corner at 80% 20%, rgba(0, 0, 0, 1) 30%, rgba(0, 0, 0, 0) 60%);
//noinspection CssInvalidPropertyValue
-webkit-mask-image: radial-gradient(farthest-corner at 80% 20%, rgba(0, 0, 0, 1) 30%, rgba(0, 0, 0, 0) 60%); /* stylelint-disable-line */

@include responsive.tablet-only() {
width: 80vw;
height: calc(140vw / 16 * 9);
}

@include responsive.mobile-only() {
width: 110vw;
height: calc(180vw / 16 * 9);
}
}
26 changes: 26 additions & 0 deletions src/components/Hero/Hero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';

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

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

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

const Hero = ({ title, description, image }: Props) => {
return (
<div className={styles.hero}>
<Image className={styles.image} image={image} width={1280} alt="" />
<div className={styles.content}>
<h1 className={styles.title}>{title}</h1>
<p>{description}</p>
</div>
</div>
);
};

export default Hero;
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { createBrowserRouter, createHashRouter, createRoutesFromElements, Route, RouterProvider } from 'react-router-dom';

import ErrorPage from '../ErrorPage/ErrorPage';
import Root from '../Root/Root';

import ErrorPage from '#src/components/ErrorPage/ErrorPage';
import Root from '#src/components/Root/Root';
import Layout from '#src/containers/Layout/Layout';
import About from '#src/screens/About/About';
import Home from '#src/screens/Home/Home';
import Movie from '#src/screens/Movie/Movie';
import Playlist from '#src/screens/Playlist/Playlist';
import Search from '#src/screens/Search/Search';
import Series from '#src/screens/Series/Series';
import User from '#src/screens/User/User';
import About from '#src/pages/About/About';
import Home from '#src/pages/Home/Home';
import Search from '#src/pages/Search/Search';
import Series from '#src/pages/Series/Series';
import User from '#src/pages/User/User';
import MediaScreenRouter from '#src/pages/ScreenRouting/MediaScreenRouter';
import PlaylistScreenRouter from '#src/pages/ScreenRouting/PlaylistScreenRouter';

type Props = {
error?: Error | null;
Expand All @@ -21,18 +20,18 @@ type Props = {
export default function Router({ error }: Props) {
const { t } = useTranslation('error');

/*
Ideally we should define the routes outside the router, but it doesn't work with the current setup because we need to pass the error to the Root component.
@todo: refactor the app to use the errorElements that can be passed to the route components. see https://reactrouter.com/en/main/route/error-element so that we can define the routes outside the router.
And we should also consider moving the Router component to the containers folder or placing it directly in the src directory.
*/

/**
* Ideally we should define the routes outside the router, but it doesn't work with the current setup because we need
* to pass the error to the Root component.
* @todo: refactor the app to use the errorElements that can be passed to the route components. see
* https://reactrouter.com/en/main/route/error-element so that we can define the routes outside the router.
*/
const routes = createRoutesFromElements(
<Route element={<Root error={error} />}>
<Route element={<Layout />}>
<Route index element={<Home />} />
<Route path="/p/:id" element={<Playlist />} />
<Route path="/m/:id/:slug" element={<Movie />} />
<Route path="/p/:id" element={<PlaylistScreenRouter />} />
<Route path="/m/:id/:slug" element={<MediaScreenRouter />} />
<Route path="/s/:id/:slug" element={<Series />} />
<Route path="/q/*" element={<Search />} />
<Route path="/u/*" element={<User />} />
Expand Down
48 changes: 48 additions & 0 deletions src/containers/SeriesRedirect/SeriesRedirect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Navigate } from 'react-router';
import { useTranslation } from 'react-i18next';

import { useSeriesData } from '#src/hooks/useSeriesData';
import useQueryParam from '#src/hooks/useQueryParam';
import { episodeURL } from '#src/utils/formatting';
import Loading from '#src/pages/Loading/Loading';
import ErrorPage from '#src/components/ErrorPage/ErrorPage';

type Props = {
seriesId: string;
episodeId?: string;
};

/**
* This container is mainly used to redirect the user to the correct episode media age.
*
* It fetches the series and redirects to the first episode. This behavior can be overridden when passing the
* `episodeId` argument.
*
* Later, the watch history can also be used to determine the last watched episode to properly support continue
* watching with series.
*/
const SeriesRedirect = ({ seriesId, episodeId }: Props) => {
const { t } = useTranslation('video');
const { isLoading, isPlaylistError, data } = useSeriesData(seriesId);
const play = useQueryParam('play') === '1';
const feedId = useQueryParam('r');

if (isLoading) {
return <Loading />;
}

if (isPlaylistError || !data) {
return <ErrorPage title={t('series_error')} />;
}

const firstEpisode = data.seriesPlaylist.playlist[0];
const toEpisode = episodeId ? data.seriesPlaylist.playlist.find(({ mediaid }) => mediaid === episodeId) : firstEpisode;

if (!toEpisode) {
return <ErrorPage title={t('episode_not_found')} />;
}

return <Navigate to={episodeURL(toEpisode, seriesId, play, feedId)} replace />;
};

export default SeriesRedirect;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
@use '../../styles/theme';
@use '../../styles/mixins/responsive';

.home {
.shelfList {
max-width: 100vw;
overflow-x: hidden;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import classNames from 'classnames';
import React, { CSSProperties, useCallback, useEffect, useRef } from 'react';
import memoize from 'memoize-one';
import WindowScroller from 'react-virtualized/dist/commonjs/WindowScroller';
import List from 'react-virtualized/dist/commonjs/List';
import WindowScroller from 'react-virtualized/dist/commonjs/WindowScroller';
import { useNavigate } from 'react-router-dom';
import classNames from 'classnames';
import shallow from 'zustand/shallow';
import memoize from 'memoize-one';

import styles from './Home.module.scss';
import styles from './ShelfList.module.scss';

import PlaylistContainer from '#src/containers/PlaylistContainer/PlaylistContainer';
import { useFavoritesStore } from '#src/stores/FavoritesStore';
import { useAccountStore } from '#src/stores/AccountStore';
import { useConfigStore } from '#src/stores/ConfigStore';
import { PersonalShelf } from '#src/enum/PersonalShelf';
import useBlurImageUpdater from '#src/hooks/useBlurImageUpdater';
import { mediaURL, slugify } from '#src/utils/formatting';
import ShelfComponent, { featuredTileBreakpoints, tileBreakpoints } from '#src/components/Shelf/Shelf';
import usePlaylist from '#src/hooks/usePlaylist';
import { PersonalShelf } from '#src/enum/PersonalShelf';
import useBreakpoint, { Breakpoint } from '#src/hooks/useBreakpoint';
import scrollbarSize from '#src/utils/dom';
import { cardUrl, slugify } from '#src/utils/formatting';
import type { PlaylistItem } from '#types/playlist';
import type { Content } from '#types/Config';
import { useConfigStore } from '#src/stores/ConfigStore';
import { useWatchHistoryStore } from '#src/stores/WatchHistoryStore';
import { useFavoritesStore } from '#src/stores/FavoritesStore';
import usePlaylist from '#src/hooks/usePlaylist';
import useBlurImageUpdater from '#src/hooks/useBlurImageUpdater';
import { useAccountStore } from '#src/stores/AccountStore';
import type { Content } from '#types/Config';
import type { PlaylistItem } from '#types/playlist';

type rowData = {
index: number;
Expand All @@ -33,29 +33,32 @@ type ItemData = {
content: Content[];
};

type Props = {
rows: Content[];
};

const createItemData = memoize((content) => ({ content }));

const Home = (): JSX.Element => {
const ShelfList: React.VFC<Props> = ({ rows }) => {
const navigate = useNavigate();
const { config, accessModel } = useConfigStore(({ config, accessModel }) => ({ config, accessModel }), shallow);
const breakpoint = useBreakpoint();
const listRef = useRef<List>() as React.MutableRefObject<List>;
const content: Content[] = config?.content;
const itemData: ItemData = createItemData(content);
const itemData: ItemData = createItemData(rows);

const watchHistory = useWatchHistoryStore((state) => state.getPlaylist());
const watchHistoryDictionary = useWatchHistoryStore((state) => state.getDictionary());
const favorites = useFavoritesStore((state) => state.favorites);

const { data: { playlist } = { playlist: [] } } = usePlaylist(content.find((el) => el.contentId)?.contentId as string);
const { data: { playlist } = { playlist: [] } } = usePlaylist(rows.find((el) => el.contentId)?.contentId as string);
const updateBlurImage = useBlurImageUpdater(playlist);

// User
const { user, subscription } = useAccountStore(({ user, subscription }) => ({ user, subscription }), shallow);

const onCardClick = useCallback(
(playlistItem, playlistId, type) => {
navigate(cardUrl(playlistItem, playlistId, type === PersonalShelf.ContinueWatching));
navigate(mediaURL(playlistItem, playlistId, type === PersonalShelf.ContinueWatching));
},
[navigate],
);
Expand Down Expand Up @@ -113,7 +116,7 @@ const Home = (): JSX.Element => {
};

const calculateHeight = (index: number): number => {
const item = content[index];
const item = rows[index];
const isDesktop = breakpoint >= Breakpoint.lg;
const isMobile = breakpoint === Breakpoint.xs;
const isTablet = !isDesktop && !isMobile;
Expand Down Expand Up @@ -153,30 +156,32 @@ const Home = (): JSX.Element => {
}, [favorites, watchHistory]);

return (
<div className={styles.home}>
<div className={styles.shelfList}>
<WindowScroller onResize={() => (listRef.current as unknown as List)?.recomputeRowHeights()}>
{({ height, isScrolling, onChildScroll, scrollTop }) => (
<List
className={styles.list}
tabIndex={-1}
ref={listRef}
autoHeight
height={height}
isScrolling={isScrolling}
onScroll={onChildScroll}
rowCount={content.length}
getScrollbarSize={scrollbarSize}
rowHeight={({ index }) => calculateHeight(index)}
rowRenderer={({ index, key, style }) => rowRenderer({ index, key, style, itemData })}
scrollTop={scrollTop}
width={document.body.offsetWidth}
isScrollingOptOut
overscanRowCount={3}
/>
)}
{({ height, isScrolling, onChildScroll, scrollTop }) => {
return (
<List
className={styles.list}
tabIndex={-1}
ref={listRef}
autoHeight
height={height}
isScrolling={isScrolling}
onScroll={onChildScroll}
rowCount={rows.length}
getScrollbarSize={scrollbarSize}
rowHeight={({ index }) => calculateHeight(index)}
rowRenderer={({ index, key, style }) => rowRenderer({ index, key, style, itemData })}
scrollTop={scrollTop}
width={document.body.offsetWidth}
isScrollingOptOut
overscanRowCount={3}
/>
);
}}
</WindowScroller>
</div>
);
};

export default Home;
export default ShelfList;
Loading

0 comments on commit 77b264c

Please sign in to comment.