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,99 @@
/* global Section, SettingsSection, Settings, QRCode */
'use strict';
(function(exports) {
function App(id) {
Section.call(this, id);
this.addChild('big-qrcode', new Section('big-qrcode'));
this.addChild('settings-section', new SettingsSection('settings-section'));
Settings.on('changed', this._onSettingsChanged.bind(this));
}
App.prototype = Object.create(Section.prototype);
// Section's Interface
App.prototype._readyToShow = function(previousId) {
this.updateStatus();
var enabled = Settings['remote-control.enabled'];
var hasConnection = !!Settings['remote-control.server-ip'];
// If the previousId is omitted, it means that this section is shown for the
// first time by bootstrap.
if (!previousId && enabled && !hasConnection) {
this.focus('exit-button');
}
};
// Section's Interface
App.prototype._handleClick = function() {
var elem = this.getFocusedElement();
switch(elem.id) {
case 'qrcode':
this.showChildSection('big-qrcode');
break;
case 'settings-button':
case 'go-to-settings-button':
this.showChildSection('settings-section');
break;
case 'exit-button':
window.close();
break;
}
};
App.prototype._onSettingsChanged = function(name, value) {
if (name == 'remote-control.server-ip') {
this.updateIP(value);
}
this.updateStatus();
};
App.prototype.updateIP = function(ip) {
document.getElementById('ip').textContent = 'http://' + ip;
this.updateQRCode('qrcode-image', ip, 260, 260);
this.updateQRCode('big-qrcode-image', ip, 540, 540);
};
App.prototype.updateQRCode = function(elemId, ip, width, height) {
// Remove the previous one if it exists.
var div = document.getElementById(elemId);
while (div.firstChild) {
div.removeChild(div.firstChild);
}
/* jshint nonew: false */
new QRCode(elemId, {
text: 'http://' + ip + '/',
width: width,
height: height,
colorDark : '#000000',
colorLight : '#ffffff',
correctLevel : QRCode.CorrectLevel.L
});
};
App.prototype.updateStatus = function() {
var setVisible = function(elemId, visible) {
var elem = document.getElementById(elemId);
elem.classList[visible ? 'add' : 'remove']('visible');
};
var enabled = Settings['remote-control.enabled'];
var hasConnection = !!Settings['remote-control.server-ip'];
setVisible('main-area', enabled && hasConnection);
setVisible('off-line-message', enabled && !hasConnection);
setVisible('disabled-message', !enabled);
setVisible('settings-icon', enabled);
if (enabled && hasConnection) {
this.focus('qrcode');
} else {
this.focus();
}
};
exports.App = App;
}(window));

17
gaia/tv_apps/remote-control/js/bootstrap.js vendored Executable file
View file

@ -0,0 +1,17 @@
/* global App, Settings, KeyNavigationAdapter */
'use strict';
window.addEventListener('load', function onLoad() {
window.removeEventListener('load', onLoad);
var app = new App('main-section');
var keyNav = new KeyNavigationAdapter();
keyNav.init();
keyNav.on('move', app.handleMove.bind(app));
keyNav.on('enter', app.handleClick.bind(app));
keyNav.on('back', app.handleBack.bind(app));
Settings.start();
Settings.once('ready', app.show.bind(app));
});

View file

@ -0,0 +1,183 @@
/* global SpatialNavigator */
'use strict';
(function(exports) {
function Section(id) {
this.id = id;
this._dom = document.getElementById(this.id);
var focusables = this._dom.querySelectorAll('.focusable');
Array.from(focusables).forEach(function(elem) {
elem.setAttribute('tabindex', -1);
});
this._spatialNav = new SpatialNavigator(
focusables,
{
ignoreHiddenElement: true,
rememberSource: true
}
);
this._spatialNav.on('focus', (elem) => {
elem.focus();
});
this._childSections = {};
this._activeChildSectionId = '';
this._defaultFocusElement = undefined;
this._focusableElementCount = focusables.length;
}
Section.prototype = {
uninit: function() {
if (this._uninit) {
this._uninit();
}
this._defaultFocusElement = undefined;
this._activeChildSectionId = '';
this._childSections = {};
this._parentSection = undefined;
this._spatialNav = undefined;
this._dom = undefined;
},
addChild: function(id, section) {
this._childSections[id] = section;
section._parentSection = this;
},
removeChild: function(id) {
this._childSections[id]._parentSection = undefined;
delete this._childSections[id];
},
focus: function(elem) {
if (typeof elem === 'string') {
elem = document.getElementById(elem);
}
return this._spatialNav.focus(elem);
},
unfocus: function() {
return this._spatialNav.unfocus();
},
handleMove: function(direction) {
var activeChildSection = this.getActiveChildSection();
if (activeChildSection) {
activeChildSection.handleMove(direction);
} else {
if (this._handleMove) {
var nextElement = this._handleMove(direction);
if (nextElement) {
if (this._spatialNav.focus(nextElement)) {
return;
}
}
}
if (!this._spatialNav.move(direction)) {
// If failed to move, re-focus the current one.
this._spatialNav.focus();
}
}
},
handleClick: function() {
var activeChildSection = this.getActiveChildSection();
if (activeChildSection) {
activeChildSection.handleClick();
} else if (this._handleClick) {
this._handleClick();
} else if (!this._focusableElementCount) {
// If there is no focusable element in this section, just close it.
this.backToParent();
}
},
handleBack: function() {
var activeChildSection = this.getActiveChildSection();
if (activeChildSection) {
if (!activeChildSection.handleBack()) {
// Hide the child section if it doesn't handle BACK key.
this.hideChildSection();
return true;
}
} else if (this._handleBack) {
return this._handleBack();
}
// Returning false can let BACK key to be bubbled to its parent.
return false;
},
getFocusedElement: function() {
return this._spatialNav.getFocusedElement();
},
getActiveChildSection: function() {
if (!this._activeChildSectionId) {
return undefined;
}
return this._childSections[this._activeChildSectionId];
},
backToParent: function() {
if (this._parentSection) {
this._parentSection.hideChildSection();
return true;
}
return false;
},
show: function(previousId) {
document.activeElement.blur();
if (this._readyToShow) {
this._readyToShow(previousId);
}
this._dom.classList.add('visible');
this._dom.removeAttribute('aria-hidden');
this._spatialNav.focus(this._defaultFocusElement);
},
hide: function() {
this._dom.classList.remove('visible');
this._dom.setAttribute('aria-hidden', true);
return this;
},
showChildSection: function(id) {
var child = this._childSections[id];
if (child) {
this.hide();
this._childSections[id].show();
this._activeChildSectionId = id;
}
},
hideChildSection: function() {
var activeChildSection = this.getActiveChildSection();
if (activeChildSection) {
var previousId = this._activeChildSectionId;
this._activeChildSectionId = '';
activeChildSection.hide();
this.show(previousId);
}
},
setDefaultFocusElement: function(elem) {
if (typeof elem === 'string') {
elem = document.getElementById(elem);
}
if (elem) {
this._defaultFocusElement = elem;
} else {
this._defaultFocusElement = undefined;
}
}
};
exports.Section = Section;
}(window));

View file

@ -0,0 +1,80 @@
/* global SettingsListener, evt */
'use strict';
(function(exports) {
var WATCH_SETTINGS = {
'remote-control.enabled': false,
'remote-control.pairing-required': false,
'remote-control.authorized-devices': null,
'remote-control.server-ip': ''
};
var Settings = evt({
_ready: false,
_observer: [],
start: function() {
for(var name in WATCH_SETTINGS) {
this._observer[name] = this._settingHandler.bind(this, name);
SettingsListener.observe(name, WATCH_SETTINGS[name],
this._observer[name]);
}
},
stop: function() {
for(var name in WATCH_SETTINGS) {
SettingsListener.unobserve(name, this._observer[name]);
this[name] = undefined;
}
this._observer = [];
this._ready = false;
},
save: function(settings) {
return new Promise((resolve, reject) => {
var validSettings = {};
var name;
for (name in settings) {
if (name in WATCH_SETTINGS) {
validSettings[name] = settings[name];
}
}
if (!Object.keys(validSettings).length) {
reject();
}
var req = SettingsListener.getSettingsLock().set(validSettings);
req.onsuccess = () => {
for (name in validSettings) {
this._settingHandler(name, validSettings[name]);
}
resolve();
};
req.onerror = () => {
reject();
};
});
},
_settingHandler: function(name, value) {
if (this[name] !== value) {
this[name] = value;
this.fire('changed', name, value);
}
// Check whether all settings are retrieved.
if (!this._ready) {
var hasValue = (name) => {
return this[name] !== undefined;
};
if (Object.keys(WATCH_SETTINGS).every(hasValue)) {
this._ready = true;
this.fire('ready');
}
}
}
});
exports.Settings = Settings;
}(window));

View file

@ -0,0 +1,148 @@
/* global Section, Settings */
'use strict';
(function(exports) {
function SettingsSection(id) {
Section.call(this, id);
Settings.on('changed', this._onSettingsChanged.bind(this));
}
SettingsSection.prototype = Object.create(Section.prototype);
// Section's Interface
SettingsSection.prototype._readyToShow = function() {
if (Settings['remote-control.enabled']) {
if (Settings['remote-control.pairing-required']) {
this._checkedOption = 'option-pair-on';
} else {
this._checkedOption = 'option-pair-off';
}
} else {
this._checkedOption = 'option-disabled';
}
this.checkRadio(this._checkedOption);
this.setDefaultFocusElement(this._checkedOption);
this.updateClearAuthorizedDevicesButton();
};
// Section's Interface
SettingsSection.prototype._handleMove = function(direction) {
var elem = this.getFocusedElement();
if ((elem.id == 'option-pair-on' && direction == 'down') ||
(elem.id == 'option-disabled' && direction == 'up')) {
return document.getElementById('clear-authorized-devices');
}
};
// Section's Interface
SettingsSection.prototype._handleClick = function() {
if (this._pendingSave) {
return;
}
var elem = this.getFocusedElement();
switch(elem.id) {
case 'option-pair-off':
case 'option-pair-on':
case 'option-disabled':
this.checkRadio(elem.id);
break;
case 'clear-authorized-devices':
navigator.mozL10n.formatValue('clear-authorized-devices-message')
.then((string) => {
if (confirm(string)) {
this.clearAuthorizedDevices();
}
});
break;
case 'save-button':
this.saveSettings();
break;
}
};
SettingsSection.prototype._onSettingsChanged = function(name, value) {
if (name == 'remote-control.authorized-devices') {
this.updateClearAuthorizedDevicesButton();
}
};
SettingsSection.prototype.checkRadio = function(id) {
var radios = document.querySelectorAll('.option-radio');
Array.from(radios).forEach(function(radio) {
if (radio.id == id) {
radio.classList.add('checked');
} else {
radio.classList.remove('checked');
}
});
};
SettingsSection.prototype.updateClearAuthorizedDevicesButton = function() {
var authorizedDevices = Settings['remote-control.authorized-devices'];
var authorizedDevicesCount = 0;
if (authorizedDevices) {
authorizedDevicesCount = Object.keys(authorizedDevices).length;
}
var button = document.getElementById('clear-authorized-devices');
if (authorizedDevicesCount) {
button.classList.remove('disabled');
button.removeAttribute('aria-hidden');
} else {
button.classList.add('disabled');
button.setAttribute('aria-hidden', true);
}
};
SettingsSection.prototype.clearAuthorizedDevices = function() {
Settings.save({
'remote-control.authorized-devices': {}
}).then(() => {
this.updateClearAuthorizedDevicesButton();
this.focus('save-button');
});
};
SettingsSection.prototype.saveSettings = function() {
var checkedOption = document.querySelector('.option-radio.checked').id;
var callback = () => {
this._pendingSave = undefined;
this._checkedOption = undefined;
this.backToParent();
};
this._pendingSave = true;
if (checkedOption == this._checkedOption) {
callback();
return;
}
// Also clear authorized devices when user changes the mode.
var settings = {
'remote-control.authorized-devices': {}
};
switch(checkedOption) {
case 'option-pair-on':
settings['remote-control.enabled'] = true;
settings['remote-control.pairing-required'] = true;
break;
case 'option-pair-off':
settings['remote-control.enabled'] = true;
settings['remote-control.pairing-required'] = false;
break;
case 'option-disabled':
settings['remote-control.enabled'] = false;
settings['remote-control.pairing-required'] = false;
break;
}
Settings.save(settings).then(callback).catch(callback);
};
exports.SettingsSection = SettingsSection;
}(window));