VulpesOS/gaia/tv_apps/tv-deck/js/tv_deck.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

327 lines
11 KiB
JavaScript
Executable file

/* 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);