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,140 @@
/* global PanelElement */
'use strict';
(function(exports) {
// The .sjs file is located in the Gecko since it needs chrome privilege.
var AJAX_URL = 'client.sjs';
// The client will send the currently-inputed string to server automatically
// whenever a user stops typing for the specified period.
var INPUT_STRING_SYNC_PERIOD = 200; // in milliseconds
var enabled = false;
var inputStringSyncTimer = null;
function sendMessage(type, detail) {
if (!enabled) {
return;
}
var data = {
type: type,
detail: (typeof detail === 'object') ? detail : detail.toString()
};
exports.sendMessage(AJAX_URL, {
message: JSON.stringify(data)
}, function onsuccess(data) {
if (!data || !data.verified) {
enabled = false;
document.l10n.formatValue('session-expired').then(function(value) {
alert(value);
window.location.reload();
});
}
});
}
function inputStringSyncHandler() {
if (inputStringSyncTimer) {
clearTimeout(inputStringSyncTimer);
}
inputStringSyncTimer = setTimeout(function() {
inputStringSyncTimer = null;
sendMessage('textinput', {
clear: true,
string: document.getElementById('input-string').value
});
}, INPUT_STRING_SYNC_PERIOD);
}
function sendFinalInputString() {
if (inputStringSyncTimer) {
clearTimeout(inputStringSyncTimer);
inputStringSyncTimer = null;
}
var input = document.getElementById('input-string');
sendMessage('textinput', {
clear: true,
string: input.value,
keycode: 13
});
input.value = '';
}
function init() {
var input = document.getElementById('input-string');
var btnSend = document.getElementById('send-string');
input.value = '';
input.addEventListener('keydown', function(evt) {
switch(evt.keyCode) {
case 13: //Enter
setTimeout(function() {
document.activeElement.blur();
sendFinalInputString();
});
break;
case 27: //Escape
input.value = '';
// Workaround Firefox's bug
input.blur();
input.focus();
break;
default:
return;
}
evt.preventDefault();
});
input.addEventListener('focus', function() {
// The "select()" doesn't work if it's triggered in a "focus" handler.
setTimeout(function() {
input.select();
});
});
input.addEventListener('input', inputStringSyncHandler);
input.addEventListener('keyup', inputStringSyncHandler);
btnSend.addEventListener('click', sendFinalInputString);
/* jshint nonew: false */
new PanelElement(document.getElementById('touch-panel'), {
touchingClass: 'touching',
dblClickTimeThreshold: 0,
handler: sendMessage
});
/* jshint nonew: false */
new PanelElement(document.getElementById('scroll-panel'), {
touchingClass: 'touching',
dblClickTimeThreshold: 0,
clickTimeThreshold: 0,
clickMoveThreshold: 0,
handler: function(type, detail) {
if (detail.dx !== undefined) {
detail.dx = 0;
}
sendMessage(type.replace('touch', 'scroll'), detail);
}
});
var buttonOnClick = function() {
var key = this.dataset.key;
if (key) {
sendMessage('keypress', key);
}
};
var buttons = document.querySelectorAll('#section-buttons .button');
[].slice.call(buttons).forEach(function(elem) {
elem.addEventListener('click', buttonOnClick);
});
enabled = true;
}
exports.ready(init);
}(window));

View file

@ -0,0 +1,95 @@
'use strict';
(function(exports) {
var DEBUG = false;
// Polyfill for Safari
if (!navigator.languages) {
var language = navigator.language.split('-', 2);
if (language[1].length == 2) {
language = language[0].toLowerCase() + '-' + language[1].toUpperCase();
} else {
language = language[0].toLowerCase() + '-' +
language[1].charAt(0).toUpperCase() +
language[1].substring(1).toLowerCase();
}
navigator.languages = [language];
}
exports.ready = function(handler) {
var handled = false;
function wrapper() {
if (!handled) {
handled = true;
document.removeEventListener('DOMContentLoaded', wrapper);
document.removeEventListener('load', wrapper);
handler();
}
}
document.addEventListener('DOMContentLoaded', wrapper);
document.addEventListener('load', wrapper);
};
exports.sendMessage = function(url, data, success, error) {
if (typeof data !== 'object') {
data = {};
}
var param = [];
for (var name in data) {
param.push(encodeURIComponent(name) + '=' +
encodeURIComponent(data[name]));
}
var paramString = param.length ? '?' + param.join('&') : '';
var finalURL = encodeURI(url) + paramString;
if (DEBUG) {
console.log('Ajax URL: ' + finalURL);
}
try {
var xhr = new XMLHttpRequest();
xhr.open('GET', finalURL, true);
xhr.onload = function() {
if (xhr.status === 200) {
var data = xhr.responseText;
if (DEBUG) {
console.log('Ajax response: ' + data);
}
if (success) {
success(data ? JSON.parse(data) : undefined);
}
} else {
if (DEBUG) {
console.error('Ajax error: ' + xhr.status);
}
if (error) {
error(xhr.status);
}
}
};
xhr.send();
} catch(err) {
if (DEBUG) {
console.error('Ajax error: ' + err.name);
}
if (error) {
error(err.name);
}
}
};
exports.setCookie = function(key, value, expires, path) {
var cookie = [];
cookie.push(encodeURIComponent(String(key)) + '=' +
encodeURIComponent(String(value)));
if (typeof expires === 'number') {
cookie.push(new Date(Date.now() + expires * 864e+5).toUTCString());
}
if (path) {
cookie.push('path=' + encodeURI(path));
}
document.cookie = cookie.join('; ');
};
}(window));

View file

@ -0,0 +1,250 @@
'use strict';
(function(exports) {
// The .sjs file is located in the Gecko since it needs chrome privilege.
var AJAX_URL = 'pairing.sjs';
function init() {
var btnSubmit = document.getElementById('connect');
var btnRestartPairing = document.getElementById('restart-pairing');
var pinCodeInput = new PinCodeInput(
document.getElementById('input-mask'),
[].slice.call(document.querySelectorAll('.code')),
btnSubmit
);
btnRestartPairing.addEventListener('click', function() {
window.location.reload();
});
btnSubmit.disabled = false;
btnSubmit.addEventListener('click', function(evt) {
var pincode = pinCodeInput.getCodes();
if (pincode === '') {
pinCodeInput.highlightEmptyCode();
return;
}
btnSubmit.disabled = true;
var onerror = function(message) {
pinCodeInput.empty();
btnSubmit.disabled = false;
showMessage(message, true);
};
exports.sendMessage(
AJAX_URL,
{
message: JSON.stringify({ pincode: pincode })
},
function success(data) {
if (data) {
if (data.verified) {
document.l10n.formatValue('connect-success')
.then(function(value) {
showMessage(value);
// The cookie will be used by server via http header.
exports.setCookie('uuid', data.uuid);
setTimeout(function() {
// Server will help redirecting to client.html when there is
// a UUID in cookie.
window.location.reload();
}, 1000);
});
} else if (data.reason == 'expired') {
document.l10n.formatValue('pin-code-expired-message')
.then(onerror);
document.getElementById('pairing-container')
.classList.add('pin-code-expired');
} else {
document.l10n.formatValue('wrong-pin').then(onerror);
}
} else {
document.l10n.formatValue('connect-error-invalid-response')
.then(onerror);
}
},
function error(status) {
document.l10n.formatValue('connect-error', {status: String(status)})
.then(onerror);
}
);
});
}
function showMessage(message, isError) {
var divMessage = document.getElementById('pairing-message');
divMessage.textContent = message;
divMessage.classList[isError ? 'add' : 'remove']('error');
}
function PinCodeInput(mask, codes, submitButton) {
var self = this;
self._mask = mask;
self._codes = codes;
self._submitButton = submitButton;
self._index = -1;
var createHandler = function(index) {
return function(evt) {
self.showMask(index);
evt.preventDefault();
};
};
for(var i = 0; i < codes.length; i++) {
var handler = createHandler(i);
codes[i].addEventListener('focus', handler);
codes[i].addEventListener('click', handler);
}
mask.addEventListener('blur', function() {
self.hideMask();
});
mask.addEventListener('keydown', function(evt) {
self.onKeyDown(evt);
});
}
PinCodeInput.prototype = {
showMask: function(index) {
this._mask.blur();
this._mask.classList.add('visible');
this.moveMask(index);
this._mask.focus();
},
hideMask: function() {
this.writeBack();
this._mask.blur();
this._mask.classList.remove('visible');
},
moveMask: function(index) {
this._index = index;
this._mask.style.left = this._codes[index].offsetLeft + 'px';
this._mask.value = this._codes[index].textContent;
this._mask.select();
},
moveNext: function() {
if (this._index < this._codes.length - 1) {
this.moveMask(this._index + 1);
return true;
}
return false;
},
movePrev: function() {
if (this._index > 0) {
this.moveMask(this._index - 1);
return true;
}
return false;
},
empty: function() {
for (var i = 0; i < this._codes.length; i++) {
this.setCode(i, '');
}
},
writeBack: function() {
this.setCode(this._index, this._mask.value);
},
setCode: function(index, value) {
if (value.length) {
this._codes[index].classList.remove('placeholder');
this._codes[index].textContent = value.charAt(0);
} else {
this._codes[index].classList.add('placeholder');
this._codes[index].textContent = '';
}
},
getCodes: function() {
var code = '';
for (var i = 0; i < this._codes.length; i++) {
if (this._codes[i].textContent !== '') {
code += this._codes[i].textContent;
} else {
return '';
}
}
return code;
},
highlightEmptyCode: function() {
for (var i = 0; i < this._codes.length; i++) {
if (this._codes[i].textContent === '') {
this.showMask(i);
return true;
}
}
return false;
},
onKeyDown: function(evt) {
var c = evt.keyCode;
if (evt.ctrlKey || evt.altKey || evt.metaKey) {
return;
}
// backspace to delete the previous field if current one is empty.
if (c == 8 && this._mask.value === '') {
if (this._index > 0) {
this.writeBack();
this.setCode(this._index - 1, '');
this.movePrev();
} else {
this.hideMask();
}
evt.preventDefault();
return;
}
// numbers
if (!evt.shiftKey && ((c >= 48 && c <= 57) || (c >= 96 && c <= 105))) {
showMessage('');
var num = String.fromCharCode(c >= 96 ? c - 96 + 48 : c);
this._mask.value = num;
this.writeBack();
if (!this.moveNext()) {
this._submitButton.focus();
}
evt.preventDefault();
return;
}
// tab
if (c == 9) {
this.writeBack();
if (evt.shiftKey) {
if (!this.movePrev()) {
this.hideMask();
}
} else {
if (!this.moveNext()) {
this._submitButton.focus();
}
}
evt.preventDefault();
return;
}
// allowed function keys: backspace, shift, del and F5.
if ([8, 16, 46, 116].indexOf(c) < 0) {
evt.preventDefault();
return;
}
}
};
exports.ready(init);
}(window));

View file

@ -0,0 +1,323 @@
'use strict';
(function(exports) {
var DEBUG = false;
var defaultOptions = {
touchReportPeriod: 60, // milliseconds
dblClickTimeThreshold: 250, // milliseconds
clickTimeThreshold: 200, // milliseconds
clickMoveThreshold: 10, // pixels
swipeMoveThreshold: 25, // pixels
touchingClass: null // CSS class
};
function PanelElement(elem, options) {
if (typeof options === 'function') {
options = {
handler: options
};
} else if (!options || typeof options !== 'object') {
options = {};
}
this.options = {};
for (var key in defaultOptions) {
this.options[key] = (typeof options[key] === 'undefined') ?
defaultOptions[key] : options[key];
}
if (typeof options.handler === 'function') {
this._handler = options.handler.bind(elem);
} else {
this._handler = function(type, detail) {};
}
this._element = elem;
this._hasMouseDown = false;
this._eventIdentifier = null;
this._startX = 0;
this._startY = 0;
this._prevDx = null;
this._prevDy = null;
this._startTime = 0;
this._waitForClickTimer = null;
this._pendingClickTimer = null;
this._reportTimer = null;
this._pendingMoveEvent = null;
this._touchIdentifier = null;
this._panelX = 0;
this._panelY = 0;
this._panelWidth = 0;
this._panelHeight = 0;
this._element.addEventListener('mousedown', this);
window.addEventListener('mousemove', this);
window.addEventListener('mouseup', this);
this._element.addEventListener('touchstart', this);
this._element.addEventListener('touchmove', this);
this._element.addEventListener('touchend', this);
window.addEventListener('resize', this);
this.updatePanelInfo();
}
PanelElement.prototype = {
uninit: function() {
window.removeEventListener('resize', this);
this._element.removeEventListener('touchend', this);
this._element.removeEventListener('touchmove', this);
this._element.removeEventListener('touchstart', this);
window.removeEventListener('mouseup', this);
window.removeEventListener('mousemove', this);
this._element.removeEventListener('mousedown', this);
this._element = null;
this._handler = null;
this.options = null;
},
updatePanelInfo: function() {
var rect = this._element.getBoundingClientRect();
this._panelX = Math.round(rect.left);
this._panelY = Math.round(rect.top);
this._panelWidth = Math.round(rect.width);
this._panelHeight = Math.round(rect.height);
},
handleEvent: function(evt) {
var touches, touch;
switch(evt.type) {
case 'resize':
this.updatePanelInfo();
break;
case 'mousedown':
if (evt.button === 0) {
this._hasMouseDown = true;
this.onStart(evt.clientX, evt.clientY);
evt.preventDefault();
}
break;
case 'mousemove':
if (this._hasMouseDown) {
this.onMove(evt.clientX, evt.clientY);
evt.preventDefault();
}
break;
case 'mouseup':
if (this._hasMouseDown && evt.button === 0) {
this._hasMouseDown = false;
this.onEnd(evt.clientX, evt.clientY);
evt.preventDefault();
}
break;
case 'touchstart':
if (this._touchIdentifier !== null) {
evt.preventDefault();
return;
}
touches = evt.changedTouches;
if (touches.length == 1) {
touch = touches[0];
this._touchIdentifier = touch.identifier;
this.onStart(touch.pageX - this._panelX,
touch.pageY - this._panelY);
evt.preventDefault();
}
break;
case 'touchmove':
case 'touchend':
touches = evt.changedTouches;
touch = null;
for(var i = 0; i < touches.length; i++) {
if (touches[i].identifier == this._touchIdentifier) {
touch = touches[i];
break;
}
}
if (touch) {
if (evt.type == 'touchend') {
this._touchIdentifier = null;
this.onEnd(touch.pageX - this._panelX,
touch.pageY - this._panelY);
} else {
this.onMove(touch.pageX - this._panelX,
touch.pageY - this._panelY);
}
}
evt.preventDefault();
break;
}
},
onStart: function(x, y) {
document.activeElement.blur();
if (this.options.touchingClass) {
this._element.classList.add(this.options.touchingClass);
}
this._startX = x;
this._startY = y;
this._startTime = Date.now();
var handleTouchStart = function() {
this._waitForClickTimer = null;
this.handleTouch('touchstart', 0, 0);
}.bind(this);
if (this.options.clickTimeThreshold) {
this._waitForClickTimer = setTimeout(handleTouchStart,
this.options.clickTimeThreshold);
} else {
handleTouchStart();
}
},
onMove: function(x, y) {
var dx = x - this._startX;
var dy = y - this._startY;
if (this._waitForClickTimer) {
var clickMoveThreshold = this.options.clickMoveThreshold;
if (Math.abs(dx) <= clickMoveThreshold &&
Math.abs(dy) <= clickMoveThreshold) {
return;
}
clearTimeout(this._waitForClickTimer);
this._waitForClickTimer = null;
this.handleTouch('touchstart', 0, 0);
}
this.handleTouch('touchmove', dx, dy);
},
onEnd: function(x, y) {
var dx = x - this._startX;
var dy = y - this._startY;
if (this._waitForClickTimer) {
clearTimeout(this._waitForClickTimer);
this._waitForClickTimer = null;
if (this.options.dblClickTimeThreshold) {
if (this._pendingClickTimer) {
clearTimeout(this._pendingClickTimer);
this._pendingClickTimer = null;
this.handleTouch('dblclick');
} else {
this._pendingClickTimer = setTimeout(function() {
this._pendingClickTimer = null;
this.handleTouch('click');
}.bind(this), this.options.dblClickTimeThreshold);
}
} else {
this.handleTouch('click');
}
} else {
var direction;
var distance = Math.round(Math.sqrt(dx * dx + dy * dy));
if (distance >= this.options.swipeMoveThreshold) {
var angle = Math.atan2(dy, dx) * 180 / Math.PI;
if (angle < 0) {
angle += 360;
}
if (angle >= 315 || angle < 45) {
direction = 'right';
} else if (angle >= 45 && angle < 135) {
direction = 'down';
} else if (angle >= 135 && angle < 225) {
direction = 'left';
} else if (angle >= 225 && angle < 315) {
direction = 'up';
}
}
this.handleTouch('touchend', dx, dy, direction);
}
if (this.options.touchingClass) {
this._element.classList.remove(this.options.touchingClass);
}
},
handleTouch: function(type, dx, dy, swipe) {
if (DEBUG) {
console.log('[PanelElement] handling ' + type);
}
switch (type) {
case 'touchstart':
this._prevDx = undefined;
this._prevDy = undefined;
// A simple unique identifier for server to distinguish touch events
// between different clients.
this._eventIdentifier =
String(Date.now()) + String(Math.floor(Math.random() * 100));
this._handler(type, {
width: this._panelWidth,
height: this._panelHeight,
identifier: this._eventIdentifier
});
this._reportTimer = setInterval(function() {
if (this._pendingMoveEvent) {
this._handler(
this._pendingMoveEvent.type,
this._pendingMoveEvent.detail
);
this._pendingMoveEvent = null;
}
}.bind(this), this.options.touchReportPeriod);
break;
case 'touchmove':
if (dx === this._prevDx && dy === this._prevDy) {
return;
}
this._prevDx = dx;
this._prevDy = dy;
this._pendingMoveEvent = {
type: type,
detail: {
dx: dx,
dy: dy,
identifier: this._eventIdentifier,
duration: Date.now() - this._startTime
}
};
break;
case 'touchend':
this._pendingMoveEvent = null;
clearInterval(this._reportTimer);
this._reportTimer = null;
this._handler(type, {
dx: dx,
dy: dy,
identifier: this._eventIdentifier,
duration: Date.now() - this._startTime,
swipe: swipe
});
this._eventIdentifier = null;
break;
case 'click':
case 'dblclick':
this._handler(type, {});
break;
}
}
};
exports.PanelElement = PanelElement;
}(window));