UNPKG

nexle-tvguide-lib

Version:
554 lines (506 loc) 23.2 kB
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { View, StyleSheet, Animated, ScrollView, FlatList, TVEventHandler, } from 'react-native'; import PropTypes from 'prop-types'; import { HeaderDatePannel, ProgramLine, ChannelItem, HeaderTimeCell, LoadingIndicator } from './components'; import { TV_GUIDE_CONSTANTS } from './constants'; import { generateTimelineData, getDefaultActiveProgramIndex, getDurationFromDateStart, getDurationToDateEnd, compareTwoDates, } from './util'; const today = new Date(); var timelineTimeOutRef, programsTimeOutRef; var displayedTimeline = []; var currentDateDisplay = new Date(); var timelineData = []; var lastProgramFocus; var viewableProgramListEndIndex = 0; var viewableProgramListStartIndex = 0; var previousDateLoadingFlag = 0; var nextDateLoadingFlag = 0; var programListSafe = []; var loadingTimeoutRef; var defaultFocusTimeoutRef; var activeProgram; const getTimeIndicatorOffset = (timelineData, timelineCellWidth) => { if (timelineData.length === 0) return 0; const now = new Date(); return Math.abs(now.getTime() - timelineData[0].start) / TV_GUIDE_CONSTANTS.HALF_HOUR_DURATION * timelineCellWidth; }; function TVGuideComponent(props) { const { channeList = [], programList = [], // onReachingEndChannel, currentDate, onDateChange, onProgramChange, onLoadingMoreProgramsByTime, programStylesColors, programContainerStyles, timeIndicatorStyles, tvGuideWidth, tvGuideHeight, timeLineHeaderHeight, numberOfChannelsDisplayed, numberOfTimelineCellDisplayed, channelListWidth, numberOfFutureDays, numberOfPastDays } = props; const scrollAnimation = useRef(new Animated.Value(0)); const timelineHeaderRef = useRef(null); const horizontalScrollRef = useRef(null); const channelListRef = useRef(null); const programListRef = useRef(null); const [timeIndicatorOffset, setTimeIndicatorOffset] = useState(null); const tvEventHandler = new TVEventHandler(); const visibleTimeIndicator = compareTwoDates(currentDateDisplay, today); const [visibleLoadingIndicator, setVisibleLoadingIndicator] = useState(false); const timeIndicatorStylesFlatten = useMemo(() => StyleSheet.flatten([styles.timeIndicator, timeIndicatorStyles]), [timeIndicatorStyles]); const containerStylesFlattten = useMemo(() => StyleSheet.flatten([styles.container, { width: tvGuideWidth, height: tvGuideHeight }]), [tvGuideWidth, tvGuideHeight]); const timelineCellWidth = (tvGuideWidth - channelListWidth) / numberOfTimelineCellDisplayed; const programLineHeight = (tvGuideHeight - timeLineHeaderHeight) / numberOfChannelsDisplayed; const programListContainerWidth = tvGuideWidth - channelListWidth; const scrollHorizontal = useCallback(offsetX => { if (!timelineHeaderRef || !horizontalScrollRef) return; timelineTimeOutRef = setTimeout(() => { timelineHeaderRef.current?.scrollToOffset({ animated: false, offset: offsetX }); }, 100); programsTimeOutRef = setTimeout(() => { horizontalScrollRef.current?.scrollTo({ x: offsetX, y: 0, animated: false }); }, 300); }, []); const scrollVertical = useCallback((lineIndex) => { try { if (!channelListRef || !programListRef) return; const index = lineIndex > 0 ? lineIndex - 1 : lineIndex; channelListRef.current?.scrollToIndex({ animated: true, index: index, }); programListRef.current?.scrollToIndex({ animated: true, index: index, }); } catch (e) { console.log(e) } }, []); const stopAnimationScroll = useCallback(() => { scrollAnimation?.current.stopAnimation(); }, []); const handleProgramFocus = (index, lineIndex, program) => { activeProgram = { index, lineIndex, program }; if (typeof displayedTimeline !== 'object' || displayedTimeline.length === 0) return; if (lineIndex > 0 && lineIndex === viewableProgramListEndIndex) { scrollVertical(lineIndex); } if (lastProgramFocus.lineIndex > 0 && lineIndex >= viewableProgramListStartIndex && lineIndex < lastProgramFocus.lineIndex) { scrollVertical(lineIndex); } const progamStartDate = program.startDateAdjusted; const progamEndDate = program.endDateAdjusted; const displayedTimelineLastItem = displayedTimeline[displayedTimeline.length - 1]; const displayedTimelineLastIndex = timelineData.findIndex(t => t.start === displayedTimelineLastItem.start); let newDisplayedTimeline = [...displayedTimeline]; if (displayedTimeline && displayedTimeline.length === numberOfTimelineCellDisplayed) { newDisplayedTimeline = [ ...displayedTimeline, { ...timelineData[displayedTimelineLastIndex + 1] }, ]; } if (displayedTimeline && displayedTimeline.length === numberOfTimelineCellDisplayed + 2) { const clonedDisplayedTimeline = [...displayedTimeline]; newDisplayedTimeline = clonedDisplayedTimeline.splice(1); } if (progamStartDate >= newDisplayedTimeline[0].start && progamEndDate <= newDisplayedTimeline[newDisplayedTimeline.length - 1].start) { if (progamStartDate === timelineData[0].start) { scrollHorizontal(0); } } else { const offset = ((progamStartDate - timelineData[0].start) / TV_GUIDE_CONSTANTS.HALF_HOUR_DURATION) * timelineCellWidth; scrollHorizontal(offset); } lastProgramFocus = { lineIndex, index }; onProgramChange({ program, lineIndex, index }); const durationToDateEnd = getDurationToDateEnd(currentDateDisplay, program.startDateAdjusted); if (durationToDateEnd <= TV_GUIDE_CONSTANTS.REMAINING_TIME_TO_LOAD_MORE_PROGRAMS) { onLoadingMoreProgramsByTime(TV_GUIDE_CONSTANTS.NEXT_DAY_EVENT_TYPE); } const durationFromDateStart = getDurationFromDateStart(currentDateDisplay, program.startDateAdjusted); if (durationFromDateStart <= TV_GUIDE_CONSTANTS.REMAINING_TIME_TO_LOAD_MORE_PROGRAMS) { onLoadingMoreProgramsByTime(TV_GUIDE_CONSTANTS.PREV_DAY_EVENT_TYPE); } }; const checkShouldChangeData = (typeCheck) => { const { lineIndex, index } = lastProgramFocus; if (typeof lastProgramFocus !== 'object' || !lastProgramFocus || !Array.isArray(programListSafe) || !programListSafe) return; if (typeCheck === TV_GUIDE_CONSTANTS.PREV_DAY_EVENT_TYPE) { if (index === 0 && activeProgram.lineIndex === lineIndex) { previousDateLoadingFlag++; if (previousDateLoadingFlag == 2) { let prevDay = new Date(currentDateDisplay); prevDay.setDate(prevDay.getDate() - 1); if (Math.abs(parseInt(today.getDate()) - parseInt(prevDay.getDate())) <= numberOfPastDays) { setVisibleLoadingIndicator(true); onDateChange(prevDay); resetGlobalVariables(typeCheck, prevDay); } } if (index !== 0 || activeProgram.lineIndex !== lineIndex) { previousDateLoadingFlag = 0; } }; } if (typeCheck === TV_GUIDE_CONSTANTS.NEXT_DAY_EVENT_TYPE) { if (index === (programListSafe[lineIndex].programs.length - 1) && activeProgram.lineIndex === lineIndex) { nextDateLoadingFlag++; if (nextDateLoadingFlag == 2) { let nextDay = new Date(currentDateDisplay); nextDay.setDate(nextDay.getDate() + 1); if (Math.abs(parseInt(nextDay.getDate()) - parseInt(today.getDate())) <= numberOfFutureDays) { setVisibleLoadingIndicator(true); onDateChange(nextDay); resetGlobalVariables(typeCheck, nextDay); } } } if ((index !== programListSafe[lineIndex].programs.length - 1) || activeProgram.lineIndex !== lineIndex) { nextDateLoadingFlag = 0; } } }; const resetGlobalVariables = useCallback((type, newDate) => { if (type === TV_GUIDE_CONSTANTS.PREV_DAY_EVENT_TYPE) { previousDateLoadingFlag = 0; } else { nextDateLoadingFlag = 0; } const { lineIndex } = lastProgramFocus; lastProgramFocus = { lineIndex: lineIndex ? lineIndex : 0, index: 0 }; global.focusManager.setFocusForRoute('selectedAt', { lineNumber: lineIndex ? lineIndex : 0, index: 0 }); viewableProgramListEndIndex = 0; viewableProgramListStartIndex = 0; displayedTimeline = []; currentDateDisplay = new Date(newDate); timelineData = [...generateTimelineData(currentDateDisplay)]; }, []); const enableTVEventHandler = () => { tvEventHandler.enable(this, function (cmp, evt) { if (evt && evt.eventType === TV_GUIDE_CONSTANTS.EVENT_KEYS.RIGHT) { checkShouldChangeData(TV_GUIDE_CONSTANTS.NEXT_DAY_EVENT_TYPE); } else if (evt && evt.eventType === TV_GUIDE_CONSTANTS.EVENT_KEYS.UP) { //console.log(evt.eventType); } else if (evt && evt.eventType === TV_GUIDE_CONSTANTS.EVENT_KEYS.LEFT) { checkShouldChangeData(TV_GUIDE_CONSTANTS.PREV_DAY_EVENT_TYPE); } else if (evt && evt.eventType === TV_GUIDE_CONSTANTS.EVENT_KEYS.DOWN) { // console.log(evt.eventType); } }); }; const disableTVEventHandler = () => { if (tvEventHandler) { tvEventHandler.disable(); } } useEffect(() => { if (channeList.length > 0 && programList.length > 0 && !lastProgramFocus) { const defaultActiveProgramIndex = getDefaultActiveProgramIndex(programList[0].programs); lastProgramFocus = { lineIndex: 0, index: defaultActiveProgramIndex }; global.focusManager.setFocusForRoute('selectedAt', { lineNumber: 0, index: defaultActiveProgramIndex }); handleProgramFocus({ index: defaultActiveProgramIndex, lineIndex: 0, program: programList[0].programs[defaultActiveProgramIndex] }) defaultFocusTimeoutRef = setTimeout(() => { const offset = getTimeIndicatorOffset(timelineData, timelineCellWidth); scrollHorizontal(offset); }, 100); } if (programList && programList.length > 0) { programListSafe = [...programList]; } setVisibleLoadingIndicator(false); }, [programList]); const onViewableTimelineChanged = useCallback(({ viewableItems }) => { const newViews = viewableItems.map(view => { return { ...view.item }; }); displayedTimeline = [...newViews]; }, []); useEffect(() => { currentDateDisplay = new Date(currentDate); }, [currentDate]); useEffect(() => { if (lastProgramFocus && lastProgramFocus.lineIndex && lastProgramFocus.lineIndex > 0) { scrollVertical(lastProgramFocus.lineIndex) } timelineData = [...generateTimelineData(currentDateDisplay)]; if (timelineData.length > 0 && timeIndicatorOffset === null) { setTimeIndicatorOffset(getTimeIndicatorOffset(timelineData, timelineCellWidth)); } const interval = setInterval(() => { const offset = getTimeIndicatorOffset(timelineData, timelineCellWidth); setTimeIndicatorOffset(offset); }, TV_GUIDE_CONSTANTS.TIME_INDICATOR_UPDATE_INTERVAL); setVisibleLoadingIndicator(true); loadingTimeoutRef = setTimeout(() => { setVisibleLoadingIndicator(false); }, TV_GUIDE_CONSTANTS.LOADING_INDICATOR_TIMEOUT); enableTVEventHandler(); return () => { clearInterval(interval); clearTimeout(timelineTimeOutRef); clearTimeout(programsTimeOutRef); clearTimeout(loadingTimeoutRef); clearTimeout(defaultFocusTimeoutRef); disableTVEventHandler(); } }, []); const viewAbilityTimelineConfigRef = useRef([ { onViewableItemsChanged: onViewableTimelineChanged }, ]); const viewTimelineConfigRef = React.useRef({ waitForInteraction: true, viewAreaCoveragePercentThreshold: timelineCellWidth, }); const onViewableProgramsChanged = useCallback(({ viewableItems }) => { if (viewableItems && viewableItems.length > 0) { viewableProgramListEndIndex = viewableItems[viewableItems.length - 1].index; viewableProgramListStartIndex = viewableItems[0].index; } }, []); const viewAbilityProgramListConfigRef = useRef([ { onViewableItemsChanged: onViewableProgramsChanged }, ]); const viewProgramListConfigRef = React.useRef({ waitForInteraction: true, viewAreaCoveragePercentThreshold: 70, }); // const onEndReachedProgramsChannels = useCallback(() => { // onReachingEndChannel(); // }, []); const renderChannelItem = useCallback(({ item, index }) => { return ( <ChannelItem channel={item} channelNumber={index} tvGuideHeight={tvGuideHeight} timeLineHeaderHeight={timeLineHeaderHeight} numberOfChannelsDisplayed={numberOfChannelsDisplayed} channelHeight={programLineHeight} channelWidth={channelListWidth} /> ); }, []); const renderTimeLineItem = useCallback( ({ item }) => <HeaderTimeCell time={item} numberOfTimelineCellDisplayed={numberOfTimelineCellDisplayed} timeLineItemWidth={timelineCellWidth} />, [], ); const getTimeLineKeyExtractor = useCallback( (item, index) => `${item.start}-${index}`, [], ); const renderProgramLine = useCallback(({ item, index }) => { const { channelExternalId, programs } = item; return ( <ProgramLine lastProgramFocus={lastProgramFocus} currentDate={currentDateDisplay} onFocus={handleProgramFocus} lineNumber={index} programs={programs} channelExternalId={channelExternalId} programStylesColors={programStylesColors} programContainerStyles={programContainerStyles} tvGuideWidth={tvGuideWidth} tvGuideHeight={tvGuideHeight} timeLineHeaderHeight={timeLineHeaderHeight} numberOfChannelsDisplayed={numberOfChannelsDisplayed} channelListWidth={channelListWidth} numberOfTimelineCellDisplayed={numberOfTimelineCellDisplayed} timelineCellWidth={timelineCellWidth} /> ); }, []); const getProgramsKeyExtractor = useCallback( item => `${item.channelExternalId}`, [], ); const getChannelsKeyExtractor = useCallback(item => `${item.id}`, []); const getChannelsLayout = useCallback( (data, index) => ({ length: programLineHeight, offset: programLineHeight * index, index, }), [], ); const getProgramsLayout = useCallback( (data, index) => ({ length: programLineHeight, offset: programLineHeight * index, index, }), [], ); const getTimelinesLayout = useCallback( (data, index) => ({ length: timelineCellWidth, offset: timelineCellWidth * index, index, }), [], ); if (visibleLoadingIndicator) { return ( <LoadingIndicator /> ); } return ( <View style={containerStylesFlattten}> <View style={styles.tvGuideContainer}> <View style={styles.flexRow}> <HeaderDatePannel currentDate={currentDate} pannelWidth={channelListWidth} pannelHeight={timeLineHeaderHeight} /> <FlatList horizontal removeClippedSubviews legacyImplementation ref={timelineHeaderRef} scrollEnabled={false} showsHorizontalScrollIndicator={false} viewabilityConfig={viewTimelineConfigRef.current} viewabilityConfigCallbackPairs={ viewAbilityTimelineConfigRef.current } data={timelineData} renderItem={renderTimeLineItem} keyExtractor={getTimeLineKeyExtractor} contentContainerStyle={styles.contentScrollTimeStyle} getItemLayout={getTimelinesLayout} /> </View> <View style={styles.flexRow}> <FlatList removeClippedSubviews legacyImplementation scrollEnabled={false} showsVerticalScrollIndicator={false} initialNumToRender={TV_GUIDE_CONSTANTS.FLAT_LIST_CONFIG.INIT_NUM_TO_RENDER} maxToRenderPerBatch={TV_GUIDE_CONSTANTS.FLAT_LIST_CONFIG.MAX_RENDER_PER_BATCH} windowSize={TV_GUIDE_CONSTANTS.FLAT_LIST_CONFIG.WINDOW_SIZE} ref={channelListRef} data={channeList} renderItem={renderChannelItem} keyExtractor={getChannelsKeyExtractor} contentContainerStyle={[styles.channelContentContainerStyle, { width: channelListWidth }]} scrollEventThrottle={TV_GUIDE_CONSTANTS.FLAT_LIST_CONFIG.SCROLL_EVENT_THROTTLE} getItemLayout={getChannelsLayout} /> <View style={{ width: programListContainerWidth }}> <ScrollView scrollEnabled={false} horizontal ref={horizontalScrollRef} nestedScrollEnabled={false} scrollEventThrottle={TV_GUIDE_CONSTANTS.FLAT_LIST_CONFIG.SCROLL_EVENT_THROTTLE} onScrollBeginDrag={stopAnimationScroll} onScrollEndDrag={stopAnimationScroll} > <FlatList scrollEnabled={false} legacyImplementation removeClippedSubviews ref={programListRef} initialNumToRender={TV_GUIDE_CONSTANTS.FLAT_LIST_CONFIG.INIT_NUM_TO_RENDER} maxToRenderPerBatch={TV_GUIDE_CONSTANTS.FLAT_LIST_CONFIG.MAX_RENDER_PER_BATCH} windowSize={TV_GUIDE_CONSTANTS.FLAT_LIST_CONFIG.WINDOW_SIZE} data={programList} renderItem={renderProgramLine} keyExtractor={getProgramsKeyExtractor} // onEndReached={onEndReachedProgramsChannels} onEndReachedThreshold={TV_GUIDE_CONSTANTS.FLAT_LIST_CONFIG.ON_END_REACHED_THRESHOLD} contentContainerStyle={styles.programsContentContainerStyle} scrollEventThrottle={TV_GUIDE_CONSTANTS.FLAT_LIST_CONFIG.SCROLL_EVENT_THROTTLE} getItemLayout={getProgramsLayout} viewabilityConfig={viewProgramListConfigRef.current} viewabilityConfigCallbackPairs={ viewAbilityProgramListConfigRef.current } /> {timeIndicatorOffset && visibleTimeIndicator && <View style={[timeIndicatorStylesFlatten, { left: timeIndicatorOffset }]} />} </ScrollView> </View> </View> </View> </View > ); } TVGuideComponent.propTypes = { channeList: PropTypes.array.isRequired, programList: PropTypes.array.isRequired, // onReachingEndChannel: PropTypes.func.isRequired, onProgramChange: PropTypes.func.isRequired, onLoadingMoreProgramsByTime: PropTypes.func.isRequired, tvGuideWidth: PropTypes.number.isRequired, tvGuideHeight: PropTypes.number.isRequired, timeLineHeaderHeight: PropTypes.number, numberOfChannelsDisplayed: PropTypes.number, numberOfTimelineCellDisplayed: PropTypes.number, channelListWidth: PropTypes.number, numberOfFutureDays: PropTypes.number, numberOfPastDays: PropTypes.number }; TVGuideComponent.defaultProps = { tvGuideWidth: TV_GUIDE_CONSTANTS.DEVICE_WIDTH, tvGuideHeight: TV_GUIDE_CONSTANTS.DEVICE_HEIGHT * 3 / 4, timeLineHeaderHeight: TV_GUIDE_CONSTANTS.HEADER_CELL_HEIGHT, numberOfChannelsDisplayed: TV_GUIDE_CONSTANTS.NUMBER_OF_CHANNELS_DISPLAYED, numberOfTimelineCellDisplayed: TV_GUIDE_CONSTANTS.NUMBER_OF_TIMELINE_CELLS_DISPLAYED, channelListWidth: TV_GUIDE_CONSTANTS.CHANNEL_LIST_WIDTH, numberOfFutureDays: TV_GUIDE_CONSTANTS.NUMBER_OF_FUTURE_DAYS, numberOfPastDays: TV_GUIDE_CONSTANTS.NUMBER_OF_PAST_DAYS, } export default React.memo(TVGuideComponent); const styles = StyleSheet.create({ container: { backgroundColor: TV_GUIDE_CONSTANTS.THEME_STYLES.CONTAINER_BG_COLOR, }, timeIndicator: { width: 5, height: '100%', position: 'absolute', zIndex: 99, backgroundColor: TV_GUIDE_CONSTANTS.THEME_STYLES.TIME_INDICATOR_BG_COLOR, }, flexRow: { flexDirection: 'row', }, tvGuideContainer: { flex: 1, }, channelContentContainerStyle: { paddingBottom: 50, backgroundColor: TV_GUIDE_CONSTANTS.THEME_STYLES.CONTAINER_BG_COLOR, }, programsContentContainerStyle: { backgroundColor: TV_GUIDE_CONSTANTS.THEME_STYLES.CONTAINER_BG_COLOR, paddingBottom: 50, }, contentScrollTimeStyle: { backgroundColor: TV_GUIDE_CONSTANTS.THEME_STYLES.CONTAINER_BG_COLOR, }, });