UNPKG

@livetv-app/tvguide

Version:

An Android TV Live Channels-like Electronic Programme Guide for React DOM and React Native applications.

391 lines (387 loc) 21.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.css = exports.CLASS_PREFIX = void 0; const React = require("react"); const util_1 = require("../util"); const constants_1 = require("../constants"); const context_1 = require("./context"); const types_1 = require("./types"); const channel_row_1 = require("./channel-row"); const header_1 = require("./header"); const programme_row_1 = require("./programme-row"); const debug_1 = require("./debug"); const DEBUG_SHOW = typeof process === 'object' && process.env.NODE_ENV === 'development' ? true : false; const DEBUG_DEFAULT_TIME = null; //new Date(2020, 11, 29, 11, 0); const DEBUG_DEFAULT_POSITION = null; class ProgrammeGuide extends React.Component { constructor(props) { var _a, _b, _c, _d; super(props); this.state = { channel: null, programme: null, time: DEBUG_DEFAULT_TIME !== null && DEBUG_DEFAULT_TIME !== void 0 ? DEBUG_DEFAULT_TIME : new Date(), leftPosition: DEBUG_DEFAULT_POSITION !== null && DEBUG_DEFAULT_POSITION !== void 0 ? DEBUG_DEFAULT_POSITION : new Date(Math.floor((Date.now() - constants_1.MIN_DEFAULT_POSITION) / constants_1.CELL_LENGTH) * constants_1.CELL_LENGTH), width: 0, heightDebug: 0, heightHeader: 0, scrollTop: null, scrollBottom: null, }; this.programmes_by_channel = {}; this.headerRowRef = null; this.setSelectedChannel = (channel) => this.setState({ channel }); this.setSelectedProgramme = (programme) => this.setState({ programme }); this.setSelectedTime = (time) => this.setState({ time }); this.setLeftPosition = (leftPosition) => this.setState({ leftPosition }); this.setWidth = (width) => this.setState({ width }); this.selection = { c: this, get channel() { return this.c.state.channel; }, get programme() { return this.c.state.programme; }, get time() { return this.c.state.time; }, setSelectedChannel: channel => this.setState({ channel }), setSelectedProgramme: programme => this.setState({ programme }), setSelectedTime: time => this.setState({ time }), }; const programmes = this.programmes_by_channel; for (const programme of this.props.programmes) { const p = (_a = programmes[programme.channel]) !== null && _a !== void 0 ? _a : (programmes[programme.channel] = []); p.push(programme); } this.state.channel = (_c = (_b = props.channel) !== null && _b !== void 0 ? _b : props.channels[0]) !== null && _c !== void 0 ? _c : null; if (this.state.channel) { const programmes = (_d = this.programmes_by_channel[this.state.channel.id]) !== null && _d !== void 0 ? _d : []; this.state.programme = util_1.getProgrammeAtTime(this.state.time, programmes); } const scrollPosition = this.calculateScrollPosition(); this.state.scrollTop = scrollPosition.scrollTop; this.state.scrollBottom = scrollPosition.scrollBottom; } get leftPosition() { return this.state.leftPosition; } componentDidUpdate(prevProps, prevState) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; if (this.props.programmes !== prevProps.programmes) { const programmes = {}; for (const programme of this.props.programmes) { const p = (_a = programmes[programme.channel]) !== null && _a !== void 0 ? _a : (programmes[programme.channel] = []); p.push(programme); } this.programmes_by_channel = programmes; } if (this.props.channel && this.props.channel !== prevProps.channel) { const time = DEBUG_DEFAULT_TIME !== null && DEBUG_DEFAULT_TIME !== void 0 ? DEBUG_DEFAULT_TIME : new Date(); const programmes = (_b = this.programmes_by_channel[this.props.channel.id]) !== null && _b !== void 0 ? _b : []; this.setState({ channel: this.props.channel, programme: util_1.getProgrammeAtTime(time, programmes), time, }); } else if (this.state.channel && !this.props.channels.includes(this.state.channel)) { const channel = (_c = this.props.channels[0]) !== null && _c !== void 0 ? _c : null; this.setState({ channel }); if (channel) { const programmes = (_d = this.programmes_by_channel[channel.id]) !== null && _d !== void 0 ? _d : []; this.setState({ programme: util_1.getProgrammeAtTime(this.state.time, programmes), }); } } if (this.headerRowRef && this.state.heightHeader !== this.headerRowRef.clientHeight) { this.setState({ heightHeader: this.headerRowRef.clientHeight, }); } if (this.props.channels !== prevProps.channels || this.props.scrollPosition !== prevProps.scrollPosition || ((_e = this.props.scrollPosition) === null || _e === void 0 ? void 0 : _e.contentOffset.y) !== ((_f = prevProps.scrollPosition) === null || _f === void 0 ? void 0 : _f.contentOffset.y) || ((_g = this.props.scrollPosition) === null || _g === void 0 ? void 0 : _g.layoutMeasurement.height) !== ((_h = prevProps.scrollPosition) === null || _h === void 0 ? void 0 : _h.layoutMeasurement.height) || this.props.scrollOffsetTop !== prevProps.scrollOffsetTop || ((_j = this.state.channel) === null || _j === void 0 ? void 0 : _j.id) !== ((_k = prevState.channel) === null || _k === void 0 ? void 0 : _k.id) || this.state.heightDebug !== prevState.heightDebug || this.state.heightHeader !== prevState.heightHeader) { this.setState(this.calculateScrollPosition()); } if (this.props.onScroll && (this.state.scrollTop !== prevState.scrollTop || this.state.scrollBottom !== prevState.scrollBottom || this.state.leftPosition !== prevState.leftPosition || this.state.width !== prevState.width)) { this.props.onScroll.call(null, { top: this.state.scrollTop, bottom: this.state.scrollBottom, left: this.state.leftPosition, right: new Date(this.state.leftPosition.getTime() + ((this.state.width / constants_1.CELL_WIDTH) * constants_1.CELL_LENGTH)), }); } } setChannel(channel) { var _a; const programmes = (_a = this.programmes_by_channel[channel.id]) !== null && _a !== void 0 ? _a : []; this.setSelectedChannel(channel); this.setSelectedProgramme(util_1.getProgrammeAtTime(this.selection.time, programmes)); } setChannelUp() { if (!this.props.channels.length) return; this.setChannel(util_1.getPreviousChannel(this.selection.channel, this.props.channels)); } setChannelDown() { if (!this.props.channels.length) return; this.setChannel(util_1.getNextChannel(this.selection.channel, this.props.channels)); } setPreviousProgramme() { var _a; if (!this.selection.channel) return; const programmes = (_a = this.programmes_by_channel[this.selection.channel.id]) !== null && _a !== void 0 ? _a : []; const time = util_1.getPreviousProgrammeTime(this.selection.time, programmes); if (!time) return; this.setSelectedTime(time); const programme = util_1.getProgrammeAtTime(time, programmes); this.setSelectedProgramme(programme); } setNextProgramme() { var _a; if (!this.selection.channel) return; const programmes = (_a = this.programmes_by_channel[this.selection.channel.id]) !== null && _a !== void 0 ? _a : []; const time = util_1.getNextProgrammeTime(this.selection.time, programmes); if (!time) return; this.setSelectedTime(time); const programme = util_1.getProgrammeAtTime(time, programmes); this.setSelectedProgramme(programme); } sendSelectKeypress() { var _a, _b, _c; if (((_a = this.props.channel) === null || _a === void 0 ? void 0 : _a.id) !== ((_b = this.state.channel) === null || _b === void 0 ? void 0 : _b.id)) { if (this.state.channel) { (_c = this.props.onSelectChannel) === null || _c === void 0 ? void 0 : _c.call(null, this.state.channel); } return types_1.KeypressResult.OK; } else { return types_1.KeypressResult.IGNORED_AS_CHANNEL_ALREADY_SELECTED; } } sendLongSelectKeypress() { var _a, _b; if (!this.state.programme) return types_1.KeypressResult.IGNORED_AS_NO_PROGRAMME_SELECTED; if (!((_a = this.state.programme.actions) === null || _a === void 0 ? void 0 : _a.length)) return types_1.KeypressResult.IGNORED_AS_PROGRAMME_HAS_NO_ACTIONS; channel_row_1.showProgrammeActions(this.state.programme, (_b = this.props.language) !== null && _b !== void 0 ? _b : util_1.DEFAULT_LANGUAGE); return types_1.KeypressResult.OK; } sendUpKeypress(wrap = true) { const channel = util_1.getPreviousChannel(this.state.channel, this.props.channels, wrap); if (channel === null) { return types_1.KeypressResult.IGNORED_DUE_TO_BOUNDARY; } this.setChannel(channel); // TODO: ensure the channel is visible return types_1.KeypressResult.OK; } sendDownKeypress(wrap = true) { const channel = util_1.getNextChannel(this.state.channel, this.props.channels, wrap); if (channel === null) { return types_1.KeypressResult.IGNORED_DUE_TO_BOUNDARY; } this.setChannel(channel); // TODO: ensure the channel is visible return types_1.KeypressResult.OK; } setSelectedTimeAndProgramme(time, scroll = true) { var _a; const length = (this.state.width / constants_1.CELL_WIDTH) * constants_1.CELL_LENGTH; const rightPosition = new Date(this.state.leftPosition.getTime() + length); const leftPosition = !scroll ? this.state.leftPosition : this.state.leftPosition.getTime() >= (time.getTime() - constants_1.MIN_DEFAULT_POSITION) ? new Date(Math.floor(time.getTime() / constants_1.CELL_LENGTH) * constants_1.CELL_LENGTH) : rightPosition.getTime() <= (time.getTime() + constants_1.MIN_DEFAULT_POSITION) ? new Date(((Math.floor(time.getTime() / constants_1.CELL_LENGTH) * constants_1.CELL_LENGTH) - length) + constants_1.CELL_LENGTH) : this.state.leftPosition; if (this.state.channel) { const programmes = (_a = this.programmes_by_channel[this.state.channel.id]) !== null && _a !== void 0 ? _a : []; const programme = util_1.getProgrammeAtTime(time, programmes); this.setState({ time, programme, leftPosition }); } else { this.setState({ time, programme: null, leftPosition }); } } scrollLeftRespectingBoundary(length) { const time = new Date(this.props.leftBoundary ? Math.max(this.props.leftBoundary.getTime(), this.state.time.getTime() - length) : this.state.time.getTime() - length); if (this.props.leftBoundary && this.props.leftBoundary.getTime() > time.getTime()) return false; this.setSelectedTimeAndProgramme(time); return true; } scrollRightRespectingBoundary(length) { const time = new Date(this.props.rightBoundary ? Math.min(this.props.rightBoundary.getTime(), this.state.time.getTime() + length) : this.state.time.getTime() + length); if (this.props.rightBoundary && this.props.rightBoundary.getTime() < time.getTime()) return false; this.setSelectedTimeAndProgramme(time); return true; } sendLeftKeypress() { var _a; const length = (this.state.width / constants_1.CELL_WIDTH) * constants_1.CELL_LENGTH; const scroll_max = length; if (!this.state.channel) { return this.scrollLeftRespectingBoundary(scroll_max) ? types_1.KeypressResult.OK : types_1.KeypressResult.IGNORED_DUE_TO_BOUNDARY; } const time = util_1.getPreviousProgrammeTime(this.state.time, (_a = this.programmes_by_channel[this.state.channel.id]) !== null && _a !== void 0 ? _a : []); if (!time) { return this.scrollLeftRespectingBoundary(scroll_max) ? types_1.KeypressResult.OK : types_1.KeypressResult.IGNORED_DUE_TO_BOUNDARY; } if (this.state.time.getTime() - time.getTime() > scroll_max) { return this.scrollLeftRespectingBoundary(scroll_max) ? types_1.KeypressResult.OK : types_1.KeypressResult.IGNORED_DUE_TO_BOUNDARY; } if (this.props.leftBoundary && this.props.leftBoundary.getTime() > time.getTime()) { return types_1.KeypressResult.IGNORED_DUE_TO_BOUNDARY; } this.setSelectedTimeAndProgramme(time); return types_1.KeypressResult.OK; } sendRightKeypress() { var _a; const length = (this.state.width / constants_1.CELL_WIDTH) * constants_1.CELL_LENGTH; const scroll_max = length; if (!this.state.channel) { return this.scrollRightRespectingBoundary(scroll_max) ? types_1.KeypressResult.OK : types_1.KeypressResult.IGNORED_DUE_TO_BOUNDARY; } const time = util_1.getNextProgrammeTime(this.state.time, (_a = this.programmes_by_channel[this.state.channel.id]) !== null && _a !== void 0 ? _a : []); if (!time) { return this.scrollRightRespectingBoundary(scroll_max) ? types_1.KeypressResult.OK : types_1.KeypressResult.IGNORED_DUE_TO_BOUNDARY; } if (time.getTime() - this.state.time.getTime() > scroll_max) { return this.scrollRightRespectingBoundary(scroll_max) ? types_1.KeypressResult.OK : types_1.KeypressResult.IGNORED_DUE_TO_BOUNDARY; } if (this.props.rightBoundary && this.props.rightBoundary.getTime() < time.getTime()) { return types_1.KeypressResult.IGNORED_DUE_TO_BOUNDARY; } this.setSelectedTimeAndProgramme(time); return types_1.KeypressResult.OK; } shouldRenderChannelRow(total, index, channel) { var _a, _b; if (((_a = this.state.channel) === null || _a === void 0 ? void 0 : _a.id) === channel.id) return true; if (this.props.scrollPosition === undefined) return true; if (this.props.scrollPosition === null) return false; const offset_top = ((_b = this.props.scrollOffsetTop) !== null && _b !== void 0 ? _b : 0) + (DEBUG_SHOW ? this.state.heightDebug : 0) + this.state.heightHeader; const position_top = offset_top + ((index * 55) - 1); const position_bottom = position_top + this.props.scrollPosition.layoutMeasurement.height; if (this.props.scrollPosition.contentOffset.y > position_bottom + 50) return false; if (this.props.scrollPosition.contentOffset.y + this.props.scrollPosition.layoutMeasurement.height < position_top - 50) return false; return true; } calculateRenderedRows() { const grouped = []; for (const i in this.props.channels) { const index = parseInt(i); const channel = this.props.channels[i]; if (this.shouldRenderChannelRow(this.props.channels.length, index, channel)) { grouped.push(channel); } else { const CHANNEL_ROW_HEIGHT = grouped.length ? 55 : 54; const end = grouped[grouped.length - 1]; if (typeof end === 'number') grouped.splice(grouped.length - 1, 1, end + CHANNEL_ROW_HEIGHT); else grouped.push(CHANNEL_ROW_HEIGHT); } } return grouped; } calculateScrollPosition() { var _a, _b; const groups = this.calculateRenderedRows(); const findFirstChannel = (c, i, arr) => typeof c === 'object' && (arr.length === i + 1 || typeof arr[i + 1] !== 'number'); const scrollTop = (_a = groups.find(findFirstChannel)) !== null && _a !== void 0 ? _a : null; const reversed = groups.reverse(); const scrollBottom = (_b = reversed.find(findFirstChannel)) !== null && _b !== void 0 ? _b : null; return { scrollTop, scrollBottom }; } render() { const { props, selection, setWidth } = this; const { channel, programme, leftPosition } = this.state; const grouped = this.calculateRenderedRows(); return React.createElement("div", { className: exports.CLASS_PREFIX + '-container' }, DEBUG_SHOW ? this.renderDebugView() : null, React.createElement(context_1.SelectionContext.Provider, { value: selection }, React.createElement(context_1.PositionContext.Provider, { value: leftPosition }, React.createElement("div", { className: [channel_row_1.CLASS_PREFIX + '-row', exports.CLASS_PREFIX + '-header'].join(' '), ref: ref => this.headerRowRef = ref }, React.createElement(header_1.TimeCell, { start: leftPosition }), React.createElement(header_1.TimeList, { start: leftPosition, onResize: width => setWidth(width) })), grouped.map((c, index) => { var _a, _b; return typeof c === 'number' ? React.createElement("div", { key: index, className: [ exports.CLASS_PREFIX + '-channelRowPlaceholder', index !== 0 ? exports.CLASS_PREFIX + '-channelRowPlaceholderNotFirstRow' : null, index === grouped.length - 1 ? exports.CLASS_PREFIX + '-channelRowPlaceholderLastRow' : null, ].filter(c => c).join(' '), style: { height: c } }) : React.createElement("div", { key: c.id, className: exports.CLASS_PREFIX + '-channelRowContainer' }, React.createElement(channel_row_1.default, { channel: c, programmes: (_a = this.programmes_by_channel[c.id]) !== null && _a !== void 0 ? _a : [], index: index, isFirstRow: index === 0, isLastRow: index === props.channels.length - 1, selected: c.id === (channel === null || channel === void 0 ? void 0 : channel.id), isPreviousRowSelected: ((_b = props.channels[index - 1]) === null || _b === void 0 ? void 0 : _b.id) === (channel === null || channel === void 0 ? void 0 : channel.id), selectedProgramme: c.id === (channel === null || channel === void 0 ? void 0 : channel.id) ? programme : null, programmesRowWidth: this.state.width, showChannelLogos: this.props.showChannelLogos }), c.id === (channel === null || channel === void 0 ? void 0 : channel.id) ? programme ? React.createElement(programme_row_1.default, { channel: channel, programme: programme }) : React.createElement(programme_row_1.EmptyProgrammeRow, null) : null); })))); } renderDebugView() { return React.createElement(debug_1.default, { channels: this.props.channels, programmes: this.props.programmes, selection: this.selection, setSelectedChannel: this.setSelectedChannel, setSelectedProgramme: this.setSelectedProgramme, setSelectedTime: this.setSelectedTime, leftPosition: this.state.leftPosition, setLeftPosition: this.setLeftPosition, width: this.state.width, scrollTop: this.state.scrollTop, scrollBottom: this.state.scrollBottom, onLayout: e => this.setState({ heightDebug: e.height }) }); } } exports.default = ProgrammeGuide; exports.CLASS_PREFIX = 'tvguide'; exports.css = ` /* Reset styles */ .${exports.CLASS_PREFIX}-container { font-size: 14px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } .${exports.CLASS_PREFIX}-container { flex: 1; padding: 30px; padding-right: 0; border-radius: 2px; box-shadow: 0 0 10px #263238cc; } .${exports.CLASS_PREFIX}-header { background-color: #607d8bee; border-top-left-radius: 2px; overflow: visible; } .${exports.CLASS_PREFIX}-channelRowContainer { overflow: hidden; } .${exports.CLASS_PREFIX}-channelRowPlaceholder { background-color: #37474fdd; } .${exports.CLASS_PREFIX}-channelRowPlaceholderNotFirstRow { border-top: 1px solid #263238dd; } .${exports.CLASS_PREFIX}-channelRowPlaceholderLastRow { border-bottom-left-radius: 2px; } `;