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

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