/* global ChannelManager, KeyNavigationAdapter, SimpleKeyNavigation, asyncStorage, ContextMenu, IACHandler */ 'use strict'; (function(exports) { /** * TVDeck is the app that can watch TV channels by connecting TVManager API. * Every (tuner, source, channel) combination creates an unique URL hash. * Thus, channel switching can be done by simply changing the URL hash and * handled by onhashchange handler. */ function TVDeck() { this._pendingPorts = []; navigator.mozApps.getSelf().onsuccess = function(evt){ if (evt.target && evt.target.result) { this.selfApp = evt.target.result; // Prevent the case when _returnMeta is called before mozApps.getSelf // object returns if (this._pendingPorts.length) { this._pendingPorts.forEach(function(port) { this._sendTVDeckMeta(port); }.bind(this)); } this._pendingPorts = []; this._fetchElements(); // XXX: Currently, we do not support tuner switching asyncStorage.getItem('Last-Tuner-ID', function(tunerId) { this._init(tunerId); this.channelManager.fetchSettingFromHash(window.location.hash); this.channelManager.scanTuners(); }.bind(this)); } }.bind(this); window.addEventListener('iac-tvdeck-getmeta', this._returnMeta.bind(this)); } var proto = {}; proto._init = function td__init(tunerId) { // lastChannelId maintains number of channel-switching user did. this.lastChannelId = 0; this.channelManager = new ChannelManager(tunerId); this.channelManager.on('scanned', this.setHash.bind(this)); this.channelManager.on('error', this._showErrorState.bind(this)); // enterNumberTimeoutDelay determines the waiting time for switching TV // channel according to the number user entered. this.enterNumberTimeoutDelay = 2000; // enterNumberTimeoutId is used to record timeout function that will switch // current TV channel according to the number user entered. We have to // cancel timeout function if an user enters one more number within waiting // time, i.e. enterNumberTimeoutDelay. this.enterNumberTimeoutId = null; // panelTimeoutDelay is the timeout delay for channel panel to be hidden. this.panelTimeoutDelay = 3000; // panelTimeoutId is used to record timeout function with 3s delay that will // hide channel-panel and button-group-panel. It will be created whenever a // channel is changed. We have to cancel this timeout function when an user // switch channel very fast within 3s. this.panelTimeoutId = null; // Dependencies this.keyNavigatorAdapter = new KeyNavigationAdapter(); this.keyNavigatorAdapter.init(); this.keyNavigatorAdapter.on('move', this._onSwitch.bind(this)); this.keyNavigatorAdapter.on('enter-keyup', this._onEnter.bind(this)); this.simpleKeyNavigation = new SimpleKeyNavigation(); this.contextmenu = new ContextMenu([{ element: this.pinButtonContextmenu, hasText: true }, { element: this.pinButton, hasText: false }], this.selfApp); // Event listeners setup window.addEventListener('hashchange', this._onHashChange.bind(this)); document.addEventListener('keyup', this._onEnterNumber.bind(this)); }; proto._fetchElements = function td__fetchElements() { this.overlay = document.getElementById('overlay'); this.tvStreamElement = document.getElementById('tv-stream'); this.channelPanel = document.getElementById('channel-panel'); this.channelNumber = document.getElementById('channel-number'); this.channelTitle = document.getElementById('channel-title'); this.buttonGroupPanel = document.getElementById('button-group-panel'); this.pinButton = document.getElementById('pin-button'); this.pinButtonContextmenu = document.getElementById('pin-button-contextmenu'); this.menuButton = document.getElementById('menu-button'); this.loadingIcon = document.getElementById('loading-icon'); this.bubbleElement = document.getElementById('bubble-animation'); // Flash animation this.channelPanel.addEventListener('animationend', function(){ this.channelPanel.classList.remove('flash'); }.bind(this)); // Hide loadingIcon when animationend this.loadingIcon.addEventListener('animationend', function(){ this.loadingIcon.classList.add('hidden'); }.bind(this)); }; /** * Handles onhashchange event. If the hash is valid, then set current source * and channel to the new value. */ proto._onHashChange = function td_onHashChange() { this.lastChannelId++; var id = this.lastChannelId; var hash = window.location.hash; this.buttonGroupPanel.classList.add('hidden'); this.overlay.classList.add('visible'); this.channelPanel.classList.remove('hidden'); if (this.simpleKeyNavigation.target) { this.simpleKeyNavigation.stop(); this.channelPanel.focus(); } this.channelManager.fetchSettingFromHash(hash); // Cancel onPanelTimeout function, otherwise, panels will be hidden // after 3 seconds. clearTimeout(this.panelTimeoutId); if (!this.channelManager.getTuner()) { this.channelManager.scanTuners(); } else if (!this.channelManager.getSource()) { this.channelManager.scanSources(); } else if (!this.channelManager.getChannel()) { this.channelManager.scanChannels(); } else { this._rotateLoadingIcon(); this._updateChannelInfo(); this.channelManager.setPlayingSource(function() { // When an user swiches back and forth very fast, we only have to // handle the last hash. Since setPlayingSource is an async function, // id is used to record the last change. if (id === this.lastChannelId) { this.buttonGroupPanel.classList.remove('hidden'); this.overlay.classList.remove('visible'); this.contextmenu.updateMenu(); this.bubbleElement.play([this.pinButton, this.menuButton]); this.simpleKeyNavigation.start( [this.pinButton, this.menuButton], SimpleKeyNavigation.DIRECTION.HORIZONTAL ); this.panelTimeoutId = setTimeout(this._onPanelTimeout.bind(this), this.panelTimeoutDelay); var newStream = this.channelManager.getTuner().tuner.stream; if (!this.tvStreamElement.mozSrcObject || this.tvStreamElement.mozSrcObject.id !== newStream.id) { this.tvStreamElement.mozSrcObject = newStream; this.tvStreamElement.play(); } } }.bind(this)); } }; proto._updateChannelInfo = function td__updateChannelInfo(title, number) { this.channelTitle.textContent = title || this.channelManager.getChannel().channel.name; this.channelNumber.textContent = number || this.channelManager.playingState.channelNumber; }; proto.setHash = function td_setHash() { if (!this.channelManager.isReady) { return; } if (this.channelManager.currentHash === window.location.hash) { this._onHashChange(); } window.location.hash = this.channelManager.currentHash; }; /** * Up and down keys are for switching channels, where left and right keys are * for navigating buttons in button-group. */ proto._onSwitch = function td__onSwitch(direction) { switch(direction) { case 'up': case 'down': this.channelManager.switchChannel(direction); break; case 'left': case 'right': if (!this.panelTimeoutId) { return; } clearTimeout(this.panelTimeoutId); this.panelTimeoutId = setTimeout(this._onPanelTimeout.bind(this), this.panelTimeoutDelay); return; default: return; } clearTimeout(this.enterNumberTimeoutId); this.enterNumberTimeoutId = null; this.setHash(); }; /** * onEnterNumber handles event when an user enters a number through keyboard. * Channel will be switched after enterNumberTimeoutDelay. */ proto._onEnterNumber = function td__onEnterNumber(evt) { var number = evt.keyCode - 48; if (number >= 0 && number <= 9) { if (!this.enterNumberTimeoutId) { this.channelNumber.textContent = ''; } clearTimeout(this.enterNumberTimeoutId); clearTimeout(this.panelTimeoutId); this.enterNumberTimeoutId = setTimeout(this._onEnter.bind(this), this.enterNumberTimeoutDelay); this.channelPanel.classList.remove('hidden'); this.channelPanel.focus(); // length is limit to 4 digits if (this.channelNumber.textContent.length < 4) { this.channelNumber.textContent += number; } } }; /** * onEnter switch channels accroding to channelNumber element. It will be * triggerred either by pressing enter key or enterNumebrTimeout. */ proto._onEnter = function td__onEnter() { if (this.enterNumberTimeoutId) { clearTimeout(this.enterNumberTimeoutId); this.enterNumberTimeoutId = null; clearTimeout(this.panelTimeoutId); this.panelTimeoutId = setTimeout(this._onPanelTimeout.bind(this), this.panelTimeoutDelay); var newNumber = this.channelNumber.textContent; var newIndex = this.channelManager.getSource().channelIndexHash[newNumber]; // reset back to current channel number if the number entered // is not valid. if (newIndex !== 0 && !newIndex) { this.channelNumber.textContent = this.channelManager.playingState.channelNumber; this.channelPanel.classList.add('flash'); this.simpleKeyNavigation.focus(); return; } this.channelManager.playingState.channelNumber = newNumber; this.setHash(); } }; proto._showErrorState = function td__showErrorState() { this._updateChannelInfo('---', '--'); this.tvStreamElement.removeAttribute('src'); this.tvStreamElement.load(); }; proto._rotateLoadingIcon = function td__rotateLoadingIcon() { if (!this.loadingIcon) { return; } this.loadingIcon.classList.remove('loading-circle'); this.loadingIcon.classList.remove('hidden'); // getComputedStyle on an element somehow will not reflow pseudo elements // under it, we have to manually reflow those pseudo elements. window.getComputedStyle(this.loadingIcon).width; window.getComputedStyle(this.loadingIcon, ':before').width; window.getComputedStyle(this.loadingIcon, ':after').width; this.loadingIcon.classList.add('loading-circle'); }; proto._onPanelTimeout = function td_onPanelTimeout() { this.buttonGroupPanel.classList.add('hidden'); this.channelPanel.classList.add('hidden'); this.simpleKeyNavigation.stop(); this.channelPanel.focus(); this.panelTimeoutId = null; }; proto._returnMeta = function td__returnMeta(evt) { var port = IACHandler.getPort('tvdeck-getmeta'); if (port) { if (this.selfApp) { this._sendTVDeckMeta(port); } else { this._pendingPorts.push(port); } } }; proto._sendTVDeckMeta = function td__sendTVDeckMeta(port) { port.postMessage({ origin: this.selfApp.origin, manifestURL: this.selfApp.manifestURL }); }; exports.TVDeck = TVDeck; TVDeck.prototype = proto; })(window);