UNPKG

@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
"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, }, });