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,180 @@
/* globals indexedDB */
/**
* This file defines an asynchronous version of the localStorage API, backed by
* an IndexedDB database. It creates a global asyncStorage object that has
* methods like the localStorage object.
*
* To store a value use setItem:
*
* asyncStorage.setItem('key', 'value');
*
* If you want confirmation that the value has been stored, pass a callback
* function as the third argument:
*
* asyncStorage.setItem('key', 'newvalue', function() {
* console.log('new value stored');
* });
*
* To read a value, call getItem(), but note that you must supply a callback
* function that the value will be passed to asynchronously:
*
* asyncStorage.getItem('key', function(value) {
* console.log('The value of key is:', value);
* });
*
* Note that unlike localStorage, asyncStorage does not allow you to store and
* retrieve values by setting and querying properties directly. You cannot just
* write asyncStorage.key; you have to explicitly call setItem() or getItem().
*
* removeItem(), clear(), length(), and key() are like the same-named methods of
* localStorage, but, like getItem() and setItem() they take a callback
* argument.
*
* The asynchronous nature of getItem() makes it tricky to retrieve multiple
* values. But unlike localStorage, asyncStorage does not require the values you
* store to be strings. So if you need to save multiple values and want to
* retrieve them together, in a single asynchronous operation, just group the
* values into a single object. The properties of this object may not include
* DOM elements, but they may include things like Blobs and typed arrays.
*
* Unit tests are in apps/gallery/test/unit/asyncStorage_test.js
*/
this.asyncStorage = (function() {
'use strict';
var DBNAME = 'asyncStorage';
var DBVERSION = 1;
var STORENAME = 'keyvaluepairs';
var db = null;
function withDatabase(f) {
if (db) {
f();
} else {
var openreq = indexedDB.open(DBNAME, DBVERSION);
openreq.onerror = function withStoreOnError() {
console.error('asyncStorage: can\'t open database:',
openreq.error.name);
};
openreq.onupgradeneeded = function withStoreOnUpgradeNeeded() {
// First time setup: create an empty object store
openreq.result.createObjectStore(STORENAME);
};
openreq.onsuccess = function withStoreOnSuccess() {
db = openreq.result;
f();
};
}
}
function withStore(type, callback, oncomplete) {
withDatabase(function() {
var transaction = db.transaction(STORENAME, type);
if (oncomplete) {
transaction.oncomplete = oncomplete;
}
callback(transaction.objectStore(STORENAME));
});
}
function getItem(key, callback) {
var req;
withStore('readonly', function getItemBody(store) {
req = store.get(key);
req.onerror = function getItemOnError() {
console.error('Error in asyncStorage.getItem(): ', req.error.name);
};
}, function onComplete() {
var value = req.result;
if (value === undefined) {
value = null;
}
callback(value);
});
}
function setItem(key, value, callback) {
withStore('readwrite', function setItemBody(store) {
var req = store.put(value, key);
req.onerror = function setItemOnError() {
console.error('Error in asyncStorage.setItem(): ', req.error.name);
};
}, callback);
}
function removeItem(key, callback) {
withStore('readwrite', function removeItemBody(store) {
var req = store.delete(key);
req.onerror = function removeItemOnError() {
console.error('Error in asyncStorage.removeItem(): ', req.error.name);
};
}, callback);
}
function clear(callback) {
withStore('readwrite', function clearBody(store) {
var req = store.clear();
req.onerror = function clearOnError() {
console.error('Error in asyncStorage.clear(): ', req.error.name);
};
}, callback);
}
function length(callback) {
var req;
withStore('readonly', function lengthBody(store) {
req = store.count();
req.onerror = function lengthOnError() {
console.error('Error in asyncStorage.length(): ', req.error.name);
};
}, function onComplete() {
callback(req.result);
});
}
function key(n, callback) {
if (n < 0) {
callback(null);
return;
}
var req;
withStore('readonly', function keyBody(store) {
var advanced = false;
req = store.openCursor();
req.onsuccess = function keyOnSuccess() {
var cursor = req.result;
if (!cursor) {
// this means there weren't enough keys
return;
}
if (n === 0 || advanced) {
// Either 1) we have the first key, return it if that's what they
// wanted, or 2) we've got the nth key.
return;
}
// Otherwise, ask the cursor to skip ahead n records
advanced = true;
cursor.advance(n);
};
req.onerror = function keyOnError() {
console.error('Error in asyncStorage.key(): ', req.error.name);
};
}, function onComplete() {
var cursor = req.result;
callback(cursor ? cursor.key : null);
});
}
return {
getItem: getItem,
setItem: setItem,
removeItem: removeItem,
clear: clear,
length: length,
key: key
};
}());

View file

@ -0,0 +1,322 @@
/* exported BluetoothHelper */
'use strict';
var BluetoothHelper = function() {
var profiles = {
'HFP': 0x111E,
'A2DP': 0x110D
};
var _bluetooth = window.navigator.mozBluetooth;
var _isReady = false;
var _callbacks = [];
var _adapter = null;
var _oldAdapter = null; // The varaible is accessed for v1 only.
var _v2 = true;
var _ready = function(callback) {
if (!callback || !_bluetooth) {
return;
}
if (_isReady) {
callback();
} else {
_callbacks.push(callback);
}
};
var _handleRequest = function(request, callback, errorcb) {
request.onsuccess = function() {
if (callback) {
callback(request.result);
}
};
request.onerror = function() {
console.log('Error handling bluetooth request');
if (errorcb) {
errorcb();
}
};
};
// run callbacks when adapter is ready
var _processCallbacks = function() {
if (_adapter) {
_isReady = true;
_callbacks.forEach(function(callback) {
callback();
});
// clean up the _callback queue
_callbacks = [];
} else {
// We can do nothing without default adapter.
console.log('BluetoothHelper(): connot get default adapter yet');
}
};
// API v2 get adapter via bluetooth
var _fetchAdapterV2 = function() {
// need time to get bluetooth adapter at first run
_bluetooth.onattributechanged = function onManagerAttributeChanged(evt) {
for (var i in evt.attrs) {
switch (evt.attrs[i]) {
case 'defaultAdapter':
console.log('defaultAdapter changed. address:',
_bluetooth.defaultAdapter.address);
_adapter = _bluetooth.defaultAdapter;
_processCallbacks();
break;
default:
break;
}
}
};
_adapter = _bluetooth.defaultAdapter;
if (_adapter) {
_processCallbacks();
}
};
// API v1 get adapter via bluetooth
var _fetchAdapter = function() {
var req = _bluetooth.getDefaultAdapter();
if (req) {
req.onsuccess = function() {
if (_adapter) {
_oldAdapter = _adapter;
}
_isReady = true;
_adapter = req.result;
// Put the callback function of onpairedstatuschanged to the new adapter
// because the new adapter won't remember those callback function which
// is registered before. In other word, we get a new adpater after
// turned on/off Bluetooth. The new adapter have no registered callback.
if (_oldAdapter && _oldAdapter.onpairedstatuschanged) {
_adapter.onpairedstatuschanged = _oldAdapter.onpairedstatuschanged;
}
_callbacks.forEach(function(callback) {
callback();
});
// clean up the _callback queue
_callbacks = [];
};
req.onerror = function() {
// We can do nothing without default adapter.
console.log('BluetoothHelper(): connot get default adapter!!!');
};
}
};
var _getAdapter = function() {
if (_v2) {
_fetchAdapterV2();
} else {
_fetchAdapter();
}
};
var _resetAdapter = function() {
if (_adapter) {
_oldAdapter = _adapter;
}
// clean up state and adapter
_isReady = false;
_adapter = null;
};
// init
if (_bluetooth) {
// detect API version
if (typeof(_bluetooth.onattributechanged) === 'undefined') {
_v2 = false;
}
if (_v2) {
_bluetooth.onadapteradded = function onAdapterAdded(evt) {
_getAdapter();
};
} else {
_bluetooth.addEventListener('enabled', _getAdapter);
_bluetooth.addEventListener('adapteradded', _getAdapter);
_bluetooth.addEventListener('disabled', _resetAdapter);
}
_getAdapter();
}
return {
profiles: profiles,
answerWaitingCall: function() {
_ready(function() {
_adapter.answerWaitingCall();
});
},
ignoreWaitingCall: function() {
_ready(function() {
_adapter.ignoreWaitingCall();
});
},
toggleCalls: function() {
_ready(function() {
_adapter.toggleCalls();
});
},
getConnectedDevicesByProfile: function(profileID, cb) {
_ready(function() {
_handleRequest(_adapter.getConnectedDevices(profileID), cb);
});
},
connectSco: function(cb) {
_ready(function() {
_handleRequest(_adapter.connectSco(), cb);
});
},
disconnectSco: function(cb) {
_ready(function() {
_handleRequest(_adapter.disconnectSco(), cb);
});
},
getPairedDevices: function(cb) {
_ready(function() {
_handleRequest(_adapter.getPairedDevices(), cb);
});
},
getAddress: function(cb) {
if (_v2) {
console.log('getAddress function is deprecated');
return;
}
_ready(function() {
var address = _adapter.address;
cb(address);
});
},
setPairingConfirmation: function(address, confirmed) {
if (_v2) {
console.log('setPairingConfirmation API is deprecated');
return;
}
_ready(function() {
_adapter.setPairingConfirmation(address, confirmed);
});
},
setPinCode: function(address, pincode) {
if (_v2) {
console.log('setPairingConfirmation API is deprecated');
return;
}
_ready(function() {
_adapter.setPinCode(address, pincode);
});
},
setPasskey: function(address, key) {
if (_v2) {
console.log('setPairingConfirmation API is deprecated');
return;
}
_ready(function() {
_adapter.setPasskey(address, key);
});
},
isScoConnected: function(cb, errorcb) {
_ready(function() {
_handleRequest(_adapter.isScoConnected(), cb, errorcb);
});
},
sendMediaMetaData: function(metadata, cb, errorcb) {
_ready(function() {
_handleRequest(_adapter.sendMediaMetaData(metadata), cb, errorcb);
});
},
sendMediaPlayStatus: function(metadata, cb, errorcb) {
_ready(function() {
_handleRequest(_adapter.sendMediaPlayStatus(metadata), cb, errorcb);
});
},
set onhfpstatuschanged(callback) {
_ready(function() {
_adapter.onhfpstatuschanged = callback;
});
},
set onscostatuschanged(callback) {
_ready(function() {
_adapter.onscostatuschanged = callback;
});
},
set ona2dpstatuschanged(callback) {
_ready(function() {
_adapter.ona2dpstatuschanged = callback;
});
},
set onrequestmediaplaystatus(callback) {
_ready(function() {
_adapter.onrequestmediaplaystatus = callback;
});
},
set onpairedstatuschanged(callback) {
if (_v2) {
console.log('onpairedstatuschanged API is deprecated');
return;
}
_ready(function() {
_adapter.onpairedstatuschanged = callback;
});
},
v2: _v2, // expose API version for app reference
// bypass the enable/disable state if works in APIv1
enable: function() {
if (_v2) {
_ready(function() {
_adapter.enable();
});
} else {
console.log('enable is not support in v1 API!');
}
},
disable: function() {
if (_v2) {
_ready(function() {
_adapter.disable();
});
} else {
console.log('disable is not support in v1 API!');
}
}
};
};

View file

@ -0,0 +1,572 @@
/*
* This shared module defines a single ImageUtils object in the global scope.
*
* ImageUtils.getSizeAndType() takes a Blob as its argument and
* returns a Promise. If the promise resolves, the resolve method is
* passed an object with width, height, and type properties. Width and
* height specify the width and height of the image in pixels. Type
* specifies the MIME type of the image. If the promise is rejected,
* the reject method will be passed a string error message.
*
* Supported image types are JPEG, PNG, GIF and BMP. The MIME type
* strings returned when a promise resolves are "image/jpeg",
* "image/png", "image/gif", and "image/bmp".
*
* If you pass a blob that holds an image of some other type (or a
* blob that is not an image at all) then promise will be rejected.
*
* getSizeAndType() does not decode the image to determine its size
* because can require large amounts of memory. Instead, it parses the
* image file to determine its size and typically only needs to read a
* prefix of the file into memory.
*
* getSizeAndType() differs from the getImageSize() function defined
* in shared/js/media/immage_size.js. That function is similar but
* also parses EXIF metadata for JPEG images and is only suitable if
* you need to access EXIF data.
*/
(function(exports) {
'use strict';
/* global Promise */
var ImageUtils = exports.ImageUtils = {};
// Define constants for the MIME types we use
const JPEG = 'image/jpeg';
const PNG = 'image/png';
const GIF = 'image/gif';
const BMP = 'image/bmp';
// Export them
ImageUtils.JPEG = JPEG;
ImageUtils.PNG = PNG;
ImageUtils.GIF = GIF;
ImageUtils.BMP = BMP;
ImageUtils.getSizeAndType = function getSizeAndType(imageBlob) {
if (!(imageBlob instanceof Blob)) {
console.log('not blob');
return Promise.reject(new TypeError('argument is not a Blob'));
}
return new Promise(function(resolve, reject) {
if (imageBlob.size <= 16) {
reject('corrupt image file');
return;
}
// Start off assuming that we'll read the first 32kb of the file
var bytesToRead = 32 * 1024;
// But if the blob indicates that this is a PNG, GIF, or BMP, then
// we can read less since the size information is always close to the
// start of the file for those formats.
if (imageBlob.type === PNG ||
imageBlob.type === GIF ||
imageBlob.type === BMP) {
bytesToRead = 512;
}
// Try to find the information we want in the first bytes of the file
findSizeAndType(imageBlob, bytesToRead, success, tryagain);
// If we got the size and type data, resolve the promise
function success(data) {
resolve(data);
}
// If we didn't find it, try again for JPEG files
function tryagain(data) {
// If we were able to verify that this blob is a jpeg file
// then we will try again because in JPEG files, the size
// comes after the EXIF data so if there is a large preview image,
// the size might be more than 32kb into the file
if (data.type === JPEG) {
findSizeAndType(imageBlob, imageBlob.size, success, failure);
}
// For all other known image types the type and size data is
// near the start of the file, if we didn't find it on this first
// try we need to give up now.
else {
reject(data.error);
}
}
// If we didn't find the data even after a second attempt then reject.
function failure(data) {
reject(data.error);
}
});
// This is the internal helper function that actually reads the blob
// and finds type and size information.
function findSizeAndType(imageBlob, amountToRead, success, failure) {
var slice = imageBlob.slice(0, Math.min(amountToRead, imageBlob.size));
var reader = new FileReader();
reader.readAsArrayBuffer(slice);
reader.onloadend = function() {
parseImageData(reader.result);
};
function parseImageData(buffer) {
var header = new Uint8Array(buffer, 0, 16);
var view = new DataView(buffer);
if (header[0] === 0x89 &&
header[1] === 0x50 && /* P */
header[2] === 0x4e && /* N */
header[3] === 0x47 && /* G */
header[4] === 0x0d && /* \r */
header[5] === 0x0a && /* \n */
header[6] === 0x1A &&
header[7] === 0x0a &&
header[12] === 0x49 && /* I */
header[13] === 0x48 && /* H */
header[14] === 0x44 && /* D */
header[15] === 0x52) /* R */
{
// This is a PNG file
try {
success({
type: PNG,
width: view.getUint32(16, false), // 32 bit big endian width
height: view.getUint32(20, false) // 32 bit big endian height
});
}
catch (ex) {
failure({ error: ex.toString()});
}
}
else if (header[0] === 0x47 && /* G */
header[1] === 0x49 && /* I */
header[2] === 0x46 && /* F */
header[3] === 0x38 && /* 8 */
(header[4] === 0x37 || /* 7 */
header[4] === 0x39) && /* or 9 */
header[5] === 0x61) /* a */
{
// This is a GIF file
try {
success({
type: GIF,
width: view.getUint16(6, true), // 16-bit little endian width
height: view.getUint16(8, true) // 16-big little endian height
});
}
catch (ex) {
failure({ error: ex.toString() });
}
}
else if (header[0] === 0x42 && /* B */
header[1] === 0x4D && /* M */
view.getUint32(2, true) === imageBlob.size)
{
// This is a BMP file
try {
var width, height;
if (view.getUint16(14, true) === 12) { // check format version
width = view.getUint16(18, true); // 16-bit little endian width
height = view.getUint16(20, true); // 16-bit little endian height
}
else { // newer versions of the format use 32-bit ints
width = view.getUint32(18, true); // 32-bit little endian width
height = view.getUint32(22, true); // 32-bit little endian height
}
success({
type: BMP,
width: width,
height: height
});
}
catch (ex) {
failure({ error: ex.toString() });
}
}
else if (header[0] === 0xFF &&
header[1] === 0xD8) {
var value = {
type: JPEG
};
// This is a JPEG file. To find its width and height we need
// to skip past the EXIF data and find the start of image data.
try {
var offset = 2;
// Loop through the segments of the file until we find an SOF
// segment with image size or until we reach the end of our data.
for (;;) {
// The byte at the current offset should be 0xFF marking
// the start of a new segment.
if (view.getUint8(offset) !== 0xFF) {
failure({ error: 'corrupt JPEG file' });
}
var segmentType = view.getUint8(offset + 1);
var segmentSize = view.getUint16(offset + 2) + 2;
// If we found an SOF "Start of Frame" segment, we can get
// the image size from it.
if ((segmentType >= 0xC0 && segmentType <= 0xC3) ||
(segmentType >= 0xC5 && segmentType <= 0xC7) ||
(segmentType >= 0xC9 && segmentType <= 0xCB) ||
(segmentType >= 0xCD && segmentType <= 0xCF))
{
// 16-bit big-endian dimensions, height first
value.height = view.getUint16(offset + 5, false);
value.width = view.getUint16(offset + 7, false);
success(value);
break;
}
// Otherwise, move on to the next segment
offset += segmentSize;
// We may not have read the entire file into our array buffer
// so we have to make sure we have not gone past the end of
// the buffer before looping again. Note that in this case
// the object we pass to the failure callback includes the
// image type. This is a signal to getSizeAndType that it
// should try again with a larger ArrayBuffer.
if (offset + 9 > view.byteLength) {
value.error = 'corrupt JPEG file';
failure(value);
break;
}
}
}
catch (ex) {
failure({ error: ex.toString() });
}
}
else {
failure({ error: 'unknown image type' });
}
}
}
};
//
// Given a blob containing an image file, this function returns a
// Promise that will resolve to a blob for an image file that has
// the specified width and height. If the blob already has that
// size, it is returned unchanged. If the image is bigger than the
// specified size, then it will be shrunk and cropped as necessary
// to fit the specified dimensions. If the image is smaller than
// the specified size, it will be enlarged and cropped to fit. The
// cropping is done in the same way as with the CSS
// background-size:cover attribute.
//
// If the image is resized and outputType is "image/jpeg" or
// "image/png" then that type will be used for the resized image. If
// a JPEG or PNG image is resized and outputType is not specified,
// then the output image will have the same type as the input image.
// In all other cases, a resized image will be encoded as a PNG image.
// Note that when an image does not need to be resized its type is not
// changed, even if outputType is specified.
// 'encoderOptions' is passed 'as is' to the 'toBlob' method of canvas:
// A Number between 0 and 1 indicating image quality if the requested type
// is image/jpeg or image/webp. If this argument is anything else,
// the default value for image quality is used.
ImageUtils.resizeAndCropToCover = function(inputImageBlob,
outputWidth, outputHeight,
outputType, encoderOptions)
{
if (!outputWidth || !isFinite(outputWidth) || outputWidth <= 0 ||
!outputHeight || !isFinite(outputHeight) || outputHeight <= 0) {
return Promise.reject(new TypeError('invalid output dimensions'));
}
outputWidth = Math.round(outputWidth);
outputHeight = Math.round(outputHeight);
return ImageUtils.getSizeAndType(inputImageBlob).then(
function resolve(data) {
var inputWidth = data.width;
var inputHeight = data.height;
// If the image already has the desired size, just return it unchanged
if (inputWidth === outputWidth && inputHeight === outputHeight) {
return inputImageBlob;
}
// Otherwise, return a promise for the resized image
return resize(data);
},
function reject(error) {
// If we couldn't determine the image size and type for some
// reason we are still going to assume that it is a valid image,
// just one we don't know about. If gecko can decode it we'll
// use it and won't reject unless gecko refuses it.
return resize({});
}
);
// Decode the image in an <img> element, resize and/or crop it into a
// <canvas> and the re-encode it to a blob again. Returns a Promise
// that resolves to the encoded blob.
function resize(data) {
var inputType = data.type;
var inputWidth = data.width;
var inputHeight = data.height;
// Ignore invalid outputType parameters
if (outputType && outputType !== JPEG && outputType !== PNG) {
console.warn('Ignoring unsupported outputType', outputType);
outputType = undefined;
}
// Determine the output type if none was specified.
if (!outputType) {
if (inputType === JPEG || inputType === PNG) {
outputType = inputType;
}
else {
outputType = PNG;
}
}
// Get a URL for the image blob so we can ask gecko to decode it.
var url = URL.createObjectURL(inputImageBlob);
// If we can downsample the image while decoding it, then add a
// #-moz-samplesize media fragment to the URL. For large JPEG inputs
// this can save megabytes of memory.
var mediaFragment;
if (inputType === JPEG &&
inputWidth > outputWidth && inputHeight > outputHeight) {
// How much would we like to scale the image by while decoding?
var reduction = Math.max(outputWidth / inputWidth,
outputHeight / inputHeight);
// Get a media fragment that will downsample the image by up to
// this amount.
mediaFragment = ImageUtils.Downsample.sizeNoMoreThan(reduction);
}
else {
mediaFragment = '';
}
// The next step is to decode (while possibly downsampling) the
// image, but that is asynchronous, so we'll have to return a
// Promise to handle that
return new Promise(function(resolve, reject) {
var offscreenImage = new Image();
offscreenImage.src = url + mediaFragment;
offscreenImage.onerror = function(e) {
cleanupImage();
reject('failed to decode image');
};
offscreenImage.onload = function() {
// The image has been decoded now, so we know its actual size.
// If getSizeAndType failed, or if we used a media fragment then
// this may be different than the inputWidth and inputHeight
// values we used previously.
var actualWidth = offscreenImage.width;
var actualHeight = offscreenImage.height;
// Now figure how much we have to scale the decoded image down
// (or up) so that it covers the specified output dimensions.
var widthScale = outputWidth / actualWidth;
var heightScale = outputHeight / actualHeight;
var scale = Math.max(widthScale, heightScale);
// Scaling the output dimensions by this much tells us the
// dimensions of the crop area on the decoded image
var cropWidth = Math.round(outputWidth / scale);
var cropHeight = Math.round(outputHeight / scale);
// Now center that crop region within the decoded image
var cropLeft = Math.floor((actualWidth - cropWidth) / 2);
var cropTop = Math.floor((actualHeight - cropHeight) / 2);
// Set up the canvas we need to copy the crop region into
var canvas = document.createElement('canvas');
canvas.width = outputWidth;
canvas.height = outputHeight;
var context = canvas.getContext('2d', { willReadFrequently: true });
// Copy the image into the canvas
context.drawImage(offscreenImage,
cropLeft, cropTop, cropWidth, cropHeight,
0, 0, outputWidth, outputHeight);
// We're done with the image now: try to release the decoded
// image memory as fast as we can
cleanupImage();
// Now encode the pixels in the canvas as an image blob
canvas.toBlob(function(blob) {
// We've got the encoded image in the blob, so we don't need
// the pixels in the canvas anymore. Try to release that memory
// right away, too.
canvas.width = 0;
resolve(blob);
}, outputType, encoderOptions);
};
function cleanupImage() {
offscreenImage.onerror = offscreenImage.onload = '';
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
URL.revokeObjectURL(url);
}
});
}
};
//
// This module defines a Downsample object with static
// methods that return objects representing media fragments for
// downsampling images while they are decoded. The current implementation
// is based on the #-moz-samplesize media fragment. But because of
// problems with that fragment (see bug 1004908) it seems likely that a
// new syntax or new fragment will be introduced. If that happens, we can
// just change this module and not have to change anything else that
// depends on it.
//
// The method Downsample.areaAtLeast(scale) returns an object
// representing a media fragment to use to decode an image downsampled by
// at least as much as the specified scale. If you are trying to preview
// an 8mp image and don't want to use more than 2mp of image memory, for
// example, you would pass a scale of .25 (2mp/8mp) here, and the
// resulting media fragment could be appended to the url to make the
// image decode at a size equal to or smaller than 2mp.
//
// The method Downsample.sizeNoMoreThan(scale) returns a media fragment
// object that you can use to reduce the dimensions of an image as much
// as possible without exceeding the specified scale. If you have a
// 1600x1200 image and want to decode it to produce an image that is as
// small as possible but at least 160x120, you would pass a scale of 0.1.
//
// The returned objects have a dimensionScale property that specifies how
// they affect the dimensions of the image and an areaScale property that
// specifies how much they affect the area (number of pixels) in an
// image. (The areaScale is just the square of the scale.) To avoid
// floating-point rounding issues, the values of these scale properties
// are rounded to the nearest hundredth.
//
// The returned objects also have a scale() method that scales a
// dimension with proper rounding (it rounds up to the nearest integer
// just as libjpeg does).
//
// Each object also has a toString() method that returns the required
// media fragment (including the hash mark) so you can simply use
// string concatentation to append one of these objects to the URL of
// the image you want to decode.
//
// Downsample.NONE is a no-op media fragment object with scale set to
// 1, and a toString() method that returns the empty string.
//
(function(exports) {
'use strict'; // jshint ignore:line
// Round to the nearest hundredth to combat floating point rounding errors
function round(x) {
return Math.round(x * 100) / 100;
}
//
// A factory method for returning an object that represents a
// #-moz-samplesize media fragment. The use of Math.ceil() in the
// scale method is from jpeg_core_output_dimensions() in
// media/libjpeg/jdmaster.c and jdiv_round_up() in media/libjpeg/jutils.c
//
function MozSampleSize(n, scale) {
return Object.freeze({
dimensionScale: round(scale),
areaScale: round(scale * scale),
toString: function() { return '#-moz-samplesize=' + n; },
scale: function(x) { return Math.ceil(x * scale); }
});
}
// A fragment object that represents no downsampling with no fragment
var NONE = Object.freeze({
dimensionScale: 1,
areaScale: 1,
toString: function() { return ''; },
scale: function(x) { return x; }
});
//
// The five possible #-moz-samplesize values.
// The mapping from sample size to scale comes from:
// the moz-samplesize code in /image/decoders/nsJPEGDecoder.cpp and
// the jpeg_core_output_dimensions() function in media/libjpeg/jdmaster.c
//
var fragments = [
NONE,
// samplesize=2 reduces size by 1/2 and area by 1/4, etc.
MozSampleSize(2, 1 / 2),
MozSampleSize(3, 3 / 8),
MozSampleSize(4, 1 / 4),
MozSampleSize(8, 1 / 8)
];
// Return the fragment object that has the largest scale and downsamples the
// dimensions of an image at least as much as the specified scale.
// If none of the choices scales enough, return the one that comes closest
function sizeAtLeast(scale) {
scale = round(scale);
for (var i = 0; i < fragments.length; i++) {
var f = fragments[i];
if (f.dimensionScale <= scale) {
return f;
}
}
return fragments[fragments.length - 1];
}
// Return the fragment object that downsamples an image as far as possible
// without going beyond the specified scale. This might return NONE.
function sizeNoMoreThan(scale) {
scale = round(scale);
for (var i = fragments.length - 1; i >= 0; i--) {
var f = fragments[i];
if (f.dimensionScale >= scale) {
return f;
}
}
return NONE;
}
// Return the fragment object that has the largest scale and downsamples the
// area of an image at least as much as the specified scale.
// If none of the choices scales enough, return the one that comes closest
function areaAtLeast(scale) {
scale = round(scale);
for (var i = 0; i < fragments.length; i++) {
var f = fragments[i];
if (f.areaScale <= scale) {
return f;
}
}
return fragments[fragments.length - 1];
}
// Return the fragment object that downsamples the area of an image
// as far as possible without going beyond the specified scale. This
// might return NONE.
function areaNoMoreThan(scale) {
scale = round(scale);
for (var i = fragments.length - 1; i >= 0; i--) {
var f = fragments[i];
if (f.areaScale >= scale) {
return f;
}
}
return NONE;
}
exports.Downsample = {
sizeAtLeast: sizeAtLeast,
sizeNoMoreThan: sizeNoMoreThan,
areaAtLeast: areaAtLeast,
areaNoMoreThan: areaNoMoreThan,
NONE: NONE,
MAX_SIZE_REDUCTION: 1 / fragments[fragments.length - 1].dimensionScale,
MAX_AREA_REDUCTION: 1 / fragments[fragments.length - 1].areaScale
};
}(exports.ImageUtils));
})(window);

View file

@ -0,0 +1,63 @@
;;;;;;;;;;;;var _createClass=(function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if('value'in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor);}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor;};})();function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError('Cannot call a class as a function');}}
(function(){'use strict';const Client=bridge.client;const channel=new BroadcastChannel('l20n-channel');const observerConfig={attributes:true,characterData:false,childList:true,subtree:true,attributeFilter:['data-l10n-id','data-l10n-args']};const observers=new WeakMap();function initMutationObserver(view){observers.set(view,{roots:new Set(),observer:new MutationObserver(mutations=>translateMutations(view,mutations))});}
function translateRoots(view){return Promise.all([...observers.get(view).roots].map(root=>_translateFragment(view,root)));}
function observe(view,root){const obs=observers.get(view);if(obs){obs.roots.add(root);obs.observer.observe(root,observerConfig);}}
function disconnect(view,root,allRoots){const obs=observers.get(view);if(obs){obs.observer.disconnect();if(allRoots){return;}
obs.roots.delete(root);obs.roots.forEach(other=>obs.observer.observe(other,observerConfig));}}
function reconnect(view){const obs=observers.get(view);if(obs){obs.roots.forEach(root=>obs.observer.observe(root,observerConfig));}}
const reOverlay=/<|&#?\w+;/;const allowed={elements:['a','em','strong','small','s','cite','q','dfn','abbr','data','time','code','var','samp','kbd','sub','sup','i','b','u','mark','ruby','rt','rp','bdi','bdo','span','br','wbr'],attributes:{global:['title','aria-label','aria-valuetext','aria-moz-hint'],a:['download'],area:['download','alt'],input:['alt','placeholder'],menuitem:['label'],menu:['label'],optgroup:['label'],option:['label'],track:['label'],img:['alt'],textarea:['placeholder'],th:['abbr']}};function overlayElement(element,translation){const value=translation.value;if(typeof value==='string'){if(!reOverlay.test(value)){element.textContent=value;}else{const tmpl=element.ownerDocument.createElement('template');tmpl.innerHTML=value;overlay(element,tmpl.content);}}
for(let key in translation.attrs){const attrName=camelCaseToDashed(key);if(isAttrAllowed({name:attrName},element)){element.setAttribute(attrName,translation.attrs[key]);}}}
function overlay(sourceElement,translationElement){const result=translationElement.ownerDocument.createDocumentFragment();let k,attr;let childElement;while(childElement=translationElement.childNodes[0]){translationElement.removeChild(childElement);if(childElement.nodeType===childElement.TEXT_NODE){result.appendChild(childElement);continue;}
const index=getIndexOfType(childElement);const sourceChild=getNthElementOfType(sourceElement,childElement,index);if(sourceChild){overlay(sourceChild,childElement);result.appendChild(sourceChild);continue;}
if(isElementAllowed(childElement)){const sanitizedChild=childElement.ownerDocument.createElement(childElement.nodeName);overlay(sanitizedChild,childElement);result.appendChild(sanitizedChild);continue;}
result.appendChild(translationElement.ownerDocument.createTextNode(childElement.textContent));}
sourceElement.textContent='';sourceElement.appendChild(result);if(translationElement.attributes){for(k=0,attr;attr=translationElement.attributes[k];k++){if(isAttrAllowed(attr,sourceElement)){sourceElement.setAttribute(attr.name,attr.value);}}}}
function isElementAllowed(element){return allowed.elements.indexOf(element.tagName.toLowerCase())!==-1;}
function isAttrAllowed(attr,element){const attrName=attr.name.toLowerCase();const tagName=element.tagName.toLowerCase();if(allowed.attributes.global.indexOf(attrName)!==-1){return true;}
if(!allowed.attributes[tagName]){return false;}
if(allowed.attributes[tagName].indexOf(attrName)!==-1){return true;}
if(tagName==='input'&&attrName==='value'){const type=element.type.toLowerCase();if(type==='submit'||type==='button'||type==='reset'){return true;}}
return false;}
function getNthElementOfType(context,element,index){let nthOfType=0;for(let i=0,child;child=context.children[i];i++){if(child.nodeType===child.ELEMENT_NODE&&child.tagName===element.tagName){if(nthOfType===index){return child;}
nthOfType++;}}
return null;}
function getIndexOfType(element){let index=0;let child;while(child=element.previousElementSibling){if(child.tagName===element.tagName){index++;}}
return index;}
function camelCaseToDashed(string){if(string==='ariaValueText'){return'aria-valuetext';}
return string.replace(/[A-Z]/g,function(match){return'-'+match.toLowerCase();}).replace(/^-/,'');}
const reHtml=/[&<>]/g;const htmlEntities={'&':'&amp;','<':'&lt;','>':'&gt;'};function setAttributes(element,id,args){element.setAttribute('data-l10n-id',id);if(args){element.setAttribute('data-l10n-args',JSON.stringify(args));}}
function getAttributes(element){return{id:element.getAttribute('data-l10n-id'),args:JSON.parse(element.getAttribute('data-l10n-args'))};}
function getTranslatables(element){const nodes=Array.from(element.querySelectorAll('[data-l10n-id]'));if(typeof element.hasAttribute==='function'&&element.hasAttribute('data-l10n-id')){nodes.push(element);}
return nodes;}
function translateMutations(view,mutations){const targets=new Set();for(let mutation of mutations){switch(mutation.type){case'attributes':targets.add(mutation.target);break;case'childList':for(let addedNode of mutation.addedNodes){if(addedNode.nodeType===addedNode.ELEMENT_NODE){if(addedNode.childElementCount){getTranslatables(addedNode).forEach(targets.add.bind(targets));}else{if(addedNode.hasAttribute('data-l10n-id')){targets.add(addedNode);}}}}
break;}}
if(targets.size===0){return;}
translateElements(view,Array.from(targets));}
function _translateFragment(view,frag){return translateElements(view,getTranslatables(frag));}
function getElementsTranslation(view,elems){const keys=elems.map(elem=>{const id=elem.getAttribute('data-l10n-id');const args=elem.getAttribute('data-l10n-args');return args?[id,JSON.parse(args.replace(reHtml,match=>htmlEntities[match]))]:id;});return view.formatEntities(...keys);}
function translateElements(view,elements){return getElementsTranslation(view,elements).then(translations=>applyTranslations(view,elements,translations));}
function applyTranslations(view,elems,translations){disconnect(view,null,true);for(let i=0;i<elems.length;i++){overlayElement(elems[i],translations[i]);}
reconnect(view);}
if(typeof NodeList==='function'&&!NodeList.prototype[Symbol.iterator]){NodeList.prototype[Symbol.iterator]=Array.prototype[Symbol.iterator];}
function documentReady(){if(document.readyState!=='loading'){return Promise.resolve();}
return new Promise(resolve=>{document.addEventListener('readystatechange',function onrsc(){document.removeEventListener('readystatechange',onrsc);resolve();});});}
function getDirection(code){const tag=code.split('-')[0];return['ar','he','fa','ps','ur'].indexOf(tag)>=0?'rtl':'ltr';}
if(navigator.languages===undefined){navigator.languages=[navigator.language];}
function getResourceLinks(head){return Array.prototype.map.call(head.querySelectorAll('link[rel="localization"]'),el=>el.getAttribute('href'));}
function getMeta(head){let availableLangs=Object.create(null);let defaultLang=null;let appVersion=null;const metas=Array.from(head.querySelectorAll('meta[name="availableLanguages"],'+'meta[name="defaultLanguage"],'+'meta[name="appVersion"]'));for(let meta of metas){const name=meta.getAttribute('name');const content=meta.getAttribute('content').trim();switch(name){case'availableLanguages':availableLangs=getLangRevisionMap(availableLangs,content);break;case'defaultLanguage':const[lang,rev]=getLangRevisionTuple(content);defaultLang=lang;if(!(lang in availableLangs)){availableLangs[lang]=rev;}
break;case'appVersion':appVersion=content;}}
return{defaultLang,availableLangs,appVersion};}
function getLangRevisionMap(seq,str){return str.split(',').reduce((seq,cur)=>{const[lang,rev]=getLangRevisionTuple(cur);seq[lang]=rev;return seq;},seq);}
function getLangRevisionTuple(str){const[lang,rev]=str.trim().split(':');return[lang,parseInt(rev)];}
const viewProps=new WeakMap();let View=(function(){function View(client,doc){_classCallCheck(this,View);this.pseudo={'fr-x-psaccent':createPseudo(this,'fr-x-psaccent'),'ar-x-psbidi':createPseudo(this,'ar-x-psbidi')};const initialized=documentReady().then(()=>init(this,client));this._interactive=initialized.then(()=>client);this.ready=initialized.then(langs=>translateView(this,langs));initMutationObserver(this);viewProps.set(this,{doc:doc,ready:false});client.on('languageschangerequest',requestedLangs=>this.requestLanguages(requestedLangs));}
_createClass(View,[{key:'requestLanguages',value:function requestLanguages(requestedLangs,isGlobal){const method=isGlobal?client=>client.method('requestLanguages',requestedLangs):client=>changeLanguages(this,client,requestedLangs);return this._interactive.then(method);}},{key:'handleEvent',value:function handleEvent(){return this.requestLanguages(navigator.languages);}},{key:'formatEntities',value:function formatEntities(...keys){return this._interactive.then(client=>client.method('formatEntities',client.id,keys));}},{key:'formatValue',value:function formatValue(id,args){return this._interactive.then(client=>client.method('formatValues',client.id,[[id,args]])).then(values=>values[0]);}},{key:'formatValues',value:function formatValues(...keys){return this._interactive.then(client=>client.method('formatValues',client.id,keys));}},{key:'translateFragment',value:function translateFragment(frag){return _translateFragment(this,frag);}},{key:'observeRoot',value:function observeRoot(root){observe(this,root);}},{key:'disconnectRoot',value:function disconnectRoot(root){disconnect(this,root);}}]);return View;})();View.prototype.setAttributes=setAttributes;View.prototype.getAttributes=getAttributes;function createPseudo(view,code){return{getName:()=>view._interactive.then(client=>client.method('getName',code)),processString:str=>view._interactive.then(client=>client.method('processString',code,str))};}
function init(view,client){const doc=viewProps.get(view).doc;const resources=getResourceLinks(doc.head);const meta=getMeta(doc.head);view.observeRoot(doc.documentElement);return getAdditionalLanguages().then(additionalLangs=>client.method('registerView',client.id,resources,meta,additionalLangs,navigator.languages));}
function changeLanguages(view,client,requestedLangs){const doc=viewProps.get(view).doc;const meta=getMeta(doc.head);return getAdditionalLanguages().then(additionalLangs=>client.method('changeLanguages',client.id,meta,additionalLangs,requestedLangs)).then(({langs,haveChanged})=>haveChanged?translateView(view,langs):undefined);}
function getAdditionalLanguages(){if(navigator.mozApps&&navigator.mozApps.getAdditionalLanguages){return navigator.mozApps.getAdditionalLanguages().catch(()=>Object.create(null));}
return Promise.resolve(Object.create(null));}
function translateView(view,langs){const props=viewProps.get(view);const html=props.doc.documentElement;if(props.ready){return translateRoots(view).then(()=>setAllAndEmit(html,langs));}
const translated=langs[0].code===html.getAttribute('lang')?Promise.resolve():translateRoots(view).then(()=>setLangDir(html,langs));return translated.then(()=>{setLangs(html,langs);props.ready=true;});}
function setLangs(html,langs){const codes=langs.map(lang=>lang.code);html.setAttribute('langs',codes.join(' '));}
function setLangDir(html,langs){const code=langs[0].code;html.setAttribute('lang',code);html.setAttribute('dir',getDirection(code));}
function setAllAndEmit(html,langs){setLangDir(html,langs);setLangs(html,langs);html.parentNode.dispatchEvent(new CustomEvent('DOMRetranslated',{bubbles:false,cancelable:false}));}
const client=new Client({service:'l20n',endpoint:channel,timeout:false});document.l10n=new View(client,document);window.addEventListener('pageshow',()=>client.connect());window.addEventListener('pagehide',()=>client.disconnect());window.addEventListener('languagechange',document.l10n);document.addEventListener('additionallanguageschange',document.l10n);navigator.mozL10n={setAttributes:document.l10n.setAttributes,getAttributes:document.l10n.getAttributes,formatValue:(...args)=>document.l10n.formatValue(...args),translateFragment:(...args)=>document.l10n.translateFragment(...args),once:cb=>document.l10n.ready.then(cb),ready:cb=>document.l10n.ready.then(()=>{document.addEventListener('DOMRetranslated',cb);cb();})};})();

View file

@ -0,0 +1,218 @@
;;;var _createClass=(function(){function defineProperties(target,props){for(var i=0;i<props.length;i++){var descriptor=props[i];descriptor.enumerable=descriptor.enumerable||false;descriptor.configurable=true;if('value'in descriptor)descriptor.writable=true;Object.defineProperty(target,descriptor.key,descriptor);}}return function(Constructor,protoProps,staticProps){if(protoProps)defineProperties(Constructor.prototype,protoProps);if(staticProps)defineProperties(Constructor,staticProps);return Constructor;};})();function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor)){throw new TypeError('Cannot call a class as a function');}}
(function(){'use strict';const Service=bridge.service;const channel=new BroadcastChannel('l20n-channel');function broadcast(type,data){return this.service.broadcast(type,data);}
function L10nError(message,id,lang){this.name='L10nError';this.message=message;this.id=id;this.lang=lang;}
L10nError.prototype=Object.create(Error.prototype);L10nError.prototype.constructor=L10nError;function load(type,url){return new Promise(function(resolve,reject){const xhr=new XMLHttpRequest();if(xhr.overrideMimeType){xhr.overrideMimeType(type);}
xhr.open('GET',url,true);if(type==='application/json'){xhr.responseType='json';}
xhr.addEventListener('load',function io_onload(e){if(e.target.status===200||e.target.status===0){resolve(e.target.response||e.target.responseText);}else{reject(new L10nError('Not found: '+url));}});xhr.addEventListener('error',reject);xhr.addEventListener('timeout',reject);try{xhr.send(null);}catch(e){if(e.name==='NS_ERROR_FILE_NOT_FOUND'){reject(new L10nError('Not found: '+url));}else{throw e;}}});}
const io={extra:function(code,ver,path,type){return navigator.mozApps.getLocalizationResource(code,ver,path,type);},app:function(code,ver,path,type){switch(type){case'text':return load('text/plain',path);case'json':return load('application/json',path);default:throw new L10nError('Unknown file type: '+type);}}};function fetchResource(res,{code,src,ver}){const url=res.replace('{locale}',code);const type=res.endsWith('.json')?'json':'text';return io[src](code,ver,url,type);}
const KNOWN_MACROS=['plural'];const MAX_PLACEABLE_LENGTH=2500;const FSI='⁨';const PDI='⁩';const resolutionChain=new WeakSet();function format(ctx,lang,args,entity){if(typeof entity==='string'){return[{},entity];}
if(resolutionChain.has(entity)){throw new L10nError('Cyclic reference detected');}
resolutionChain.add(entity);let rv;try{rv=resolveValue({},ctx,lang,args,entity.value,entity.index);}finally{resolutionChain.delete(entity);}
return rv;}
function resolveIdentifier(ctx,lang,args,id){if(KNOWN_MACROS.indexOf(id)>-1){return[{},ctx._getMacro(lang,id)];}
if(args&&args.hasOwnProperty(id)){if(typeof args[id]==='string'||typeof args[id]==='number'&&!isNaN(args[id])){return[{},args[id]];}else{throw new L10nError('Arg must be a string or a number: '+id);}}
if(id==='__proto__'){throw new L10nError('Illegal id: '+id);}
const entity=ctx._getEntity(lang,id);if(entity){return format(ctx,lang,args,entity);}
throw new L10nError('Unknown reference: '+id);}
function subPlaceable(locals,ctx,lang,args,id){let newLocals,value;try{[newLocals,value]=resolveIdentifier(ctx,lang,args,id);}catch(err){return[{error:err},FSI+'{{ '+id+' }}'+PDI];}
if(typeof value==='number'){const formatter=ctx._getNumberFormatter(lang);return[newLocals,formatter.format(value)];}
if(typeof value==='string'){if(value.length>=MAX_PLACEABLE_LENGTH){throw new L10nError('Too many characters in placeable ('+value.length+', max allowed is '+MAX_PLACEABLE_LENGTH+')');}
return[newLocals,FSI+value+PDI];}
return[{},FSI+'{{ '+id+' }}'+PDI];}
function interpolate(locals,ctx,lang,args,arr){return arr.reduce(function([localsSeq,valueSeq],cur){if(typeof cur==='string'){return[localsSeq,valueSeq+cur];}else{const[,value]=subPlaceable(locals,ctx,lang,args,cur.name);return[localsSeq,valueSeq+value];}},[locals,'']);}
function resolveSelector(ctx,lang,args,expr,index){let selectorName;if(index[0].type==='call'&&index[0].expr.type==='prop'&&index[0].expr.expr.name==='cldr'){selectorName='plural';}else{selectorName=index[0].name;}
const selector=resolveIdentifier(ctx,lang,args,selectorName)[1];if(typeof selector!=='function'){return selector;}
const argValue=index[0].args?resolveIdentifier(ctx,lang,args,index[0].args[0].name)[1]:undefined;if(selectorName==='plural'){if(argValue===0&&'zero'in expr){return'zero';}
if(argValue===1&&'one'in expr){return'one';}
if(argValue===2&&'two'in expr){return'two';}}
return selector(argValue);}
function resolveValue(locals,ctx,lang,args,expr,index){if(!expr){return[locals,expr];}
if(typeof expr==='string'||typeof expr==='boolean'||typeof expr==='number'){return[locals,expr];}
if(Array.isArray(expr)){return interpolate(locals,ctx,lang,args,expr);}
if(index){const selector=resolveSelector(ctx,lang,args,expr,index);if(selector in expr){return resolveValue(locals,ctx,lang,args,expr[selector]);}}
const defaultKey=expr.__default||'other';if(defaultKey in expr){return resolveValue(locals,ctx,lang,args,expr[defaultKey]);}
throw new L10nError('Unresolvable value');}
const locales2rules={'af':3,'ak':4,'am':4,'ar':1,'asa':3,'az':0,'be':11,'bem':3,'bez':3,'bg':3,'bh':4,'bm':0,'bn':3,'bo':0,'br':20,'brx':3,'bs':11,'ca':3,'cgg':3,'chr':3,'cs':12,'cy':17,'da':3,'de':3,'dv':3,'dz':0,'ee':3,'el':3,'en':3,'eo':3,'es':3,'et':3,'eu':3,'fa':0,'ff':5,'fi':3,'fil':4,'fo':3,'fr':5,'fur':3,'fy':3,'ga':8,'gd':24,'gl':3,'gsw':3,'gu':3,'guw':4,'gv':23,'ha':3,'haw':3,'he':2,'hi':4,'hr':11,'hu':0,'id':0,'ig':0,'ii':0,'is':3,'it':3,'iu':7,'ja':0,'jmc':3,'jv':0,'ka':0,'kab':5,'kaj':3,'kcg':3,'kde':0,'kea':0,'kk':3,'kl':3,'km':0,'kn':0,'ko':0,'ksb':3,'ksh':21,'ku':3,'kw':7,'lag':18,'lb':3,'lg':3,'ln':4,'lo':0,'lt':10,'lv':6,'mas':3,'mg':4,'mk':16,'ml':3,'mn':3,'mo':9,'mr':3,'ms':0,'mt':15,'my':0,'nah':3,'naq':7,'nb':3,'nd':3,'ne':3,'nl':3,'nn':3,'no':3,'nr':3,'nso':4,'ny':3,'nyn':3,'om':3,'or':3,'pa':3,'pap':3,'pl':13,'ps':3,'pt':3,'rm':3,'ro':9,'rof':3,'ru':11,'rwk':3,'sah':0,'saq':3,'se':7,'seh':3,'ses':0,'sg':0,'sh':11,'shi':19,'sk':12,'sl':14,'sma':7,'smi':7,'smj':7,'smn':7,'sms':7,'sn':3,'so':3,'sq':3,'sr':11,'ss':3,'ssy':3,'st':3,'sv':3,'sw':3,'syr':3,'ta':3,'te':3,'teo':3,'th':0,'ti':4,'tig':3,'tk':3,'tl':4,'tn':3,'to':0,'tr':0,'ts':3,'tzm':22,'uk':11,'ur':3,'ve':3,'vi':0,'vun':3,'wa':4,'wae':3,'wo':0,'xh':3,'xog':3,'yo':0,'zh':0,'zu':3};function isIn(n,list){return list.indexOf(n)!==-1;}
function isBetween(n,start,end){return typeof n===typeof start&&start<=n&&n<=end;}
const pluralRules={'0':function(){return'other';},'1':function(n){if(isBetween(n%100,3,10)){return'few';}
if(n===0){return'zero';}
if(isBetween(n%100,11,99)){return'many';}
if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'2':function(n){if(n!==0&&n%10===0){return'many';}
if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'3':function(n){if(n===1){return'one';}
return'other';},'4':function(n){if(isBetween(n,0,1)){return'one';}
return'other';},'5':function(n){if(isBetween(n,0,2)&&n!==2){return'one';}
return'other';},'6':function(n){if(n===0){return'zero';}
if(n%10===1&&n%100!==11){return'one';}
return'other';},'7':function(n){if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'8':function(n){if(isBetween(n,3,6)){return'few';}
if(isBetween(n,7,10)){return'many';}
if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'9':function(n){if(n===0||n!==1&&isBetween(n%100,1,19)){return'few';}
if(n===1){return'one';}
return'other';},'10':function(n){if(isBetween(n%10,2,9)&&!isBetween(n%100,11,19)){return'few';}
if(n%10===1&&!isBetween(n%100,11,19)){return'one';}
return'other';},'11':function(n){if(isBetween(n%10,2,4)&&!isBetween(n%100,12,14)){return'few';}
if(n%10===0||isBetween(n%10,5,9)||isBetween(n%100,11,14)){return'many';}
if(n%10===1&&n%100!==11){return'one';}
return'other';},'12':function(n){if(isBetween(n,2,4)){return'few';}
if(n===1){return'one';}
return'other';},'13':function(n){if(isBetween(n%10,2,4)&&!isBetween(n%100,12,14)){return'few';}
if(n!==1&&isBetween(n%10,0,1)||isBetween(n%10,5,9)||isBetween(n%100,12,14)){return'many';}
if(n===1){return'one';}
return'other';},'14':function(n){if(isBetween(n%100,3,4)){return'few';}
if(n%100===2){return'two';}
if(n%100===1){return'one';}
return'other';},'15':function(n){if(n===0||isBetween(n%100,2,10)){return'few';}
if(isBetween(n%100,11,19)){return'many';}
if(n===1){return'one';}
return'other';},'16':function(n){if(n%10===1&&n!==11){return'one';}
return'other';},'17':function(n){if(n===3){return'few';}
if(n===0){return'zero';}
if(n===6){return'many';}
if(n===2){return'two';}
if(n===1){return'one';}
return'other';},'18':function(n){if(n===0){return'zero';}
if(isBetween(n,0,2)&&n!==0&&n!==2){return'one';}
return'other';},'19':function(n){if(isBetween(n,2,10)){return'few';}
if(isBetween(n,0,1)){return'one';}
return'other';},'20':function(n){if((isBetween(n%10,3,4)||n%10===9)&&!(isBetween(n%100,10,19)||isBetween(n%100,70,79)||isBetween(n%100,90,99))){return'few';}
if(n%1000000===0&&n!==0){return'many';}
if(n%10===2&&!isIn(n%100,[12,72,92])){return'two';}
if(n%10===1&&!isIn(n%100,[11,71,91])){return'one';}
return'other';},'21':function(n){if(n===0){return'zero';}
if(n===1){return'one';}
return'other';},'22':function(n){if(isBetween(n,0,1)||isBetween(n,11,99)){return'one';}
return'other';},'23':function(n){if(isBetween(n%10,1,2)||n%20===0){return'one';}
return'other';},'24':function(n){if(isBetween(n,3,10)||isBetween(n,13,19)){return'few';}
if(isIn(n,[2,12])){return'two';}
if(isIn(n,[1,11])){return'one';}
return'other';}};function getPluralRule(code){const index=locales2rules[code.replace(/-.*$/,'')];if(!(index in pluralRules)){return function(){return'other';};}
return pluralRules[index];}
const L20nIntl=typeof Intl!=='undefined'?Intl:{NumberFormat:function(){return{format:function(v){return v;}};}};let Context=(function(){function Context(env,langs,resIds){_classCallCheck(this,Context);this.langs=langs;this.resIds=resIds;this.env=env;this.emit=(type,evt)=>env.emit(type,evt,this);}
_createClass(Context,[{key:'_formatTuple',value:function _formatTuple(lang,args,entity,id,key){try{return format(this,lang,args,entity);}catch(err){err.id=key?id+'::'+key:id;err.lang=lang;this.emit('resolveerror',err);return[{error:err},err.id];}}},{key:'_formatEntity',value:function _formatEntity(lang,args,entity,id){const[,value]=this._formatTuple(lang,args,entity,id);const formatted={value,attrs:null};if(entity.attrs){formatted.attrs=Object.create(null);for(let key in entity.attrs){const[,attrValue]=this._formatTuple(lang,args,entity.attrs[key],id,key);formatted.attrs[key]=attrValue;}}
return formatted;}},{key:'_formatValue',value:function _formatValue(lang,args,entity,id){return this._formatTuple(lang,args,entity,id)[1];}},{key:'fetch',value:function fetch(langs=this.langs){if(langs.length===0){return Promise.resolve(langs);}
return Promise.all(this.resIds.map(resId=>this.env._getResource(langs[0],resId))).then(()=>langs);}},{key:'_resolve',value:function _resolve(langs,keys,formatter,prevResolved){const lang=langs[0];if(!lang){return reportMissing.call(this,keys,formatter,prevResolved);}
let hasUnresolved=false;const resolved=keys.map((key,i)=>{if(prevResolved&&prevResolved[i]!==undefined){return prevResolved[i];}
const[id,args]=Array.isArray(key)?key:[key,undefined];const entity=this._getEntity(lang,id);if(entity){return formatter.call(this,lang,args,entity,id);}
this.emit('notfounderror',new L10nError('"'+id+'"'+' not found in '+lang.code,id,lang));hasUnresolved=true;});if(!hasUnresolved){return resolved;}
return this.fetch(langs.slice(1)).then(nextLangs=>this._resolve(nextLangs,keys,formatter,resolved));}},{key:'formatEntities',value:function formatEntities(...keys){return this.fetch().then(langs=>this._resolve(langs,keys,this._formatEntity));}},{key:'formatValues',value:function formatValues(...keys){return this.fetch().then(langs=>this._resolve(langs,keys,this._formatValue));}},{key:'_getEntity',value:function _getEntity(lang,id){const cache=this.env.resCache;for(let i=0,resId;resId=this.resIds[i];i++){const resource=cache.get(resId+lang.code+lang.src);if(resource instanceof L10nError){continue;}
if(id in resource){return resource[id];}}
return undefined;}},{key:'_getNumberFormatter',value:function _getNumberFormatter(lang){if(!this.env.numberFormatters){this.env.numberFormatters=new Map();}
if(!this.env.numberFormatters.has(lang)){const formatter=L20nIntl.NumberFormat(lang);this.env.numberFormatters.set(lang,formatter);return formatter;}
return this.env.numberFormatters.get(lang);}},{key:'_getMacro',value:function _getMacro(lang,id){switch(id){case'plural':return getPluralRule(lang.code);default:return undefined;}}}]);return Context;})();function reportMissing(keys,formatter,resolved){const missingIds=new Set();keys.forEach((key,i)=>{if(resolved&&resolved[i]!==undefined){return;}
const id=Array.isArray(key)?key[0]:key;missingIds.add(id);resolved[i]=formatter===this._formatValue?id:{value:id,attrs:null};});this.emit('notfounderror',new L10nError('"'+Array.from(missingIds).join(', ')+'"'+' not found in any language',missingIds));return resolved;}
var MAX_PLACEABLES=100;var PropertiesParser={patterns:null,entryIds:null,emit:null,init:function(){this.patterns={comment:/^\s*#|^\s*$/,entity:/^([^=\s]+)\s*=\s*(.*)$/,multiline:/[^\\]\\$/,index:/\{\[\s*(\w+)(?:\(([^\)]*)\))?\s*\]\}/i,unicode:/\\u([0-9a-fA-F]{1,4})/g,entries:/[^\r\n]+/g,controlChars:/\\([\\\n\r\t\b\f\{\}\"\'])/g,placeables:/\{\{\s*([^\s]*?)\s*\}\}/};},parse:function(emit,source){if(!this.patterns){this.init();}
this.emit=emit;var entries={};var lines=source.match(this.patterns.entries);if(!lines){return entries;}
for(var i=0;i<lines.length;i++){var line=lines[i];if(this.patterns.comment.test(line)){continue;}
while(this.patterns.multiline.test(line)&&i<lines.length){line=line.slice(0,-1)+lines[++i].trim();}
var entityMatch=line.match(this.patterns.entity);if(entityMatch){try{this.parseEntity(entityMatch[1],entityMatch[2],entries);}catch(e){if(!this.emit){throw e;}}}}
return entries;},parseEntity:function(id,value,entries){var name,key;var pos=id.indexOf('[');if(pos!==-1){name=id.substr(0,pos);key=id.substring(pos+1,id.length-1);}else{name=id;key=null;}
var nameElements=name.split('.');if(nameElements.length>2){throw this.error('Error in ID: "'+name+'".'+' Nested attributes are not supported.');}
var attr;if(nameElements.length>1){name=nameElements[0];attr=nameElements[1];if(attr[0]==='$'){throw this.error('Attribute can\'t start with "$"');}}else{attr=null;}
this.setEntityValue(name,attr,key,this.unescapeString(value),entries);},setEntityValue:function(id,attr,key,rawValue,entries){var value=rawValue.indexOf('{{')>-1?this.parseString(rawValue):rawValue;var isSimpleValue=typeof value==='string';var root=entries;var isSimpleNode=typeof entries[id]==='string';if(!entries[id]&&(attr||key||!isSimpleValue)){entries[id]=Object.create(null);isSimpleNode=false;}
if(attr){if(isSimpleNode){const val=entries[id];entries[id]=Object.create(null);entries[id].value=val;}
if(!entries[id].attrs){entries[id].attrs=Object.create(null);}
if(!entries[id].attrs&&!isSimpleValue){entries[id].attrs[attr]=Object.create(null);}
root=entries[id].attrs;id=attr;}
if(key){isSimpleNode=false;if(typeof root[id]==='string'){const val=root[id];root[id]=Object.create(null);root[id].index=this.parseIndex(val);root[id].value=Object.create(null);}
root=root[id].value;id=key;isSimpleValue=true;}
if(isSimpleValue){if(id in root){throw this.error('Duplicated id: '+id);}
root[id]=value;}else{if(!root[id]){root[id]=Object.create(null);}
root[id].value=value;}},parseString:function(str){var chunks=str.split(this.patterns.placeables);var complexStr=[];var len=chunks.length;var placeablesCount=(len-1)/2;if(placeablesCount>=MAX_PLACEABLES){throw this.error('Too many placeables ('+placeablesCount+', max allowed is '+MAX_PLACEABLES+')');}
for(var i=0;i<chunks.length;i++){if(chunks[i].length===0){continue;}
if(i%2===1){complexStr.push({type:'idOrVar',name:chunks[i]});}else{complexStr.push(chunks[i]);}}
return complexStr;},unescapeString:function(str){if(str.lastIndexOf('\\')!==-1){str=str.replace(this.patterns.controlChars,'$1');}
return str.replace(this.patterns.unicode,function(match,token){return String.fromCodePoint(parseInt(token,16));});},parseIndex:function(str){var match=str.match(this.patterns.index);if(!match){throw new L10nError('Malformed index');}
if(match[2]){return[{type:'call',expr:{type:'prop',expr:{type:'glob',name:'cldr'},prop:'plural',cmpt:false},args:[{type:'idOrVar',name:match[2]}]}];}else{return[{type:'idOrVar',name:match[1]}];}},error:function(msg,type='parsererror'){const err=new L10nError(msg);if(this.emit){this.emit(type,err);}
return err;}};const MAX_PLACEABLES$1=100;var L20nParser={parse:function(emit,string){this._source=string;this._index=0;this._length=string.length;this.entries=Object.create(null);this.emit=emit;return this.getResource();},getResource:function(){this.getWS();while(this._index<this._length){try{this.getEntry();}catch(e){if(e instanceof L10nError){this.getJunkEntry();if(!this.emit){throw e;}}else{throw e;}}
if(this._index<this._length){this.getWS();}}
return this.entries;},getEntry:function(){if(this._source[this._index]==='<'){++this._index;const id=this.getIdentifier();if(this._source[this._index]==='['){++this._index;return this.getEntity(id,this.getItemList(this.getExpression,']'));}
return this.getEntity(id);}
if(this._source.startsWith('/*',this._index)){return this.getComment();}
throw this.error('Invalid entry');},getEntity:function(id,index){if(!this.getRequiredWS()){throw this.error('Expected white space');}
const ch=this._source[this._index];const hasIndex=index!==undefined;const value=this.getValue(ch,hasIndex,hasIndex);let attrs;if(value===undefined){if(ch==='>'){throw this.error('Expected ">"');}
attrs=this.getAttributes();}else{const ws1=this.getRequiredWS();if(this._source[this._index]!=='>'){if(!ws1){throw this.error('Expected ">"');}
attrs=this.getAttributes();}}
++this._index;if(id in this.entries){throw this.error('Duplicate entry ID "'+id,'duplicateerror');}
if(!attrs&&!index&&typeof value==='string'){this.entries[id]=value;}else{this.entries[id]={value,attrs,index};}},getValue:function(ch=this._source[this._index],index=false,required=true){switch(ch){case'\'':case'"':return this.getString(ch,1);case'{':return this.getHash(index);}
if(required){throw this.error('Unknown value type');}
return;},getWS:function(){let cc=this._source.charCodeAt(this._index);while(cc===32||cc===10||cc===9||cc===13){cc=this._source.charCodeAt(++this._index);}},getRequiredWS:function(){const pos=this._index;let cc=this._source.charCodeAt(pos);while(cc===32||cc===10||cc===9||cc===13){cc=this._source.charCodeAt(++this._index);}
return this._index!==pos;},getIdentifier:function(){const start=this._index;let cc=this._source.charCodeAt(this._index);if(cc>=97&&cc<=122||cc>=65&&cc<=90||cc===95){cc=this._source.charCodeAt(++this._index);}else{throw this.error('Identifier has to start with [a-zA-Z_]');}
while(cc>=97&&cc<=122||cc>=65&&cc<=90||cc>=48&&cc<=57||cc===95){cc=this._source.charCodeAt(++this._index);}
return this._source.slice(start,this._index);},getUnicodeChar:function(){for(let i=0;i<4;i++){let cc=this._source.charCodeAt(++this._index);if(cc>96&&cc<103||cc>64&&cc<71||cc>47&&cc<58){continue;}
throw this.error('Illegal unicode escape sequence');}
this._index++;return String.fromCharCode(parseInt(this._source.slice(this._index-4,this._index),16));},stringRe:/"|'|{{|\\/g,getString:function(opchar,opcharLen){const body=[];let placeables=0;this._index+=opcharLen;const start=this._index;let bufStart=start;let buf='';while(true){this.stringRe.lastIndex=this._index;const match=this.stringRe.exec(this._source);if(!match){throw this.error('Unclosed string literal');}
if(match[0]==='"'||match[0]==='\''){if(match[0]!==opchar){this._index+=opcharLen;continue;}
this._index=match.index+opcharLen;break;}
if(match[0]==='{{'){if(placeables>MAX_PLACEABLES$1-1){throw this.error('Too many placeables, maximum allowed is '+MAX_PLACEABLES$1);}
placeables++;if(match.index>bufStart||buf.length>0){body.push(buf+this._source.slice(bufStart,match.index));buf='';}
this._index=match.index+2;this.getWS();body.push(this.getExpression());this.getWS();this._index+=2;bufStart=this._index;continue;}
if(match[0]==='\\'){this._index=match.index+1;const ch2=this._source[this._index];if(ch2==='u'){buf+=this._source.slice(bufStart,match.index)+this.getUnicodeChar();}else if(ch2===opchar||ch2==='\\'){buf+=this._source.slice(bufStart,match.index)+ch2;this._index++;}else if(this._source.startsWith('{{',this._index)){buf+=this._source.slice(bufStart,match.index)+'{{';this._index+=2;}else{throw this.error('Illegal escape sequence');}
bufStart=this._index;}}
if(body.length===0){return buf+this._source.slice(bufStart,this._index-opcharLen);}
if(this._index-opcharLen>bufStart||buf.length>0){body.push(buf+this._source.slice(bufStart,this._index-opcharLen));}
return body;},getAttributes:function(){const attrs=Object.create(null);while(true){this.getAttribute(attrs);const ws1=this.getRequiredWS();const ch=this._source.charAt(this._index);if(ch==='>'){break;}else if(!ws1){throw this.error('Expected ">"');}}
return attrs;},getAttribute:function(attrs){const key=this.getIdentifier();let index;if(this._source[this._index]==='['){++this._index;this.getWS();index=this.getItemList(this.getExpression,']');}
this.getWS();if(this._source[this._index]!==':'){throw this.error('Expected ":"');}
++this._index;this.getWS();const hasIndex=index!==undefined;const value=this.getValue(undefined,hasIndex);if(key in attrs){throw this.error('Duplicate attribute "'+key,'duplicateerror');}
if(!index&&typeof value==='string'){attrs[key]=value;}else{attrs[key]={value,index};}},getHash:function(index){const items=Object.create(null);++this._index;this.getWS();let defKey;while(true){const[key,value,def]=this.getHashItem();items[key]=value;if(def){if(defKey){throw this.error('Default item redefinition forbidden');}
defKey=key;}
this.getWS();const comma=this._source[this._index]===',';if(comma){++this._index;this.getWS();}
if(this._source[this._index]==='}'){++this._index;break;}
if(!comma){throw this.error('Expected "}"');}}
if(defKey){items.__default=defKey;}else if(!index){throw this.error('Unresolvable Hash Value');}
return items;},getHashItem:function(){let defItem=false;if(this._source[this._index]==='*'){++this._index;defItem=true;}
const key=this.getIdentifier();this.getWS();if(this._source[this._index]!==':'){throw this.error('Expected ":"');}
++this._index;this.getWS();return[key,this.getValue(),defItem];},getComment:function(){this._index+=2;const start=this._index;const end=this._source.indexOf('*/',start);if(end===-1){throw this.error('Comment without a closing tag');}
this._index=end+2;},getExpression:function(){let exp=this.getPrimaryExpression();while(true){let ch=this._source[this._index];if(ch==='.'||ch==='['){++this._index;exp=this.getPropertyExpression(exp,ch==='[');}else if(ch==='('){++this._index;exp=this.getCallExpression(exp);}else{break;}}
return exp;},getPropertyExpression:function(idref,computed){let exp;if(computed){this.getWS();exp=this.getExpression();this.getWS();if(this._source[this._index]!==']'){throw this.error('Expected "]"');}
++this._index;}else{exp=this.getIdentifier();}
return{type:'prop',expr:idref,prop:exp,cmpt:computed};},getCallExpression:function(callee){this.getWS();return{type:'call',expr:callee,args:this.getItemList(this.getExpression,')')};},getPrimaryExpression:function(){const ch=this._source[this._index];switch(ch){case'$':++this._index;return{type:'var',name:this.getIdentifier()};case'@':++this._index;return{type:'glob',name:this.getIdentifier()};default:return{type:'id',name:this.getIdentifier()};}},getItemList:function(callback,closeChar){const items=[];let closed=false;this.getWS();if(this._source[this._index]===closeChar){++this._index;closed=true;}
while(!closed){items.push(callback.call(this));this.getWS();let ch=this._source.charAt(this._index);switch(ch){case',':++this._index;this.getWS();break;case closeChar:++this._index;closed=true;break;default:throw this.error('Expected "," or "'+closeChar+'"');}}
return items;},getJunkEntry:function(){const pos=this._index;let nextEntity=this._source.indexOf('<',pos);let nextComment=this._source.indexOf('/*',pos);if(nextEntity===-1){nextEntity=this._length;}
if(nextComment===-1){nextComment=this._length;}
let nextEntry=Math.min(nextEntity,nextComment);this._index=nextEntry;},error:function(message,type='parsererror'){const pos=this._index;let start=this._source.lastIndexOf('<',pos-1);const lastClose=this._source.lastIndexOf('>',pos-1);start=lastClose>start?lastClose+1:start;const context=this._source.slice(start,pos+10);const msg=message+' at pos '+pos+': `'+context+'`';const err=new L10nError(msg);if(this.emit){this.emit(type,err);}
return err;}};function walkEntry(entry,fn){if(typeof entry==='string'){return fn(entry);}
const newEntry=Object.create(null);if(entry.value){newEntry.value=walkValue(entry.value,fn);}
if(entry.index){newEntry.index=entry.index;}
if(entry.attrs){newEntry.attrs=Object.create(null);for(let key in entry.attrs){newEntry.attrs[key]=walkEntry(entry.attrs[key],fn);}}
return newEntry;}
function walkValue(value,fn){if(typeof value==='string'){return fn(value);}
if(value.type){return value;}
const newValue=Array.isArray(value)?[]:Object.create(null);const keys=Object.keys(value);for(let i=0,key;key=keys[i];i++){newValue[key]=walkValue(value[key],fn);}
return newValue;}
function createGetter(id,name){let _pseudo=null;return function getPseudo(){if(_pseudo){return _pseudo;}
const reAlphas=/[a-zA-Z]/g;const reVowels=/[aeiouAEIOU]/g;const reWords=/[^\W0-9_]+/g;const reExcluded=/(%[EO]?\w|\{\s*.+?\s*\}|&[#\w]+;|<\s*.+?\s*>)/;const charMaps={'fr-x-psaccent':'ȦƁƇḒḖƑƓĦĪĴĶĿḾȠǾƤɊŘŞŦŬṼẆẊẎẐ[\\]^_`ȧƀƈḓḗƒɠħīĵķŀḿƞǿƥɋřşŧŭṽẇẋẏẑ','ar-x-psbidi':'∀ԐↃpƎɟפHIſӼ˥WNOԀÒᴚS⊥∩ɅMXʎZ[\\]ᵥ_,ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz'};const mods={'fr-x-psaccent':val=>val.replace(reVowels,match=>match+match.toLowerCase()),'ar-x-psbidi':val=>val.replace(reWords,match=>'‮'+match+'‬')};const replaceChars=(map,val)=>val.replace(reAlphas,match=>map.charAt(match.charCodeAt(0)-65));const transform=val=>replaceChars(charMaps[id],mods[id](val));const apply=(fn,val)=>{if(!val){return val;}
const parts=val.split(reExcluded);const modified=parts.map(function(part){if(reExcluded.test(part)){return part;}
return fn(part);});return modified.join('');};return _pseudo={name:transform(name),process:str=>apply(transform,str)};};}
const pseudo=Object.defineProperties(Object.create(null),{'fr-x-psaccent':{enumerable:true,get:createGetter('fr-x-psaccent','Runtime Accented')},'ar-x-psbidi':{enumerable:true,get:createGetter('ar-x-psbidi','Runtime Bidi')}});function emit(listeners,...args){const type=args.shift();if(listeners['*']){listeners['*'].slice().forEach(listener=>listener.apply(this,args));}
if(listeners[type]){listeners[type].slice().forEach(listener=>listener.apply(this,args));}}
function addEventListener(listeners,type,listener){if(!(type in listeners)){listeners[type]=[];}
listeners[type].push(listener);}
function removeEventListener(listeners,type,listener){const typeListeners=listeners[type];const pos=typeListeners.indexOf(listener);if(pos===-1){return;}
typeListeners.splice(pos,1);}
let Env=(function(){function Env(fetchResource){_classCallCheck(this,Env);this.fetchResource=fetchResource;this.resCache=new Map();this.resRefs=new Map();this.numberFormatters=null;this.parsers={properties:PropertiesParser,l20n:L20nParser};const listeners={};this.emit=emit.bind(this,listeners);this.addEventListener=addEventListener.bind(this,listeners);this.removeEventListener=removeEventListener.bind(this,listeners);}
_createClass(Env,[{key:'createContext',value:function createContext(langs,resIds){const ctx=new Context(this,langs,resIds);resIds.forEach(resId=>{const usedBy=this.resRefs.get(resId)||0;this.resRefs.set(resId,usedBy+1);});return ctx;}},{key:'destroyContext',value:function destroyContext(ctx){ctx.resIds.forEach(resId=>{const usedBy=this.resRefs.get(resId)||0;if(usedBy>1){return this.resRefs.set(resId,usedBy-1);}
this.resRefs.delete(resId);this.resCache.forEach((val,key)=>key.startsWith(resId)?this.resCache.delete(key):null);});}},{key:'_parse',value:function _parse(syntax,lang,data){const parser=this.parsers[syntax];if(!parser){return data;}
const emit=(type,err)=>this.emit(type,amendError(lang,err));return parser.parse.call(parser,emit,data);}},{key:'_create',value:function _create(lang,entries){if(lang.src!=='pseudo'){return entries;}
const pseudoentries=Object.create(null);for(let key in entries){pseudoentries[key]=walkEntry(entries[key],pseudo[lang.code].process);}
return pseudoentries;}},{key:'_getResource',value:function _getResource(lang,res){const cache=this.resCache;const id=res+lang.code+lang.src;if(cache.has(id)){return cache.get(id);}
const syntax=res.substr(res.lastIndexOf('.')+1);const saveEntries=data=>{const entries=this._parse(syntax,lang,data);cache.set(id,this._create(lang,entries));};const recover=err=>{err.lang=lang;this.emit('fetcherror',err);cache.set(id,err);};const langToFetch=lang.src==='pseudo'?{code:'en-US',src:'app',ver:lang.ver}:lang;const resource=this.fetchResource(res,langToFetch).then(saveEntries,recover);cache.set(id,resource);return resource;}}]);return Env;})();function amendError(lang,err){err.lang=lang;return err;}
function prioritizeLocales(def,availableLangs,requested){let supportedLocale;for(let i=0;i<requested.length;i++){const locale=requested[i];if(availableLangs.indexOf(locale)!==-1){supportedLocale=locale;break;}}
if(!supportedLocale||supportedLocale===def){return[def];}
return[supportedLocale,def];}
function negotiateLanguages({appVersion,defaultLang,availableLangs},additionalLangs,prevLangs,requestedLangs){const allAvailableLangs=Object.keys(availableLangs).concat(Object.keys(additionalLangs)).concat(Object.keys(pseudo));const newLangs=prioritizeLocales(defaultLang,allAvailableLangs,requestedLangs);const langs=newLangs.map(code=>({code:code,src:getLangSource(appVersion,availableLangs,additionalLangs,code),ver:appVersion}));return{langs,haveChanged:!arrEqual(prevLangs,newLangs)};}
function arrEqual(arr1,arr2){return arr1.length===arr2.length&&arr1.every((elem,i)=>elem===arr2[i]);}
function getMatchingLangpack(appVersion,langpacks){for(let i=0,langpack;langpack=langpacks[i];i++){if(langpack.target===appVersion){return langpack;}}
return null;}
function getLangSource(appVersion,availableLangs,additionalLangs,code){if(additionalLangs&&additionalLangs[code]){const lp=getMatchingLangpack(appVersion,additionalLangs[code]);if(lp&&(!(code in availableLangs)||parseInt(lp.revision)>availableLangs[code])){return'extra';}}
if(code in pseudo&&!(code in availableLangs)){return'pseudo';}
return'app';}
let Remote=(function(){function Remote(fetchResource,broadcast){_classCallCheck(this,Remote);this.broadcast=broadcast;this.env=new Env(fetchResource);this.ctxs=new Map();}
_createClass(Remote,[{key:'registerView',value:function registerView(view,resources,meta,additionalLangs,requestedLangs){const{langs}=negotiateLanguages(meta,additionalLangs,[],requestedLangs);this.ctxs.set(view,this.env.createContext(langs,resources));return langs;}},{key:'unregisterView',value:function unregisterView(view){this.ctxs.delete(view);return true;}},{key:'formatEntities',value:function formatEntities(view,keys){return this.ctxs.get(view).formatEntities(...keys);}},{key:'formatValues',value:function formatValues(view,keys){return this.ctxs.get(view).formatValues(...keys);}},{key:'changeLanguages',value:function changeLanguages(view,meta,additionalLangs,requestedLangs){const oldCtx=this.ctxs.get(view);const prevLangs=oldCtx.langs;const newLangs=negotiateLanguages(meta,additionalLangs,prevLangs,requestedLangs);this.ctxs.set(view,this.env.createContext(newLangs.langs,oldCtx.resIds));return newLangs;}},{key:'requestLanguages',value:function requestLanguages(requestedLangs){this.broadcast('languageschangerequest',requestedLangs);}},{key:'getName',value:function getName(code){return pseudo[code].name;}},{key:'processString',value:function processString(code,str){return pseudo[code].process(str);}}]);return Remote;})();const remote=new Remote(fetchResource,broadcast);remote.service=new Service('l20n').method('registerView',(...args)=>remote.registerView(...args)).method('requestLanguages',(...args)=>remote.requestLanguages(...args)).method('changeLanguages',(...args)=>remote.changeLanguages(...args)).method('formatEntities',(...args)=>remote.formatEntities(...args)).method('formatValues',(...args)=>remote.formatValues(...args)).method('getName',(...args)=>remote.getName(...args)).method('processString',(...args)=>remote.processString(...args)).on('disconnect',clientId=>remote.unregisterView(clientId)).listen(channel);})();

View file

@ -0,0 +1,16 @@
;;;;;;;(function(global){'use strict';const helperCache=new Map();const knownObjects={datetime:{create:function(options){const customOptions=Object.assign({},options);if(options.hour){customOptions.hour12=navigator.mozHour12;}
return Intl.DateTimeFormat(navigator.languages,customOptions);},isAffected:function(obj,reason){if(['languagechange','moztimechange'].includes(reason)){return true;}
if(reason==='timeformatchange'){return'hour'in obj.options;}
return false;}},mozdatetime:{create:function(options){const customOptions=Object.assign({},options);if(options.hour){customOptions.hour12=navigator.mozHour12;}
return mozIntl.DateTimeFormat(navigator.languages,customOptions);},isAffected:function(obj,reason){if(['languagechange','moztimechange'].includes(reason)){return true;}
if(reason==='timeformatchange'){return'hour'in obj.options;}
return false;}},mozduration:{create:function(options){return mozIntl.DurationFormat(navigator.languages,options);},isAffected:function(obj,reason){return reason==='languagechange';}},number:{create:function(options){return Intl.NumberFormat(navigator.languages,options);},isAffected:function(obj,reason){return reason==='languagechange';}},collator:{create:function(options){return Intl.Collator(navigator.languages,options);},isAffected:function(obj,reason){return reason==='languagechange';}},};global.IntlHelper={define:function(name,type,options){if(!knownObjects.hasOwnProperty(type)){throw new Error('Unknown type: '+type);}
if(helperCache.has(name)){throw new Error('Intl Object with name "'+name+'" already exists');}
helperCache.set(name,{options,type,intlObject:undefined,listeners:new Set()});},get:function(name){if(!helperCache.has(name)){throw new Error('Intl Object with name "'+name+'" is not defined');}
const obj=helperCache.get(name);if(!knownObjects.hasOwnProperty(obj.type)){throw new Error('Unknown type: '+obj.type);}
if(obj.intlObject===undefined){obj.intlObject=knownObjects[obj.type].create(obj.options);}
return obj.intlObject;},observe:function(names,cb){if(typeof names==='string'){names=[names];}
for(var name of names){if(!helperCache.has(name)){throw new Error('Intl Object with name "'+name+'" is not defined');}
const obj=helperCache.get(name);obj.listeners.add(cb);}},unobserve:function(name,cb){const obj=helperCache.get(name);obj.listeners.delete(cb);},_fireObservers:function(affectedObjects,...args){const affectedCallbacks=new Set();affectedObjects.forEach(obj=>obj.listeners.forEach(cb=>affectedCallbacks.add(cb)));affectedCallbacks.forEach(cb=>{try{cb(...args);}catch(e){console.error('Error in callback: '+e.toString());console.error(e.stack);}});},handleEvent:function(evt){const affectedObjects=resetObjects(evt.type);if(evt.type==='languagechange'&&navigator.mozL10n){waitOnDOMLocalized(()=>global.IntlHelper._fireObservers(affectedObjects));}else{global.IntlHelper._fireObservers(affectedObjects);}},_resetObjectCache:function(){helperCache.clear();}};function waitOnDOMLocalized(cb){const resolver=()=>{document.removeEventListener('DOMRetranslated',resolver);window.removeEventListener('localized',resolver);cb();};document.addEventListener('DOMRetranslated',resolver,false);window.addEventListener('localized',resolver,false);}
function resetObjects(reason){const affectedObjects=new Set();helperCache.forEach((obj,name)=>{if(knownObjects[obj.type].isAffected(obj,reason)){obj.intlObject=undefined;affectedObjects.add(obj);}});return affectedObjects;}
window.addEventListener('timeformatchange',global.IntlHelper,false);window.addEventListener('languagechange',global.IntlHelper,false);window.addEventListener('moztimechange',global.IntlHelper,false);})(this);

View file

@ -0,0 +1,11 @@
;;;;'use strict';var LazyLoader=(function(){function LazyLoader(){this._loaded={};this._isLoading={};}
LazyLoader.prototype={_js:function(file,callback){var script=document.createElement('script');script.src=file;script.async=false;script.addEventListener('load',callback);document.head.appendChild(script);this._isLoading[file]=script;},_css:function(file,callback){var style=document.createElement('link');style.type='text/css';style.rel='stylesheet';style.href=file;document.head.appendChild(style);callback();},_html:function(domNode,callback){if(domNode.getAttribute('is')){this.load(['/shared/js/html_imports.js'],function(){HtmlImports.populate(callback);}.bind(this));return;}
for(var i=0;i<domNode.childNodes.length;i++){if(domNode.childNodes[i].nodeType==document.COMMENT_NODE){domNode.innerHTML=domNode.childNodes[i].nodeValue;break;}}
window.dispatchEvent(new CustomEvent('lazyload',{detail:domNode}));callback();},getJSON:function(file,mozSystem){return new Promise(function(resolve,reject){var xhr;if(mozSystem){xhr=new XMLHttpRequest({mozSystem:true});}else{xhr=new XMLHttpRequest();}
xhr.open('GET',file,true);xhr.responseType='json';xhr.onerror=function(error){reject(error);};xhr.onload=function(){if(xhr.response!==null){resolve(xhr.response);}else{reject(new Error('No valid JSON object was found ('+
xhr.status+' '+xhr.statusText+')'));}};xhr.send();});},load:function(files,callback){var deferred={};deferred.promise=new Promise(resolve=>{deferred.resolve=resolve;});if(!Array.isArray(files)){files=[files];}
var loadsRemaining=files.length,self=this;function perFileCallback(file){if(self._isLoading[file]){delete self._isLoading[file];}
self._loaded[file]=true;if(--loadsRemaining===0){deferred.resolve();if(callback){callback();}}}
for(var i=0;i<files.length;i++){var file=files[i];if(this._loaded[file.id||file]){perFileCallback(file);}else if(this._isLoading[file]){this._isLoading[file].addEventListener('load',perFileCallback.bind(null,file));}else{var method,idx;if(typeof file==='string'){method=file.match(/\.([^.]+)$/)[1];idx=file;}else{method='html';idx=file.id;}
this['_'+method](file,perFileCallback.bind(null,idx));}}
return deferred.promise;}};return new LazyLoader();}());

View file

@ -0,0 +1,379 @@
/* globals BluetoothHelper */
'use strict';
/**
* Media Remote Controls
*
* For media players, like music and video apps should support the capabilities
* to receive and interact with the remote media commands from the connected
* device/client, it might be a bluetooth device that supports A2DP and AVRCP,
* or a remote client, such as the controls in Lock Screen/Utility tray, which
* in the system app and uses the IAC.
*
* Since both AVRCP and IAC use system message to communication, this module
* is designed for handling all the remote commands from system message. With
* this module, players can just set the command listeners to the corresponding
* commands, they might be the common media actions that a player should have,
* so a player that uses this module doesn't need to write duplicate logic on
* handling the same commands from the different requesters, and doesn't need
* to care about where the requests are from.
*
* More, this module also handle the bluetooth states because to receive the
* remote request or send the metadata and play status depends on the bluetooth
* availability and the connection of A2DP, the player that uses the module
* will automatically receive the requests or send the play information to the
* remote client when the bluetooth states change.
*
*/
// Commands for AVRCP.
var AVRCP = {
PLAY_PRESS: 'media-play-button-press',
PLAY_RELEASE: 'media-play-button-release',
PAUSE_PRESS: 'media-pause-button-press',
PAUSE_RELEASE: 'media-pause-button-release',
PLAY_PAUSE_PRESS: 'media-play-pause-button-press',
PLAY_PAUSE_RELEASE: 'media-play-pause-button-release',
STOP_PRESS: 'media-stop-button-press',
STOP_RELEASE: 'media-stop-button-release',
NEXT_PRESS: 'media-next-track-button-press',
NEXT_RELEASE: 'media-next-track-button-release',
PREVIOUS_PRESS: 'media-previous-track-button-press',
PREVIOUS_RELEASE: 'media-previous-track-button-release',
FAST_FORWARD_PRESS: 'media-fast-forward-button-press',
FAST_FORWARD_RELEASE: 'media-fast-forward-button-release',
REWIND_PRESS: 'media-rewind-button-press',
REWIND_RELEASE: 'media-rewind-button-release'
};
// Commands for Inter-App Communication.
var IAC = {
PLAY_PRESS: 'play',
PLAY_PAUSE_PRESS: 'playpause',
PAUSE_PRESS: 'pause',
STOP_PRESS: 'stop',
NEXT_PRESS: 'nexttrack',
PREVIOUS_PRESS: 'prevtrack',
FAST_FORWARD_PRESS: 'fastforwardstart',
FAST_FORWARD_RELEASE: 'fastforwardend',
REWIND_PRESS: 'rewindstart',
REWIND_RELEASE: 'rewindend',
UPDATE_METADATA: 'updatemetadata',
UPDATE_PLAYSTATUS: 'updateplaystatus'
};
// Commands for the players to register, events will be fired to the listeners.
var REMOTE_CONTROLS = {
PLAY: 'play',
PLAY_PAUSE: 'playpause',
PAUSE: 'pause',
STOP: 'stop',
NEXT: 'next',
PREVIOUS: 'previous',
SEEK_PRESS: 'seekpress',
SEEK_RELEASE: 'seekrelease',
UPDATE_METADATA: 'updatemetadata',
UPDATE_PLAYSTATUS: 'updateplaystatus'
};
/**
* @constructor
*/
function MediaRemoteControls() {
this._bluetoothHelper = null;
this._commandListeners = {};
this._isSCOConnected = false;
// Create the empty object for all command listeners.
for (var command in REMOTE_CONTROLS) {
this._commandListeners[REMOTE_CONTROLS[command]] = [];
}
}
/**
* Add the command listener so that it will be executed when the remote
* commands fired.
*
* @param {String} command
* @param {Object} listener
*/
MediaRemoteControls.prototype.addCommandListener = function(command, listener) {
if (this._commandListeners[command]) {
this._commandListeners[command].push(listener);
}
};
/**
* Remove the command listener.
*
* @param {String} name
* @param {Object} listener
*/
MediaRemoteControls.prototype.removeCommandListener = function(name, listener) {
if (this._commandListeners[name]) {
var index = -1;
this._commandListeners[name].forEach(function(currListener, i) {
if (currListener === listener) {
index = i;
}
});
if (index !== -1) {
this._commandListeners[name].splice(index, 1);
}
}
};
/**
* Setup the bluetooth/system message, IAC and wired controls.
*/
MediaRemoteControls.prototype.start = function(callback) {
this._setupBluetooth(callback);
this._setupIAC();
};
/*
* Setup and configure the bluetooth/system message.
*/
MediaRemoteControls.prototype._setupBluetooth = function(callback) {
var self = this;
// AVRCP commands use system message.
navigator.mozSetMessageHandler(
'media-button', this._commandHandler.bind(this)
);
// bluetooth will be handled with BluetoothHelper
// to send metadata and play status when those information are changed.
this._bluetoothHelper = new BluetoothHelper();
this._bluetoothHelper.onrequestmediaplaystatus = playstatusHandler;
this._bluetoothHelper.ona2dpstatuschanged = a2dpConnectionHandler;
this._bluetoothHelper.onscostatuschanged = scoConnectionHandler;
if (callback) {
callback();
}
function playstatusHandler() {
if (self._commandListeners.updateplaystatus.length > 0) {
self._commandHandler(REMOTE_CONTROLS.UPDATE_PLAYSTATUS);
}
}
// A2DP is connected: update the status to the bluetooth device.
// A2DP is disconnected: pause the player like the headphone is unplugged.
function a2dpConnectionHandler(event) {
var isConnected = event.status;
if (isConnected && self._commandListeners.updatemetadata.length > 0) {
self._commandHandler(REMOTE_CONTROLS.UPDATE_METADATA);
}
}
// Also expose the SCO status with the custom event because the SCO connection
// could occupy the audio channel that the media apps using, besides sending
// the play/pause commands, these additional status can help the media apps
// to decide if they want more interaction with users while SCO is connected.
function scoConnectionHandler(event) {
self._isSCOConnected = event.status;
if (self._isSCOConnected) {
self._commandHandler(AVRCP.PAUSE_PRESS);
}
else {
self._commandHandler(AVRCP.PLAY_PRESS);
}
}
};
/*
* Setup and configure the IAC.
*/
MediaRemoteControls.prototype._setupIAC = function() {
var self = this;
this._queuedMessages = [];
// Set up Inter-App Communications
navigator.mozApps.getSelf().onsuccess = function() {
var app = this.result;
// If IAC doesn't exist, just bail out.
if (!app.connect) {
this._queuedMessages = null;
return;
}
app.connect('mediacomms').then(function(ports) {
self._ports = ports;
self._ports.forEach(function(port) {
port.onmessage = function(event) {
self._commandHandler(event.data.command);
};
self._queuedMessages.forEach(function(message) {
port.postMessage(message);
});
});
self._queuedMessages = null;
});
};
};
// Synchronous connection-oriented(SCO) link is the type of radio link used
// for voice data of bluetooth, it establishes when the user is in a call via
// bluetooth hands-free headset. And since only voice will be routed to the
// headset during a call, the media apps can use this api to know the SCO
// status, so that the apps can adjust the ui or notify the user to get better
// user experience, for more detail, please see:
// http://en.wikipedia.org/wiki/Bluetooth_protocols
MediaRemoteControls.prototype.getSCOStatus = function(callback) {
this._bluetoothHelper.isScoConnected(callback, function onerror() {
callback(false);
});
};
MediaRemoteControls.prototype._postMessage = function(name, value) {
var message = {type: name, data: value};
if (!this._ports) {
if (this._queuedMessages) {
this._queuedMessages.push(message);
}
} else {
this._ports.forEach(function(port) {
port.postMessage(message);
});
}
};
/**
* Convert the received messages to the REMOTE_CONTROLS commands, and create
* custom event then pass the new event to the command listeners executer.
*
* @param {String} message
*/
MediaRemoteControls.prototype._commandHandler = function(message) {
var type = 'remote';
var option = {};
switch (message) {
case AVRCP.PLAY_PRESS:
case IAC.PLAY_PRESS:
option.detail = {
command: REMOTE_CONTROLS.PLAY,
isSCOConnected: this._isSCOConnected
};
break;
case AVRCP.PLAY_PAUSE_PRESS:
case IAC.PLAY_PAUSE_PRESS:
option.detail = {
command: REMOTE_CONTROLS.PLAY_PAUSE,
isSCOConnected: this._isSCOConnected
};
break;
case AVRCP.PAUSE_PRESS:
case IAC.PAUSE_PRESS:
option.detail = {
command: REMOTE_CONTROLS.PAUSE,
isSCOConnected: this._isSCOConnected
};
break;
case AVRCP.STOP_PRESS:
case IAC.STOP_PRESS:
option.detail = { command: REMOTE_CONTROLS.STOP };
break;
case AVRCP.NEXT_PRESS:
case IAC.NEXT_PRESS:
option.detail = { command: REMOTE_CONTROLS.NEXT };
break;
case AVRCP.PREVIOUS_PRESS:
case IAC.PREVIOUS_PRESS:
option.detail = { command: REMOTE_CONTROLS.PREVIOUS };
break;
case AVRCP.FAST_FORWARD_PRESS:
case IAC.FAST_FORWARD_PRESS:
option.detail = { command: REMOTE_CONTROLS.SEEK_PRESS, direction: 1 };
break;
case AVRCP.REWIND_PRESS:
case IAC.REWIND_PRESS:
option.detail = { command: REMOTE_CONTROLS.SEEK_PRESS, direction: -1 };
break;
case AVRCP.FAST_FORWARD_RELEASE:
case IAC.FAST_FORWARD_RELEASE:
case AVRCP.REWIND_RELEASE:
case IAC.REWIND_RELEASE:
option.detail = { command: REMOTE_CONTROLS.SEEK_RELEASE };
break;
case IAC.UPDATE_METADATA:
case IAC.UPDATE_PLAYSTATUS:
case REMOTE_CONTROLS.UPDATE_METADATA:
case REMOTE_CONTROLS.UPDATE_PLAYSTATUS:
option.detail = { command: message };
break;
default:
return;
}
var event = new CustomEvent(type, option);
this._executeCommandListeners(event);
};
/**
* Fire the event to the command listeners.
*
* @param {Object} event
*/
MediaRemoteControls.prototype._executeCommandListeners = function(event) {
if (!event.detail) {
return;
}
this._commandListeners[event.detail.command].forEach(function(listener) {
listener(event);
});
};
MediaRemoteControls.prototype.notifyAppInfo = function(info) {
// Send the app info via IAC.
this._postMessage('appinfo', info);
};
/**
* Send the updated metadata to the remote requester/receiver.
*
* @param {Object} metadata
*/
MediaRemoteControls.prototype.notifyMetadataChanged = function(metadata) {
// Send the new metadata via bluetooth.
this._bluetoothHelper.sendMediaMetaData(metadata,
function onsuccess() {
// To do nothing here, expect onsuccess normally.
},
function onerror() {
console.log('Sending Metadata error');
});
// Now, send it via IAC.
this._postMessage('nowplaying', metadata);
};
/**
* Send the updated play status to the remote requester/receiver.
*
* @param {Object} status
*/
MediaRemoteControls.prototype.notifyStatusChanged = function(status) {
// Don't send the interrupted statuses to the remote client because
// they are not the AVRCP statuses.
if (status === 'mozinterruptbegin') {
return;
}
// Send the new status via bluetooth.
this._bluetoothHelper.sendMediaPlayStatus(status,
function onsuccess() {
// To do nothing here, expect onsuccess normally.
},
function onerror() {
console.log('Sending Playstatus error');
});
// Now, send it via IAC.
this._postMessage('status', status);
};

View file

@ -0,0 +1,148 @@
;;;'use strict';var MediaDB=(function(){function MediaDB(mediaType,metadataParser,options){this.mediaType=mediaType;this.metadataParser=metadataParser;if(!options){options={};}
this.indexes=options.indexes||[];this.version=options.version||1;this.mimeTypes=options.mimeTypes;this.autoscan=(options.autoscan!==undefined)?options.autoscan:true;this.state=MediaDB.OPENING;this.scanning=false;this.initialScanComplete=false;this.parsingBigFiles=false;this.updateRecord=options.updateRecord;this.reparsedRecord=options.reparsedRecord;if(options.excludeFilter&&(options.excludeFilter instanceof RegExp)){this.clientExcludeFilter=options.excludeFilter;}
this.batchHoldTime=options.batchHoldTime||100;this.batchSize=options.batchSize||0;this.dbname='MediaDB/'+this.mediaType+'/';var media=this;this.details={eventListeners:{},pendingInsertions:[],pendingDeletions:[],whenDoneProcessing:[],pendingCreateNotifications:[],pendingDeleteNotifications:[],pendingNotificationTimer:null,newestFileModTime:0};if(!this.metadataParser){this.metadataParser=function(file,callback){setTimeout(function(){callback({});},0);};}
var dbVersion=(0xFFFF&this.version)<<16|(0xFFFF&MediaDB.VERSION);var openRequest=indexedDB.open(this.dbname,dbVersion);openRequest.onerror=function(e){console.error('MediaDB():',openRequest.error.name);};openRequest.onblocked=function(e){console.error('indexedDB.open() is blocked in MediaDB()');};openRequest.onupgradeneeded=function(e){var db=openRequest.result;var transaction=e.target.transaction;var oldVersion=e.oldVersion;var oldDbVersion=0xFFFF&oldVersion;var oldClientVersion=0xFFFF&(oldVersion>>16);if(oldClientVersion===0){oldDbVersion=2;oldClientVersion=oldVersion/oldDbVersion;}
if(0===db.objectStoreNames.length){createObjectStores(db);}else{handleUpgrade(db,transaction,oldDbVersion,oldClientVersion);}};openRequest.onsuccess=function(e){media.db=openRequest.result;media.db.onerror=function(event){console.error('MediaDB: ',event.target.error&&event.target.error.name);};changeState(media,MediaDB.ENUMERABLE);var cursorRequest=media.db.transaction('files','readonly').objectStore('files').index('date').openCursor(null,'prev');cursorRequest.onerror=function(){console.error('MediaDB initialization error',cursorRequest.error);};cursorRequest.onsuccess=function(){var cursor=cursorRequest.result;if(cursor){media.details.newestFileModTime=cursor.value.date;}
else{media.details.newestFileModTime=0;}
initDeviceStorage();};};function createObjectStores(db){var filestore=db.createObjectStore('files',{keyPath:'name'});filestore.createIndex('date','date');media.indexes.forEach(function(indexName){if(indexName==='name'||indexName==='date'){return;}
filestore.createIndex(indexName,indexName);});}
function enumerateOldFiles(store,callback){var openCursorReq=store.openCursor();openCursorReq.onsuccess=function(){var cursor=openCursorReq.result;if(cursor){callback(cursor.value);cursor.continue();}};}
function handleUpgrade(db,trans,oldDbVersion,oldClientVersion){media.state=MediaDB.UPGRADING;var evtDetail={'oldMediaDBVersion':oldDbVersion,'oldClientVersion':oldClientVersion,'newMediaDBVersion':MediaDB.VERSION,'newClientVersion':media.version};dispatchEvent(media,'upgrading',evtDetail);var store=trans.objectStore('files');if(media.version!=oldClientVersion){upgradeIndexesChanges(store);}
var clientUpgradeNeeded=(media.version!=oldClientVersion)&&media.updateRecord;if((2!=oldDbVersion||3!=MediaDB.VERSION)&&!clientUpgradeNeeded){return;}
enumerateOldFiles(store,function doUpgrade(dbfile){if(2==oldDbVersion&&3==MediaDB.VERSION){upgradeDBVer2to3(store,dbfile);}
if(clientUpgradeNeeded){handleClientUpgrade(store,dbfile,oldClientVersion);}});}
function upgradeIndexesChanges(store){var dbIndexes=store.indexNames;var clientIndexes=media.indexes;for(var i=0;i<dbIndexes.length;i++){if('name'===dbIndexes[i]||'date'===dbIndexes[i]){continue;}
if(clientIndexes.indexOf(dbIndexes[i])<0){store.deleteIndex(dbIndexes[i]);}}
for(i=0;i<clientIndexes.length;i++){if(!dbIndexes.contains(clientIndexes[i])){store.createIndex(clientIndexes[i],clientIndexes[i]);}}}
function upgradeDBVer2to3(store,dbfile){if(dbfile.name[0]==='/'){return;}
store.delete(dbfile.name);dbfile.name='/sdcard/'+dbfile.name;store.add(dbfile);}
function handleClientUpgrade(store,dbfile,oldClientVersion){try{dbfile.metadata=media.updateRecord(dbfile,oldClientVersion,media.version);if(dbfile.needsReparse&&!media.reparsedRecord){console.warn('client app requested reparse, but no reparsedRecord was set');delete dbfile.needsReparse;}
store.put(dbfile);}catch(ex){console.warn('client app updates record, '+dbfile.name+', failed: '+ex.message);}}
function initDeviceStorage(){var details=media.details;details.storages=navigator.getDeviceStorages(mediaType);details.availability={};getStorageAvailability();function getStorageAvailability(){var next=0;getNextAvailability();function getNextAvailability(){if(next>=details.storages.length){setupHandlers();return;}
var s=details.storages[next++];var name=s.storageName;var req=s.available();req.onsuccess=function(e){details.availability[name]=req.result;getNextAvailability();};req.onerror=function(e){details.availability[name]='unavailable';getNextAvailability();};}}
function setupHandlers(){for(var i=0;i<details.storages.length;i++){details.storages[i].addEventListener('change',changeHandler);}
details.dsEventListener=changeHandler;sendInitialEvent();}
function sendInitialEvent(){var state=getState(details.availability);changeState(media,state);if(media.autoscan){scan(media);}}
function getState(availability){var n=0;var a=0;var u=0;var s=0;for(var name in availability){n++;switch(availability[name]){case'available':a++;break;case'unavailable':u++;break;case'shared':s++;break;}}
if(s>0){return MediaDB.UNMOUNTED;}
if(u===n){return MediaDB.NOCARD;}
return MediaDB.READY;}
function changeHandler(e){switch(e.reason){case'modified':case'deleted':fileChangeHandler(e);return;case'available':case'unavailable':case'shared':volumeChangeHandler(e);return;default:return;}}
function volumeChangeHandler(e){var storageName=e.target.storageName;if(details.availability[storageName]===e.reason){return;}
var oldState=media.state;details.availability[storageName]=e.reason;var newState=getState(details.availability);if(newState!==oldState){changeState(media,newState);if(newState===MediaDB.READY){if(media.autoscan){scan(media);}}
else{endscan(media);}}
else if(newState===MediaDB.READY){if(e.reason==='available'){dispatchEvent(media,'ready');if(media.autoscan){scan(media);}}
else if(e.reason==='unavailable'){dispatchEvent(media,'cardremoved');deleteAllFiles(storageName);}}}
function fileChangeHandler(e){var filename=e.path;if(ignoreName(media,filename)){return;}
if(e.reason==='modified'){insertRecord(media,filename);}else{deleteRecord(media,filename);}}
function deleteAllFiles(storageName){var storagePrefix=storageName?'/'+storageName+'/':'';var store=media.db.transaction('files').objectStore('files');var cursorRequest=store.openCursor();cursorRequest.onsuccess=function(){var cursor=cursorRequest.result;if(cursor){if(cursor.value.name.startsWith(storagePrefix)){deleteRecord(media,cursor.value.name);}
cursor.continue();}};}}}
MediaDB.prototype={close:function close(){this.db.close();for(var i=0;i<this.details.storages.length;i++){var s=this.details.storages[i];s.removeEventListener('change',this.details.dsEventListener);}
changeState(this,MediaDB.CLOSED);},addEventListener:function addEventListener(type,listener){if(!this.details.eventListeners.hasOwnProperty(type)){this.details.eventListeners[type]=[];}
var listeners=this.details.eventListeners[type];if(listeners.indexOf(listener)!==-1){return;}
listeners.push(listener);},removeEventListener:function removeEventListener(type,listener){if(!this.details.eventListeners.hasOwnProperty(type)){return;}
var listeners=this.details.eventListeners[type];var position=listeners.indexOf(listener);if(position===-1){return;}
listeners.splice(position,1);},getFileInfo:function getFile(filename,callback,errback){if(this.state===MediaDB.OPENING){throw Error('MediaDB is not ready. State: '+this.state);}
var media=this;var read=media.db.transaction('files','readonly').objectStore('files').get(filename);read.onerror=function(){var msg='MediaDB.getFileInfo: unknown filename: '+filename;if(errback){errback(msg);}else{console.error(msg);}};read.onsuccess=function(){if(callback){callback(read.result);}};},getFile:function getFile(filename,callback,errback){if(this.state!==MediaDB.READY){throw Error('MediaDB is not ready. State: '+this.state);}
var storage=navigator.getDeviceStorage(this.mediaType);var getRequest=storage.get(filename);getRequest.onsuccess=function(){callback(getRequest.result);};getRequest.onerror=function(){var errmsg=getRequest.error&&getRequest.error.name;if(errback){errback(errmsg);}else{console.error('MediaDB.getFile:',errmsg);}};},deleteFile:function deleteFile(filename){if(this.state!==MediaDB.READY){throw Error('MediaDB is not ready. State: '+this.state);}
var storage=navigator.getDeviceStorage(this.mediaType);storage.delete(filename).onerror=function(e){console.error('MediaDB.deleteFile(): Failed to delete',filename,'from DeviceStorage:',e.target.error);};},addFile:function addFile(filename,file){if(this.state!==MediaDB.READY){throw Error('MediaDB is not ready. State: '+this.state);}
var media=this;var storage=navigator.getDeviceStorage(media.mediaType);var deletereq=storage.delete(filename);deletereq.onsuccess=deletereq.onerror=save;function save(){var request=storage.addNamed(file,filename);request.onerror=function(){console.error('MediaDB: Failed to store',filename,'in DeviceStorage:',request.error);};}},updateMetadata:function(filename,metadata,callback){if(this.state===MediaDB.OPENING){throw Error('MediaDB is not ready. State: '+this.state);}
var media=this;var read=media.db.transaction('files','readonly').objectStore('files').get(filename);read.onerror=function(){console.error('MediaDB.updateMetadata called with unknown filename');};read.onsuccess=function(){var fileinfo=read.result;Object.keys(metadata).forEach(function(key){fileinfo.metadata[key]=metadata[key];});var write=media.db.transaction('files','readwrite').objectStore('files').put(fileinfo);write.onerror=function(){console.error('MediaDB.updateMetadata: database write failed',write.error&&write.error.name);};if(callback){write.onsuccess=function(){callback();};}};},count:function(key,range,callback){if(this.state!==MediaDB.READY&&this.state!==MediaDB.ENUMERABLE){throw Error('MediaDB is not ready or enumerable. State: '+this.state);}
if(arguments.length===1){callback=key;range=undefined;key=undefined;}
else if(arguments.length===2){callback=range;range=key;key=undefined;}
var store=this.db.transaction('files').objectStore('files');if(key&&key!=='name'){store=store.index(key);}
var countRequest=store.count(range||null);countRequest.onerror=function(){console.error('MediaDB.count() failed with',countRequest.error);};countRequest.onsuccess=function(e){callback(e.target.result);};},enumerate:function enumerate(key,range,direction,callback){if(this.state!==MediaDB.READY&&this.state!==MediaDB.ENUMERABLE){throw Error('MediaDB is not ready or enumerable. State: '+this.state);}
var handle={state:'enumerating'};if(arguments.length===1){callback=key;key=undefined;}
else if(arguments.length===2){callback=range;range=undefined;}
else if(arguments.length===3){callback=direction;direction=undefined;}
var store=this.db.transaction('files').objectStore('files');if(key&&key!=='name'){store=store.index(key);}
var cursorRequest=store.openCursor(range||null,direction||'next');cursorRequest.onerror=function(){console.error('MediaDB.enumerate() failed with',cursorRequest.error);handle.state='error';};cursorRequest.onsuccess=function(){if(handle.state==='cancelling'){handle.state='cancelled';return;}
var cursor=cursorRequest.result;if(cursor){try{var fileinfo=cursor.value;if(!fileinfo.fail){callback(fileinfo);}}
catch(e){console.warn('MediaDB.enumerate(): callback threw',e,e.stack);}
cursor.continue();}
else{handle.state='complete';callback(null);}};return handle;},advancedEnumerate:function(key,range,direction,index,callback){if(this.state!==MediaDB.READY){throw Error('MediaDB is not ready. State: '+this.state);}
var handle={state:'enumerating'};var store=this.db.transaction('files').objectStore('files');if(key&&key!=='name'){store=store.index(key);}
var cursorRequest=store.openCursor(range||null,direction||'next');var isTarget=false;cursorRequest.onerror=function(){console.error('MediaDB.enumerate() failed with',cursorRequest.error);handle.state='error';};cursorRequest.onsuccess=function(){if(handle.state==='cancelling'){handle.state='cancelled';return;}
var cursor=cursorRequest.result;if(cursor){try{if(index===0){isTarget=true;}
var fileinfo=cursor.value;if(!fileinfo.fail&&isTarget){callback(cursor.value);cursor.continue();}
else{cursor.advance(index);isTarget=true;}}
catch(e){console.warn('MediaDB.enumerate(): callback threw',e,e.stack);}}
else{handle.state='complete';callback(null);}};return handle;},enumerateAll:function enumerateAll(key,range,direction,callback){var batch=[];if(arguments.length===1){callback=key;key=undefined;}
else if(arguments.length===2){callback=range;range=undefined;}
else if(arguments.length===3){callback=direction;direction=undefined;}
return this.enumerate(key,range,direction,function(fileinfo){if(fileinfo!==null){batch.push(fileinfo);}else{callback(batch);}});},cancelEnumeration:function cancelEnumeration(handle){if(handle.state==='enumerating'){handle.state='cancelling';}},getAll:function getAll(callback){if(this.state!==MediaDB.READY&&this.state!==MediaDB.ENUMERABLE){throw Error('MediaDB is not ready or enumerable. State: '+this.state);}
var store=this.db.transaction('files').objectStore('files');var request=store.mozGetAll();request.onerror=function(){console.error('MediaDB.getAll() failed with',request.error);};request.onsuccess=function(){var all=request.result;var good=all.filter(function(fileinfo){return!fileinfo.fail;});callback(good);};},scan:function(){scan(this);},freeSpace:function freeSpace(callback){if(this.state!==MediaDB.READY){throw Error('MediaDB is not ready. State: '+this.state);}
var storage=navigator.getDeviceStorage(this.mediaType);var freereq=storage.freeSpace();freereq.onsuccess=function(){callback(freereq.result);};}};MediaDB.VERSION=3;MediaDB.OPENING='opening';MediaDB.UPGRADING='upgrading';MediaDB.ENUMERABLE='enumerable';MediaDB.READY='ready';MediaDB.NOCARD='nocard';MediaDB.UNMOUNTED='unmounted';MediaDB.CLOSED='closed';function ignore(media,file){if(ignoreName(media,file.name)){return true;}
if(media.mimeTypes&&media.mimeTypes.indexOf(file.type)===-1){return true;}
return false;}
function ignoreName(media,filename){if(media.clientExcludeFilter&&media.clientExcludeFilter.test(filename)){return true;}else{return(filename[0]==='.'||filename.indexOf('/.')!==-1);}}
function scan(media){media.scanning=true;dispatchEvent(media,'scanstart');quickScan(media.details.newestFileModTime);function quickScan(timestamp){var options;if(timestamp>0){media.details.firstscan=false;options={since:new Date(timestamp+1)};}
else{media.details.firstscan=true;media.details.records=[];options={};}
scanDS(media.details.storages,null,options).then(processFiles).catch(handleScanError);function processFiles(files){if(!media.scanning){return;}
files=files.sort(function(a,b){return b.lastModifiedDate-a.lastModifiedDate;});for(var i=0;i<files.length;i++){insertRecord(media,files[i]);}
whenDoneProcessing(media,function(){sendNotifications(media);if(media.details.firstscan){endscan(media);}
else{fullScan();}});}
function handleScanError(error){console.warn('Error while scanning',error);endscan(media);}}
function fullScan(){if(media.state!==MediaDB.READY){endscan(media);return;}
var dsfiles;scanDS(media.details.storages).then(function(files){dsfiles=files;getDBFiles();}).catch(function(error){console.warn('Error while scanning',error);endscan(media);});function getDBFiles(){var store=media.db.transaction('files').objectStore('files');var getAllRequest=store.mozGetAll();getAllRequest.onsuccess=function(){if(!media.scanning){return;}
var dbfiles=getAllRequest.result;compareLists(dbfiles,dsfiles);};}
function compareLists(dbfiles,dsfiles){dsfiles.sort(function(a,b){if(a.name<b.name){return-1;}else{return 1;}});var dsindex=0,dbindex=0;while(true){var dsfile;if(dsindex<dsfiles.length){dsfile=dsfiles[dsindex];}else{dsfile=null;}
var dbfile;if(dbindex<dbfiles.length){dbfile=dbfiles[dbindex];}else{dbfile=null;}
if(dsfile===null&&dbfile===null){break;}
if(dbfile===null){insertRecord(media,dsfile);dsindex++;continue;}
if(dsfile===null){deleteRecord(media,dbfile.name);dbindex++;continue;}
if(dsfile.name===dbfile.name){var lastModified=dsfile.lastModifiedDate;var timeDifference=lastModified.getTime()-dbfile.date;var sameTime=(timeDifference===0||((Math.abs(timeDifference)<=12*60*60*1000)&&(timeDifference%10*60*1000===0)));var sameSize=dsfile.size===dbfile.size;if(!sameTime||!sameSize){deleteRecord(media,dbfile.name);insertRecord(media,dsfile);}else if(dbfile.needsReparse){deleteRecord(media,dbfile.name);insertRecord(media,dsfile,dbfile.metadata);}
dsindex++;dbindex++;continue;}
if(dsfile.name<dbfile.name){insertRecord(media,dsfile);dsindex++;continue;}
if(dsfile.name>dbfile.name){deleteRecord(media,dbfile.name);dbindex++;continue;}
console.error('Assertion failed');}
insertRecord(media,null);}}
function scanDS(storage,directory,options){if(Array.isArray(storage)){return Promise.all(storage.map((s)=>scanDS(s,directory,options))).then(function(arrays){return Array.prototype.concat.apply([],arrays);});}
return new Promise(function(resolve,reject){var files=[];var cursor=storage.enumerate(directory||'',options||{});cursor.onerror=function(){if(cursor.error.name==='NotFoundError'){resolve(files);}
else{reject(cursor.error);}};cursor.onsuccess=function(){var result=cursor.result;if(result){if(!ignore(media,result)){files.push(result);}
cursor.continue();}
else{resolve(files);}};});}}
function endscan(media){if(media.scanning){media.initialScanComplete=true;media.scanning=false;media.parsingBigFiles=false;dispatchEvent(media,'scanend');}}
function insertRecord(media,fileOrName,oldMetadata){var details=media.details;details.pendingInsertions.push([fileOrName,oldMetadata]);if(details.processingQueue){return;}
processQueue(media);}
function deleteRecord(media,filename){var details=media.details;details.pendingDeletions.push(filename);if(details.processingQueue){return;}
processQueue(media);}
function whenDoneProcessing(media,f){var details=media.details;if(details.processingQueue){details.whenDoneProcessing.push(f);}else{f();}}
function processQueue(media){var details=media.details;details.processingQueue=true;next();function next(){if(details.pendingDeletions.length>0){deleteFiles();}
else if(details.pendingInsertions.length>0){insertFile(...details.pendingInsertions.shift());}
else{details.processingQueue=false;if(details.whenDoneProcessing.length>0){var functions=details.whenDoneProcessing;details.whenDoneProcessing=[];functions.forEach(function(f){f();});}}}
function deleteFiles(){var transaction=media.db.transaction('files','readwrite');var store=transaction.objectStore('files');deleteNextFile();function deleteNextFile(){if(details.pendingDeletions.length===0){next();return;}
var filename=details.pendingDeletions.shift();var request=store.delete(filename);request.onerror=function(){console.warn('MediaDB: Unknown file in deleteRecord:',filename,request.error);deleteNextFile();};request.onsuccess=function(){queueDeleteNotification(media,filename);deleteNextFile();};}}
function insertFile(f,oldMetadata){if(f===null){sendNotifications(media);endscan(media);next();return;}
if(typeof f==='string'){var storage=navigator.getDeviceStorage(media.mediaType);var getreq=storage.get(f);getreq.onerror=function(){console.warn('MediaDB: Unknown file in insertRecord:',f,getreq.error);next();};getreq.onsuccess=function(){if(media.mimeTypes&&ignore(media,getreq.result)){next();}else{parseMetadata(getreq.result,f,oldMetadata);}};}
else{parseMetadata(f,f.name,oldMetadata);}}
function parseMetadata(file,filename,oldMetadata){if(!file.lastModifiedDate){console.warn('MediaDB: parseMetadata: no lastModifiedDate for',filename,'using Date.now() until #793955 is fixed');}
var fileinfo={name:filename,type:file.type,size:file.size,date:file.lastModifiedDate?file.lastModifiedDate.getTime():Date.now()};if(fileinfo.date>details.newestFileModTime){details.newestFileModTime=fileinfo.date;}
media.metadataParser(file,gotMetadata,metadataError,parsingBigFile);function parsingBigFile(){media.parsingBigFiles=true;}
function metadataError(e){console.warn('MediaDB: error parsing metadata for',filename,':',e);fileinfo.fail=true;storeRecord(fileinfo);}
function gotMetadata(metadata){fileinfo.metadata=metadata;if(oldMetadata&&media.reparsedRecord){fileinfo.metadata=media.reparsedRecord(oldMetadata,metadata);}
storeRecord(fileinfo);if(!media.scanning){media.parsingBigFiles=false;}}}
function storeRecord(fileinfo){if(media.details.firstscan){media.details.records.push(fileinfo);if(!fileinfo.fail){queueCreateNotification(media,fileinfo);}
next();}
else{var transaction=media.db.transaction('files','readwrite');var store=transaction.objectStore('files');var request=store.add(fileinfo);request.onsuccess=function(){if(!fileinfo.fail){queueCreateNotification(media,fileinfo);}
next();};request.onerror=function(event){if(request.error.name==='ConstraintError'){event.stopPropagation();event.preventDefault();var putrequest=store.put(fileinfo);putrequest.onsuccess=function(){queueDeleteNotification(media,fileinfo.name);if(!fileinfo.fail){queueCreateNotification(media,fileinfo);}
next();};putrequest.onerror=function(){console.error('MediaDB: unexpected ConstraintError','in insertRecord for file:',fileinfo.name);next();};}
else{console.error('MediaDB: unexpected error in insertRecord:',request.error,'for file:',fileinfo.name);next();}};}}}
function queueCreateNotification(media,fileinfo){var creates=media.details.pendingCreateNotifications;creates.push(fileinfo);if(media.batchSize&&creates.length>=media.batchSize){sendNotifications(media);}else{resetNotificationTimer(media);}}
function queueDeleteNotification(media,filename){var deletes=media.details.pendingDeleteNotifications;deletes.push(filename);if(media.batchSize&&deletes.length>=media.batchSize){sendNotifications(media);}else{resetNotificationTimer(media);}}
function resetNotificationTimer(media){var details=media.details;if(details.pendingNotificationTimer){clearTimeout(details.pendingNotificationTimer);}
details.pendingNotificationTimer=setTimeout(function(){sendNotifications(media);},media.scanning?media.batchHoldTime:100);}
function sendNotifications(media){var details=media.details;if(details.pendingNotificationTimer){clearTimeout(details.pendingNotificationTimer);details.pendingNotificationTimer=null;}
if(details.pendingDeleteNotifications.length>0){var deletions=details.pendingDeleteNotifications;details.pendingDeleteNotifications=[];dispatchEvent(media,'deleted',deletions);}
if(details.pendingCreateNotifications.length>0){var creations=details.pendingCreateNotifications;details.pendingCreateNotifications=[];if(details.firstscan&&details.records.length>0){var transaction=media.db.transaction('files','readwrite');var store=transaction.objectStore('files');for(var i=0;i<details.records.length;i++){store.add(details.records[i]);}
details.records.length=0;transaction.oncomplete=function(){dispatchEvent(media,'created',creations);};}
else{dispatchEvent(media,'created',creations);}}}
function dispatchEvent(media,type,detail){var handler=media['on'+type];var listeners=media.details.eventListeners[type];if(!handler&&(!listeners||listeners.length===0)){return;}
var event={type:type,target:media,currentTarget:media,timestamp:Date.now(),detail:detail};if(typeof handler==='function'){try{handler.call(media,event);}
catch(e){console.warn('MediaDB: ','on'+type,'event handler threw',e,e.stack);}}
if(!listeners){return;}
for(var i=0;i<listeners.length;i++){try{var listener=listeners[i];if(typeof listener==='function'){listener.call(media,event);}
else{listener.handleEvent(event);}}
catch(e){console.warn('MediaDB: ',type,'event listener threw',e,e.stack);}}}
function changeState(media,state){if(media.state!==state){media.state=state;switch(state){case MediaDB.READY:case MediaDB.ENUMERABLE:dispatchEvent(media,state);break;default:dispatchEvent(media,'unavailable',state);break;}}}
return MediaDB;}());

View file

@ -0,0 +1,35 @@
;(function(global){'use strict';global.mozIntl={formatList:function(list){return navigator.mozL10n.formatValue('listSeparator_middle').then(sep=>list.join(sep));},DateTimeFormat:function(locales,options){const resolvedOptions=Object.assign({},options);if(resolvedOptions.dayperiod){if(resolvedOptions.hour===undefined){resolvedOptions.hour='numeric';}}
if(resolvedOptions.hour!==undefined&&resolvedOptions.hour12===undefined){resolvedOptions.hour12=navigator.mozHour12;}
if(resolvedOptions.dayperiod===undefined&&resolvedOptions.hour12===true){resolvedOptions.dayperiod=true;}
var intlFormat=new Intl.DateTimeFormat(locales,resolvedOptions);resolvedOptions.locale=intlFormat.resolvedOptions().locale;resolvedOptions.hour12=intlFormat.resolvedOptions().hour12;var hourFormatter;if(resolvedOptions.dayperiod!==undefined&&resolvedOptions.hour12===true){hourFormatter=Intl.DateTimeFormat(locales,{hour:'numeric',hour12:false});}
return{resolvedOptions(){return resolvedOptions;},format:function(date,tokenFormats){var dayPeriod;var string=intlFormat.format(date);if(resolvedOptions.dayperiod===false&&resolvedOptions.hour12===true){dayPeriod=getDayPeriodTokenForDate(date,hourFormatter);string=string.replace(dayPeriod,'').trim();}else if(resolvedOptions.dayperiod===true&&options.hour===undefined){dayPeriod=getDayPeriodTokenForDate(date,hourFormatter);const hour=date.toLocaleString(navigator.languages,{hour12:true,hour:'numeric'}).replace(dayPeriod,'').trim();string=string.replace(hour,'').trim();}
for(var token in tokenFormats){if(token==='dayperiod'&&resolvedOptions.hour12===false){continue;}
const localOptions={[token]:resolvedOptions[token],};var formatter=global.mozIntl.DateTimeFormat(navigator.languages,localOptions);var tokenString=formatter.format(date);string=string.replace(tokenString,tokenFormats[token]);}
return string;},};},calendarInfo:function(token){switch(token){case'firstDayOfTheWeek':return navigator.mozL10n.formatValue('firstDayOfTheWeek').then(firstDayOfTheWeek=>parseInt(firstDayOfTheWeek)%7);default:throw new Error('Unknown token: '+token);}},DurationFormat:function(locales=navigator.languages,options={}){const resolvedOptions=Object.assign({locale:locales[0],maxUnit:'hour',minUnit:'second',},options);const numFormatter=Intl.NumberFormat(locales,{style:'decimal',useGrouping:false,minimumIntegerDigits:2});const maxUnitIdx=getDurationUnitIdx(resolvedOptions.maxUnit,0);const minUnitIdx=getDurationUnitIdx(resolvedOptions.minUnit,durationFormatOrder.length-1);return navigator.mozL10n.formatValue('durationPattern').then(fmt=>({resolvedOptions:function(){return resolvedOptions;},format:function(input){const minValue=durationFormatElements[resolvedOptions.minUnit].value;input=Math.round(input/minValue)*minValue;const duration=splitIntoTimeUnits(input,maxUnitIdx,minUnitIdx);var string=trimDurationPattern(fmt,resolvedOptions.maxUnit,resolvedOptions.minUnit);for(var unit in duration){const token=durationFormatElements[unit].token;string=string.replace(token,numFormatter.format(duration[unit]));}
if(input<0){return'-'+string;}
return string;}}));},RelativeTimeFormat:function(locales,options){return{resolvedOptions:function(){return options;},format:function(x){const{unit,value}=relativeTimeFormatId(x,options);return navigator.mozL10n.formatValue(unit,{value});},};},UnitFormat:function(locales,options){const unitGroup=getUnitFormatGroupName(options.unit);if(unitGroup===undefined){throw new RangeError(`invalid value ${options.unit} for option unit`);}
if(!unitFormatGroups[unitGroup].styles.includes(options.style)){throw new RangeError(`invalid value ${options.style} for option style`);}
const unit=`${unitGroup}-${options.unit}-${options.style}`;return{format:function(x){return document.l10n.formatValue(unit,{value:x});},};},_gaia:{relativePart:function(milliseconds){const units=computeTimeUnits(milliseconds);const unit=getBestMatchUnit(units);return{unit:unit+'s',value:Math.abs(units[unit])};},RelativeDate:function(locales,options){const style=options&&options.style||'long';const maxFormatter=Intl.DateTimeFormat(locales,{year:'numeric',month:'numeric',day:'numeric'});const relativeFmtOptions={unit:'bestFit',style:style,minUnit:'minute',};return{format:function(time,maxDiff){maxDiff=maxDiff||86400*10;const secDiff=(Date.now()-time)/1000;if(isNaN(secDiff)){return navigator.mozL10n.formatValue('incorrectDate');}
if(secDiff>maxDiff){return Promise.resolve(maxFormatter.format(time));}
const{unit,value}=relativeTimeFormatId(time,relativeFmtOptions);return navigator.mozL10n.formatValue(unit,{value});},formatElement:function(element,time,maxDiff){maxDiff=maxDiff||86400*10;const secDiff=(Date.now()-time)/1000;if(isNaN(secDiff)){element.setAttribute('data-l10n-id','incorrectDate');}
element.removeAttribute('data-l10n-id');if(secDiff>maxDiff){element.textContent=maxFormatter.format(time);}
const{unit,value}=relativeTimeFormatId(time,relativeFmtOptions);navigator.mozL10n.setAttributes(element,unit,{value});},};},getFormattedUnit:function(type,style,v){if(!unitFormatData.hasOwnProperty(type)){throw new RangeError(`invalid type ${type}`);}
if(!unitFormatGroups[type].styles.includes(style)){throw new RangeError(`invalid style ${style} for type ${type}`);}
var units=unitFormatData[type];var scale=0;for(let i=1;i<units.length;i++){if(v<units[i].value*unitFormatGroups[type].rounding){scale=i-1;break;}else if(i===units.length-1){scale=i;}}
var value=Math.round(v/units[scale].value*100)/100;return global.mozIntl.UnitFormat(navigator.languages,{unit:units[scale].name,style:style}).format(value);},}};const durationFormatOrder=['hour','minute','second','millisecond'];const durationFormatElements={'hour':{value:3600000,token:'hh'},'minute':{value:60000,token:'mm'},'second':{value:1000,token:'ss'},'millisecond':{value:10,token:'SS'}};const unitFormatData={'duration':[{'name':'second','value':1},{'name':'minute','value':60},{'name':'hour','value':60*60},{'name':'day','value':24*60*60},{'name':'month','value':30*24*60*60},],'digital':[{'name':'byte','value':1},{'name':'kilobyte','value':1024},{'name':'megabyte','value':1024*1024},{'name':'gigabyte','value':1024*1024*1024},{'name':'terabyte','value':1024*1024*1024*1024},],};const unitFormatGroups={'duration':{'units':['second','minute','hour','day','month'],'styles':['narrow'],'rounding':1},'digital':{'units':['byte','kilobyte','megabyte','gigabyte','terabyte'],'styles':['short'],'rounding':0.8}};function getDurationUnitIdx(name,defaultValue){if(!name){return defaultValue;}
const pos=durationFormatOrder.indexOf(name);if(pos===-1){throw new Error('Unknown unit type: '+name);}
return pos;}
function splitIntoTimeUnits(v,maxUnitIdx,minUnitIdx){const units={};var input=Math.abs(v);for(var i=maxUnitIdx;i<=minUnitIdx;i++){const key=durationFormatOrder[i];const{value}=durationFormatElements[key];units[key]=i==minUnitIdx?Math.round(input/value):Math.floor(input/value);input-=units[key]*value;}
return units;}
function trimDurationPattern(string,maxUnit,minUnit){const maxToken=durationFormatElements[maxUnit].token;const minToken=durationFormatElements[minUnit].token;string=string.substring(string.indexOf(maxToken),string.indexOf(minToken)+minToken.length);return string;}
function getDayPeriodTokenForDate(date,hourFormatter){const hourToken=hourFormatter.format(date);const newDate=new Date(date);newDate.setHours(parseInt(hourToken));return new Intl.DateTimeFormat(navigator.language, {hour:'numeric', hour12:true}).formatToParts(newDate).find(part => part.type === 'dayPeriod').value;}
function computeTimeUnits(v){const units={};const millisecond=Math.round(v);const second=Math.round(millisecond/1000);const minute=Math.round(second/60);const hour=Math.round(minute/60);const day=Math.round(hour/24);const rawYear=day*400/146097;units.millisecond=millisecond;units.second=second;units.minute=minute;units.hour=hour;units.day=day;units.week=Math.round(day/7);units.month=Math.round(rawYear*12);units.quarter=Math.round(rawYear*4);units.year=Math.round(rawYear);return units;}
function getBestMatchUnit(units){if(Math.abs(units.minute)<45){return'minute';}
if(Math.abs(units.hour)<22){return'hour';}
if(Math.abs(units.day)<7){return'day';}
if(Math.abs(units.week)<4){return'week';}
if(Math.abs(units.month)<11){return'month';}
return'year';}
function relativeTimeFormatId(x,options){const ms=x-Date.now();const units=computeTimeUnits(ms);const unit=options.unit==='bestFit'?getBestMatchUnit(units):options.unit;const v=units[unit];const tl=v<0?'-ago':'-until';const style=options.style||'long';const entry=unit+'s'+tl+'-'+style;return{unit:entry,value:Math.abs(v)};}
function getUnitFormatGroupName(unitName){for(let groupName in unitFormatGroups){if(unitFormatGroups[groupName].units.includes(unitName)){return groupName;}}
return undefined;}})(this);

View file

@ -0,0 +1,261 @@
/* exported ForwardLock */
/*
* This file defines a ForwardLock object with functions for locking and
* unlocking content and for key management for the FirefoxOS implementation
* of OMA Forward Lock DRM.
*
* A locked file has the following structure:
*
* 6 bytes: ASCII "LOCKED" magic number
* 2 bytes: version number as 2 ASCII characters. e.g: " 1"
* 1 byte: ASCII space
* n bytes: MIME type of the locked content, in ASCII
* 1 byte: ASCII \n delimiter
* n bytes: other ASCII metadata as key:value pairs, delimited with newlines.
* Both key and value passed to JavaScript escape()
* 1 byte : 0. NUL terminates the ASCII text, and marks the beginning
* of the audio content
* rest: The audio content xor'ed with the secret key
*
* The blob returned by the lock functions shares the first part of its
* MIME type ("image/", "audio/", etc.) with the content. But it has a
* sub-type of "vnd.mozilla.oma.drm.fl" to indicate that it is locked.
*
* The secret key used for locking and unlocking content is stored in
* the settings database. The security of the key is based entirely on
* the fact that only certified apps can read settings. So the secret
* is shared among all certified apps.
*/
'use strict';
(function(exports) {
// Make sure we only run once
if (exports.ForwardLock) {
return;
}
const mimeSubtype = 'vnd.mozilla.oma.drm.fl';
const SECRET_SETTINGS_ID = 'oma.drm.forward_lock.secret.key';
var secret = null;
// XOR the buffer contents with the key.
// This is pretty fast, but maybe using asm.js could make it faster.
function xor(buffer, key) {
// View the array buffer as 32-bit words. There may be 1 to 3
// bytes at the end of the buffer that are not encrypted.
var words = new Uint32Array(buffer, 0, buffer.byteLength >> 2);
for (var i = 0, n = words.length; i < n; i++) {
words[i] ^= key;
}
}
// Return a blob in .lcka format for specified audio content,
// metadata and secret key. Key is a 4-byte integer. content is an
// ArrayBuffer holding the file content, and metadata is an object
// that can be serialized with JSON.stringify.
function lockBuffer(secret, content, type, metadata) {
var header = 'LOCKED 1 ' + escape(type) + '\n';
if (metadata) {
for (var p in metadata) {
header += escape(p) + ':' + escape(metadata[p]) + '\n';
}
}
header += '\0';
// Make a private copy of the buffer so we don't modify the caller's copy.
var buffer = new Uint8Array(new Uint8Array(content)).buffer;
// Perform the encryption
xor(buffer, secret);
// And return the blob with a custom MIME type
return new Blob([header, buffer], {
type: type.split('/')[0] + '/' + mimeSubtype
});
}
// Given the secret key the unlocked blob, and some metadata,
// create a locked blob and pass it to the callback
function lockBlob(secret, blob, metadata, callback) {
var reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onload = function() {
callback(lockBuffer(secret, reader.result, blob.type, metadata));
};
}
// Given a .lcka blob, and the key used to encrypt it, parse the
// metadata and decrypt the content and pass them to the specified
// callback. If the input blob is not in the right format, call
// errorCallback with an string message.
function unlockBlob(secret, blob, callback, errorCallback) {
if (!secret) {
return error('no secret supplied');
}
var reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onload = function() {
var buffer = reader.result;
var bytes = new Uint8Array(buffer);
var header = '';
var contentStart;
for (var i = 0; i < bytes.length; i++) {
if (bytes[i] === 0) {
contentStart = i + 1;
break;
}
header += String.fromCharCode(bytes[i]);
}
if (!header.startsWith('LOCKED')) {
return error('Bad magic number');
}
if (header.substring(6, 9) !== ' 1 ') {
return error('Unsupported version number');
}
if (!contentStart) {
return error('No content');
}
var eol = header.indexOf('\n');
if (eol === -1) {
return error('malformed header');
}
var type = unescape(header.substring(9, eol).trim());
var metadata = {};
var lines = header.substring(eol + 1).split('\n');
for (var j = 0; j < lines.length; j++) {
var line = lines[j];
if (!line) { // ignore blank line at the end of the key:value pairs
continue;
}
var [key, value] = line.split(':');
if (!key || !value) {
return error('malformed metadata');
}
metadata[unescape(key)] = unescape(value);
}
var content = buffer.slice(contentStart);
// Decryption is the same as encryption
xor(content, secret);
var blob = new Blob([content], { type: type });
try {
callback(blob, metadata);
}
catch (e) {
console.error('Exception in content callback', e);
}
};
function error(msg) {
msg = 'LCKA.decrypt(): ' + msg;
if (errorCallback) {
try {
errorCallback(msg);
}
catch (e) {
console.error('Exception in error callback', e);
}
}
else {
console.error(msg);
}
}
}
// If we already know the secret key, pass it directly to the callback.
// Otherwise, look it up in the settings database and call the callback
// asynchronously. If the key does not exist in the db, pass null.
// This requires read permission for the settings database.
// Only certified apps can have that permission
function getKey(callback) {
try {
if (secret !== null) {
report(secret);
return;
}
var lock = navigator.mozSettings.createLock();
var getreq = lock.get(SECRET_SETTINGS_ID);
getreq.onsuccess = function() {
secret = getreq.result[SECRET_SETTINGS_ID] || null;
report(secret);
};
getreq.onerror = function() {
console.error('Error getting ForwardLock setting', getreq.error);
report(null);
};
}
catch (e) {
console.error('Exception in ForwardLock.getKey():', e);
report(null);
}
function report(secret) {
if (callback) {
try {
callback(secret);
}
catch (e) {
console.error('Exception in ForwardLock.getKey() callback', e);
}
}
}
}
// This function works like getKey, but if the key does not exist, it
// is created. This requires write access to the settings DB. The intent
// is that only the Locked Content app will ever call this
function getOrCreateKey(callback) {
getKey(function(key) {
if (key !== null) {
report(key);
return;
}
// The key does not exist yet so create one that is not zero.
secret = ((Math.random() * 0xFFFFFFFF) | 0) + 1;
// Now attempt to save the key to the settings db.
var setting = {};
setting[SECRET_SETTINGS_ID] = secret;
var setreq = navigator.mozSettings.createLock().set(setting);
setreq.onsuccess = function() {
// If we have permission to write to the settings db, then
// the key is now saved for other apps to read
report(secret);
};
setreq.onerror = function() {
// Only apps with correct permissions should call this function
// so this should not happen. We don't call the callback if
// this happens because we want to fail early in this case.
console.error('Failed to set key in ForwardLock.getOrCreateKey()',
setreq.error.name);
secret = null;
};
});
function report(secret) {
if (callback) {
try {
callback(secret);
}
catch (e) {
console.error('Exception in ForwardLock.getOrCreateKey() callback');
}
}
}
}
exports.ForwardLock = {
lockBuffer: lockBuffer,
lockBlob: lockBlob,
unlockBlob: unlockBlob,
mimeSubtype: mimeSubtype,
getKey: getKey,
getOrCreateKey: getOrCreateKey
};
}(window));

View file

@ -0,0 +1,2 @@
;(function(root,factory){'use strict';if(typeof define==='function'&&define.amd){define(factory);}else if(typeof exports==='object'){module.exports=factory();}else{root.Sanitizer=factory();}}(this,function(){'use strict';var Sanitizer={_entity:/[&<>"'/]/g,_entities:{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;','\'':'&apos;','/':'&#x2F;'},getEntity:function(s){return Sanitizer._entities[s];},escapeHTML:function(strings,...values){var result='';for(var i=0;i<strings.length;i++){result+=strings[i];if(i<values.length){result+=String(values[i]).replace(Sanitizer._entity,Sanitizer.getEntity);}}
return result;},createSafeHTML:function(strings,...values){var escaped=Sanitizer.escapeHTML(strings,...values);return{__html:escaped,toString:function(){return'[object WrappedHTMLObject]';},info:'This is a wrapped HTML object. See https://developer.mozilla.or'+'g/en-US/Firefox_OS/Security/Security_Automation for more.'};},unwrapSafeHTML:function(...htmlObjects){var markupList=htmlObjects.map(function(obj){return obj.__html;});return markupList.join('');}};return Sanitizer;}));

View file

@ -0,0 +1,150 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
/**
* text_normalizer.js: a basic string normalizer library that provides various
* methods to normalize strings of characters
*
* The following methods are available:
* toAscii(string): convert strings containing accented-form characters to
* their ASCII equivalent
*
* escapeHTML(string, boolean): escape HTML tags
* escapeRegExp(string): escape regular expressions
*/
var Normalizer = {
/**
* Initialize the ASCII normalizer
*/
initAsciiNormalizer: function normalizer_init() {
// Map from lowercase ASCII to all known accented forms of the letter
var equivalentChars = {
'a': 'áăǎâäȧạȁàảȃāąåḁⱥãǽǣæ',
'A': 'ÁĂǍÂÄȦẠȀÀẢȂĀĄÅḀȺÃǼǢÆ',
'b': 'ḃḅɓḇƀƃ',
'B': 'ḂḄƁḆɃƂ',
'c': 'ćčçĉċƈȼ',
'C': 'ĆČÇĈĊƇȻ',
'd': 'ďḑḓḋḍɗḏđƌð',
'D': 'ĎḐḒḊḌƊḎĐƋ',
'e': 'éĕěȩêḙëėẹȅèẻȇēę',
'E': 'ÉĔĚȨÊḘËĖẸȄÈẺȆĒĘ',
'f': 'ḟƒ',
'F': 'ḞƑ',
'g': 'ǵğǧģĝġɠḡǥ',
'G': 'ǴĞǦĢĜĠƓḠǤ',
'h': 'ḫȟḩĥⱨḧḣḥħ',
'H': 'ḪȞḨĤⱧḦḢḤĦ',
'i': 'íĭǐîïịȉìỉȋīįɨĩḭı',
'I': 'ÍĬǏÎÏỊȈÌỈȊĪĮƗĨḬ',
'j': 'ĵɉ',
'J': 'ĴɈ',
'k': 'ḱǩķⱪꝃḳƙḵꝁ',
'K': 'ḰǨĶⱩꝂḲƘḴꝀ',
'l': 'ĺƚľļḽḷⱡꝉḻŀɫł',
'L': 'ĹȽĽĻḼḶⱠꝈḺĿⱢŁ',
'm': 'ḿṁṃɱ',
'M': 'ḾṀṂⱮ',
'n': 'ńňņṋṅṇǹɲṉƞñ',
'N': 'ŃŇŅṊṄṆǸƝṈȠÑ',
'o': 'óŏǒôöȯọőȍòỏơȏꝋꝍōǫøõœ',
'O': 'ÓŎǑÔÖȮỌŐȌÒỎƠȎꝊꝌŌǪØÕŒ',
'p': 'ṕṗꝓƥᵽꝑ',
'P': 'ṔṖꝒƤⱣꝐ',
'q': 'ꝗ',
'Q': 'Ꝗ',
'r': 'ŕřŗṙṛȑȓṟɍɽ',
'R': 'ŔŘŖṘṚȐȒṞɌⱤ',
's': 'śšşŝșṡṣß',
'S': 'ŚŠŞŜȘṠṢ',
't': 'ťţṱțⱦṫṭƭṯʈŧ',
'T': 'ŤŢṰȚȾṪṬƬṮƮŦ',
'u': 'úŭǔûṷüṳụűȕùủưȗūųůũṵ',
'U': 'ÚŬǓÛṶÜṲỤŰȔÙỦƯȖŪŲŮŨṴ',
'v': 'ṿʋṽ',
'V': 'ṾƲṼ',
'w': 'ẃŵẅẇẉẁⱳ',
'W': 'ẂŴẄẆẈẀⱲ',
'x': 'ẍẋ',
'X': 'ẌẊ',
'y': 'ýŷÿẏỵỳƴỷỿȳɏỹ',
'Y': 'ÝŶŸẎỴỲƳỶỾȲɎỸ',
'z': 'źžẑⱬżẓȥẕƶ',
'Z': 'ŹŽẐⱫŻẒȤẔƵ'
};
// Create the reverse map (i.e. the accented chars to their ASCII
// equivalent) and build the regexp string
this._toAsciiForm = {};
for (var letter in equivalentChars) {
var accentedForms = equivalentChars[letter];
for (var i = accentedForms.length - 1; i >= 0; i--) {
this._toAsciiForm[accentedForms[i]] = letter;
}
}
},
/**
* Convert a string of characters to its ASCII equivalent
* @param {string} str a string of characters.
* @return {string} the normalized (ASCII) string in lower case.
*/
toAscii: function normalizer_toAscii(str) {
if (!str || typeof str != 'string') {
return '';
}
if (!this._toAsciiForm) {
Normalizer.initAsciiNormalizer();
}
// Convert accented form to ASCII equivalent
var result = '';
for (var i = 0, len = str.length; i < len; i++) {
result += this._toAsciiForm[str.charAt(i)] || str.charAt(i);
}
return result;
},
/**
* Escape HTML tags in a string of characters
* @param {string} str a string of characters.
* @param {boolean} escapeQuotes (optional) escape quotes.
* @return {string} the HTML-escaped string.
*/
escapeHTML: function normalizer_escapeHTML(str, escapeQuotes) {
if (Array.isArray(str)) {
return Normalizer.escapeHTML(str.join(' '), escapeQuotes);
}
if (!str || typeof str != 'string') {
return '';
}
var escaped = str.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
if (escapeQuotes) {
return escaped.replace(/"/g, '&quot;').replace(/'/g, '&#x27;');
}
return escaped;
},
unescapeHTML: function normalizer_unescapeHTML(text) {
return text.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#x27;/g, '\'');
},
/**
* Escape regular expressions in a string of characters
* @param {string} str a string of characters.
* @return {string} the regexp-escaped string.
*/
escapeRegExp: function normalizer_escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
}
};