@livetv-app/tvguide
Version:
An Android TV Live Channels-like Electronic Programme Guide for React DOM and React Native applications.
350 lines (349 loc) • 18.6 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.styles = exports.EmptyProgrammeCell = exports.ProgrammeCell = exports.handleProgrammeAction = exports.showProgrammeActions = exports.NowIndicator = exports.ProgrammeRow = void 0;
const React = require("react");
const react_1 = require("react");
const react_native_1 = require("react-native");
const focus_manager_1 = require("react-native-tvfocus/dist/focus-manager");
const constants_1 = require("../constants");
const context_1 = require("./context");
const util_1 = require("../util");
const MIN_PROGRAMME_DURATION = 60000;
function ChannelRow(props) {
var _a, _b, _c;
const screenFocused = react_1.useContext(focus_manager_1.FocusManagerContext).active;
const name = util_1.useLocalised(props.channel.name);
const programmes = react_1.useMemo(() => props.programmes.filter(p => p.channel === props.channel.id), [props.programmes, props.channel.id]);
const { setSelectedChannel: sC, setSelectedProgramme: sP } = react_1.useContext(context_1.SelectionContext);
const setSelectedProgramme = react_1.useCallback((programme) => (sC === null || sC === void 0 ? void 0 : sC(props.channel), sP === null || sP === void 0 ? void 0 : sP(programme)), [sC, sP, props.channel.id]);
const leftPosition = react_1.useContext(context_1.PositionContext);
const inner = React.createElement(React.Fragment, null,
React.createElement(react_native_1.View, { style: [
exports.styles.cell,
exports.styles.main,
props.active ? exports.styles.mainActive : null,
props.selected ? exports.styles.mainSelected : null,
] },
React.createElement(react_native_1.Text, { style: exports.styles.number, numberOfLines: 1 }, props.channel.number ?
(props.channel.number_group ? props.channel.number_group + '-' : '') +
props.channel.number : (_a = props.index) !== null && _a !== void 0 ? _a : '0'),
React.createElement(ChannelIcon, { name: name, icon: ((_b = props.showChannelLogos) !== null && _b !== void 0 ? _b : true) ? props.channel.icon : undefined, renderIcon: ((_c = props.showChannelLogos) !== null && _c !== void 0 ? _c : true) ? props.channel.renderIcon : undefined })),
React.createElement(exports.ProgrammeRow, { position: leftPosition, programmes: programmes, selected: props.selected && props.selectedProgramme || null, setSelected: setSelectedProgramme, isFirstRow: props.isFirstRow, isPreviousRowSelected: props.isPreviousRowSelected, width: props.programmesRowWidth }));
return React.createElement(react_native_1.View, { style: [
exports.styles.row,
props.isFirstRow ? null : 'isFirstRow' in props ? exports.styles.rowNotFirst : null,
props.isLastRow ? exports.styles.rowLast : null,
] }, react_native_1.Platform.isTV ? React.createElement(react_native_1.TouchableHighlight
// @ts-expect-error
, {
// @ts-expect-error
isTVSelectable: screenFocused && props.selected, hasTVPreferredFocus: screenFocused && props.selected, tvParallaxProperties: { shiftDistanceX: 0, tiltAngle: 0 }, onFocus: () => { var _a; return !props.selected && ((_a = props.onFocus) === null || _a === void 0 ? void 0 : _a.call(props)); }, style: exports.styles.touchable, children: inner }) : inner);
}
exports.default = React.memo(ChannelRow);
const _ChannelIcon = function ChannelIcon(props) {
const [loaded, setLoaded] = react_1.useState(false);
const [error, setError] = react_1.useState(null);
const hasIcon = props.icon || props.renderIcon;
return React.createElement(react_native_1.View, { style: exports.styles.icon },
hasIcon && !error ? props.renderIcon ? React.createElement(react_native_1.View, { style: {
position: 'absolute', opacity: loaded && !error ? 1 : 0,
} }, props.renderIcon.call(null, {
width: constants_1.SCALE * 100,
height: constants_1.SCALE * 34,
onLoad: () => setLoaded(true),
onError: err => (setLoaded(true), setError(err)),
})) : React.createElement(react_native_1.Image, { source: { uri: props.icon, height: constants_1.SCALE * 34, width: constants_1.SCALE * 100, cache: 'force-cache' }, style: { position: 'absolute', opacity: loaded && !error ? 1 : 0, resizeMode: 'contain' }, onLoad: () => setLoaded(true), onError: err => (setLoaded(true), setError(err)) }) : null,
!hasIcon || (!loaded || error) ? React.createElement(react_native_1.Text, { style: exports.styles.name }, props.name) : null);
};
const ChannelIcon = React.memo(_ChannelIcon);
const _ProgrammeRow = function ProgrammeRow(props) {
var _a, _b, _c, _d;
const language = react_1.useContext(util_1.LanguageContext).toString();
const leftPosition = props.position;
const rightPosition = react_1.useMemo(() => new Date(leftPosition.getTime() + ((props.width / constants_1.SCALE / constants_1.CELL_WIDTH) * constants_1.CELL_LENGTH)), [leftPosition, props.width]);
const programmes = react_1.useMemo(() => {
const programmes = {};
const sorted = props.programmes.sort((p1, p2) => {
if (p1.start === p2.start)
return 0;
if (p1.start < p2.start)
return -1;
return 1;
});
let last = Math.floor(leftPosition.getTime() / MIN_PROGRAMME_DURATION) * MIN_PROGRAMME_DURATION;
for (const programme of sorted) {
const start = Math.floor(programme.start.getTime() / MIN_PROGRAMME_DURATION) * MIN_PROGRAMME_DURATION;
const end = Math.floor(programme.end.getTime() / MIN_PROGRAMME_DURATION) * MIN_PROGRAMME_DURATION;
if (end - start < MIN_PROGRAMME_DURATION)
continue;
if (leftPosition.getTime() >= end)
continue;
if (rightPosition.getTime() < start)
continue;
if (start in programmes)
continue;
if (last && start > last)
programmes[last] = null;
programmes[start] = programme;
last = Math.floor(programme.end.getTime() / MIN_PROGRAMME_DURATION) * MIN_PROGRAMME_DURATION;
}
programmes[last] = null;
return programmes;
}, [props.programmes, leftPosition, rightPosition]);
const entries = Object.entries(programmes);
const list = [];
for (const i in entries) {
const [_start, programme] = entries[i];
const start = parseInt(_start);
const index = parseInt(i);
const end = parseInt((_a = entries[index + 1]) === null || _a === void 0 ? void 0 : _a[0]);
const next = (_b = entries[index + 1]) === null || _b === void 0 ? void 0 : _b[1];
if (programme) {
const offset = Math.max(0, leftPosition.getTime() - start);
const length = end - start - offset;
const cells = Math.min(length, rightPosition.getTime() - start) / constants_1.CELL_LENGTH;
const width = cells * constants_1.CELL_WIDTH;
list.push(React.createElement(ProgrammeCell, { key: start, programme: programme, next: next, selected: ((_c = props.selected) === null || _c === void 0 ? void 0 : _c.id) === programme.id && ((_d = props.selected) === null || _d === void 0 ? void 0 : _d.start) === programme.start, width: width, onPress: e => { var _a, _b; return ((_a = props.setSelected) === null || _a === void 0 ? void 0 : _a.call(null, programme), (_b = programme.onPress) === null || _b === void 0 ? void 0 : _b.call(null, e)); }, onLongPress: e => { var _a; return ((_a = programme.actions) === null || _a === void 0 ? void 0 : _a.length) ? showProgrammeActions(programme, language) : null; } }));
}
else {
const length = isNaN(end) ? rightPosition.getTime() - start : end - start;
const cells = length / constants_1.CELL_LENGTH;
const width = cells * constants_1.CELL_WIDTH;
list.push(React.createElement(EmptyProgrammeCell, { key: start, width: width }));
}
}
return React.createElement(react_native_1.View, { style: exports.styles.programmeRow },
React.createElement(react_native_1.View, { style: exports.styles.scrollContainer },
React.createElement(react_native_1.View, { style: exports.styles.scroller }, list)),
React.createElement(exports.NowIndicator, { left: leftPosition, width: props.width, isFirstRow: props.isFirstRow, isPreviousRowSelected: props.isPreviousRowSelected }));
};
exports.ProgrammeRow = React.memo(_ProgrammeRow);
const _NowIndicator = function NowIndicator(props) {
// Current time, to one minute
const now = util_1.useCurrentTime(60000);
const nowindicator_position = now.getTime() - props.left.getTime();
const nowindicator_cells = nowindicator_position / constants_1.CELL_LENGTH;
const nowindicator = nowindicator_cells * constants_1.CELL_WIDTH;
return nowindicator >= 0 && props.width >= nowindicator ? React.createElement(React.Fragment, null,
React.createElement(react_native_1.View, { style: [
exports.styles.nowIndicator,
props.isFirstRow || props.isPreviousRowSelected ? null : exports.styles.nowIndicatorNotFirstRow,
{ left: constants_1.SCALE * nowindicator },
] })) : null;
};
exports.NowIndicator = React.memo(_NowIndicator);
function showProgrammeActions(programme, language) {
// This doesn't work in web browsers
// https://github.com/necolas/react-native-web/issues/1026
var _a, _b;
const actions = (_b = (_a = programme.actions) === null || _a === void 0 ? void 0 : _a.map(action => {
return {
text: util_1.getLocalised(language, action.name),
onPress: e => handleProgrammeAction(programme, action, e),
};
})) !== null && _b !== void 0 ? _b : [];
actions.push({
text: 'Cancel',
onPress: () => { },
style: 'cancel',
});
const et = util_1.formatEpisodeText(programme);
react_native_1.Alert.alert(util_1.getLocalised(language, programme.name), (et ? et + '\n\n' : '') + util_1.getLocalised(language, programme.description), actions, { cancelable: true });
}
exports.showProgrammeActions = showProgrammeActions;
function handleProgrammeAction(programme, action, event) {
return __awaiter(this, void 0, void 0, function* () {
for (const url of action.urls) {
try {
if (!(yield react_native_1.Linking.canOpenURL(url)))
continue;
}
catch (err) { }
yield react_native_1.Linking.openURL(url);
break;
}
//
});
}
exports.handleProgrammeAction = handleProgrammeAction;
function ProgrammeCell(props) {
var _a;
const name = util_1.useLocalised(props.programme.name);
const desc = util_1.useLocalised(props.programme.description);
const et = typeof props.programme.episode === 'number' || props.programme.episode_name ?
typeof props.programme.series === 'number' && typeof props.programme.episode === 'number' &&
props.programme.episode_name ?
`S${props.programme.series + 1}, E${props.programme.episode + 1}: ${props.programme.episode_name}` :
typeof props.programme.series === 'number' && typeof props.programme.episode === 'number' ?
`Series ${props.programme.series + 1}, Episode ${props.programme.episode + 1}` +
(props.programme.episode_name ? ': ' + props.programme.episode_name : '') :
typeof props.programme.series === 'number' && props.programme.episode_name ?
`S${props.programme.series + 1}: ${props.programme.episode_name}` :
typeof props.programme.episode === 'number' && props.programme.episode_name ?
`E${props.programme.episode + 1}: ${props.programme.episode_name}` :
typeof props.programme.episode === 'number' ?
`Episode ${props.programme.episode + 1}` :
props.programme.episode_name ?
props.programme.episode_name :
null : null;
const large = props.width >= (constants_1.MIN_LARGE_CELL_WIDTH * constants_1.SCALE);
return React.createElement(react_native_1.TouchableHighlight, { onPress: (_a = props.onPress) !== null && _a !== void 0 ? _a : props.programme.onPress, onLongPress: props.onLongPress,
// @ts-expect-error
isTVSelectable: false, style: [
exports.styles.cell,
exports.styles.programme,
props.selected ? exports.styles.programmeSelected : null,
props.next ? exports.styles.programmeHasSeparator : null,
{ width: constants_1.SCALE * props.width },
] },
React.createElement(React.Fragment, null,
typeof props.programme.recording === 'number' ? React.createElement(react_native_1.View, { style: exports.styles.programmeRecordingIndicator }) : null,
React.createElement(react_native_1.View, { style: exports.styles.programmeDetail },
React.createElement(react_native_1.Text, { numberOfLines: large ? 1 : 2, ellipsizeMode: "tail", style: [
exports.styles.programmeName,
props.selected ? exports.styles.programmeSelectedName : null,
] }, name),
large && (et || (desc && !props.selected)) ? React.createElement(react_native_1.View, { style: exports.styles.programmeDescription },
et ? React.createElement(react_native_1.Text, { numberOfLines: 1, ellipsizeMode: "tail", style: [
exports.styles.programmeDescriptionText,
exports.styles.programmeEpisodeText,
props.selected ? exports.styles.programmeSelectedDescriptionText : null,
] }, et) : null,
!et && desc && !props.selected ? React.createElement(react_native_1.Text, { numberOfLines: 1, ellipsizeMode: "tail", style: exports.styles.programmeDescriptionText }, desc) : null) : null)));
}
exports.ProgrammeCell = ProgrammeCell;
function EmptyProgrammeCell(props) {
return React.createElement(react_native_1.View, { style: [exports.styles.cell, exports.styles.programme, exports.styles.emptyProgramme, { width: constants_1.SCALE * props.width }] },
React.createElement(react_native_1.View, { style: exports.styles.programmeDetail },
React.createElement(react_native_1.Text, { numberOfLines: 2, ellipsizeMode: "tail", style: exports.styles.emptyProgrammeText }, "No information")));
}
exports.EmptyProgrammeCell = EmptyProgrammeCell;
exports.styles = react_native_1.StyleSheet.create({
row: {
backgroundColor: '#37474fdd',
flexDirection: 'row',
},
rowNotFirst: {
borderTopColor: '#263238dd',
borderTopWidth: 1,
},
rowLast: {
borderBottomLeftRadius: constants_1.SCALE * 2,
},
touchable: {
flexDirection: 'row',
marginBottom: react_native_1.Platform.isTV ? -200 : 0,
paddingBottom: react_native_1.Platform.isTV ? 200 : 0,
},
header: {},
cell: {
paddingVertical: constants_1.SCALE * 10,
paddingHorizontal: constants_1.SCALE * 20,
},
main: {
width: constants_1.SCALE * (70 + 20 + 110),
borderRightColor: '#263238aa',
borderRightWidth: 1,
flexDirection: 'row',
},
mainActive: {
backgroundColor: '#607d8baa',
},
mainSelected: {},
number: {
fontSize: constants_1.SCALE * 26,
color: '#eeeeee',
width: constants_1.SCALE * 70,
paddingRight: constants_1.SCALE * 10,
},
icon: {
width: constants_1.SCALE * 100,
justifyContent: 'center',
},
name: {
color: '#eeeeee',
width: constants_1.SCALE * 100,
fontSize: constants_1.SCALE * 14,
},
programmeRow: {
flex: 1,
},
scrollContainer: {
flex: 1,
},
scroller: {
flex: 1,
flexDirection: 'row',
},
programme: {
paddingHorizontal: constants_1.SCALE * 10,
flexDirection: 'row',
overflow: 'hidden',
height: constants_1.SCALE * (34 + 20),
alignItems: 'center',
},
programmeSelected: {
backgroundColor: '#90a4aeaa',
},
programmeHasSeparator: {
borderRightColor: '#263238aa',
borderRightWidth: 1,
},
programmeRecordingIndicator: {
height: constants_1.SCALE * 10,
width: constants_1.SCALE * 10,
borderRadius: constants_1.SCALE * 15,
marginRight: constants_1.SCALE * 10,
backgroundColor: '#b71c1caa',
},
programmeDetail: {
flex: 1,
justifyContent: 'center',
minWidth: constants_1.SCALE * 50,
},
programmeName: {
color: '#eeeeee',
fontSize: constants_1.SCALE * 14,
},
programmeSelectedName: {
color: '#ffffff',
},
programmeDescription: {},
programmeDescriptionText: {
color: '#90a4ae',
fontSize: constants_1.SCALE * 13,
},
programmeEpisodeText: {
fontWeight: '700',
},
programmeSelectedDescriptionText: {
color: '#eceff1',
},
emptyProgramme: {
backgroundColor: '#455a64aa',
},
emptyProgrammeText: {
fontStyle: 'italic',
color: '#aaaaaa',
fontSize: constants_1.SCALE * 14,
},
nowIndicator: {
position: 'absolute',
height: '100%',
width: 2,
backgroundColor: '#00b0ff',
},
nowIndicatorNotFirstRow: {
marginTop: -1,
height: constants_1.SCALE * 55,
},
});