Skip to content

Commit

Permalink
Merge pull request #49 from Videodock/feat/continue-watching-shelf
Browse files Browse the repository at this point in the history
Feat / watchhistory shelf home
  • Loading branch information
ChristiaanScheermeijer authored Jun 16, 2021
2 parents 81ce7b7 + de271a8 commit 9bf4b08
Show file tree
Hide file tree
Showing 17 changed files with 283 additions and 112 deletions.
1 change: 1 addition & 0 deletions .commitlintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports = {
'playlist',
'videodetail',
'search',
'watchhistory',
'favorites',
],
],
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ node_modules
/coverage
src/i18n/locales/**/*_old.json
/output
/.vscode/settings.json
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"jira-plugin.workingProject": ""
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ The allowed scopes are:
- playlist
- videodetail
- search
- watchhistory
- favorites

### Subject
Expand Down
41 changes: 22 additions & 19 deletions src/containers/Cinema/Cinema.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useContext, useEffect, useState } from 'react';
import type { Config } from 'types/Config';
import type { PlaylistItem } from 'types/playlist';
import type { WatchHistoryItem } from 'types/watchHistory';
import type { VideoProgress } from 'types/video';

import { watchHistoryStore, useWatchHistoryUpdater } from '../../stores/WatchHistoryStore';
import { useWatchHistoryListener } from '../../hooks/useWatchHistoryListener';
import { watchHistoryStore, useWatchHistory } from '../../stores/WatchHistoryStore';
import { ConfigContext } from '../../providers/ConfigProvider';
import { addScript } from '../../utils/dom';

Expand All @@ -21,41 +22,43 @@ const Cinema: React.FC<Props> = ({ item, onPlay, onPause }: Props) => {
const file = item.sources[0]?.file;
const scriptUrl = `https://content.jwplatform.com/libraries/${config.player}.js`;

const createWatchHistoryItem = (): WatchHistoryItem | undefined => {
const getProgress = (): VideoProgress | null => {
const player = window.jwplayer && (window.jwplayer('cinema') as jwplayer.JWPlayer);
if (!player) return null;

if (!player) return;
const duration = player.getDuration();
const progress = player.getPosition() / duration;

return {
mediaid: item.mediaid,
position: player.getPosition(),
} as WatchHistoryItem;
return { duration, progress } as VideoProgress;
};
const watchHistory = watchHistoryStore.useState((state) => state.watchHistory);
const updateWatchHistory = useWatchHistoryUpdater(createWatchHistoryItem);
const { saveItem } = useWatchHistory();
useWatchHistoryListener(() => saveItem(item, getProgress));

useEffect(() => {
const getPlayer = () => window.jwplayer && (window.jwplayer('cinema') as jwplayer.JWPlayer);
const loadVideo = () => {
const player = getPlayer();
player.setup({ file, image: item.image, title: item.title, autostart: 'viewable' });
player.on('ready', () => {
const { watchHistory } = watchHistoryStore.getRawState();
const position = watchHistory.find((historyItem) => historyItem.mediaid === item.mediaid)?.position;
const { watchHistory } = watchHistoryStore.getRawState();
const watchHistoryItem = watchHistory.find(({ mediaid }) => mediaid === item.mediaid);
let applyWatchHistory = !!watchHistory;

if (position) {
setTimeout(() => player.seek(position), 1000);
}
});
player.setup({ file, image: item.image, title: item.title, autostart: 'viewable' });
player.on('play', () => onPlay && onPlay());
player.on('pause', () => onPause && onPause());
player.on('beforePlay', () => {
if (applyWatchHistory) {
applyWatchHistory = false; // Only the first time beforePlay
const { progress, duration } = watchHistoryItem || {};
progress && duration && player.seek(duration * progress);
}
});
};

if (config.player && !initialized) {
getPlayer() ? loadVideo() : addScript(scriptUrl, loadVideo);
setInitialized(true);
}
}, [item, onPlay, onPause, config.player, file, scriptUrl, initialized, watchHistory, updateWatchHistory]);
}, [item, onPlay, onPause, config.player, file, scriptUrl, initialized]);

return <div className={styles.Cinema} id="cinema" />;
};
Expand Down
38 changes: 25 additions & 13 deletions src/containers/Shelf/Shelf.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React from 'react';
import type { PlaylistItem } from 'types/playlist';

import { PersonalShelf, PersonalShelves } from '../../enum/PersonalShelf';
import { useWatchHistory } from '../../stores/WatchHistoryStore';
import usePlaylist, { UsePlaylistResult } from '../../hooks/usePlaylist';
import ShelfComponent from '../../components/Shelf/Shelf';
import { favoritesStore } from '../../stores/FavoritesStore';
import { useFavorites } from '../../stores/FavoritesStore';

type ShelfProps = {
playlistId: string;
Expand All @@ -14,18 +16,28 @@ type ShelfProps = {
title?: string;
};

const alternativeShelves = ['favorites'];
const Shelf = ({ playlistId, onCardClick, onCardHover, relatedMediaId, featured = false, title }: ShelfProps): JSX.Element | null => {
const isAlternativeShelf = alternativeShelves.includes(playlistId);
const {
isLoading,
error,
data: playlist = { title: '', playlist: [] },
}: UsePlaylistResult = usePlaylist(playlistId, relatedMediaId, !isAlternativeShelf);

const favoritesPlaylist = favoritesStore.useState((s) => s.favorites);

const shelfPlaylist = playlistId === 'favorites' ? favoritesPlaylist : playlist;
const Shelf = ({
playlistId,
onCardClick,
onCardHover,
relatedMediaId,
featured = false,
title,
}: ShelfProps): JSX.Element | null => {
const isAlternativeShelf = PersonalShelves.includes(playlistId as PersonalShelf);
const { isLoading, error, data: playlist = { title: '', playlist: [] } }: UsePlaylistResult = usePlaylist(
playlistId,
relatedMediaId,
!isAlternativeShelf,
);
const { getPlaylist: getFavoritesPlayist } = useFavorites();
const favoritesPlaylist = getFavoritesPlayist();
const { getPlaylist: getWatchHistoryPlayist } = useWatchHistory();
const watchHistoryPlayist = getWatchHistoryPlayist();

let shelfPlaylist = playlist;
if (playlistId === PersonalShelf.Favorites) shelfPlaylist = favoritesPlaylist;
if (playlistId === PersonalShelf.ContinueWatching) shelfPlaylist = watchHistoryPlayist;

if (!playlistId) return <p>No playlist id</p>;
if (!isLoading && !shelfPlaylist?.playlist.length) return null;
Expand Down
5 changes: 5 additions & 0 deletions src/enum/PersonalShelf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export enum PersonalShelf {
ContinueWatching = 'continue-watching',
Favorites = 'favorites',
}
export const PersonalShelves = [PersonalShelf.Favorites, PersonalShelf.ContinueWatching];
16 changes: 16 additions & 0 deletions src/hooks/useWatchHistoryListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEffect } from 'react';

export const useWatchHistoryListener = (saveItem: () => void): void => {
useEffect(() => {
const visibilityListener = () => document.visibilityState === 'hidden' && saveItem();

window.addEventListener('beforeunload', saveItem);
window.addEventListener('visibilitychange', visibilityListener);

return () => {
saveItem();
window.removeEventListener('beforeunload', saveItem);
window.removeEventListener('visibilitychange', visibilityListener);
};
}, []);
};
18 changes: 16 additions & 2 deletions src/screens/Home/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { CSSProperties, useContext, useRef } from 'react';
import React, { CSSProperties, useContext, useRef, useEffect } from 'react';
import memoize from 'memoize-one';
import WindowScroller from 'react-virtualized/dist/commonjs/WindowScroller';
import List from 'react-virtualized/dist/commonjs/List';
Expand All @@ -7,6 +7,9 @@ import type { Config, Content } from 'types/Config';
import type { PlaylistItem } from 'types/playlist';
import classNames from 'classnames';

import { favoritesStore } from '../../stores/FavoritesStore';
import { PersonalShelf } from '../../enum/PersonalShelf';
import { watchHistoryStore } from '../../stores/WatchHistoryStore';
import useBlurImageUpdater from '../../hooks/useBlurImageUpdater';
import { featuredTileBreakpoints, tileBreakpoints } from '../../components/Shelf/Shelf';
import Shelf from '../../containers/Shelf/Shelf';
Expand Down Expand Up @@ -35,7 +38,10 @@ const Home = (): JSX.Element => {
const config: Config = useContext(ConfigContext);
const breakpoint = useBreakpoint();
const listRef = useRef<List>() as React.MutableRefObject<List>;
const content: Content[] = config ? config.content : [];
const content: Content[] = config?.content;
const watchHistory = watchHistoryStore.useState((state) => state.watchHistory);
const watchHistoryLoaded = watchHistoryStore.useState((state) => state.playlistItemsLoaded);
const favorites = favoritesStore.useState((state) => state.favorites);

const { data: { playlist } = { playlist: [] } } = usePlaylist(content[0]?.playlistId);
const updateBlurImage = useBlurImageUpdater(playlist);
Expand Down Expand Up @@ -72,6 +78,8 @@ const Home = (): JSX.Element => {
const isLargeScreen = breakpoint >= Breakpoint.md;

if (!item) return 0;
if (item.playlistId === PersonalShelf.ContinueWatching && !watchHistory.length) return 0;
if (item.playlistId === PersonalShelf.Favorites && !favorites.length) return 0;

const calculateFeatured = () => {
const tilesToShow = featuredTileBreakpoints[breakpoint];
Expand All @@ -98,6 +106,12 @@ const Home = (): JSX.Element => {
return item.featured ? calculateFeatured() : calculateRegular();
};

useEffect(() => {
if (watchHistoryLoaded) {
((listRef.current as unknown) as List)?.recomputeRowHeights();
}
}, [watchHistoryLoaded]);

return (
<div className={styles.home}>
<WindowScroller onResize={() => ((listRef.current as unknown) as List)?.recomputeRowHeights()}>
Expand Down
23 changes: 13 additions & 10 deletions src/screens/Movie/Movie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ type MovieRouteParams = {
id: string;
};

const Movie = (
{
match: {
params: { id },
},
location,
}: RouteComponentProps<MovieRouteParams>): JSX.Element => {
const Movie = ({
match: {
params: { id },
},
location,
}: RouteComponentProps<MovieRouteParams>): JSX.Element => {
const config = useContext(ConfigContext);
const searchParams = new URLSearchParams(location.search);
const { isLoading, error, data: item } = useMedia(id);
Expand Down Expand Up @@ -64,7 +63,9 @@ const Movie = (
return (
<React.Fragment>
<Helmet>
<title>{item.title} - {config.siteName}</title>
<title>
{item.title} - {config.siteName}
</title>
<meta name="description" content={item.description} />
<meta property="og:description" content={item.description} />
<meta property="og:title" content={`${item.title} - ${config.siteName}`} />
Expand All @@ -81,7 +82,9 @@ const Movie = (
<meta property="og:video:type" content="text/html" />
<meta property="og:video:width" content="1280" />
<meta property="og:video:height" content="720" />
{item.tags.split(',').map(tag => <meta property="og:video:tag" content={tag} key={tag} />)}
{item.tags.split(',').map((tag) => (
<meta property="og:video:tag" content={tag} key={tag} />
))}
</Helmet>
<VideoComponent
item={item}
Expand All @@ -93,7 +96,7 @@ const Movie = (
hasShared={hasShared}
onShareClick={onShareClick}
isFavorited={isFavorited}
onFavoriteButtonClick={() => isFavorited ? removeItem(item) : saveItem(item)}
onFavoriteButtonClick={() => (isFavorited ? removeItem(item) : saveItem(item))}
relatedShelf={
config.recommendationsPlaylist ? (
<Shelf
Expand Down
17 changes: 12 additions & 5 deletions src/services/config.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Config, Content, Options, Menu } from 'types/Config';
import { string, boolean, array, object, SchemaOf } from 'yup';

import { PersonalShelf } from '../enum/PersonalShelf';

/**
* Set config setup changes in both config.services.ts and config.d.ts
* */
Expand Down Expand Up @@ -62,7 +64,7 @@ const loadConfig = async (configLocation: string) => {

const data = await response.json();

addFavoritesShelf(data);
addPersonalShelves(data);

if (data.version) {
return parseDeprecatedConfig(data);
Expand All @@ -74,12 +76,17 @@ const loadConfig = async (configLocation: string) => {
};

/**
* Add the favorites shelf if not already defined
* Add the personal shelves if not already defined: Favorites, ContinueWatching
* @param {Config} data
*/
const addFavoritesShelf = (data: Config) => {
if (!data.content.some(row => row.playlistId === 'favorites')) {
data.content.push({ playlistId: 'favorites' });
const addPersonalShelves = (data: Config) => {
if (!data.content.some(({ playlistId }) => playlistId === PersonalShelf.Favorites)) {
data.content.push({ playlistId: PersonalShelf.Favorites });
}
if (data.options.enableContinueWatching) {
if (!data.content.some(({ playlistId }) => playlistId === PersonalShelf.ContinueWatching)) {
data.content.splice(1, 0, { playlistId: PersonalShelf.ContinueWatching });
}
}
};

Expand Down
Loading

0 comments on commit 9bf4b08

Please sign in to comment.