/* global Clock, EPGController, SpatialNavigator, KeyNavigationAdapter, ContextMenu */ 'use strict'; (function(exports) { function EPG() { this._fetchElement(); // Unit of EPG timeline is 30 min this.timelineUnit = 30 * 60000; // Time when EPG is opened. It is used as a reference for setting left // attribute of program list and timeline this.initialTime = Math.floor(Date.now() / this.timelineUnit); // Time offset represents the most left visible time in program container this.visibleTimeOffset = this.initialTime; // Number of timeslot that can be shown in program container this.visibleTimeSize = 4; // Channel offset represents the first visible channel in program container this.visibleChannelOffset = 0; // Number of channel that can be shown in program container this.visibleChannelSize = 5; this.translate = { x: 0, y: 0 }; this.keyNavigatorAdapter = new KeyNavigationAdapter(); this.keyNavigatorAdapter.init(); this.keyNavigatorAdapter.on('move', this._onMove.bind(this)); this.spatialNavigator = new SpatialNavigator([], { ignoreHiddenElement: true }); this.spatialNavigator.on('focus', this._onFocus.bind(this)); this.spatialNavigator.on('unfocus', this._onUnfocus.bind(this)); this.epgController = new EPGController( this.visibleTimeOffset - this.visibleTimeSize, this.timelineUnit ); this.clock = new Clock(); navigator.mozL10n.ready(function() { this.clock.stop(); this.clock.start(this._updateClock.bind(this)); }.bind(this)); navigator.mozApps.getSelf().onsuccess = function(evt){ if (evt.target && evt.target.result) { this.contextmenu = new ContextMenu([{ element: document.getElementById('pin-button-contextmenu'), hasText: true }], evt.target.result); } }.bind(this); this.epgController.on('scanned', this._onScanned.bind(this)); this.epgController.on('appendChannel', this._appendChannel.bind(this)); this.epgController.on('updateProgram', this._updateProgramSlot.bind(this)); this.epgController.on('addTimeline', this._addTimeline.bind(this)); this.epgController.on( 'allTimelineAdded', this._allTimelineAdded.bind(this)); this.epgController.on( 'allChannelFetched', this._allChannelFetched.bind(this)); } var proto = {}; proto._fetchElement = function epg__fetchElement(){ this.timeElement = document.getElementById('time'); this.dateElement = document.getElementById('date'); this.videoElement = document.getElementById('video-thumbnail'); this.timelineElement = document.getElementById('timeline'); this.timeMarkerElement = document.getElementById('time-marker'); this.timePrefixElement = document.getElementById('time-prefix'); this.channelListElement = document.getElementById('channel-list'); this.programListElement = document.getElementById('program-list'); this.programMetaElement = document.getElementById('program-meta'); this.progressBarElement = document.getElementById('progress-bar'); this.programTitleElement = document.getElementById('program-title'); this.programDetailElement = document.getElementById('program-detail'); this.timeMarkerContainerElement = document.getElementById('time-marker-container'); }; /** * Align timelineOffset and use it as x-axis translate reference point */ proto._allTimelineAdded = function epg__allTimelineAdded() { var timeDiff = this.initialTime - this.epgController.timelineOffset; this.timelineElement.style.left = (-timeDiff * EPG.COLUMN_WIDTH) + 'rem'; this.programListElement.style.left = (-timeDiff * EPG.COLUMN_WIDTH) + 'rem'; }; /** * Align channelOffset and use it as y-axis translate reference point */ proto._allChannelFetched = function epg__allChannelFetched() { this.visibleChannelOffset = this.epgController.channelOffset; this.channelListElement.style.top = (-this.visibleChannelOffset * EPG.ROW_HEIGHT) + 'rem'; this.programListElement.style.top = (-this.visibleChannelOffset * EPG.ROW_HEIGHT) + 'rem'; }; proto._onScanned = function epg__onScanned(stream) { this.videoElement.mozSrcObject = stream; this.epgController.fetchPrograms( this.visibleTimeOffset - this.visibleTimeSize, 3 * this.visibleTimeSize ).then(function() { var timeIndex = this.visibleTimeOffset - this.epgController.timelineOffset; var programElement = this.epgController.programTable[timeIndex][this.visibleChannelOffset] .element; this.spatialNavigator.focus(programElement); this._updateProgramProgress(this._currentTime); this._updateDate(this.visibleTimeOffset * this.timelineUnit); }.bind(this)).catch(function(err) { console.error(err); }); }; /** * Add one more timeslot to timeline element. */ proto._addTimeline = function epg__addTimeline(index, time) { var timeSlotElement = document.createElement('LI'); timeSlotElement.textContent = this._timeToString(time).timeWithPrefix; if (index < this.timelineElement.children.length) { this.timelineElement.insertBefore( timeSlotElement, this.timelineElement.children[index]); } else { this.timelineElement.appendChild(timeSlotElement); } this._addProgramSlot(index, time / this.timelineUnit); }; /** * Add one more timeslot for every channels to program list. */ proto._addProgramSlot = function epg__addProgramSlot(index, time) { var rowElement; var programElement; var i; for (i = 0; i < this.programListElement.children.length; i++) { rowElement = this.programListElement.children[i]; programElement = document.createElement('epg-program'); programElement.startTime = time; if (index < this.timelineElement.children.length) { rowElement.insertBefore(programElement, rowElement.children[index]); } else { rowElement.appendChild(programElement); } } }; /** * Create channel elements for input channel */ proto._appendChannel = function epg__appendChannel(channel, index) { var channelElement = document.createElement('LI'); var channelTitle = document.createElement('SPAN'); channelTitle.classList.add('title'); channelTitle.textContent = channel.name; channelElement.textContent = channel.number; channelElement.appendChild(channelTitle); this.channelListElement.appendChild(channelElement); var programRow = document.createElement('UL'); programRow.dataset.row = index; this.channelListElement.appendChild(channelElement); this.programListElement.appendChild(programRow); }; proto._updateProgramSlot = function epg__updateProgramSlot(configs) { var rowElement = this.programListElement.children[configs.row]; var programElement = rowElement.children[configs.column]; var duration = programElement.duration; if (configs.title) { programElement.title = configs.title; } if (configs.duration) { duration = configs.duration; programElement.duration = duration; } if (configs.isVisible) { programElement.show(); this.spatialNavigator.add(programElement); programElement.resetProgressElement(this.initialTime); } else { programElement.hide(); // If the old focus element only contains partial program segment, we have // to refocus to the first visible element of the same program. if (programElement === this.spatialNavigator.getFocusedElement()) { this.spatialNavigator.focus( this.epgController.programTable[configs.column][configs.row].element); } this.spatialNavigator.remove(programElement); } if (configs.item) { configs.item.element = programElement; } }; proto._updateClock = function epg_updateClock(date) { var timeString = this._timeToString(date); this.timePrefixElement.textContent = timeString.prefix; this.timeElement.textContent = timeString.time; this._currentTime = new Date(date).getTime(); this._updateProgramProgress(this._currentTime); }; proto._updateProgramProgress = function epg__updateProgramProgress(time) { var timeIndex = Math.floor(time / this.timelineUnit) - this.epgController.timelineOffset; var playingPrograms = this.epgController.programTable[timeIndex]; var prevPrograms = this.epgController.programTable[timeIndex - 1]; var row; var programElement; // Update progress bar in every currently playing program for (row in playingPrograms) { programElement = playingPrograms[row].element; programElement.progress = time / this.timelineUnit; this.timeMarkerElement.style.transform = 'translateX(' + ((time / this.timelineUnit - this.initialTime) * EPG.COLUMN_WIDTH) + 'rem)'; // Privous program may not be 100% filled because because of little // variance of timestamp if (prevPrograms && prevPrograms[row] && programElement !== prevPrograms[row].element) { programElement = prevPrograms[row].element; programElement.fillProgress(); } } programElement = this.spatialNavigator.getFocusedElement(); if (programElement) { this.progressBarElement.style.transform = programElement.progressElement.style.transform; } }; proto._timeToString = function epg__timeToString(time) { var now = new Date(time); var _ = navigator.mozL10n.get; var use12Hour = window.navigator.mozHour12; var f = new navigator.mozL10n.DateTimeFormat(); var timeFormat = use12Hour ? _('shortTimeFormat12') : _('shortTimeFormat24'); timeFormat = timeFormat.replace('%p', '').trim(); var formatted = f.localeFormat(now, timeFormat); var prefix = use12Hour ? f.localeFormat(now, '%p') : ''; return { prefix: prefix, time: formatted, timeWithPrefix: formatted + prefix }; }; proto._onFocus = function epg__onFocus(programElement) { var rowElement = programElement.parentElement; var rowIndex = parseInt(rowElement.dataset.row, 10); var rowOffset = this.epgController.channelOffset; var startTime = programElement.startTime; var timelineOffset = this.epgController.timelineOffset; programElement.classList.add('focus'); rowElement.classList.remove('hidden'); this._setTitlePadding({ setToNull: true }); this._displayProgramInfo( startTime - this.epgController.timelineOffset, rowIndex); this.epgController.switchChannel(rowIndex); this.progressBarElement.style.transform = programElement.progressElement.style.transform; if (rowIndex < this.visibleChannelOffset) { // Move up this.translate.y = (rowOffset - rowIndex) * EPG.ROW_HEIGHT; this.visibleChannelOffset = rowIndex; } else if (rowIndex >= this.visibleChannelOffset + this.visibleChannelSize ) { // Move down this.translate.y = (rowOffset + this.visibleChannelSize - rowIndex - 1) * EPG.ROW_HEIGHT; this.visibleChannelOffset = rowIndex - this.visibleChannelSize + 1; } if (startTime < this.visibleTimeOffset || startTime >= (this.visibleTimeOffset + this.visibleTimeSize)) { // Move left and right this.translate.x = (this.initialTime - startTime) * EPG.COLUMN_WIDTH; this.visibleTimeOffset = startTime; this._updateDate(startTime * this.timelineUnit); } this._setTitlePadding(); // Prefetch more older programs if (this.visibleTimeOffset - 2 * this.visibleTimeSize < timelineOffset) { this.epgController.fetchPrograms( timelineOffset - 2 * this.visibleTimeSize, 2 * this.visibleTimeSize ).then(function() { this._updateProgramProgress.bind(this._currentTime); }.bind(this)).catch(function(err) { console.error(err); }); } // Prefetch more future programs if (this.visibleTimeOffset + 2 * this.visibleTimeSize > timelineOffset + this.epgController.totalTimeslotCount) { this.epgController.fetchPrograms( timelineOffset + this.epgController.totalTimeslotCount, 2 * this.visibleTimeSize ).then(function() { this._updateProgramProgress.bind(this._currentTime); }.bind(this)).catch(function(err) { console.error(err); }); } this.channelListElement.style.transform = 'translateY(' + this.translate.y + 'rem) translateZ(0.01rem)'; this.timelineElement.style.transform = 'translateX(' + this.translate.x + 'rem) translateZ(0.01rem)'; this.programListElement.style.transform = 'translate(' + this.translate.x + 'rem,' + this.translate.y + 'rem) translateZ(0.01rem)'; // Move left instead of translateX in order to disable OMTA and sync with // timelineElement and programListElement this.timeMarkerContainerElement.style.left = this.translate.x + 'rem'; }; /** * For every visible programs in the first column, it has to be aligned to * the left edge of visible window. If opts.setToNull is true, then the * left-padding of each program will be cleared. */ proto._setTitlePadding = function epg__setTitlePadding(opts) { var rowOffset = this.visibleChannelOffset; var size = this.visibleChannelSize; var row; var timeOffset = this.visibleTimeOffset - this.epgController.timelineOffset; var programElement; for(row = rowOffset; row < rowOffset + size; row++) { if (this.epgController.programTable[timeOffset][row]) { programElement = this.epgController.programTable[timeOffset][row].element; if (opts && opts.setToNull) { programElement.titlePadding = null; } else { programElement.titlePadding = EPG.COLUMN_WIDTH * (this.visibleTimeOffset - programElement.startTime) + 'rem'; } } } }; proto._displayProgramInfo = function epg__displayProgramInfo(column, row) { var program = this.epgController.programTable[column][row].program; this.programTitleElement.textContent = program.title; this.programDetailElement.textContent = program.description; this.programMetaElement.innerHTML = ''; var timeIntervalElement = document.createElement('LI'); var timeText = this._timeToString(program.startTime).timeWithPrefix; timeText += ' - '; timeText += this._timeToString(program.startTime + program.duration).timeWithPrefix; timeIntervalElement.textContent = timeText; this.programMetaElement.appendChild(timeIntervalElement); }; proto._updateDate = function epg__updateDate(time) { if (navigator.mozL10n.readyState === 'complete') { var now = new Date(time); var timeFormat = navigator.mozL10n.get('EPGDate'); var dtf = new navigator.mozL10n.DateTimeFormat(); this.dateElement.textContent = dtf.localeFormat(now, timeFormat); } }; proto._onUnfocus = function epg__onUnfocus(programElement) { programElement.classList.remove('focus'); }; proto._onMove = function epg__onMove(key) { this.spatialNavigator.move(key); }; EPG.ROW_HEIGHT = 11.5; EPG.COLUMN_WIDTH = 33.8; EPG.COLUMN_MARGIN = 0.3; exports.EPG = EPG; EPG.prototype = proto; })(window);