@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
JavaScript
"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;
}
`;