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.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

19
gaia/tv_apps/tv-epg/bower.json Executable file
View file

@ -0,0 +1,19 @@
{
"name": "tv-epg",
"version": "0.0.0",
"homepage": "https://github.com/mozilla-b2g/gaia/tree/master/tv_apps/tv-epg",
"description": "Electronic program guide",
"main": "index.html",
"license": "MPL",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"evt": "fxos-components/evt#~0.4.0"
}
}

View file

@ -0,0 +1,29 @@
{
"name": "evt",
"main": "index.js",
"version": "0.4.0",
"homepage": "https://github.com/wilsonpage/evt",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"package.json",
"test",
"tests",
"README.md"
],
"_release": "0.4.0",
"_resolution": {
"type": "version",
"tag": "v0.4.0",
"commit": "3593dd6b4dd19f683d00fd0eaf2581c11a128da6"
},
"_source": "git://github.com/gaia-components/evt.git",
"_target": "~0.4.0",
"_originalSource": "gaia-components/evt",
"_direct": true
}

View file

@ -0,0 +1,19 @@
{
"name": "evt",
"main": "index.js",
"version": "0.4.0",
"homepage": "https://github.com/wilsonpage/evt",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"package.json",
"test",
"tests",
"README.md"
]
}

View file

@ -0,0 +1,154 @@
/**
* Evt
*
* A super lightweight
* event emitter library.
*
* @version 0.3.3
* @author Wilson Page <wilson.page@me.com>
*/
;(function() {
/**
* Locals
*/
var proto = Events.prototype;
var slice = [].slice;
/**
* Creates a new event emitter
* instance, or if passed an
* object, mixes the event logic
* into it.
*
* @param {Object} obj
* @return {Object}
*/
function Events(obj) {
if (!(this instanceof Events)) return new Events(obj);
if (obj) return mixin(obj, proto);
}
/**
* Registers a callback
* with an event name.
*
* @param {String} name
* @param {Function} cb
* @return {Event}
*/
proto.on = function(name, cb) {
this._cbs = this._cbs || {};
(this._cbs[name] || (this._cbs[name] = [])).push(cb);
return this;
};
/**
* Attach a callback that once
* called, detaches itself.
*
* TODO: Implement `.off()` to work
* with `once()` callbacks.
*
* @param {String} name
* @param {Function} cb
* @public
*/
proto.once = function(name, cb) {
this.on(name, one);
function one() {
cb.apply(this, arguments);
this.off(name, one);
}
};
/**
* Removes a single callback,
* or all callbacks associated
* with the passed event name.
*
* @param {String} name
* @param {Function} cb
* @return {Event}
*/
proto.off = function(name, cb) {
this._cbs = this._cbs || {};
if (!name) { this._cbs = {}; return; }
if (!cb) { return delete this._cbs[name]; }
var cbs = this._cbs[name] || [];
var i;
while (cbs && ~(i = cbs.indexOf(cb))) { cbs.splice(i, 1); }
return this;
};
/**
* Fires an event, triggering
* all callbacks registered on this
* event name.
*
* @param {String} name
* @return {Event}
*/
proto.fire = proto.emit = function(options) {
var cbs = this._cbs = this._cbs || {};
var name = options.name || options;
var batch = (cbs[name] || []).concat(cbs['*'] || []);
var ctx = options.ctx || this;
if (batch.length) {
this._fireArgs = arguments;
var args = slice.call(arguments, 1);
while (batch.length) {
batch.shift().apply(ctx, args);
}
}
return this;
};
proto.firer = function(name) {
var self = this;
return function() {
var args = slice.call(arguments);
args.unshift(name);
self.fire.apply(self, args);
};
};
/**
* Util
*/
/**
* Mixes in the properties
* of the second object into
* the first.
*
* @param {Object} a
* @param {Object} b
* @return {Object}
*/
function mixin(a, b) {
for (var key in b) a[key] = b[key];
return a;
}
/**
* Expose 'Event' (UMD)
*/
if (typeof exports === 'object') {
module.exports = Events;
} else if (typeof define === 'function' && define.amd) {
define(function(){ return Events; });
} else {
window.evt = Events;
}
})();

109
gaia/tv_apps/tv-epg/index.html Executable file
View file

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<title>Apps</title>
<!-- stylesheets of building blocks -->
<link rel="stylesheet" type="text/css" href="shared/style/smart-screen/navigation.css">
<link rel="stylesheet" type="text/css" href="shared/style/smart-screen/icons.css">
<link rel="stylesheet" type="text/css" href="style/epg_program.css">
<link rel="stylesheet" type="text/css" href="style/tv_epg.css">
<!-- For test purpose on phone only -->
<script defer src="shared/js/gesture_detector.js"></script>
<script defer src="shared/js/smart-screen/init_gesture.js"></script>
<!-- Shared libraries -->
<script src="shared/js/l10n.js"></script>
<script src="shared/js/manifest_helper.js"></script>
<script defer src="/shared/js/l10n_date.js"></script>
<script defer src="bower_components/evt/index.js"></script>
<script defer src="shared/js/async_semaphore.js"></script>
<script defer src="shared/js/async_storage.js"></script>
<script defer src="shared/js/smart-screen/key_navigation_adapter.js"></script>
<script defer src="shared/js/smart-screen/spatial_navigator.js"></script>
<script defer src="shared/js/smart-screen/shared_utils.js"></script>
<script defer src="shared/js/smart-screen/applications.js"></script>
<script defer src="shared/js/smart-screen/piped_promise.js"></script>
<script defer src="shared/js/smart-screen/card_store.js"></script>
<script defer src="shared/js/smart-screen/cards/card.js"></script>
<script defer src="shared/js/smart-screen/cards/application.js"></script>
<script defer src="shared/js/smart-screen/cards/deck.js"></script>
<script defer src="shared/js/smart-screen/cards/folder.js"></script>
<script defer src="shared/js/smart-screen/card_manager.js"></script>
<script defer src="shared/js/smart-screen/channel_manager.js"></script>
<script defer src="shared/js/smart-screen/pin_card.js"></script>
<script defer src="shared/js/smart-screen/clock.js"></script>
<!-- building blocks -->
<!-- Specific code -->
<script defer src="js/context_menu.js"></script>
<script defer src="js/epg_program.js"></script>
<script defer src="js/epg_controller.js"></script>
<script defer src="js/epg.js"></script>
<script defer src="js/index.js"></script>
<!-- L10n properties -->
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/tv-epg.{locale}.properties"/>
<link rel="localization" href="shared/locales/date/date.{locale}.properties"/>
</head>
<body>
<article id="main-section" contextmenu="context-menu">
<section role="region" id="main-container">
<div id="top-container">
<div id="header">
<div id="label">TV Guide</div>
<div id="time"></div>
<div id="time-prefix-container">
<div id="time-prefix"></div>
</div>
</div>
<div id="epg-container">
<div id="epg-left">
<div id="date"></div>
<div id="channel-list-container">
<ul id="channel-list">
</ul>
</div>
</div>
<div id="epg-right">
<div id="time-marker-container">
<div id="time-marker"></div>
</div>
<div id="timeline-container">
<ul id="timeline">
</ul>
</div>
<div id="program-list-container">
<div id="program-list">
</div>
</div>
</div>
</div>
</div>
<div id="bottom-container">
<video id="video-thumbnail"></video>
<div id="detail-container">
<div id="program-title-container">
<div id="program-title"></div>
</div>
<ul id="program-meta">
</ul>
<div id="program-detail"></div>
<div id="progress-bar-back"></div>
<div id="progress-bar"></div>
</div>
</div>
</section>
<menu type="context" id="context-menu">
<menuitem icon="style/icons/ic_pin.png" data-l10n-id="pin-to-home" id="pin-button-contextmenu"></menuitem>
</menu>
</article>
</body>
</html>

View file

@ -0,0 +1,102 @@
/* global MozActivity, PinCard */
'use strict';
(function(exports) {
function ContextMenu(buttonConfigs, app) {
this.selfApp = app;
this.buttonConfigs = buttonConfigs;
document.addEventListener('contextmenu', this.updateMenu.bind(this));
// Get app metadata like origin and manifestURL from TVDeck for pinning
// channels to home
this.selfApp.connect('tvdeck-getmeta').then(function (ports) {
ports.forEach(function(port) {
port.postMessage('ping');
port.onmessage = function(message) {
if (message) {
this.tvDeckMeta = message.data;
this.pinCard = new PinCard(this.tvDeckMeta);
this.pinCard.on('update-pin-button', this.updateMenu.bind(this));
port.onmessage = null;
}
}.bind(this);
}.bind(this));
}.bind(this));
}
var proto = {};
proto.updateMenu = function cm_showMenu() {
var currentHash = window.location.hash;
if (this.pinCard.pinnedChannels[currentHash]) {
// Show unpin button if current channel is pinned.
this._updatePinButton('unpin-from-home', this._unpinFromHome.bind(this));
} else {
// Show pin button if current channel is not pinned yet.
this._updatePinButton('pin-to-home', this._pinToHome.bind(this));
}
};
proto._updatePinButton = function cm_updatePinButton(l10nId, onclick) {
this.buttonConfigs.forEach(function(buttonConfig) {
if (buttonConfig.element) {
buttonConfig.element.onclick = onclick;
if (buttonConfig.hasText) {
buttonConfig.element.setAttribute('data-l10n-id', l10nId);
}
}
}.bind(this));
};
proto._pinToHome = function cm__pinToHome() {
if (!this.tvDeckMeta) {
console.error('TVDeck meta not found.');
return;
}
var number = window.location.hash.split(',')[2];
/* jshint nonew:false */
new MozActivity({
name: 'pin',
data: {
type: 'Application',
group: 'tv',
name: {
id: 'channel-name',
args: {
number: number
}
},
manifestURL: this.tvDeckMeta.manifestURL,
launchURL: this.tvDeckMeta.origin + '/index.html' + window.location.hash
}
});
};
proto._unpinFromHome = function cm__unpinFromHome() {
var message = {
type: 'unpin',
data: {
manifestURL: this.tvDeckMeta.manifestURL,
launchURL: this.tvDeckMeta.origin + '/index.html' + window.location.hash
}
};
if (!this.unpinPorts) {
this.selfApp.connect('appdeck-channel').then(function (ports) {
this.unpinPorts = ports;
this.unpinPorts.forEach(function(port) {
port.postMessage(message);
});
}.bind(this));
} else {
this.unpinPorts.forEach(function(port) {
port.postMessage(message);
});
}
};
ContextMenu.prototype = proto;
exports.ContextMenu = ContextMenu;
})(window);

421
gaia/tv_apps/tv-epg/js/epg.js Executable file
View file

@ -0,0 +1,421 @@
/* 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);

View file

@ -0,0 +1,172 @@
/* global evt, Promise, ChannelManager */
'use strict';
(function(exports) {
function EPGController(timelineOffset, timelineUnit) {
this.totalTimeslotCount = 0;
this.timelineUnit = timelineUnit;
this.timelineOffset = timelineOffset;
this.channelOffset = 0;
this.channelManager = new ChannelManager();
this.programTable = [];
this.channelManager.scanTuners();
this.channelManager.on('scanned', this._onScanned.bind(this));
}
var proto = Object.create(new evt());
proto._onScanned = function epg__onScanned() {
this.programTable = [];
this.channelManager.setPlayingSource(function() {
window.location.hash = this.channelManager.currentHash;
}.bind(this));
this._createChannelList(this.channelManager.getSource().channels);
this.fire('scanned', this.channelManager.getTuner().tuner.stream);
};
/**
* fetchPrograms function fetches more programs starting from startTime to
* startTime + duration.
*/
proto.fetchPrograms = function epg_fetchPrograms(startTime, duration) {
return new Promise(function (resolve) {
this._addTimeline(startTime, duration);
this.fire('allTimelineAdded');
var fetchedChannelCount = 0;
var channelItems = this.channelManager.getSource().channels;
channelItems.forEach(function(channelItem, index) {
channelItem.channel.getPrograms({
startTime: startTime * this.timelineUnit,
duration: duration * this.timelineUnit
}).then(function(programs) {
this._addPrograms(index, programs);
fetchedChannelCount++;
if (fetchedChannelCount === channelItems.length) {
resolve();
}
}.bind(this)).catch(function(err) {
console.error(err);
});
}.bind(this));
}.bind(this));
};
/**
* Add more timestamp to timeline
*/
proto._addTimeline = function epg__addTimeline(startTime, duration) {
var time;
var i;
// Scan from left to right, append to end if the timeslot does not exist.
for(i = 0; i < duration; i++) {
time = startTime + i;
if (time > this.timelineOffset + this.programTable.length - 1) {
this.totalTimeslotCount++;
this.programTable.push({});
this.fire(
'addTimeline',
this.programTable.length - 1,
time * this.timelineUnit
);
}
}
// Scan from right to left, add to head if the timeslot does not exist.
for(i = duration - 1; i >= 0; i--) {
time = startTime + i;
if (time < this.timelineOffset) {
this.totalTimeslotCount++;
this.programTable.unshift({});
this.timelineOffset--;
this.fire('addTimeline', 0, time * this.timelineUnit);
}
}
};
/**
* Add more programs to programTable. Note that a program, from startTime to
* startTime + duration, may not perfectly lies in existing timeline interval,
* in order to easily controll the table, timeslot that goes beyond the
* interval will be filtered out.
*/
proto._addPrograms = function epg__addPrograms(row, programs) {
var column;
var startTime;
var endTime;
var isFirstProgramSlot;
var program;
var time;
var i;
// Scan programs from right to left (assume programs are sorted)
for (i = programs.length - 1; i >= 0; i--) {
program = programs[i];
isFirstProgramSlot = true;
startTime = Math.floor(program.startTime / this.timelineUnit);
endTime = startTime + Math.ceil(program.duration / this.timelineUnit);
endTime =
Math.min(endTime, this.timelineOffset + this.totalTimeslotCount);
for(time = startTime; time < endTime; time++) {
column = time - this.timelineOffset;
// filter out non-existing timeline
if (this.programTable[column]) {
// only the first timeslot within the interval will be displayed
this.programTable[column][row] = {
program: program
};
if (isFirstProgramSlot) {
this.fire('updateProgram', {
title: new Date(program.startTime).getHours() + ':' +
new Date(program.startTime).getMinutes() + ', ' +
Math.ceil(program.duration / this.timelineUnit) + ', ' +
program.title,
row: row,
column: column,
duration: endTime - time,
isVisible: true,
item: this.programTable[column][row]
});
isFirstProgramSlot = false;
} else {
// Easier to index the first visible element of a program
this.programTable[column][row].element =
this.programTable[column - 1][row].element;
this.fire('updateProgram', {
row: row,
column: column,
isVisible: false
});
}
}
}
}
};
proto.switchChannel = function epg_switchChannel(index) {
var number = this.channelManager.getSource().channels[index].channel.number;
this.channelManager.playingState.channelNumber = number;
this.channelManager.setPlayingChannel(function() {
window.location.hash = this.channelManager.currentHash;
}.bind(this));
};
proto._createChannelList = function epg__createChannelList(channels) {
var fetchedChannelCount = 0;
channels.forEach(function(channelItem, index) {
this.fire('appendChannel', channelItem.channel, index);
fetchedChannelCount++;
if (fetchedChannelCount === channels.length) {
this.fire('allChannelFetched');
}
}.bind(this));
};
exports.EPGController = EPGController;
EPGController.prototype = proto;
})(window);

View file

@ -0,0 +1,91 @@
/* global EPG */
'use strict';
window.EPGProgram = (function(exports) {
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
this._duration = 1;
this.dataset.duration = '1';
this.titleElement = document.createElement('DIV');
this.titleElement.classList.add('title');
this.appendChild(this.titleElement);
this.progressElement = document.createElement('DIV');
this.progressElement.classList.add('background-progress');
this.progressElement.style.transform = 'scaleX(0)';
this.appendChild(this.progressElement);
};
Object.defineProperty(proto, 'progress', {
set: function(time) {
if (!this.startTime || !time) {
return;
}
var scaleX = (time - this.startTime) * EPG.COLUMN_WIDTH;
scaleX = scaleX / (this.duration * EPG.COLUMN_WIDTH - EPG.COLUMN_MARGIN);
scaleX = Math.min(scaleX, 1);
this.progressElement.style.transform = 'scaleX(' + scaleX + ')';
}
});
Object.defineProperty(proto, 'startTime', {
set: function(time) {
this._startTime = time;
},
get: function() {
return this._startTime;
}
});
Object.defineProperty(proto, 'title', {
set: function(title) {
this.titleElement.textContent = title;
}
});
Object.defineProperty(proto, 'titlePadding', {
set: function(padding) {
this.titleElement.style.paddingLeft = padding;
}
});
Object.defineProperty(proto, 'duration', {
set: function(duration) {
this._duration = duration;
this.dataset.duration = duration;
},
get: function() {
return this._duration;
}
});
proto.resetProgressElement = function(time) {
if (!this.startTime || !time) {
return;
}
if (this.startTime + this.duration <= time) {
this.fillProgress();
} else {
this.progressElement.classList.add('smooth');
}
this.appendChild(this.progressElement);
};
proto.fillProgress = function() {
this.progressElement.style.transform = 'scaleX(1)';
};
proto.hide = function() {
this.classList.add('hidden');
};
proto.show = function() {
this.classList.remove('hidden');
};
return document.registerElement('epg-program', { prototype: proto });
})(window);

View file

@ -0,0 +1,9 @@
/* global EPG */
'use strict';
(function(exports) {
window.addEventListener('load', function() {
/* jshint nonew: false */
window.epg = new EPG();
});
})(window);

18
gaia/tv_apps/tv-epg/jsdoc.json Executable file
View file

@ -0,0 +1,18 @@
{
"name": "tv-epg",
"tags": {
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["tv_apps/tv-epg/js/**/*.js"]
},
"plugins": [
"plugins/markdown",
"plugins/summarize"
],
"opts": {
"private": true,
"recurse": true,
"destination": "docs/tv-epg"
}
}

View file

@ -0,0 +1,5 @@
EPGDate = %a, %h %e
pin-to-home=Pin to Home
pin-to-home.label=Pin to Home
unpin-from-home=Unpin From Home
unpin-from-home.label=Unpin From Home

View file

@ -0,0 +1,32 @@
{
"name": "EPG",
"description": "Electronic program guide",
"launch_path": "/index.html",
"type": "certified",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"locales": {
"en-US": {
"name": "Program Guide",
"description": "Electronic program guide"
}
},
"default_locale": "en-US",
"icons": {
"354": "/style/icons/ic_default_app.png"
},
"orientation": "landscape",
"permissions": {
"tv": {},
"webapps-manage": {},
"moz-extremely-unstable-and-will-change-webcomponents":{}
},
"datastores-access": {
"home_cards": {
"access": "readonly",
"description": "Read card list"
}
}
}

View file

@ -0,0 +1,85 @@
epg-program {
display: inline-block;
position: relative;
width: 33.5rem;
font-size: 2.5rem;
line-height: 10.8rem;
overflow: hidden;
padding-bottom: 0.3rem;
padding-right: 0.3rem;
border-bottom: 0.1rem #5e5e5e solid;
margin-top: 0.3rem;
background-color: #404040;
height: 11.2rem;
background-clip: content-box;
box-sizing: border-box;
outline: none;
}
epg-program:after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 0.1rem;
background-color: #5e5e5e;
height: 10.8rem;
}
epg-program.focus {
background-color: #00caf2;
}
epg-program.hidden {
display: none;
}
epg-program[data-duration="1"] {
width: 33.8rem;
}
epg-program[data-duration="2"] {
width: 67.6rem;
}
epg-program[data-duration="3"] {
width: 101.4rem;
}
epg-program[data-duration="4"] {
width: 135.2rem;
}
epg-program[data-duration="5"] {
width: 169rem;
}
epg-program[data-duration="6"] {
width: 202.8rem;
}
epg-program[data-duration="7"] {
width: 236.9rem;
}
epg-program > .title {
margin-left: 2rem;
overflow: hidden;
text-overflow: ellipsis;
width: calc(100% - 2rem);
box-sizing: border-box;
position: absolute;
}
epg-program > .background-progress {
position: absolute;
background-color: rgba(0, 0, 0, 0.2);
height: calc(100% - 0.3rem);
transform: scaleX(0);
transform-origin: 0;
width: calc(100% - 0.3rem);
}
epg-program > .background-progress.smooth {
transition: transform 0.3s;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1,321 @@
html {
font-size: 10px;
font-family: "Fira Sans";
font-weight: 100;
font-style: italic;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
color: white;
}
a {
outline: 0;
}
a:hover, a:active, a:focus {
outline: 0;
}
#main-section {
width: 100%;
height: 100%;
}
#main-container {
width: 100%;
height: 100%;
box-sizing: border-box;
}
#top-container {
box-sizing: border-box;
padding: 0 5rem 0 5rem;
background-color: #404040;
height: calc(100% - 31.8rem);
}
#header {
width: 100%;
height: 15rem;
padding-top: 4.2rem;
box-sizing: border-box;
color: #b4b4b4;
line-height: 6.8rem;
}
#label {
width: 80%;
height: 6.8rem;
display: inline-block;
font-style: italic;
font-size: 7rem;
}
#time {
float: right;
font-style: normal;
font-size: 9rem;
}
#time-prefix-container {
float: right;
font-style: normal;
font-size: 1.6rem;
font-weight: normal;
margin-right: 0.4rem;
}
#time-prefix {
vertical-align: bottom;
height: 4rem;
display: inline-block;
}
#epg-container {
font-size: 2.8rem;
height: calc(100% - 15rem);
width: 100%;
overflow: hidden;
}
#epg-left {
position: relative;
width: 46.8rem;
height: 100%;
float: left;
box-sizing: border-box;
}
#date {
width: 100%;
float: left;
height: 3.8rem;
padding-left: 1.6rem;
border-bottom: #5e5e5e 0.6rem solid;
}
#channel-list-container {
width: 100%;
float: left;
overflow: hidden;
position: relative;
height: calc(100% - 4rem);
}
#channel-list {
margin: 0;
padding: 0;
font-size: 3.4rem;
list-style: none;
transition: transform 0.3s;
background-color: #5e5e5e;
position: relative;
transform: translateZ(0.01rem);
}
#channel-list > li {
font-weight: normal;
line-height: 10.8rem;
border-bottom: 0.3rem #404040 solid;
border-top: 0.3rem #404040 solid;
border-right: 0.4rem #404040 solid;
padding-left: 1.6rem;
margin-bottom: 0.1rem;
position: relative;
}
#channel-list > li.hidden {
display: none;
}
#channel-list > li > .title {
position: absolute;
left: 11.2rem;
font-size: 3.2rem;
font-weight: 100;
}
#epg-right {
position: relative;
float: left;
width: calc(100% - 46.8rem);
height: 100%;
overflow: hidden;
}
#time-marker-container {
position: absolute;
transition: left 0.3s;
z-index: 1;
}
#time-marker {
width: 0;
height: 0;
position: relative;
border-left: 1rem solid transparent;
border-right: 1rem solid transparent;
border-top: 1.5rem solid white;
top: 3.1rem;
left: -1rem;
transition: transform 0.3s;
}
#timeline-container {
overflow: hidden;
padding-left: 2rem;
display: inline-block;
height: 3.8rem;
border-bottom: #5e5e5e 0.6rem solid;
width: 100%;
position: relative;
float: left;
}
#timeline {
list-style: none;
margin: 0;
padding: 0;
display: block;
overflow: hidden;
white-space: nowrap;
transition: transform 0.3s;
position: relative;
display: inline-block;
transform: translateZ(0.01rem);
}
#timeline > li {
display: inline-block;
width: 33.8rem;
box-sizing: border-box;
}
#program-list-container {
float: left;
width: 100%;
font-size: 0;
overflow: hidden;
position: relative;
height: calc(100% - 4.6rem);
}
#program-list {
display: inline-block;
transition: transform 0.3s;
position: relative;
transform: translateZ(0.01rem);
}
#program-list > ul {
margin: 0;
padding: 0;
list-style: none;
margin-bottom: 0.7rem;
white-space: nowrap;
font-size: 0;
display: block;
height: 10.8rem;
}
#program-list > ul.hidden {
display: none;
}
#bottom-container {
width: 100%;
box-sizing: border-box;
height: 31.8rem;
background-color: #232323;
padding: 2.8rem 5rem 2.8rem 5rem;
}
#video-thumbnail {
background-color: green;
width: 46.4rem;
height: 26.2rem;
float: left;
}
#detail-container {
float: left;
overflow: hidden;
margin-left: 2rem;
width: calc(100% - 51.4rem);
position: relative;
white-space: nowrap;
height: 100%;
}
#program-title-container {
font-size: 5rem;
color: #d4d4d4;
height: 10.6rem;
margin-bottom: 1.8rem;
position: relative;
}
#program-title {
position: absolute;
bottom: 0;
color: #d4d4d4;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
}
#program-detail {
font-size: 4rem;
overflow: hidden;
text-overflow: ellipsis;
padding-bottom: 4rem;
height: 3.8rem;
}
#program-meta {
font-size: 4rem;
list-style: none;
margin: 0;
padding: 0;
margin-bottom: 1.4rem;
height: 3.8rem;
}
#program-meta > li {
display: inline-block;
padding-right: 4.9rem;
padding-left: 5rem;
border-right: 0.1rem #5e5e5e solid;
}
#program-meta > li:first-child {
padding-left: 0;
}
#program-meta > li:last-child {
border-right: 0;
}
#progress-bar {
width: 100%;
height: 0.6rem;
position: absolute;
bottom: 0;
background: linear-gradient(135deg, rgba(0, 202, 242, 1) 25%, rgba(0, 202, 242, 1) 50%, rgba(0, 202, 242, 1) 75%, rgba(59, 59, 59, 1) 75%);
transition: transform 0.3s;
transform-origin: 0;
}
#progress-bar-back {
width: 100%;
height: 0.6rem;
position: absolute;
bottom: 0;
background-color: #3b3b3b;
}

View file

@ -0,0 +1,14 @@
{
"extends": "../../../../.jshintrc",
"node": true,
"predef": [
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"marionette",
"marionetteScriptFinished"
]
}

View file

@ -0,0 +1,129 @@
'use strict';
var APP_URL = 'app://tv-epg.gaiamobile.org';
var SHARED_PATH = __dirname + '/../../../../shared/test/integration/';
var Keys = {
'up': '\ue013',
'right': '\ue014',
'down': '\ue015',
'left': '\ue012'
};
var assert = require('chai').assert;
// Bug 1207453 - Skip the test due to unknown test enviroment issue for now.
// We should investigate the issue and re-enable the test later.
marionette.skip('Test Program Navigation', function() {
var opts = {
apps: {},
hostOptions: {
screen: {
width: 1480,
height: 960
}
}
};
var client = marionette.client({
profile: opts,
// XXX: Set this to true once Accessibility is implemented in TV
desiredCapabilities: { raisesAccessibilityExceptions: false }
});
var actions;
setup(function() {
actions = client.loader.getActions();
launchModalDialogApp();
});
function launchModalDialogApp() {
// Launch test app
client.contentScript.inject(SHARED_PATH + '/mock_tv_service.js');
client.apps.launch(APP_URL);
client.apps.switchToApp(APP_URL);
}
test('Wait for focus element', {devices: ['tv']},function() {
client.helper.waitForElement('#program-list .focus');
});
test('Key down five times', {devices: ['tv']},function() {
client.helper.waitForElement('#program-list .focus');
var body = client.helper.waitForElement('body');
var i;
for(i = 0; i < 5; i++) {
body.sendKeys(Keys.down);
}
var transform = client.helper.waitForElement('#program-list').scriptWith(
function(element) {
return element.style.transform;
});
var reg = /^translate\([\d\.\-]+(?:rem|px)?,\s*([\d\.\-]+)/i;
assert.notEqual(reg.exec(transform)[1], '0');
});
test('Key down five times and then key up five times', {devices: ['tv']},
function() {
client.helper.waitForElement('#program-list .focus');
var body = client.helper.waitForElement('body');
var i;
for(i = 0; i < 5; i++) {
body.sendKeys(Keys.down);
}
for(i = 0; i < 5; i++) {
body.sendKeys(Keys.up);
}
var transform = client.helper.waitForElement('#program-list').scriptWith(
function(element) {
return element.style.transform;
});
var reg = /^translate\([\d\.\-]+(?:rem|px)?,\s*([\d\.\-]+)/i;
assert.equal(reg.exec(transform)[1], '0');
});
test('Key left', {devices: ['tv']},function() {
client.helper.waitForElement('#program-list .focus');
var body = client.helper.waitForElement('body');
var initialTitle =
client.helper.waitForElement('#program-title').scriptWith(
function(element) {
return element.textContent;
});
body.sendKeys(Keys.left);
var transform = client.helper.waitForElement('#program-list').scriptWith(
function(element) {
return element.style.transform;
});
var reg = /^translate\(([\d\.\-]+)/i;
assert.notEqual(reg.exec(transform)[1], '0');
var newTitle = client.helper.waitForElement('#program-title').scriptWith(
function(element) {
return element.textContent;
});
assert.notEqual(initialTitle, newTitle);
});
test('Key right four times', {devices: ['tv']},function() {
client.helper.waitForElement('#program-list .focus');
var body = client.helper.waitForElement('body');
var i;
for(i = 0; i < 4; i++) {
body.sendKeys(Keys.right);
}
var transform = client.helper.waitForElement('#program-list').scriptWith(
function(element) {
return element.style.transform;
});
var reg = /^translate\(([\d\.\-]+)/i;
assert.notEqual(reg.exec(transform)[1], '0');
});
});

View file

@ -0,0 +1,18 @@
{
"extends": "../../../../.jshintrc",
"predef": [
"assert",
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"require",
"requireApp",
"sinon",
"mocha",
"loadBodyHTML"
]
}

View file

@ -0,0 +1,88 @@
'use strict';
/* jshint browser: true */
/* global ContextMenu, MocksHelper, MozActivity */
require('/shared/test/unit/mocks/mock_moz_activity.js');
require('/shared/test/unit/mocks/smart-screen/mock_pin_card.js');
require('/js/context_menu.js');
var mocksHelper = new MocksHelper([
'MozActivity',
'PinCard'
]).init();
suite('tv-epg/contextmenu', function() {
var contextmenu;
var button;
mocksHelper.attachTestHelpers();
setup(function() {
var mockPorts = [{
postMessage: function() {},
}];
button = document.createElement('DIV');
contextmenu = new ContextMenu([{
element: button,
hasText: true
}], {
origin: 'app://contextmenu.test',
manifestURL: 'app://contextmenu.test/manifest.webapp',
connect: function() {
return {
then: function(callback) {
callback(mockPorts);
if (mockPorts[0].onmessage) {
mockPorts[0].onmessage({
data: {
origin: 'app://tvdeck.test',
manifestURL: 'app://tvdeck.test/manifest.webapp'
}
});
}
}
};
}
});
});
suite('Initialization', function() {
test('Initialize members', function() {
assert.equal(contextmenu.tvDeckMeta.origin, 'app://tvdeck.test');
assert.equal(
contextmenu.tvDeckMeta.manifestURL,
'app://tvdeck.test/manifest.webapp'
);
assert.isDefined(contextmenu.pinCard);
});
});
suite('updateMenu', function() {
setup(function() {
MozActivity.mSetup();
window.location.hash = '#1,dvb-0,12';
});
teardown(function() {
window.location.hash = '';
MozActivity.mTeardown();
});
test('Pin card to home', function() {
contextmenu.updateMenu();
button.click();
assert.equal(button.getAttribute('data-l10n-id'), 'pin-to-home');
assert.equal(MozActivity.calls[0].name, 'pin');
});
test('Unpin card to home', function() {
contextmenu.pinCard.pinnedChannels = {
'#1,dvb-0,12': true
};
contextmenu.updateMenu();
button.click();
assert.equal(button.getAttribute('data-l10n-id'), 'unpin-from-home');
});
});
});

View file

@ -0,0 +1,155 @@
'use strict';
/* jshint browser: true */
/* global EPGController, MocksHelper */
require('/bower_components/evt/index.js');
require('/shared/test/unit/mocks/smart-screen/mock_channel_manager.js');
require('/js/epg_controller.js');
var mocksHelper = new MocksHelper([
'ChannelManager'
]).init();
suite('tv-epg/epg_controller', function() {
var epgController;
mocksHelper.attachTestHelpers();
setup(function() {
epgController = new EPGController(4, 30 * 60000);
});
suite('_onScanned', function() {
var fetchPrograms;
var _createChannelList;
setup(function() {
fetchPrograms = this.sinon.stub(epgController, 'fetchPrograms');
_createChannelList = this.sinon.stub(epgController, '_createChannelList');
});
test('_createChannelList should be called', function() {
epgController._onScanned();
assert.isTrue(_createChannelList.called);
});
});
suite('fetchPrograms', function() {
var mockPrograms;
setup(function() {
mockPrograms = [{
startTime: -2 * epgController.timelineUnit,
duration: 4 * epgController.timelineUnit
}, {
startTime: 2 * epgController.timelineUnit,
duration: 4 * epgController.timelineUnit
}, {
startTime: 6 * epgController.timelineUnit,
duration: 4 * epgController.timelineUnit
}];
epgController.timelineOffset = 0;
this.sinon.stub(epgController.channelManager, 'getSource').returns({
channels: [
{
channel: {
getPrograms: function() {
return {
then: function(callback) {
callback(mockPrograms);
}
};
}
}
}
]
});
});
test('Eight timeslot should be created in programTable', function() {
epgController.programTable = [];
epgController.fetchPrograms(-4, 8);
assert.equal(epgController.programTable.length, 8);
assert.equal(epgController.totalTimeslotCount, 8);
});
test('Eight timeslot should be created in programTable', function() {
epgController.programTable = [];
epgController.fetchPrograms(-4, 8);
assert.equal(epgController.programTable.length, 8);
assert.equal(epgController.totalTimeslotCount, 8);
});
test('fetchPrograms from 0 to 8', function() {
epgController.programTable = [];
epgController.fetchPrograms(0, 8);
var answers = [{
program: mockPrograms[0],
duration: 2
}, {
program: mockPrograms[0],
duration: 1
}, {
program: mockPrograms[1],
duration: 4
}, {
program: mockPrograms[1],
duration: 3
}, {
program: mockPrograms[1],
duration: 2
}, {
program: mockPrograms[1],
duration: 1
}, {
program: mockPrograms[2],
duration: 2
}, {
program: mockPrograms[2],
duration: 1
}];
var i;
for(i = 0; i < epgController.programTable.length; i++) {
assert.equal(
epgController.programTable[i][0].program, answers[i].program);
}
});
test('fetchPrograms from 0 to 2 and then -2 to 7', function() {
epgController.programTable = [];
epgController.fetchPrograms(0, 2);
epgController.fetchPrograms(-2, 7);
var answers = [{
program: mockPrograms[0],
duration: 4
}, {
program: mockPrograms[0],
duration: 3
}, {
program: mockPrograms[0],
duration: 2
}, {
program: mockPrograms[0],
duration: 1
}, {
program: mockPrograms[1],
duration: 3
}, {
program: mockPrograms[1],
duration: 2
}, {
program: mockPrograms[1],
duration: 1
}];
assert.equal(epgController.programTable.length, 7);
assert.equal(epgController.totalTimeslotCount, 7);
var i;
for(i = 0; i < epgController.programTable.length; i++) {
assert.equal(
epgController.programTable[i][0].program, answers[i].program);
}
});
});
});

View file

@ -0,0 +1,102 @@
'use strict';
/* jshint browser: true */
/* global EPGProgram */
require('/js/epg_program.js');
suite('tv-epg/epg_program', function() {
var epgProgram;
suiteSetup(function() {
window.EPG = {
COLUMN_WIDTH: 33.8,
COLUMN_MARGIN: 0.6
};
});
setup(function() {
epgProgram = new EPGProgram();
});
suite('createdCallback', function() {
test('Initialization', function() {
assert.equal(epgProgram.duration, 1);
assert.equal(epgProgram.dataset.duration, '1');
assert.isDefined(epgProgram.titleElement);
assert.isDefined(epgProgram.progressElement);
});
});
suite('resetProgressElement', function() {
setup(function() {
epgProgram.startTime = 10;
epgProgram.duration = 3;
});
test('Program has been played', function() {
epgProgram.resetProgressElement(20);
assert.equal(epgProgram.progressElement.style.transform, 'scaleX(1)');
});
test('Program is still playing', function() {
epgProgram.resetProgressElement(12);
assert.equal(epgProgram.progressElement.style.transform, 'scaleX(0)');
assert.isTrue(epgProgram.progressElement.classList.contains('smooth'));
});
});
suite('fillProgress', function() {
test('Fill color of progress element', function() {
epgProgram.fillProgress();
assert.equal(epgProgram.progressElement.style.transform, 'scaleX(1)');
});
});
suite('startTime', function() {
test('Set start time', function() {
epgProgram.startTime = 20;
assert.equal(epgProgram.startTime, 20);
});
});
suite('title', function() {
test('Set title of title element', function() {
epgProgram.title = 'title';
assert.equal(epgProgram.titleElement.textContent, 'title');
});
});
suite('setTitlePadding', function() {
test('Set padding of title element', function() {
epgProgram.titlePadding = '10rem';
assert.equal(epgProgram.titleElement.style.paddingLeft, '10rem');
});
});
suite('setDuration', function() {
test('Set duration of epgProgram', function() {
epgProgram.duration = 10;
assert.equal(epgProgram.duration, 10);
assert.equal(epgProgram.dataset.duration, '10');
});
});
suite('hide', function() {
test('Hide epgProgram', function() {
epgProgram.hide();
assert.isTrue(epgProgram.classList.contains('hidden'));
});
});
suite('show', function() {
test('Show epgProgram', function() {
epgProgram.show();
assert.isFalse(epgProgram.classList.contains('hidden'));
});
});
suiteTeardown(function() {
window.EPG = null;
});
});

View file

@ -0,0 +1,299 @@
'use strict';
/* jshint browser: true */
/* global MocksHelper, MockL10n, EPG */
require('/shared/test/unit/mocks/smart-screen/mock_spatial_navigator.js');
require('/shared/test/unit/mocks/smart-screen/mock_key_navigation_adapter.js');
require('/shared/test/unit/mocks/smart-screen/mock_clock.js');
require('/shared/test/unit/mocks/mock_l10n.js');
require('/test/unit/mock_epg_controller.js');
require('/js/epg_program.js');
require('/js/epg.js');
var mocksHelper = new MocksHelper([
'EPGController',
'Clock',
'SpatialNavigator',
'KeyNavigationAdapter'
]).init();
suite('tv-epg/epg', function() {
var realL10n;
var epg;
mocksHelper.attachTestHelpers();
function createMockElement(id, type) {
var element;
element = document.createElement(type || 'div');
element.id = id;
document.body.appendChild(element);
return element;
}
function createMockUI() {
createMockElement('time');
createMockElement('date');
createMockElement('video-thumbnail', 'video');
createMockElement('timeline');
createMockElement('time-prefix');
createMockElement('channel-list');
createMockElement('progress-bar');
createMockElement('program-list');
createMockElement('program-title');
createMockElement('program-detail');
createMockElement('time-marker-container');
createMockElement('time-marker');
}
suiteSetup(function() {
createMockUI();
});
setup(function() {
realL10n = window.navigator.mozL10n;
window.navigator.mozL10n = MockL10n;
epg = new EPG();
this.sinon.stub(epg, '_updateClock');
});
teardown(function() {
window.navigator.mozL10n = realL10n;
});
suite('_addTimeline', function() {
setup(function() {
epg.programListElement.appendChild(document.createElement('UL'));
epg.programListElement.appendChild(document.createElement('UL'));
});
teardown(function() {
epg.programListElement.innerHTML = '';
epg.timelineElement.innerHTML = '';
});
test('Add one timeslot to timeline', function() {
var time = new Date('Sat May 16 2015 19:24:55 GMT+0800 (CST)');
epg._addTimeline(0, time.getTime());
assert.equal(epg.timelineElement.children.length, 1);
assert.equal(epg.programListElement.children[0].children.length, 1);
assert.equal(epg.programListElement.children[1].children.length, 1);
});
});
suite('_appendChannel', function() {
test('Append two channels', function() {
epg._appendChannel({
name: 'ESPN',
number: '10'
});
epg._appendChannel({
name: 'ESPNHD',
number: '11'
});
assert.equal(epg.channelListElement.children.length, 2);
assert.equal(epg.programListElement.children.length, 2);
});
teardown(function() {
epg.programListElement.innerHTML = '';
epg.channelListElement.innerHTML = '';
});
});
suite('_updateProgramSlot', function() {
var programElement;
setup(function() {
var rowElement = document.createElement('UL');
programElement = document.createElement('epg-program');
rowElement.appendChild(programElement);
epg.programListElement.appendChild(rowElement);
});
teardown(function() {
epg.programListElement.innerHTML = '';
});
test('Hide column', function() {
var configs = {
row: 0,
column: 0,
title: 'program1',
isVisible: false,
duration: 10
};
epg._updateProgramSlot(configs);
assert.equal(programElement.titleElement.textContent, 'program1');
assert.isTrue(programElement.classList.contains('hidden'));
assert.equal(programElement.dataset.duration, '10');
});
test('Visible column with item', function() {
var configs = {
row: 0,
column: 0,
title: 'program1',
isVisible: true,
duration: 10
};
epg._updateProgramSlot(configs);
assert.equal(programElement.titleElement.textContent, 'program1');
assert.isFalse(programElement.classList.contains('hidden'));
assert.equal(programElement.dataset.duration, '10');
});
teardown(function() {
epg.programListElement.innerHTML = '';
});
});
suite('_allChannelFetched', function() {
setup(function() {
epg.epgController.channelOffset = 6;
});
teardown(function() {
epg.channelListElement.style.top = null;
epg.programListElement.style.top = null;
});
test('Adjust top offset', function() {
var answer = (-6 * EPG.ROW_HEIGHT) + 'rem';
epg._allChannelFetched();
assert.equal(epg.channelListElement.style.top, answer);
assert.equal(epg.programListElement.style.top, answer);
});
});
suite('_onUnfocus', function() {
test('Class focus should be removed', function() {
var element = document.createElement('DIV');
element.classList.add('focus');
epg._onUnfocus(element);
assert.isFalse(element.classList.contains('focus'));
});
});
suite('_onFocus', function() {
var fetchPrograms;
var columnElement;
var rowElement;
setup(function() {
this.sinon.stub(epg, '_setTitlePadding');
this.sinon.stub(epg, '_displayProgramInfo');
fetchPrograms = this.sinon.stub(epg.epgController, 'fetchPrograms')
.returns({
then: function() {
return {
catch: function() {}
};
}
});
columnElement = document.createElement('epg-program');
rowElement = document.createElement('DIV');
rowElement.appendChild(columnElement);
epg.visibleChannelOffset = 10;
epg.epgController.channelOffset = 10;
epg.initialTime = 10;
epg.visibleTimeOffset = 10;
});
teardown(function() {
epg.channelListElement.style.transform = null;
epg.programListElement.style.transform = null;
});
test('Move down', function() {
var channelAnswer = 'translateY(' + EPG.ROW_HEIGHT +
'rem) translateZ(0.01rem)';
var programAnswer = 'translate(0rem, ' + EPG.ROW_HEIGHT +
'rem) translateZ(0.01rem)';
rowElement.dataset.row = 9;
epg._onFocus(columnElement);
assert.equal(epg.visibleChannelOffset, 9);
assert.equal(
epg.channelListElement.style.transform, channelAnswer);
assert.equal(
epg.programListElement.style.transform, programAnswer);
});
test('Move up', function() {
var channelAnswer = 'translateY(' + (-EPG.ROW_HEIGHT) +
'rem) translateZ(0.01rem)';
var programAnswer = 'translate(0rem, ' + (-EPG.ROW_HEIGHT) +
'rem) translateZ(0.01rem)';
rowElement.dataset.row = 15;
epg._onFocus(columnElement);
assert.equal(epg.visibleChannelOffset, 11);
assert.equal(
epg.channelListElement.style.transform, channelAnswer);
assert.equal(
epg.programListElement.style.transform, programAnswer);
});
test('Move left', function() {
var timelineAnswer = 'translateX(' + (EPG.COLUMN_WIDTH) +
'rem) translateZ(0.01rem)';
var programAnswer = 'translate(' + (EPG.COLUMN_WIDTH) +
'rem, 0rem) translateZ(0.01rem)';
epg.epgController.timelineOffset = 4;
columnElement.startTime = epg.visibleTimeOffset - 1;
epg._onFocus(columnElement);
assert.equal(
epg.timelineElement.style.transform, timelineAnswer);
assert.equal(
epg.programListElement.style.transform, programAnswer);
assert.isTrue(fetchPrograms.called);
});
test('Move right', function() {
var timelineAnswer = 'translateX(' + (-EPG.COLUMN_WIDTH * 4) +
'rem) translateZ(0.01rem)';
var programAnswer = 'translate(' + (-EPG.COLUMN_WIDTH * 4) +
'rem, 0rem) translateZ(0.01rem)';
epg.epgController.timelineOffset = 14;
columnElement.startTime = epg.visibleTimeOffset + 4;
epg._onFocus(columnElement);
assert.equal(
epg.timelineElement.style.transform, timelineAnswer);
assert.equal(
epg.programListElement.style.transform, programAnswer);
assert.isTrue(fetchPrograms.called);
});
});
suite('_setTitlePadding', function() {
var programElement;
setup(function() {
programElement = document.createElement('epg-program');
programElement.startTime = 0;
epg.visibleTimeOffset = 1;
epg.epgController.timelineOffset = 0;
epg.epgController.programTable = {
1: {
0: {
element: programElement
}
}
};
});
test('Clear padding-left of title element', function() {
epg._setTitlePadding({
setToNull: true
});
assert.equal(programElement.titleElement.style.paddingLeft, '');
});
test('Set padding-left of title element', function() {
epg._setTitlePadding();
assert.equal(programElement.titleElement.style.paddingLeft, '33.8rem');
});
});
});

View file

@ -0,0 +1,34 @@
(function(exports) {
'use strict';
function MockCardManager() {
this.init = function() {
return {
then: function(callback) {}
};
};
var callbacks = {};
this.on = function(name, callback) {
callbacks[name] = callback;
};
this.mTriggerChange = function(name) {
if (callbacks[name]) {
callbacks[name]();
}
};
this.getCardList = function() {
return {
then: function(callback) {
if (this.cards) {
callback(this.cards);
}
}.bind(this)
};
};
}
exports.MockCardManager = MockCardManager;
}(window));

View file

@ -0,0 +1,22 @@
(function(exports) {
'use strict';
function MockEPGController() {
this.on = function() {};
this.fetchPrograms = function() {
var promise = {
then: function(callback) {
callback();
return promise;
},
catch: function() {
return promise;
}
};
return promise;
};
this.switchChannel = function() {};
}
exports.MockEPGController = MockEPGController;
}(window));

View file

@ -0,0 +1,45 @@
'use strict';
/* jshint browser: true */
/* global PinCard, MocksHelper */
require('/bower_components/evt/index.js');
require('/test/unit/mock_card_manager.js');
require('/shared/js/smart-screen/pin_card.js');
var mocksHelper = new MocksHelper([
'CardManager'
]).init();
suite('tv-epg/contextmenu', function() {
var pinCard;
mocksHelper.attachTestHelpers();
setup(function() {
pinCard = new PinCard({
origin: 'app://tvdeck.test',
manifestURL: 'app://tvdeck.test/manifest.webapp'
});
});
suite('Initialization', function() {
test('Initialize member variables', function() {
assert.equal(pinCard.origin, 'app://tvdeck.test');
assert.equal(pinCard.manifestURL, 'app://tvdeck.test/manifest.webapp');
});
});
suite('updatePinnedChannels', function() {
setup(function() {
pinCard.cardManager.cards = [{
launchURL: 'app://tvdeck.test/index.html#1,dvbt,12'
}, {
launchURL: 'app://player.test/index.html'
}];
pinCard.cardManager.mTriggerChange('cardlist-changed');
});
test('Channel #1,dvbt,12 should be pinned', function() {
assert.isTrue(pinCard.pinnedChannels['#1,dvbt,12']);
});
});
});

View file

@ -0,0 +1,3 @@
'use strict';
require('/shared/test/unit/mocks/mocks_helper.js');