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

8
gaia/apps/camera/.jshintrc Executable file
View file

@ -0,0 +1,8 @@
{
"extends": "../../.jshintrc",
"predef": [
"define",
"require",
"requirejs"
]
}

31
gaia/apps/camera/bower.json Executable file
View file

@ -0,0 +1,31 @@
{
"name": "camera",
"version": "0.0.0",
"homepage": "https://github.com/mozilla-b2g/gaia/tree/master/apps/camera",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "index.html",
"license": "MIT",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"debug": "https://raw.githubusercontent.com/visionmedia/debug/1.0.2/dist/debug.js",
"requirejs": "https://raw.githubusercontent.com/jrburke/requirejs/2.1.14/require.js",
"evt": "fxos-components/evt#~0.4.0",
"attach": "fxos-components/attach#0.1.1",
"device-orientation": "fxos-components/device-orientation",
"view": "fxos-components/view#~0.1.1",
"model": "fxos-components/model#~0.1.0",
"drag": "fxos-components/drag#0.1.4",
"gaia-header": "fxos-components/gaia-header#~0.9.5",
"gaia-theme": "fxos-components/gaia-theme#~0.4.1",
"gaia-icons": "fxos-components/gaia-icons#~1.0.2"
}
}

View file

@ -0,0 +1,31 @@
{
"name": "attach",
"main": "index.js",
"version": "0.1.1",
"homepage": "https://github.com/wilsonpage/attach",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"description": "Small event delegation library",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"examples",
"test",
"tests",
"package.json",
"README.md",
"LICENSE"
],
"_release": "0.1.1",
"_resolution": {
"type": "version",
"tag": "v0.1.1",
"commit": "f5195ba8fe78c2a4f421b26caf431f385fa6b61d"
},
"_source": "git://github.com/fxos-components/attach.git",
"_target": "0.1.1",
"_originalSource": "fxos-components/attach"
}

View file

@ -0,0 +1,22 @@
{
"name": "attach",
"main": "index.js",
"version": "0.1.1",
"homepage": "https://github.com/wilsonpage/attach",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"description": "Small event delegation library",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"examples",
"test",
"tests",
"package.json",
"README.md",
"LICENSE"
]
}

View file

@ -0,0 +1,254 @@
;(function() {
'use strict';
/**
* Namespace to store
* references under on root
*/
var ns = '_attach';
/**
* Normalize `matchesSelector`
*/
var proto = Element.prototype;
var matches = proto.matchesSelector
|| proto.webkitMatchesSelector
|| proto.mozMatchesSelector
|| proto.msMatchesSelector
|| proto.oMatchesSelector;
/**
* Bind an event listener
* to the given element.
*
* Example:
*
* attach(myEl, 'click', '.my-class', function(event, el) {
* // Do stuff
* });
*
* @param {Element} root
* @param {String} type
* @param {String} selector (optional)
* @param {Function} fn
* @param {Object} ctx (optional)
*/
function attach(root, type, selector, fn, ctx) {
if (arguments.length === 2) {
return attach.many.apply(null, arguments);
}
// `selector` is optional
if (typeof selector === 'function') {
ctx = fn;
fn = selector;
selector = null;
}
// We use the key 'null' to
// indicate that we are binding
// an event handler to the root.
selector = selector || 'null';
var store = getStore(root);
var master = store.master[type];
var delegates = store.delegates[type] = (store.delegates[type] || {});
// Add the function to the delegates
delegates[selector] = fn;
// Only one master event listener
// is needed per event type.
if (master) { return; }
// Add the master callbak to the
// root node and to the store.
master = store.master[type] = callback;
root.addEventListener(type, master);
/**
* The master callback passed
* to `addEventListener`.
*
* @param {Event} event
*/
function callback(e) {
var el = e.target;
var selector;
var matched;
var out;
var fn;
// Walk up the DOM tree
// until we hit the root
while (el) {
// Loop over each selector
// bound to this e type.
for (selector in delegates) {
fn = delegates[selector];
// There are two types of match. A
// 'null' selector at the root node,
// or a selector match on the current el.
matched = (el === root && selector === 'null') ||
matches.call(el, selector);
if (matched) {
out = fn.call(ctx || el, e, el);
// Stop propagation if the
// user returns false from the
// callback. Ideally we would
// use .stopPropagation, but I
// don't know of any way to detect
// if this has been called.
if (out === false) { return e.stopPropagation(); }
}
}
// Don't go any higher
// than the root element.
if (el == root) break;
// Move on up!
el = el.parentNode;
}
}
}
attach.on = attach;
/**
* Unbind an event attach
* from the given root element.
*
* If no selector if given, all
* callbacks for the given event
* type are removed.
*
* Example:
*
* // Remove one
* attach.off(myEl, 'click', '.my-class');
*
* // Remove all
* attach.off(myEl, 'click');
*
* @param {Element} root
* @param {String} type (optional)
* @param {String} selector (optional)
*/
attach.off = function(root, type, selector) {
var store = getStore(root);
var master = store.master[type];
var delegates = store.delegates[type];
// Remove just one
if (type && selector) {
delete delegates[selector];
}
// Remove all of type
else if (type) {
delete store.delegates[type];
}
// Remove all
else {
for (type in store.master) {
attach.off(root, type);
}
}
// If there aren't any callbacks
// of this type left, remove the master.
if (isEmpty(store.delegates[type])) {
root.removeEventListener(type, master);
delete store.master[type];
}
};
/**
* Handles Backbone style
* shorthand event binding.
*
* Example:
*
* attach(myElement, {
* 'click .foo': onFooClick,
* 'click .bar': onBarClick
* });
*
* @param {element} root
* @param {Object} config
* @param {Object} ctx
*/
attach.many = function(root, config, ctx) {
var parts;
var key;
for (key in config) {
parts = key.split(' ');
attach.on(
root,
parts[0],
parts[1],
config[key],
ctx);
}
};
/**
* Gets the reference store
* attached to the given node.
*
* If one is not found, we
* create a fresh one.
*
* @param {Element} el
* @return {Object}
*/
function getStore(el) {
return el[ns] || createStore(el);
}
/**
* Creates a fresh reference
* store on the given element.
*
* @param {Element} el
* @return {Object}
*/
function createStore(el) {
el[ns] = { master: {}, delegates: {} };
return el[ns];
}
/**
* Checks if the given
* element has no keys.
*
* @param {Object} ob
* @return {Boolean}
*/
function isEmpty(ob) {
for (var key in ob) { return false; }
return true;
}
/**
* Expose 'attach' (UMD)
*/
if (typeof exports === "object") {
module.exports = attach;
} else if (typeof define === "function" && define.amd) {
define(function(){ return attach; });
} else {
window.attach = attach;
}
})();

View file

@ -0,0 +1,13 @@
{
"name": "debug",
"_cacheHeaders": {
"ETag": "\"3fa8ae0741057b60f6e8629b9ede7fda1e09af27\"",
"Content-Length": "10073",
"Content-Type": "text/plain; charset=utf-8"
},
"_release": "e-tag:3fa8ae074",
"main": "index.js",
"_source": "https://raw.githubusercontent.com/visionmedia/debug/1.0.2/dist/debug.js",
"_target": "*",
"_originalSource": "https://raw.githubusercontent.com/visionmedia/debug/1.0.2/dist/debug.js"
}

View file

@ -0,0 +1,461 @@
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.debug=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
/**
* This is the web browser implementation of `debug()`.
*
* Expose `debug()` as the module.
*/
exports = module.exports = _dereq_('./debug');
exports.log = log;
exports.formatArgs = formatArgs;
exports.save = save;
exports.load = load;
exports.useColors = useColors;
/**
* Colors.
*/
exports.colors = [
'lightseagreen',
'forestgreen',
'goldenrod',
'dodgerblue',
'darkorchid',
'crimson'
];
/**
* Currently only WebKit-based Web Inspectors and the Firebug
* extension (*not* the built-in Firefox web inpector) are
* known to support "%c" CSS customizations.
*
* TODO: add a `localStorage` variable to explicitly enable/disable colors
*/
function useColors() {
// is webkit? http://stackoverflow.com/a/16459606/376773
return ('WebkitAppearance' in document.documentElement.style) ||
// is firebug? http://stackoverflow.com/a/398120/376773
(window.console && (console.firebug || (console.exception && console.table)));
}
/**
* Map %j to `JSON.stringify()`, since no Web Inspectors do that by default.
*/
exports.formatters.j = function(v) {
return JSON.stringify(v);
};
/**
* Colorize log arguments if enabled.
*
* @api public
*/
function formatArgs() {
var args = arguments;
var useColors = this.useColors;
args[0] = (useColors ? '%c' : '')
+ this.namespace
+ (useColors ? '%c ' : ' ')
+ args[0]
+ (useColors ? '%c ' : ' ')
+ '+' + exports.humanize(this.diff);
if (!useColors) return args
var c = 'color: ' + this.color;
args = [args[0], c, ''].concat(Array.prototype.slice.call(args, 1));
// the final "%c" is somewhat tricky, because there could be other
// arguments passed either before or after the %c, so we need to
// figure out the correct index to insert the CSS into
var index = 0;
var lastC = 0;
args[0].replace(/%[a-z%]/g, function(match) {
if ('%%' === match) return;
index++;
if ('%c' === match) {
// we only are interested in the *last* %c
// (the user may have provided their own)
lastC = index;
}
});
args.splice(lastC, 0, c);
return args;
}
/**
* Invokes `console.log()` when available.
* No-op when `console.log` is not a "function".
*
* @api public
*/
function log() {
// This hackery is required for IE8,
// where the `console.log` function doesn't have 'apply'
return 'object' == typeof console
&& 'function' == typeof console.log
&& Function.prototype.apply.call(console.log, console, arguments);
}
/**
* Save `namespaces`.
*
* @param {String} namespaces
* @api private
*/
function save(namespaces) {
try {
if (null == namespaces) {
localStorage.removeItem('debug');
} else {
localStorage.debug = namespaces;
}
} catch(e) {}
}
/**
* Load `namespaces`.
*
* @return {String} returns the previously persisted debug modes
* @api private
*/
function load() {
var r;
try {
r = localStorage.debug;
} catch(e) {}
return r;
}
/**
* Enable namespaces listed in `localStorage.debug` initially.
*/
exports.enable(load());
},{"./debug":2}],2:[function(_dereq_,module,exports){
/**
* This is the common logic for both the Node.js and web browser
* implementations of `debug()`.
*
* Expose `debug()` as the module.
*/
exports = module.exports = debug;
exports.coerce = coerce;
exports.disable = disable;
exports.enable = enable;
exports.enabled = enabled;
exports.humanize = _dereq_('ms');
/**
* The currently active debug mode names, and names to skip.
*/
exports.names = [];
exports.skips = [];
/**
* Map of special "%n" handling functions, for the debug "format" argument.
*
* Valid key names are a single, lowercased letter, i.e. "n".
*/
exports.formatters = {};
/**
* Previously assigned color.
*/
var prevColor = 0;
/**
* Previous log timestamp.
*/
var prevTime;
/**
* Select a color.
*
* @return {Number}
* @api private
*/
function selectColor() {
return exports.colors[prevColor++ % exports.colors.length];
}
/**
* Create a debugger with the given `namespace`.
*
* @param {String} namespace
* @return {Function}
* @api public
*/
function debug(namespace) {
// define the `disabled` version
function disabled() {
}
disabled.enabled = false;
// define the `enabled` version
function enabled() {
var self = enabled;
// set `diff` timestamp
var curr = +new Date();
var ms = curr - (prevTime || curr);
self.diff = ms;
self.prev = prevTime;
self.curr = curr;
prevTime = curr;
// add the `color` if not set
if (null == self.useColors) self.useColors = exports.useColors();
if (null == self.color && self.useColors) self.color = selectColor();
var args = Array.prototype.slice.call(arguments);
args[0] = exports.coerce(args[0]);
if ('string' !== typeof args[0]) {
// anything else let's inspect with %o
args = ['%o'].concat(args);
}
// apply any `formatters` transformations
var index = 0;
args[0] = args[0].replace(/%([a-z%])/g, function(match, format) {
// if we encounter an escaped % then don't increase the array index
if (match === '%%') return match;
index++;
var formatter = exports.formatters[format];
if ('function' === typeof formatter) {
var val = args[index];
match = formatter.call(self, val);
// now we need to remove `args[index]` since it's inlined in the `format`
args.splice(index, 1);
index--;
}
return match;
});
if ('function' === typeof exports.formatArgs) {
args = exports.formatArgs.apply(self, args);
}
var logFn = exports.log || enabled.log || console.log.bind(console);
logFn.apply(self, args);
}
enabled.enabled = true;
var fn = exports.enabled(namespace) ? enabled : disabled;
fn.namespace = namespace;
return fn;
}
/**
* Enables a debug mode by namespaces. This can include modes
* separated by a colon and wildcards.
*
* @param {String} namespaces
* @api public
*/
function enable(namespaces) {
exports.save(namespaces);
var split = (namespaces || '').split(/[\s,]+/);
var len = split.length;
for (var i = 0; i < len; i++) {
if (!split[i]) continue; // ignore empty strings
namespaces = split[i].replace('*', '.*?');
if (namespaces[0] === '-') {
exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$'));
} else {
exports.names.push(new RegExp('^' + namespaces + '$'));
}
}
}
/**
* Disable debug output.
*
* @api public
*/
function disable() {
exports.enable('');
}
/**
* Returns true if the given mode name is enabled, false otherwise.
*
* @param {String} name
* @return {Boolean}
* @api public
*/
function enabled(name) {
var i, len;
for (i = 0, len = exports.skips.length; i < len; i++) {
if (exports.skips[i].test(name)) {
return false;
}
}
for (i = 0, len = exports.names.length; i < len; i++) {
if (exports.names[i].test(name)) {
return true;
}
}
return false;
}
/**
* Coerce `val`.
*
* @param {Mixed} val
* @return {Mixed}
* @api private
*/
function coerce(val) {
if (val instanceof Error) return val.stack || val.message;
return val;
}
},{"ms":3}],3:[function(_dereq_,module,exports){
/**
* Helpers.
*/
var s = 1000;
var m = s * 60;
var h = m * 60;
var d = h * 24;
var y = d * 365.25;
/**
* Parse or format the given `val`.
*
* Options:
*
* - `long` verbose formatting [false]
*
* @param {String|Number} val
* @param {Object} options
* @return {String|Number}
* @api public
*/
module.exports = function(val, options){
options = options || {};
if ('string' == typeof val) return parse(val);
return options.long
? long(val)
: short(val);
};
/**
* Parse the given `str` and return milliseconds.
*
* @param {String} str
* @return {Number}
* @api private
*/
function parse(str) {
var match = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(str);
if (!match) return;
var n = parseFloat(match[1]);
var type = (match[2] || 'ms').toLowerCase();
switch (type) {
case 'years':
case 'year':
case 'y':
return n * y;
case 'days':
case 'day':
case 'd':
return n * d;
case 'hours':
case 'hour':
case 'h':
return n * h;
case 'minutes':
case 'minute':
case 'm':
return n * m;
case 'seconds':
case 'second':
case 's':
return n * s;
case 'ms':
return n;
}
}
/**
* Short format for `ms`.
*
* @param {Number} ms
* @return {String}
* @api private
*/
function short(ms) {
if (ms >= d) return Math.round(ms / d) + 'd';
if (ms >= h) return Math.round(ms / h) + 'h';
if (ms >= m) return Math.round(ms / m) + 'm';
if (ms >= s) return Math.round(ms / s) + 's';
return ms + 'ms';
}
/**
* Long format for `ms`.
*
* @param {Number} ms
* @return {String}
* @api private
*/
function long(ms) {
return plural(ms, d, 'day')
|| plural(ms, h, 'hour')
|| plural(ms, m, 'minute')
|| plural(ms, s, 'second')
|| ms + ' ms';
}
/**
* Pluralization helper.
*/
function plural(ms, n, name) {
if (ms < n) return;
if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name;
return Math.ceil(ms / n) + ' ' + name + 's';
}
},{}]},{},[1])
(1)
});

View file

@ -0,0 +1,23 @@
{
"name": "device-orientation",
"main": "device-orientation.js",
"homepage": "https://github.com/fxos-components/device-orientation",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"README.md"
],
"version": "1.0.0",
"_release": "1.0.0",
"_resolution": {
"type": "version",
"tag": "v1.0.0",
"commit": "6f6921e80c4f1ab8a2600f9ecebf6afabdca02c9"
},
"_source": "git://github.com/fxos-components/device-orientation.git",
"_target": "*",
"_originalSource": "fxos-components/device-orientation"
}

View file

@ -0,0 +1,13 @@
{
"name": "device-orientation",
"main": "device-orientation.js",
"homepage": "https://github.com/fxos-components/device-orientation",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"README.md"
]
}

View file

@ -0,0 +1,192 @@
(function(define){define(function(require,exports,module){
'use strict';
// The time interval is allowed between two orientation change event
const ORIENTATION_CHANGE_INTERVAL = 300;
// The maximum sample inter-arrival time in milliseconds. If the acceleration
// samples are further apart than this amount in time, we reset the state of
// the low-pass filter and orientation properties. This helps to handle
// boundary conditions when the app becomes invisisble, wakes from suspend or
// there is a significant gap in samples.
const MAX_MOTION_FILTER_TIME = 1000;
// Filtering adds latency proportional the time constant (inversely
// proportional to the cutoff frequency) so we don't want to make the time
// constant too large or we can lose responsiveness. Likewise we don't want
// to make it too small or we do a poor job suppressing acceleration spikes.
// Empirically, 100ms seems to be too small and 500ms is too large. Android
// default is 200. The original version is from:
// http://mxr.mozilla.org/mozilla-central/source/widget/gonk/
// ProcessOrientation.cpp
const MOTION_FILTER_TIME_CONSTANT = 200;
var lastMotionFilteredTime = 0;
var lastMotionData = {x: 0, y: 0, z: 0, t: 0};
var pendingOrientation = null;
var orientationChangeTimer = 0;
var eventListeners = {'orientation': []};
function applyFilter(x, y, z) {
var now = new Date().getTime();
var filterReset = false;
// The motion event is too far from last filtered data, reset the data.
// This may be the case of hide app and go back.
if (now > lastMotionData.t + MAX_MOTION_FILTER_TIME) {
// clear data to re-initialize it.
lastMotionData.x = 0;
lastMotionData.y = 0;
lastMotionData.z = 0;
filterReset = true;
}
// applying the exponential moving average to x, y, z, when we already have
// value of it.
if (lastMotionData.x || lastMotionData.y || lastMotionData.z) {
// use time to calculate alpha
var diff = now - lastMotionFilteredTime;
var alpha = diff / (MOTION_FILTER_TIME_CONSTANT + diff);
// weight the x, y, z with alpha
x = alpha * (x - lastMotionData.x) + lastMotionData.x;
y = alpha * (y - lastMotionData.y) + lastMotionData.y;
z = alpha * (z - lastMotionData.z) + lastMotionData.z;
}
// update the filter state.
lastMotionData.x = x;
lastMotionData.y = y;
lastMotionData.z = z;
lastMotionData.t = now;
return filterReset;
}
function calcOrientation(x, y) {
// use atan2(-x, y) to calculate the rotation on z axis.
var orientationAngle = (Math.atan2(-x, y) * 180 / Math.PI);
// The value range of atan2 is [-180, 180]. To have the [0, 360] value
// range, we need to add 360 degrees when the angle is less than 0.
if (orientationAngle < 0) {
orientationAngle += 360;
}
// find the nearest orientation.
// If an angle is >= 45 degrees, we view it as 90 degrees. If an angle is <
// 45, we view it as 0 degree.
var orientation = (((orientationAngle + 45) / 90) >> 0) % 4 * 90;
return orientation;
}
function handleMotionEvent(e) {
if (!e.accelerationIncludingGravity) {
return;
}
var filterReset = applyFilter(e.accelerationIncludingGravity.x,
e.accelerationIncludingGravity.y,
e.accelerationIncludingGravity.z);
// We don't need to process the event when filter is reset or no data.
if (filterReset) {
return;
}
var x = lastMotionData.x;
var y = lastMotionData.y;
var z = lastMotionData.z;
// We only want to measure gravity, so ignore events when there is
// significant acceleration in addition to gravity because this means the
// user is moving the phone.
if ((x * x + y * y + z * z) > 110) {
return;
}
// If the camera is close to horizontal (pointing up or down) then we can't
// tell what orientation the user intends, so we just return now without
// changing the orientation. The constant 9.2 is the force of gravity (9.8)
// times the cosine of 20 degrees. So if the phone is within 20 degrees of
// horizontal, we will never change the orientation.
if (z > 9.2 || z < -9.2) {
return;
}
var orientation = calcOrientation(x, y);
if (orientation === pendingOrientation) {
return;
}
// When phone keeps the same orientation for ORIENTATION_CHANGE_INTERVAL
// time interval, we change the orientation. Otherwrise the change is
// cancelled. This may be that user rotates phone rapidly but captured by
// device motion.
if (orientationChangeTimer) {
window.clearTimeout(orientationChangeTimer);
}
// If we don't have any current orientation, then send an event right away
// Otherwise, wait to make sure we're stable before sending it.
if (pendingOrientation === null) {
pendingOrientation = orientation;
fireOrientationChangeEvent(pendingOrientation);
}
else {
// create timer for waiting to rotate the phone
pendingOrientation = orientation;
orientationChangeTimer = window.setTimeout(function doOrient() {
fireOrientationChangeEvent(pendingOrientation);
orientationChangeTimer = 0;
}, ORIENTATION_CHANGE_INTERVAL);
}
}
function start() {
// Reset our state so that the first devicemotion event we get
// will always generate an orientation event.
pendingOrientation = null;
window.addEventListener('devicemotion', handleMotionEvent);
}
function stop() {
window.removeEventListener('devicemotion', handleMotionEvent);
if (orientationChangeTimer) {
clearTimeout(orientationChangeTimer);
orientationChangeTimer = 0;
}
}
function addEventListener(type, listener) {
if (eventListeners[type] && listener) {
eventListeners[type].push(listener);
}
}
function removeEventListener(type, listener) {
if (!eventListeners[type]) {
return;
}
var idx = eventListeners[type].indexOf(listener);
if (idx > -1) {
eventListeners.slice(idx, 1);
}
}
function fireOrientationChangeEvent(orientation) {
eventListeners.orientation.forEach(function(listener) {
if (listener.handleEvent) {
listener.handleEvent(orientation);
} else if ((typeof listener) === 'function') {
listener(orientation);
}
});
}
module.exports = {
start: start,
stop: stop,
on: addEventListener,
off: removeEventListener
};
});})((function(n,w){return typeof define=='function'&&define.amd?
define:typeof module=='object'?function(c){c(require,exports,module);}:function(c){
var m={exports:{}},r=function(n){return w[n];};w[n]=c(r,m.exports,m)||m.exports;};})('device-orientation',this));

View file

@ -0,0 +1,17 @@
{
"name": "device-orientation",
"description": "Tracks device orientation using accelerometer when orientation locked.",
"main": "device-orientation.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/fxos-components/device-orientation.git"
},
"bugs": {
"url": "https://github.com/fxos-components/device-orientation/issues"
},
"homepage": "https://github.com/fxos-components/device-orientation",
"version": "1.0.0"
}

View file

@ -0,0 +1,29 @@
{
"name": "drag",
"main": "index.js",
"version": "0.1.4",
"homepage": "https://github.com/gaia-components/drag",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"evt": "gaia-components/evt#~0.4.0"
},
"_release": "0.1.4",
"_resolution": {
"type": "version",
"tag": "v0.1.4",
"commit": "c56942e7e15abd42f38d3634e540739f96d545d3"
},
"_source": "git://github.com/fxos-components/drag.git",
"_target": "0.1.4",
"_originalSource": "fxos-components/drag"
}

View file

@ -0,0 +1,20 @@
{
"name": "drag",
"main": "index.js",
"version": "0.1.4",
"homepage": "https://github.com/gaia-components/drag",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"evt": "gaia-components/evt#~0.4.0"
}
}

View file

@ -0,0 +1,229 @@
(function(define){'use strict';define(function(require,exports,module){
/*globals define*//*jshint node:true*/
/**
* Dependencies
*/
var events = require('evt');
/**
* Exports
*/
module.exports = Drag;
/**
* Mixin Emitter
*/
events(Drag.prototype);
/**
* Pointer event abstraction to make
* it work for touch and mouse.
*
* @type {Object}
*/
var pointer = [
{ down: 'touchstart', up: 'touchend', move: 'touchmove' },
{ down: 'mousedown', up: 'mouseup', move: 'mousemove' }
]['ontouchstart' in window ? 0 : 1];
/**
* Drag creates a draggable 'handle' element,
* constrained within a 'container' element.
*
* Drag instances emit useful events and provides
* methods to support common draggable UI use-cases,
* like `snapToClosestEdge`
*
* In Gaia we use `Drag` for our switch components.
*
* Usage:
*
* // Create a new `Drag`
* var drag = new Drag({
* container: myContainer,
* handle: myHandle
* });
*
* // Once elements are in the DOM we
* // need to take some measurements
* drag.updateDimensions();
*
* Options:
*
* - {Element} `container`
* - {Element} `handle`
*
* @param {Object} options
*/
function Drag(options) {
this.container = { el: options.container };
this.handle = { el: options.handle };
this.onTouchStart = this.onTouchStart.bind(this);
this.onTouchMove = this.onTouchMove.bind(this);
this.onTouchEnd = this.onTouchEnd.bind(this);
this.slideDuration = options.slideDuration || 140;
this.tapTime = options.tapTime || 180;
this.bindEvents();
}
Drag.prototype.bindEvents = function() {
this.container.el.addEventListener(pointer.down, this.onTouchStart);
};
Drag.prototype.onTouchStart = function(e) {
this.updateDimensions();
this.touch = ~e.type.indexOf('mouse') ? e : e.touches[0];
this.firstTouch = this.touch;
this.startTime = e.timeStamp;
addEventListener(pointer.move, this.onTouchMove);
addEventListener(pointer.up, this.onTouchEnd);
};
Drag.prototype.onTouchMove = function(e) {
e.preventDefault();
e = ~e.type.indexOf('mouse') ? e : e.touches[0];
var delta = {
x: e.clientX - this.touch.clientX,
y: e.clientY - this.touch.clientY
};
this.dragging = true;
this.move(delta);
this.touch = e;
};
Drag.prototype.onTouchEnd = function(e) {
var tapped = (e.timeStamp - this.startTime) < this.tapTime;
this.dragging = false;
removeEventListener(pointer.move, this.onTouchMove);
removeEventListener(pointer.up, this.onTouchEnd);
if (tapped) { this.emit('tapped', e); }
else { this.emit('ended', e); }
};
Drag.prototype.move = function(delta) {
this.translate({
x: this.handle.position.x + delta.x,
y: this.handle.position.y + delta.y
});
};
Drag.prototype.set = function(pos) {
if (!this.edges) { this.pendingSet = pos; return; }
var x = typeof pos.x === 'string' ? this.edges[pos.x] : (pos.x || 0);
var y = typeof pos.y === 'string' ? this.edges[pos.y] : (pos.y || 0);
this.translate({ x: x, y: y });
};
Drag.prototype.snapToClosestEdge = function() {
var edges = this.getClosestEdges();
this.translate({
x: this.edges[edges.x],
y: this.edges[edges.y]
});
this.emit('snapped', edges);
};
Drag.prototype.translate = function(options) {
var position = this.clamp(options);
var translate = 'translate(' + position.x + 'px,' + position.y + 'px)';
var ratio = {
x: (position.x / this.max.x) || 0,
y: (position.y / this.max.y) || 0
};
this.setTransition(position);
// Set the transform to move the handle
this.handle.el.style.transform = translate;
// Update the handle position reference
this.handle.position = position;
// Emit event with useful data
this.emit('translate', {
position: {
px: position,
ratio: ratio
}
});
};
Drag.prototype.clamp = function(position) {
return {
x: Math.max(this.min.x, Math.min(this.max.x, position.x)),
y: Math.max(this.min.y, Math.min(this.max.y, position.y)),
};
};
/**
* [setTransition description]
* @param {[type]} position [description]
*/
Drag.prototype.setTransition = function(position) {
var duration = !this.dragging ? this.transitionDuration(position) : 0;
this.handle.el.style.transitionDuration = duration + 'ms';
};
Drag.prototype.transitionDuration = function(position) {
var current = this.handle.position;
var distanceX = Math.abs(current.x - position.x);
var distanceY = Math.abs(current.y - position.y);
var distance = Math.max(distanceX, distanceY);
var axis = distanceY > distanceX ? 'y' : 'x';
var ratio = distance / this.max[axis];
return this.slideDuration * ratio;
};
Drag.prototype.getClosestEdges = function() {
return {
x: this.handle.position.x <= (this.max.x / 2) ? 'left' : 'right',
y: this.handle.position.y <= (this.max.y / 2) ? 'top' : 'bottom'
};
};
Drag.prototype.updateDimensions = function() {
var container = this.container.el.getBoundingClientRect();
var handle = this.handle.el.getBoundingClientRect();
this.min = { x: 0, y: 0 };
this.max = {
x: container.width - handle.width,
y: container.height - handle.height
};
this.edges = {
top: this.min.y,
right: this.max.x,
bottom: this.max.y,
left: this.min.x
};
this.handle.position = {
x: handle.left - container.left,
y: handle.top - container.top
};
this.clearPendingSet();
};
Drag.prototype.clearPendingSet = function() {
if (!this.pendingSet) { return; }
this.set(this.pendingSet);
delete this.pendingSet;
};
});})((function(n,w){'use strict';return typeof define=='function'&&define.amd?
define:typeof module=='object'?function(c){c(require,exports,module);}:
function(c){var m={exports:{}},r=function(n){return w[n];};
w[n]=c(r,m.exports,m)||m.exports;};})('drag',this));

View file

@ -0,0 +1,25 @@
{
"name": "drag",
"version": "0.1.1",
"description": "",
"main": "index.js",
"scripts": {
"test": "gaia-test-unit"
},
"repository": {
"type": "git",
"url": "https://github.com/gaia-components/drag.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/gaia-components/drag/issues"
},
"homepage": "https://github.com/gaia-components/drag",
"dependencies": {
"evt": "git://github.com/gaia-components/evt#v0.4.0"
},
"devDependencies": {
"gaia-test": "git://github.com/gaia-components/gaia-test"
}
}

View file

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

View file

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

154
gaia/apps/camera/bower_components/evt/index.js vendored Executable file
View file

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

View file

@ -0,0 +1,28 @@
{
"name": "font-fit",
"main": "font-fit.js",
"version": "0.3.1",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"README.md",
"index.html",
"package.json"
],
"homepage": "https://github.com/gaia-components/font-fit",
"_release": "0.3.1",
"_resolution": {
"type": "version",
"tag": "v0.3.1",
"commit": "07a497519b335a3d86a66268f990d8844b83d517"
},
"_source": "git://github.com/gaia-components/font-fit.git",
"_target": "~0.3.0",
"_originalSource": "gaia-components/font-fit"
}

View file

@ -0,0 +1,18 @@
{
"name": "font-fit",
"main": "font-fit.js",
"version": "0.3.1",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"README.md",
"index.html",
"package.json"
]
}

View file

@ -0,0 +1,132 @@
;(function(define){define(function(require,exports,module){
'use strict';
/**
* Simple logger.
*
* @return {Function}
*/
var debug = 0 ? console.log.bind(console) : function(){};
/**
* Global canvas cache.
*
* @type {Object}
*/
var cache = {};
/**
* Default min/max font-size.
*
* @type {Number}
*/
var MIN = 16;
var MAX = 24;
/**
* The number of pixels to subtract from
* the given `config.space` to ensure
* HTML text doesn't overflow container.
*
* Ideally we would use 1px, but in some
* cases italicised text in canvas is ~2px
* longer than the same text in HTML.
*
* http://bugzil.la/1126391
*
* @type {Number}
*/
var BUFFER = 3;
/**
* Get the font-size that closest fits
* the given space with the given font.
*
* Config:
*
* - {String} `text` The text string
* - {String} `font` Font shorthand string
* - {Number} `space` Width (px) to fit the text into
* - {Number} `min` Min font-size (px) (optional)
* - {Number} `max` Max font-size (px) (optional)
*
* @param {Object} config
* @return {Object} {fontSize,overflowing,textWidth}
*/
module.exports = function(config) {
debug('font fit', config);
var space = config.space - BUFFER;
var min = config.min || MIN;
var max = config.max || MAX;
var text = trim(config.text);
var fontSize = max;
var textWidth;
var font;
do {
font = config.font.replace(/\d+px/, fontSize + 'px');
textWidth = getTextWidth(text, font);
} while (textWidth > space && fontSize !== min && fontSize--);
return {
textWidth: textWidth,
fontSize: fontSize,
overflowing: textWidth > space
};
};
/**
* Get the width of the given text
* with the given font style.
*
* @param {String} text
* @param {String} font (CSS shorthand)
* @return {Number} (px)
*/
function getTextWidth(text, font) {
var ctx = getCanvasContext(font);
var width = ctx.measureText(text).width;
debug('got text width', width);
return width;
}
/**
* Get a canvas context configured
* to the given font style.
*
* @param {String} font
* @return {CanvasRenderingContext2D}
*/
function getCanvasContext(font) {
debug('get canvas context', font);
var cached = cache[font];
if (cached) { return cached; }
var canvas = document.createElement('canvas');
canvas.setAttribute('moz-opaque', 'true');
canvas.setAttribute('width', '1px');
canvas.setAttribute('height', '1px');
debug('created canvas', canvas);
var ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.font = font;
return cache[font] = ctx;
}
/**
* Trim leading, trailing
* and excess whitespace.
*
* @param {String} text
* @return {String}
*/
function trim(text) {
return text.replace(/\s+/g, ' ').trim();
}
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('font-fit',this));

View file

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

View file

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

View file

@ -0,0 +1,460 @@
/* globals define */
;(function(define){'use strict';define(function(require,exports,module){
/**
* Locals
*/
var textContent = Object.getOwnPropertyDescriptor(Node.prototype,
'textContent');
var innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
var removeAttribute = Element.prototype.removeAttribute;
var setAttribute = Element.prototype.setAttribute;
var noop = function() {};
/**
* Register a new component.
*
* @param {String} name
* @param {Object} props
* @return {constructor}
* @public
*/
exports.register = function(name, props) {
var baseProto = getBaseProto(props.extends);
var template = props.template || baseProto.templateString;
// Components are extensible by default but can be declared
// as non extensible as an optimization to avoid
// storing the template strings
var extensible = props.extensible = props.hasOwnProperty('extensible')?
props.extensible : true;
// Clean up
delete props.extends;
// Pull out CSS that needs to be in the light-dom
if (template) {
// Stores the string to be reprocessed when
// a new component extends this one
if (extensible && props.template) {
props.templateString = props.template;
}
var output = processCss(template, name);
props.template = document.createElement('template');
props.template.innerHTML = output.template;
props.lightCss = output.lightCss;
props.globalCss = props.globalCss || '';
props.globalCss += output.globalCss;
}
// Inject global CSS into the document,
// and delete as no longer needed
injectGlobalCss(props.globalCss);
delete props.globalCss;
// Merge base getter/setter attributes with the user's,
// then define the property descriptors on the prototype.
var descriptors = mixin(props.attrs || {}, base.descriptors);
// Store the orginal descriptors somewhere
// a little more private and delete the original
props._attrs = props.attrs;
delete props.attrs;
// Create the prototype, extended from base and
// define the descriptors directly on the prototype
var proto = createProto(baseProto, props);
Object.defineProperties(proto, descriptors);
// Register the custom-element and return the constructor
try {
return document.registerElement(name, { prototype: proto });
} catch (e) {
if (e.name !== 'NotSupportedError') {
throw e;
}
}
};
var base = {
properties: {
GaiaComponent: true,
attributeChanged: noop,
attached: noop,
detached: noop,
created: noop,
createdCallback: function() {
if (this.dirObserver) { addDirObserver(); }
injectLightCss(this);
this.created();
},
/**
* It is very common to want to keep object
* properties in-sync with attributes,
* for example:
*
* el.value = 'foo';
* el.setAttribute('value', 'foo');
*
* So we support an object on the prototype
* named 'attrs' to provide a consistent
* way for component authors to define
* these properties. When an attribute
* changes we keep the attr[name]
* up-to-date.
*
* @param {String} name
* @param {String||null} from
* @param {String||null} to
*/
attributeChangedCallback: function(name, from, to) {
var prop = toCamelCase(name);
if (this._attrs && this._attrs[prop]) { this[prop] = to; }
this.attributeChanged(name, from, to);
},
attachedCallback: function() {
if (this.dirObserver) {
this.setInnerDirAttributes = setInnerDirAttributes.bind(null, this);
document.addEventListener('dirchanged', this.setInnerDirAttributes);
}
this.attached();
},
detachedCallback: function() {
if (this.dirObserver) {
document.removeEventListener('dirchanged', this.setInnerDirAttributes);
}
this.detached();
},
/**
* A convenient method for setting up
* a shadow-root using the defined template.
*
* @return {ShadowRoot}
*/
setupShadowRoot: function() {
if (!this.template) { return; }
var node = document.importNode(this.template.content, true);
this.createShadowRoot().appendChild(node);
if (this.dirObserver) { setInnerDirAttributes(this); }
return this.shadowRoot;
},
/**
* Sets an attribute internally
* and externally. This is so that
* we can style internal shadow-dom
* content.
*
* @param {String} name
* @param {String} value
*/
setAttr: function(name, value) {
var internal = this.shadowRoot.firstElementChild;
setAttribute.call(internal, name, value);
setAttribute.call(this, name, value);
},
/**
* Removes an attribute internally
* and externally. This is so that
* we can style internal shadow-dom
* content.
*
* @param {String} name
* @param {String} value
*/
removeAttr: function(name) {
var internal = this.shadowRoot.firstElementChild;
removeAttribute.call(internal, name);
removeAttribute.call(this, name);
}
},
descriptors: {
textContent: {
set: function(value) {
textContent.set.call(this, value);
if (this.lightStyle) { this.appendChild(this.lightStyle); }
},
get: function() {
return textContent.get();
}
},
innerHTML: {
set: function(value) {
innerHTML.set.call(this, value);
if (this.lightStyle) { this.appendChild(this.lightStyle); }
},
get: innerHTML.get
}
}
};
/**
* The default base prototype to use
* when `extends` is undefined.
*
* @type {Object}
*/
var defaultPrototype = createProto(HTMLElement.prototype, base.properties);
/**
* Returns a suitable prototype based
* on the object passed.
*
* @private
* @param {HTMLElementPrototype|undefined} proto
* @return {HTMLElementPrototype}
*/
function getBaseProto(proto) {
if (!proto) { return defaultPrototype; }
proto = proto.prototype || proto;
return !proto.GaiaComponent ?
createProto(proto, base.properties) : proto;
}
/**
* Extends the given proto and mixes
* in the given properties.
*
* @private
* @param {Object} proto
* @param {Object} props
* @return {Object}
*/
function createProto(proto, props) {
return mixin(Object.create(proto), props);
}
/**
* Detects presence of shadow-dom
* CSS selectors.
*
* @private
* @return {Boolean}
*/
var hasShadowCSS = (function() {
var div = document.createElement('div');
try { div.querySelector(':host'); div.querySelector('::content'); return true; }
catch (e) { return false; }
})();
/**
* Regexs used to extract shadow-css
*
* @type {Object}
*/
var regex = {
shadowCss: /(?:\:host|\:\:content)[^{]*\{[^}]*\}/g,
':host': /(?:\:host)/g,
':host()': /\:host\((.+)\)(?: \:\:content)?/g,
':host-context': /\:host-context\((.+)\)([^{,]+)?/g,
'::content': /(?:\:\:content)/g
};
/**
* Extracts the :host and ::content rules
* from the shadow-dom CSS and rewrites
* them to work from the <style scoped>
* injected at the root of the component.
*
* @private
* @return {String}
*/
function processCss(template, name) {
var globalCss = '';
var lightCss = '';
if (!hasShadowCSS) {
template = template.replace(regex.shadowCss, function(match) {
var hostContext = regex[':host-context'].exec(match);
if (hostContext) {
globalCss += match
.replace(regex['::content'], '')
.replace(regex[':host-context'], '$1 ' + name + '$2')
.replace(/ +/g, ' '); // excess whitespace
} else {
lightCss += match
.replace(regex[':host()'], name + '$1')
.replace(regex[':host'], name)
.replace(regex['::content'], name);
}
return '';
});
}
return {
template: template,
lightCss: lightCss,
globalCss: globalCss
};
}
/**
* Some CSS rules, such as @keyframes
* and @font-face don't work inside
* scoped or shadow <style>. So we
* have to put them into 'global'
* <style> in the head of the
* document.
*
* @private
* @param {String} css
*/
function injectGlobalCss(css) {
if (!css) {return;}
var style = document.createElement('style');
style.innerHTML = css.trim();
headReady().then(function() {
document.head.appendChild(style);
});
}
/**
* Resolves a promise once document.head is ready.
*
* @private
*/
function headReady() {
return new Promise(function(resolve) {
if (document.head) { return resolve(); }
window.addEventListener('load', function fn() {
window.removeEventListener('load', fn);
resolve();
});
});
}
/**
* The Gecko platform doesn't yet have
* `::content` or `:host`, selectors,
* without these we are unable to style
* user-content in the light-dom from
* within our shadow-dom style-sheet.
*
* To workaround this, we clone the <style>
* node into the root of the component,
* so our selectors are able to target
* light-dom content.
*
* @private
*/
function injectLightCss(el) {
if (hasShadowCSS) { return; }
var stylesheet = el.querySelector('style');
if (!stylesheet) {
stylesheet = document.createElement('style');
stylesheet.setAttribute('scoped', '');
stylesheet.appendChild(document.createTextNode(el.lightCss));
el.appendChild(stylesheet);
}
el.lightStyle = stylesheet;
}
/**
* Convert hyphen separated
* string to camel-case.
*
* Example:
*
* toCamelCase('foo-bar'); //=> 'fooBar'
*
* @private
* @param {String} string
* @return {String}
*/
function toCamelCase(string) {
return string.replace(/-(.)/g, function replacer(string, p1) {
return p1.toUpperCase();
});
}
/**
* Observer (singleton)
*
* @type {MutationObserver|undefined}
*/
var dirObserver;
/**
* Workaround for bug 1100912: applies a `dir` attribute to all shadowRoot
* children so that :-moz-dir() selectors work on shadow DOM elements.
*
* In order to keep decent performances, the `dir` is the component dir if
* defined, or the document dir otherwise. This won't work if the component's
* direction is defined by CSS or inherited from a parent container.
*
* This method should be removed when bug 1100912 is fixed.
*
* @private
* @param {WebComponent}
*/
function setInnerDirAttributes(component) {
var dir = component.dir || document.dir;
Array.from(component.shadowRoot.children).forEach(element => {
if (element.nodeName !== 'STYLE') {
element.dir = dir;
}
});
}
/**
* Observes the document `dir` (direction) attribute and when it changes:
* - dispatches a global `dirchanged` event;
* - forces the `dir` attribute of all shadowRoot children.
*
* Components can listen to this event and make internal changes if needed.
*
* @private
*/
function addDirObserver() {
if (dirObserver) { return; }
dirObserver = new MutationObserver(onChanged);
dirObserver.observe(document.documentElement, {
attributeFilter: ['dir'],
attributes: true
});
function onChanged(mutations) {
document.dispatchEvent(new Event('dirchanged'));
}
}
/**
* Copy the values of all properties from
* source object `target` to a target object `source`.
* It will return the target object.
*
* @private
* @param {Object} target
* @param {Object} source
* @returns {Object}
*/
function mixin(target, source) {
for (var key in source) {
target[key] = source[key];
}
return target;
}
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-component',this));

View file

@ -0,0 +1,39 @@
{
"name": "gaia-header",
"version": "0.9.5",
"homepage": "https://github.com/gaia-components/gaia-header",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "gaia-header.js",
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"test",
"index.html",
"package.json",
"README.md"
],
"dependencies": {
"gaia-icons": "gaia-components/gaia-icons#~0.10.3",
"gaia-component": "gaia-components/gaia-component#~0.4.0",
"font-fit": "gaia-components/font-fit#~0.3.0"
},
"devDependencies": {
"gaia-theme": "gaia-components/gaia-theme",
"gaia-fonts": "gaia-components/gaia-fonts",
"base": "gaia-components/base"
},
"_release": "0.9.5",
"_resolution": {
"type": "version",
"tag": "v0.9.5",
"commit": "aa4c9c5cb83ce8813c8472860671ea514f07274f"
},
"_source": "git://github.com/fxos-components/gaia-header.git",
"_target": "~0.9.5",
"_originalSource": "fxos-components/gaia-header"
}

View file

@ -0,0 +1,33 @@
{
"camelcase": false,
"curly": true,
"forin": false,
"latedef": "nofunc",
"newcap": false,
"noarg": true,
"node": true,
"nonew": true,
"quotmark": "single",
"undef": true,
"unused": "vars",
"strict": true,
"trailing": true,
"maxlen": 80,
"eqnull": true,
"esnext": true,
"expr": true,
"globalstrict": true,
"maxerr": 1000,
"regexdash": true,
"laxcomma": true,
"proto": true,
"browser": true,
"devel": true,
"nonstandard": true,
"worker": true,
"-W078": true
}

View file

@ -0,0 +1,30 @@
{
"name": "gaia-header",
"version": "0.9.5",
"homepage": "https://github.com/gaia-components/gaia-header",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "gaia-header.js",
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"test",
"index.html",
"package.json",
"README.md"
],
"dependencies": {
"gaia-icons": "gaia-components/gaia-icons#~0.10.3",
"gaia-component": "gaia-components/gaia-component#~0.4.0",
"font-fit": "gaia-components/font-fit#~0.3.0"
},
"devDependencies": {
"gaia-theme": "gaia-components/gaia-theme",
"gaia-fonts": "gaia-components/gaia-fonts",
"base": "gaia-components/base"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
{
"name": "gaia-icons",
"homepage": "https://github.com/wilsonpage/gaia-icons",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": [
"gaia-icons.js",
"gaia-icons.css",
"fonts/gaia-icons.ttf",
"gaia-icons-embedded.css",
"bidi-helper.css"
],
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"templates",
"images",
"test",
"tests",
"index.html",
"create-svg-icon-set-file.py",
"Gruntfile.js",
"Dockerfile",
"bin",
"README.md",
"package.json"
],
"version": "1.0.2",
"_release": "1.0.2",
"_resolution": {
"type": "version",
"tag": "v1.0.2",
"commit": "42dfa3ed9a9ffa09733dc8db08a06d3dd9d0a081"
},
"_source": "git://github.com/fxos-components/gaia-icons.git",
"_target": "~1.0.2",
"_originalSource": "fxos-components/gaia-icons"
}

View file

@ -0,0 +1,33 @@
{
"camelcase": false,
"curly": true,
"forin": false,
"latedef": "nofunc",
"newcap": false,
"noarg": true,
"node": true,
"nonew": true,
"quotmark": "single",
"undef": true,
"unused": "vars",
"strict": true,
"trailing": true,
"maxlen": 80,
"eqnull": true,
"esnext": true,
"expr": true,
"globalstrict": true,
"maxerr": 1000,
"regexdash": true,
"laxcomma": true,
"proto": true,
"browser": true,
"devel": true,
"nonstandard": true,
"worker": true,
"-W078": true
}

View file

@ -0,0 +1,74 @@
/* Some BiDi-proof icons
* In the future, we might handle this at the font level, see
* https://bugzilla.mozilla.org/show_bug.cgi?id=1214127
*/
[data-icon="back"]:-moz-dir(ltr):before {
content: 'left';
}
[data-icon="back"]:-moz-dir(rtl):before {
content: 'right';
}
[data-icon="forward"]:-moz-dir(ltr):before {
content: 'right';
}
[data-icon="forward"]:-moz-dir(rtl):before {
content: 'left';
}
[data-icon="back-light"]:-moz-dir(ltr):before {
content: 'left-light';
}
[data-icon="back-light"]:-moz-dir(rtl):before {
content: 'right-light';
}
[data-icon="forward-light"]:-moz-dir(ltr):before {
content: 'right-light';
}
[data-icon="forward-light"]:-moz-dir(rtl):before {
content: 'left-light';
}
[data-icon="arrow-back"]:-moz-dir(ltr):before {
content: 'arrow-left';
}
[data-icon="arrow-back"]:-moz-dir(rtl):before {
content: 'arrow-right';
}
[data-icon="arrow-forward"]:-moz-dir(ltr):before {
content: 'arrow-right';
}
[data-icon="arrow-forward"]:-moz-dir(rtl):before {
content: 'arrow-left';
}
[data-icon="clear-input"]:-moz-dir(ltr):before {
content: 'clear-input-left';
}
[data-icon="clear-input"]:-moz-dir(rtl):before {
content: 'clear-input-right';
}
[data-icon="expand"]:-moz-dir(ltr):before {
content: 'expand-left';
}
[data-icon="expand"]:-moz-dir(rtl):before {
content: 'expand-right';
}
[data-icon="send"]:-moz-dir(ltr):before {
content: 'send-right';
}
[data-icon="send"]:-moz-dir(rtl):before {
content: 'send-left';
}
[data-icon="edit"]:-moz-dir(ltr):before {
content: 'edit-left';
}
[data-icon="edit"]:-moz-dir(rtl):before {
content: 'edit-right';
}

View file

@ -0,0 +1,32 @@
{
"name": "gaia-icons",
"homepage": "https://github.com/wilsonpage/gaia-icons",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": [
"gaia-icons.js",
"gaia-icons.css",
"fonts/gaia-icons.ttf",
"gaia-icons-embedded.css",
"bidi-helper.css"
],
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"templates",
"images",
"test",
"tests",
"index.html",
"create-svg-icon-set-file.py",
"Gruntfile.js",
"Dockerfile",
"bin",
"README.md",
"package.json"
]
}

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,40 @@
/* Generated by grunt-webfont */
/* Based on https://github.com/endtwist/fontcustom/blob/master/lib/fontcustom/templates/fontcustom.css */
@font-face {
font-family: "gaia-icons";
src: url("fonts/gaia-icons.ttf") format("truetype");
font-weight: 500;
font-style: normal;
}
[data-icon]:before,
.ligature-icons {
font-family: "gaia-icons";
content: attr(data-icon);
display: inline-block;
font-weight: 500;
font-style: normal;
text-decoration: inherit;
text-transform: none;
text-rendering: optimizeLegibility;
font-size: 30px;
-webkit-font-smoothing: antialiased;
}
[data-icon]:not([data-l10n-id]):not([aria-label]):not([aria-hidden="true"]):before {
/* Highlight cases where icons are not localized and still visible to screen
reader */
background: rgba(255, 0, 0, 0.4);
}
[data-icon]:not([data-l10n-id]):not([aria-label]):not([aria-hidden="true"]):after {
background: #333;
color: #fff;
position: absolute;
width: 20rem;
border-radius: 0.5rem;
padding: 0.5rem;
content: "Icon is not localized and is still visible to screen reader. Please add data-l10n-id or aria-label for localization or use aria-hidden to hide the icon from screen reader.";
z-index: 9999;
}

View file

@ -0,0 +1,38 @@
/* global define */
(function(define){'use strict';define(function(require,exports,module){
/**
* Exports
*/
var base = window.GAIA_ICONS_BASE_URL ||
window.COMPONENTS_BASE_URL ||
'bower_components/';
if (!document.documentElement) {
window.addEventListener('load', load);
} else {
load();
}
function load() {
if (isLoaded()) { return; }
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = base + 'gaia-icons/gaia-icons.css';
document.head.appendChild(link);
exports.loaded = true;
}
function isLoaded() {
return exports.loaded ||
document.querySelector('link[href*=gaia-icons]') ||
document.documentElement.classList.contains('gaia-icons-loaded');
}
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-icons',this));

View file

@ -0,0 +1,35 @@
{
"name": "gaia-theme",
"version": "0.4.7",
"homepage": "https://github.com/gaia-components/gaia-theme",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"test",
"tests",
"index.html",
"README.md"
],
"devDependencies": {
"gaia-icons": "gaia-components/gaia-icons",
"gaia-header": "gaia-components/gaia-header",
"gaia-button": "gaia-components/gaia-button",
"gaia-switch": "gaia-components/gaia-switch",
"gaia-checkbox": "gaia-components/gaia-checkbox"
},
"_release": "0.4.7",
"_resolution": {
"type": "version",
"tag": "v0.4.7",
"commit": "a1b3220e6ae53503b0809eb04de22f28bc701612"
},
"_source": "git://github.com/fxos-components/gaia-theme.git",
"_target": "~0.4.1",
"_originalSource": "fxos-components/gaia-theme"
}

View file

@ -0,0 +1,4 @@
{
"strict": false,
"node": true
}

View file

@ -0,0 +1,26 @@
{
"name": "gaia-theme",
"version": "0.4.7",
"homepage": "https://github.com/gaia-components/gaia-theme",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"test",
"tests",
"index.html",
"README.md"
],
"devDependencies": {
"gaia-icons": "gaia-components/gaia-icons",
"gaia-header": "gaia-components/gaia-header",
"gaia-button": "gaia-components/gaia-button",
"gaia-switch": "gaia-components/gaia-switch",
"gaia-checkbox": "gaia-components/gaia-checkbox"
}
}

View file

@ -0,0 +1,244 @@
/*
Gaia theme variables
*/
:root {
/** Grey Colors
---------------------------------------------------------*/
--color-alpha: #333333;
--color-beta: #ffffff;
--color-gamma: #4d4d4d;
--color-delta: #5f5f5f;
--color-epsilon: #858585;
--color-zeta: #a6a6a6;
--color-eta: #c7c7c7;
--color-theta: #e7e7e7;
--color-iota: #f4f4f4;
/** Brand Colors
---------------------------------------------------------*/
--color-darkblue: #00539f;
--color-blue: #00caf2;
--color-turquoise: #27c8c2;
--color-darkorange: #e66000;
--color-orange: #ff9500;
--color-yellow: #ffcb00;
--color-violet: #c40c84;
--color-warning: #fbbd3c;
--color-destructive: #e2443a;
--color-preffered: #00ba91;
/** Background
---------------------------------------------------------*/
--background: var(--color-beta);
--background-plus: var(--color-theta);
--background-minus: var(--color-zeta);
/** Borders
---------------------------------------------------------*/
--border-color: var(--color-theta);
/** Text Color
---------------------------------------------------------*/
--text-color: var(--color-gamma);
--text-color-minus: var(--color-eta);
/** Button
---------------------------------------------------------*/
--button-background: var(--color-iota);
/** Links
---------------------------------------------------------*/
--link-color: var(--highlight-color);
/** Inputs
---------------------------------------------------------*/
--input-background: var(--color-beta);
--input-color: var(--color-alpha);
--input-clear-background: #909ca7;
/** Buttons
---------------------------------------------------------*/
--button-box-shadow:
inset 0 -1px 0 #d8d8d8,
inset 0 1px 0 #eeeeee;
--button-box-shadow-active: var(--button-box-shadow);
/** Header
---------------------------------------------------------*/
--header-disabled-button-color: rgba(255,255,255,0.3);
/** Transitions
---------------------------------------------------------*/
--transition-duration: 0.2s;
}
.theme-productivity {
/** Brand Color
---------------------------------------------------------*/
--highlight-color: var(--color-orange);
/** Header
---------------------------------------------------------*/
--header-background: var(--highlight-color);
--header-color: var(--color-beta);
}
.theme-communications {
/** Brand Color
---------------------------------------------------------*/
--highlight-color: var(--color-turquoise);
/** Links
---------------------------------------------------------*/
--link-color: #177874;
/** Header
---------------------------------------------------------*/
--header-background: var(--highlight-color);
--header-color: var(--color-beta);
--header-icon-color: var(--color-beta);
--header-button-color: var(--link-color);
}
.theme-settings {
/** Background
---------------------------------------------------------*/
--background: var(--color-iota);
--background-plus: var(--color-beta);
--background-minus: var(--color-epsilon);
--background-minus-minus: var(--color-zeta);
/** Highlight
---------------------------------------------------------*/
--highlight-color: var(--color-blue);
/** Borders
---------------------------------------------------------*/
--border-color: var(--color-theta);
/** Text Color
---------------------------------------------------------*/
--text-color: var(--color-gamma);
/** Titles
---------------------------------------------------------*/
--title-color: var(--background-minus);
/** Buttons
---------------------------------------------------------*/
--button-background: var(--background-plus);
/** Switch
---------------------------------------------------------*/
--switch-background: var(--background-minus-minus);
/** Checkbox
---------------------------------------------------------*/
--checkbox-border-color: var(--background-minus-minus);
/** Header
---------------------------------------------------------*/
--header-color: var(--title-color);
--header-background: var(--background);
--header-button-color: var(--highlight-color);
--header-disabled-button-color: var(--color-theta);
--header-action-button-color: var(--title-color);
}
.theme-media {
/** Highlight Color
---------------------------------------------------------*/
--highlight-color: var(--color-blue);
/** Background
---------------------------------------------------------*/
--background: var(--color-alpha);
--background-plus: var(--color-gamma);
--background-minus: #2B2B2B;
--background-minus-minus: #1a1a1a;
/** Text Color
---------------------------------------------------------*/
--text-color: var(--color-beta);
/** Button
---------------------------------------------------------*/
--button-background: var(--background-plus);
/** Header
---------------------------------------------------------*/
--header-background: var(--background);
--header-icon-color: var(--text-color);
--header-button-color: var(--highlight-color);
/** Borders
---------------------------------------------------------*/
--border-color: var(--color-gamma);
/** Inputs
---------------------------------------------------------*/
--input-background: var(--background-plus);
/** Text Input
---------------------------------------------------------*/
--text-input-background: var(--background-minus);
/** Switch
---------------------------------------------------------*/
--switch-head-border-color: var(--background-minus-minus);
--switch-background: var(--background-minus-minus);
/** Checkbox
---------------------------------------------------------*/
--checkbox-border-color: var(--background-minus-minus);
/** Buttons
---------------------------------------------------------*/
--button-box-shadow: none;
--button-box-shadow-active: none;
}

View file

@ -0,0 +1,111 @@
(function(define){'use strict';define(function(require,exports,module){
/*jshint esnext:true*/
/**
* Extend from the `HTMLElement` prototype
*
* @type {Object}
*/
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
this.createShadowRoot().innerHTML = template;
this.els = {
themes: this.shadowRoot.querySelector('.themes'),
rtl: this.shadowRoot.querySelector('.toggle-rtl')
};
this.els.rtl.addEventListener('click', this.onRtlClick.bind(this));
this.els.themes.addEventListener('click', this.onThemeClick.bind(this));
this.styleHack();
this.set(localStorage.theme || 'settings');
};
proto.styleHack = function() {
var style = this.shadowRoot.querySelector('style');
style.setAttribute('scoped', '');
this.appendChild(style.cloneNode(true));
};
proto.onThemeClick = function(e) {
var theme = e.target.value;
if (theme) { this.set(theme); }
};
proto.onRtlClick = function() {
document.dir = this.els.rtl.checked ? 'rtl' : 'ltr';
};
proto.set = function(theme) {
var radio = this.shadowRoot.querySelector('[value="' + theme + '"]');
document.body.classList.remove('theme-' + this.theme);
document.body.classList.add('theme-' + theme);
radio.checked = true;
this.theme = theme;
localStorage.theme = theme;
};
var template = `
<style>
* { margin: 0; padding: 0; }
gaia-theme-selector {
position: fixed;
top: 0; left: 0;
z-index: 101;
width: 100%;
height: 30px;
background: rgba(255,255,255,0.8);
color: #000;
font-size: 9px;
text-align: center;
box-shadow: 0 1px 1px rgba(0,0,0,0.1);
direction: ltr;
}
form {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
}
form label {
-moz-margin-start: 9px;
}
form input {
-moz-margin-end: 6px;
vertical-align: middle;
}
.rtl {
position: absolute;
top: 0;
left: 0;
height: 100%;
display: flex;
align-items: center;
}
</style>
<form>
<label class="rtl"><input type="checkbox" class="toggle-rtl" />RTL</label>
<span class="themes">
<label><input type="radio" name="theme" value="productivity"/>Prod</label>
<label><input type="radio" name="theme" value="communications"/>Comms</label>
<label><input type="radio" name="theme" value="media"/>Media</label>
<label><input type="radio" name="theme" value="settings"/>Settings</label>
</span>
</form>`;
document.registerElement('gaia-theme-selector', { prototype: proto });
});})((function(n,w){'use strict';return typeof define=='function'&&define.amd?
define:typeof module=='object'?function(c){c(require,exports,module);}:
function(c){var m={exports:{}},r=function(n){return w[n];};
w[n]=c(r,m.exports,m)||m.exports;};})('gaia-theme-selector',this));

View file

@ -0,0 +1,30 @@
{
"name": "model",
"main": "index.js",
"version": "0.1.0",
"homepage": "https://github.com/gaia-components/model",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"README.md"
],
"dependencies": {
"evt": "wilsonpage/evt#~0.4.0"
},
"_release": "0.1.0",
"_resolution": {
"type": "version",
"tag": "v0.1.0",
"commit": "7464471d4da0743ba4042918083a3429f0fc0fd2"
},
"_source": "git://github.com/fxos-components/model.git",
"_target": "~0.1.0",
"_originalSource": "fxos-components/model"
}

View file

@ -0,0 +1,21 @@
{
"name": "model",
"main": "index.js",
"version": "0.1.0",
"homepage": "https://github.com/gaia-components/model",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"README.md"
],
"dependencies": {
"evt": "wilsonpage/evt#~0.4.0"
}
}

View file

@ -0,0 +1,104 @@
(function(define){define(function(require,exports,module){
'use strict';
/**
* Dependencies
*/
var events = require('evt');
/**
* Exports
*/
module.exports = Model;
function Model(obj) {
if (!(this instanceof Model)) { return mix(obj, Model.prototype); }
this.reset(obj, { silent: true });
this.id = obj.id || obj.key;
}
Model.prototype = events({
/**
* Returns the value of the given
* key, or if not key is given a
* shallow clone of the model is
* returned.
*
* We check `aguments.length` so that
* when calling `get()` with an unknown
* key, `undefined` is returned and not
* the entire model.
*
* Example:
*
* model.get(); //=> { ... }
* model.get('undefinedKey'); //=> undefined
*
* @param {String} key
* @return {*}
* @public
*/
get: function(key) {
var data = this._getData();
return arguments.length ? data[key] : mix({}, data);
},
set: function(key, value, options) {
options = typeof key === 'object' ? value : options;
var silent = options && options.silent;
var data = this._getData();
var keys;
switch (typeof key) {
case 'string':
data[key] = value;
if (!silent) {
this.onKeyChange(key);
this.emit('change', [key]);
}
return;
case 'object':
mix(data, key);
if (!silent) {
keys = Object.keys(key);
keys.forEach(this.onKeyChange, this);
this.emit('change', keys);
}
return;
}
},
setter: function(key, value1) {
return (function(value2) { this.set(key, value1 || value2); }).bind(this);
},
reset: function(data, options) {
if (!data) { return; }
var silent = options && options.silent;
var isArray = data instanceof Array;
this._data = !isArray ? mix({}, data) : data;
if (!silent) { this.emit('reset'); }
},
onKeyChange: function(key) {
var data = this._getData();
this.emit('change:' + key, data[key]);
},
_getData: function() {
this._data = this._data || {};
return this._data;
}
});
function mix(a, b) {
for (var key in b) { a[key] = b[key]; }
return a;
}
});})((function(n,w){return typeof define=='function'&&define.amd?
define:typeof module=='object'?function(c){c(require,exports,module);}:function(c){
var m={exports:{}},r=function(n){return w[n];};w[n]=c(r,m.exports,m)||m.exports;};})('model',this));

View file

@ -0,0 +1,13 @@
{
"name": "requirejs",
"_cacheHeaders": {
"ETag": "\"7f31fa20417d5e7f9bd26dde74d8678c34ff20af\"",
"Content-Length": "83083",
"Content-Type": "text/plain; charset=utf-8"
},
"_release": "e-tag:7f31fa204",
"main": "index.js",
"_source": "https://raw.githubusercontent.com/jrburke/requirejs/2.1.14/require.js",
"_target": "*",
"_originalSource": "https://raw.githubusercontent.com/jrburke/requirejs/2.1.14/require.js"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,30 @@
{
"name": "view",
"version": "0.1.1",
"main": "index.js",
"homepage": "https://github.com/gaia-components/view",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"README.md"
],
"dependencies": {
"evt": "wilsonpage/evt#~0.4.0"
},
"_release": "0.1.1",
"_resolution": {
"type": "version",
"tag": "v0.1.1",
"commit": "5363c45a7150dcdba00815bea91611c14237a9e6"
},
"_source": "git://github.com/fxos-components/view.git",
"_target": "~0.1.1",
"_originalSource": "fxos-components/view"
}

View file

@ -0,0 +1,21 @@
{
"name": "view",
"version": "0.1.1",
"main": "index.js",
"homepage": "https://github.com/gaia-components/view",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"README.md"
],
"dependencies": {
"evt": "wilsonpage/evt#~0.4.0"
}
}

View file

@ -0,0 +1,292 @@
(function(define){define(function(require,exports,module){
'use strict';
/**
* Module Dependencies
*/
var events = require('evt');
/**
* Locals
*/
var counter = 1;
var noop = function() {};
/**
* Exports
*/
module.exports = View;
/**
* Base view class. Accepts
* or creates a root element
* which we template into.
*
* @constructor
*/
function View(options) {
options = options || {};
this.el = options.el || this.el || document.createElement(this.tag);
this.el.id = this.el.id || ('view' + counter++);
this.name = options.name || this.name;
this.els = {};
if (!this.el.className) {
if (this.name) this.el.className = this.name;
if (this.className) this.el.className += ' ' + this.className;
}
bindAll(this);
this.initialize.apply(this, arguments);
}
/**
* Base view prototype,
* mixed in event emitter.
*
* @type {Object}
*/
events(View.prototype);
// Allow for 'emit' or
// 'fire' to trigger events
View.prototype.fire = View.prototype.fire || View.prototype.emit;
/**
* Default tagName
*
* @type {String}
*/
View.prototype.tag = 'div';
View.prototype.name = 'noname';
/**
* Appends the root element
* to the given parent.
*
* @param {Element} parent
* @return {View}
*/
View.prototype.appendTo = function(parent) {
if (!parent) return this;
parent.appendChild(this.el);
this.fire('inserted');
return this;
};
/**
* Prepends the root element
* to the given parent.
*
* @param {Element} parent
* @return {View}
*/
View.prototype.prependTo = function(parent) {
if (!parent) return this;
var first = parent.firstChild;
if (first) parent.insertBefore(this.el, first);
else this.appendTo(parent);
this.fire('inserted');
return this;
};
/**
* Convenient shorthand
* querySelector.
*
* @param {String} query
* @return { Element | null}
*/
View.prototype.find = function(query) {
return this.el.querySelector(query);
};
/**
* Removes the element from
* its current DOM location.
*
* @param {Object} options
* @return {View}
*/
View.prototype.remove = function(options) {
var silent = options && options.silent;
var parent = this.el.parentNode;
if (!parent) return this;
parent.removeChild(this.el);
if (!silent) this.fire('remove');
return this;
};
View.prototype.set = function(key, value) {
value = value === undefined ? '' : value;
this.el.setAttribute(toDashed(key), value);
};
View.prototype.get = function(key) {
return this.el.getAttribute(key);
};
/**
* Returns a function that when called
* will .set() the given key.
*
* If a value is passed to .setter(),
* that value will always be used
* when the returned function is called.
* Else the value passed to the given
* function will be used.
*
* Example:
*
* var setter = this.setter('key', 'value');
* setter(); //=> this.set('key', 'value');
* setter('value2'); //=> this.set('key', 'value');
*
* var setter = this.setter('key');
* setter('value'); //=> this.set('key', 'value');
* setter(); //=> this.set('key');
*
* @param {String} key
* @param {*} value
* @return {Function}
*/
View.prototype.setter = function(key, forced) {
var self = this;
return function(passed) {
var value = forced !== undefined ? forced : passed;
self.set(key, value);
};
};
View.prototype.enable = function(key, value) {
switch(arguments.length) {
case 0:
value = true;
key = 'enabled';
break;
case 1:
if (typeof key === 'boolean') {
value = key;
key = 'enabled';
} else {
value = true;
key = key ? key + '-enabled' : 'enabled';
}
break;
default:
key = key ? key + '-enabled' : 'enabled';
}
this.set(key, !!value);
};
View.prototype.disable = function(key) {
this.enable(key, false);
};
View.prototype.enabler = function(key) {
return (function(value) { this.enable(key, value); }).bind(this);
};
View.prototype.hide = function(key) {
this.toggle(key, false);
};
View.prototype.show = function(key) {
this.toggle(key, true);
};
View.prototype.toggle = function(key, value) {
if (arguments.length === 1 && typeof key === 'boolean') {
value = key;
key = '';
} else {
key = key ? key + '-' : '';
}
this.el.classList.toggle(key + 'hidden', !value);
this.el.classList.toggle(key + 'visible', value);
};
/**
* Removes the element from
* it's current context, firing
* a 'destroy' event to allow
* views to perform cleanup.
*
* Then clears any internal
* references to aid GC.
*
* @return {[type]} [description]
*/
View.prototype.destroy = function(options) {
var noRemove = options && options.noRemove;
if (!noRemove) this.remove();
this.fire('destroy');
this.el = null;
};
View.prototype.toString = function() {
return '[object View]';
};
// Overwrite as required
View.prototype.initialize = noop;
View.prototype.template = function() { return ''; };
/**
* Extends the base view
* class with the given
* properties.
*
* TODO: Pull this out to
* standalone module.
*
* @param {Object} properties
* @return {Function}
*/
View.extend = function(props) {
var Parent = this;
// The extended constructor
// calls the parent constructor
var Child = function() {
Parent.apply(this, arguments);
};
Child.prototype = Object.create(Parent.prototype);
Child.extend = View.extend;
mixin(Child.prototype, props);
return Child;
};
function toDashed(s) {
return s.replace(/\W+/g, '-')
.replace(/([a-z\d])([A-Z])/g, '$1-$2')
.toLowerCase();
}
function mixin(a, b) {
for (var key in b) { a[key] = b[key]; }
return a;
}
function bindAll(object) {
var key;
var fn;
for (key in object) {
fn = object[key];
if (typeof fn === 'function') {
object[key] = fn.bind(object);
}
}
}
});})((function(n,w){return typeof define=='function'&&define.amd?
define:typeof module=='object'?function(c){c(require,exports,module);}:function(c){
var m={exports:{}},r=function(n){return w[n];};w[n]=c(r,m.exports,m)||m.exports;};})('view',this));

67
gaia/apps/camera/build/build.js Executable file
View file

@ -0,0 +1,67 @@
'use strict';
/* jshint node: true */
var utils = require('utils');
var esomin = require('esomin');
function execute(options) {
var appName = utils.basename(options.APP_DIR);
var config = utils.getFile(options.APP_DIR, 'build',
'require_config.jslike');
var rjsPath = utils.joinPath(options.GAIA_DIR, 'build', 'r.js');
var requirejs;
if (utils.isNode()) {
requirejs = require(rjsPath);
} else {
var sandbox = utils.createSandbox();
sandbox.arguments = [];
sandbox.requirejsAsLib = true;
sandbox.print = function() {
utils.log(appName, Array.prototype.join.call(arguments, ' '));
};
utils.runScriptInSandbox(rjsPath, sandbox);
requirejs = sandbox.requirejs;
}
// logLevel set 4 for silent, set 0 for all
var log = 'logLevel=' + (options.VERBOSE === '1' ? '0' : '4');
var optimize = 'optimize=none';
var build = new Promise(function(resolve, reject) {
requirejs.optimize([config.path, optimize, log], resolve, reject);
});
return build
.then(function() {
utils.log(appName, 'require.js optimize done');
})
.then(function() {
if (options.GAIA_OPTIMIZE === '1') {
utils.log(appName, 'esomin minify done');
return esomin.minifyDir(options.STAGE_APP_DIR);
}
})
.catch(function(err) {
utils.log(appName, 'running customize build failed');
utils.log(appName, err);
});
}
function copyUserConfig(options) {
var targetDir = utils.joinPath(options.STAGE_APP_DIR, 'js', 'config');
var filename = 'config.js';
if (options.GAIA_DISTRIBUTION_DIR) {
var distConfig = utils.getFile(options.GAIA_DISTRIBUTION_DIR,
'camera-config.js');
if (distConfig.exists()) {
utils.copyFileTo(distConfig, targetDir, filename);
}
}
}
exports.execute = function(options) {
copyUserConfig(options);
return execute(options);
};

View file

@ -0,0 +1,30 @@
{
appDir: '..',
baseUrl: 'js',
mainConfigFile: '../js/config/require.js',
dir: '../../../build_stage/camera',
insertRequire: ['main'],
name: 'main',
include: [
'../bower_components/requirejs/index',
'config/require',
],
// Be sure to normalize all define() calls by extracting
// dependencies so Function toString is not needed, and
// lower capability devices like Tarako can optimize
// memory by discarding function sources. This is
// automatically done when an 'optimize' value other than
// 'none' is used. This setting makes sure it happens for
// builds where 'none' is used for 'optimize'.
normalizeDirDefines: 'all',
// This is defined in the makefile
//optimize: 'uglify',
optimizeCss: 'standard',
// Keeping build dir since Makefile cleans it up and
// preps build dir with the shared directory
keepBuildDir: true,
removeCombined: true
}

46
gaia/apps/camera/index.html Executable file
View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html class="gaia-icons-loaded">
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<meta name="theme-color" content="#242d33">
<title>Camera</title>
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/camera.{locale}.properties">
<link rel="localization" href="/shared/locales/date/date.{locale}.properties">
<link rel="localization" href="/shared/locales/media/media.{locale}.properties">
<script defer src="/shared/js/intl/l20n.js"></script>
<script defer src="shared/js/lazy_loader.js"></script>
<!-- <link rel="stylesheet" type="text/css" href="/shared/style/confirm.css" /> -->
<!-- <link rel="stylesheet" type="text/css" href="/shared/style/buttons.css" /> -->
<!-- <link rel="stylesheet" type="text/css" href="/shared/style/action_menu.css" /> -->
<!-- <link rel="stylesheet" type="text/css" href="/shared/style/progress_activity.css" /> -->
<!--
The following scripts are lazy loaded but left here to ensure
they get copied over from shared during the build
-->
<!-- <script src="/shared/js/async_storage.js"></script> -->
<!-- <script src="/shared/js/media/jpeg_metadata_parser.js"></script> -->
<!-- <script src="/shared/js/media/get_video_rotation.js"></script> -->
<!-- <script src="/shared/js/media/video_player.js"></script> -->
<!-- <script src="/shared/js/media/media_frame.js"></script> -->
<!-- <script src="/shared/js/media/downsample.js"></script> -->
<!-- <script src="/shared/js/media/image_size.js"></script> -->
<!-- <script src="/shared/js/media/jpeg-exif.js"></script> -->
<!-- <script src="/shared/js/media/crop_resize_rotate.js"></script> -->
<!-- <script src="/shared/js/gesture_detector.js"></script> -->
<!-- <script src="/shared/js/blobview.js"></script> -->
<!-- <script src="/shared/js/custom_dialog.js"></script> -->
<!-- <script src="/shared/js/format.js"></script> -->
<!-- <script src="/shared/js/stop_recording_event.js"></script> -->
<!-- <script src="/shared/js/sanitizer.js"></script> -->
<link rel="stylesheet" type="text/css" href="style/main.css" />
</head>
<body class="theme-media">
<script src="js/main.js"></script>
</body>
</html>

523
gaia/apps/camera/js/app.js Executable file
View file

@ -0,0 +1,523 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var stopRecordingEvent = require('stop-recording-event');
var NotificationView = require('views/notification');
var LoadingView = require('views/loading-screen');
var orientation = require('lib/orientation');
var bindAll = require('lib/bind-all');
var debug = require('debug')('app');
var Pinch = require('lib/pinch');
var bind = require('lib/bind');
var model = require('model');
/**
* Exports
*/
module.exports = App;
/**
* Mixin `Model` API
*/
model(App.prototype);
/**
* Initialize a new `App`
*
* Options:
*
* - `root` The node to inject content into
*
* @param {Object} options
* @constructor
*/
function App(options) {
debug('initialize');
bindAll(this);
this.views = {};
this.dynamicLazy = {};
this.el = options.el;
this.win = options.win;
this.doc = options.doc;
this.perf = options.perf || {};
this.pinch = options.pinch || new Pinch(this.el); // Test hook
this.require = options.require || window.requirejs; // Test hook
this.LoadingView = options.LoadingView || LoadingView; // test hook
this.orientation = options.orientation || orientation; // test hook
this.inSecureMode = (this.win.location.hash === '#secure');
this.controllers = options.controllers;
this.geolocation = options.geolocation;
this.settings = options.settings;
this.camera = options.camera;
this.activity = {};
this.sounds = options.sounds;
debug('initialized');
}
/**
* Runs all the methods to boot the app.
*
* The loading screen is shown until the
* camera is 'ready', then it is taken down.
*
* @public
*/
App.prototype.boot = function() {
debug('boot');
if (this.booted) { return; }
this.showSpinner('requestingCamera');
this.bindEvents();
this.initializeViews();
this.runControllers();
// PERFORMANCE MARKER (1): navigationLoaded
// Designates that the app's *core* chrome or navigation interface
// exists in the DOM and is marked as ready to be displayed.
// PERFORMANCE MARKER (2): navigationInteractive
// Designates that the app's *core* chrome or navigation interface
// has its events bound and is ready for user interaction.
window.performance.mark('navigationLoaded');
window.performance.mark('navigationInteractive');
this.injectViews();
this.booted = true;
debug('booted');
};
App.prototype.dispatchEvent = function(name) {
this.win.dispatchEvent(new CustomEvent(name));
};
/**
* Runs controllers to glue all
* the parts of the app together.
*
* @private
*/
App.prototype.runControllers = function() {
debug('run controllers');
this.controllers.overlay(this);
this.controllers.battery(this);
this.controllers.settings(this);
this.controllers.activity(this);
this.controllers.camera(this);
this.controllers.viewfinder(this);
this.controllers.hud(this);
this.controllers.controls(this);
this.controllers.storage(this);
debug('controllers run');
};
/**
* Load an extra lazy controller. Used for
* controllers only needed based on configuration.
*
* @param {String} controller
*/
App.prototype.loadLazyController = function(controller) {
if (!this.criticalPathDone) {
this.controllers.lazy.push(controller);
return;
}
if (!this.dynamicLazy[controller]) {
this.dynamicLazy[controller] = this.loadLazyControllers([controller]);
}
return this.dynamicLazy[controller];
};
/**
* Load and run all the lazy controllers.
*
* @param {Function} done
*/
App.prototype.loadLazyControllers = function(controllers) {
debug('load lazy controllers');
var self = this;
return new Promise(function(resolve, reject) {
self.require(controllers, function() {
[].forEach.call(arguments, function(controller) { controller(self); });
debug('controllers loaded');
self.emit('lazyloaded');
resolve();
});
});
};
/**
* Initialize views.
*
* @private
*/
App.prototype.initializeViews = function() {
debug('initializing views');
this.views.notification = new NotificationView();
debug('views initialized');
};
/**
* Put views in the DOM.
*
* @private
*/
App.prototype.injectViews = function() {
debug('injecting views');
this.views.notification.appendTo(this.el);
debug('views injected');
};
/**
* Attaches event handlers.
*
* @private
*/
App.prototype.bindEvents = function() {
debug('binding events');
// App
this.once('storage:checked:healthy', this.geolocationWatch);
this.on('storage:checked', function() { this.storageChecked = true; }.bind(this));
this.once('viewfinder:visible', this.onCriticalPathDone);
this.once('camera:error', this.onCriticalPathDone);
this.once('activity', this.onActivity);
this.once('newthumbnail', this.onNewThumbnail);
this.once('preview', this.onPreview);
this.once('camera:willchange', this.onWillChange);
this.on('camera:willchange', this.firer('busy'));
this.on('ready', this.clearSpinner);
this.on('visible', this.onVisible);
this.on('hidden', this.onHidden);
this.on('reboot', this.onReboot);
this.on('busy', this.onBusy);
// Settings
if (this.settings.countdown.selected('key') !== 'off') {
this.onCountdown();
} else {
this.settings.countdown.once('change:selected', this.onCountdown);
}
// Pinch
this.pinch.on('changed', this.firer('pinch:changed'));
this.on('previewgallery:opened', this.pinch.disable);
this.on('previewgallery:closed', this.pinch.enable);
this.on('settings:opened', this.pinch.disable);
this.on('settings:closed', this.pinch.enable);
// DOM
bind(this.doc, 'visibilitychange', this.onVisibilityChange);
// we bind to window.onlocalized in order not to depend
// on l10n.js loading (which is lazy). See bug 999132
bind(this.doc, 'DOMRetranslated', this.firer('localized'));
bind(this.win, 'beforeunload', this.onBeforeUnload);
bind(this.win, 'keydown', this.onKeyDown);
bind(this.el, 'click', this.onClick);
debug('events bound');
};
/**
* Tasks to run when the
* app becomes visible.
*
* Check the storage again as users
* may have made changes since the
* app was minimised
*/
App.prototype.onVisible = function() {
this.geolocationWatch();
this.orientation.start();
this.orientation.lock();
debug('visible');
};
/**
* Tasks to run when the
* app is minimised/hidden.
*
* @private
*/
App.prototype.onHidden = function() {
this.geolocation.stopWatching();
this.orientation.stop();
debug('hidden');
};
/**
* Reboots the application
*
* @private
*/
App.prototype.onReboot = function() {
debug('reboot');
window.location.reload();
};
/**
* Emit a click event that other
* modules can listen to.
*
* @private
*/
App.prototype.onClick = function() {
debug('click');
this.emit('click');
};
/**
* Log when critical path has completed.
*
* @private
*/
App.prototype.onCriticalPathDone = function() {
if (this.criticalPathDone) { return; }
debug('critical path done');
// PERFORMANCE MARKER (3): visuallyLoaded
// Designates that the app is visually loaded (e.g.: all of the
// "above-the-fold" content exists in the DOM and is marked as
// ready to be displayed).
window.performance.mark('visuallyLoaded');
// Load non-critical modules
this.listenForStopRecordingEvent();
this.loadLazyModules();
this.perf.criticalPath = Date.now();
this.criticalPathDone = true;
this.emit('criticalpathdone');
};
/**
* Lazy load activity dependencies.
*
* @private
*/
App.prototype.onActivity = function() {
this.loadLazyController('controllers/confirm');
};
App.prototype.onCountdown = function() {
this.loadLazyController('controllers/countdown');
};
App.prototype.onNewThumbnail = function() {
this.loadLazyController('controllers/preview-gallery');
};
App.prototype.onPreview = function() {
var self = this;
this.emit('busy', 'lazyLoading');
this.loadLazyController('controllers/preview-gallery').then(function() {
self.emit('preview');
self.emit('ready');
});
};
App.prototype.onWillChange = function() {
this.loadLazyController('controllers/recording-timer');
};
App.prototype.loadLazyModules = function() {
debug('load lazy modules');
var self = this;
var load = this.loadLazyControllers(this.controllers.lazy);
var storage = new Promise(function(resolve, reject) {
if (self.storageChecked) { resolve(); }
else { self.once('storage:checked', resolve); }
});
// All done
Promise.all([load, storage]).then(function() {
debug('app fully loaded');
// PERFORMANCE MARKER (4): contentInteractive
// Designates that the app has its events bound for the minimum
// set of functionality to allow the user to interact with the
// "above-the-fold" content.
window.performance.mark('contentInteractive');
self.loaded = true;
self.emit('loaded');
self.perf.loaded = Date.now();
self.logPerf();
// PERFORMANCE MARKER (5): fullyLoaded
// Designates that the app is *completely* loaded and all relevant
// "below-the-fold" content exists in the DOM, is marked visible,
// has its events bound and is ready for user interaction. All
// required startup background processing should be complete.
window.performance.mark('fullyLoaded');
});
};
App.prototype.logPerf =function() {
var timing = window.performance.timing;
console.log('first module: %s',
this.perf.firstModule - this.perf.jsStarted + 'ms');
console.log('critical-path: %s',
this.perf.criticalPath - timing.domLoading + 'ms');
console.log('app-fully-loaded: %s',
this.perf.loaded - timing.domLoading + 'ms');
};
/**
* Begins watching location if not within
* a pending activity and the app isn't
* currently hidden.
*
* Watching is delayed by the `promptDelay`
* defined in settings.
*
* @private
*/
App.prototype.geolocationWatch = function() {
var shouldWatch = !this.activity.pick && !this.hidden;
if (shouldWatch) { this.geolocation.watch(); }
};
/**
* Responds to the `visibilitychange`
* event, emitting useful app events
* that allow us to perform relate
* work elsewhere.
*
* @private
*/
App.prototype.onVisibilityChange = function() {
this.hidden = this.doc.hidden;
this.emit(this.hidden ? 'hidden' : 'visible');
};
/**
* Runs just before the
* app is destroyed.
*
* @private
*/
App.prototype.onBeforeUnload = function() {
this.emit('beforeunload');
debug('beforeunload');
};
/**
* Shows the loading screen after the
* number of ms defined in config.js
*
* @param {String} type
* @private
*/
App.prototype.showSpinner = function(key) {
debug('show loading type: %s', key);
this.busy = true;
var view = this.views.loading;
if (view) {
return;
}
var ms = this.settings.spinnerTimeouts.get(key) || 0;
var self = this;
clearTimeout(this.spinnerTimeout);
this.spinnerTimeout = setTimeout(function() {
self.views.loading = new self.LoadingView();
self.views.loading.appendTo(self.el).show();
debug('loading shown');
}, ms);
};
/**
* Clears the loadings screen, or
* any pending loading screen.
*
* @private
*/
App.prototype.clearSpinner = function() {
debug('clear loading');
this.busy = false;
var view = this.views.loading;
clearTimeout(this.spinnerTimeout);
if (!view) { return; }
view.hide(view.destroy);
this.views.loading = null;
};
/**
* When the camera indicates it's busy it
* sometimes passes a `type` string. When
* this type matches one of our keys in the
* `spinnerTimeouts` config, we display the
* loading screen passing on the type.
*
* @param {String} type
* @private
*/
App.prototype.onBusy = function(type) {
debug('camera busy, type: %s', type);
this.busy = true;
var delay = this.settings.spinnerTimeouts.get(type);
if (delay) { this.showSpinner(type); }
};
/**
* If the system app is opening an attention screen (because
* of an incoming call or an alarm, e.g.) and if we are
* currently recording a video then we need to stop recording
* before the ringer or alarm starts sounding. We will be sent
* to the background shortly after this and will stop recording
* when that happens, but by that time it is too late and we
* have already recorded some sound. See bugs 995540 and 1006200.
*
* Similarly, if the user presses the Home button or switches to
* another app while recording, we need to stop recording right away,
* even if the system is overloaded and we don't get a visiblitychange
* event right away. See bug 1046167.
*
* To make this work, we rely on shared/js/stop_recording_event.js which
* abuses the settings API to allow the system app to broadcast a "you
* will soon be hidden" message to any certified apps that care. There
* ought to be a better way, but this is a quick way to fix a
* last-minute release blocker.
*
* @private
*/
App.prototype.listenForStopRecordingEvent = function() {
debug('listen for stop recording events');
// Start the module that generates
// the stoprecording events and listen
// for those custom DOM events and emit
// them using our internal event emitter
stopRecordingEvent.start();
addEventListener('stoprecording', this.firer('stoprecording'));
};
/**
* When the device's hardware keys
* are pressed we emit a global
* app event that other controllers
* can respond to.
*
* TIP: Check config.js for the map
* of key names to types.
*
* @param {Event} e
* @private
*/
App.prototype.onKeyDown = function(e) {
var key = e.key.toLowerCase();
var type = this.settings.keyDownEvents.get(key);
if (type) { this.emit('keydown:' + type, e); }
};
});

View file

@ -0,0 +1,377 @@
define(function(require, exports, module) {
'use strict';
module.exports = {
// This remaining globals are required by external dependencies of camera.
// shared/js/media/jpeg_metadata_parser.js
// shared/js/media/media_frame.js
globals : {
// The maximum picture size that camera is allowed to take
CONFIG_MAX_IMAGE_PIXEL_SIZE: 24 * 1024 * 1024,
CONFIG_MAX_SNAPSHOT_PIXEL_SIZE: 24 * 1024 * 1024,
// Size of the exif preview embeded in images taken by camera
CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH: 0,
CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT: 0
},
zoom: {
disabled: false,
// The viewfinder preview stream should automatically
// reflect the current zoom value. However, on some
// devices, the viewfinder needs to be scaled by the
// application. Set this flag if the preview stream
// does not visually reflect the zoom value properly.
useZoomPreviewAdjustment: false
},
focus: {
// Set this properties to false if you
// want to disable focus modes
// even on hardware that supports them
// -----------------------------------
// The camera will be continously focusing
// on a point of the scene. It is the center of the image
// unless touch to focus is enabled and the user selects a
// different region.
continuousAutoFocus: true,
// The user can select the area of the image
// where the camera is going to try to focus the scene.
touchFocus: true,
// The camera detects faces and tries to focus
// on them.
faceDetection: true
},
previewGallery: {
// Dimensions for thumbnail image (will automatically be
// multiplied by the devicePixelRatio)
thumbnailWidth: 54,
thumbnailHeight: 54
},
viewfinder: {
scaleType: 'fill',
// Used to adjust sensitivity for pinch-to-zoom gesture
// (smaller values = more sensitivity)
zoomGestureSensitivity: 0.425
},
battery: {
levels: {
low: 15,
verylow: 10,
critical: 6,
shutdown: 5,
healthy: 100
}
},
sounds: {
list: [
{
name: 'shutter',
url: './resources/sounds/shutter.opus',
setting: 'camera.sound.enabled'
},
{
name: 'countdown',
url: './resources/sounds/countdown.opus',
setting: 'camera.sound.enabled'
},
{
name: 'recordingStart',
url: './resources/sounds/camcorder_start.opus',
setting: 'camera.sound.enabled'
},
{
name: 'recordingEnd',
url: './resources/sounds/camcorder_end.opus',
setting: 'camera.sound.enabled'
}
]
},
keyDownEvents: {
camera: 'capture',
volumeup: 'capture',
volumedown: 'capture',
mozcamerafocusadjust: 'focus',
},
activity: {
// The amount to scale pixelSize derived from
// 'pick' activities that define `width` or `height`
// parameters. The larger the scale factor, the larger
// the activity `maxPixelSize` icreasing the probability
// that a larger pictureSize will be chosen for the activity.
maxPixelSizeScaleFactor: 2.5,
// Reduce the size of images returned by pick activities.
// A pick activity can specify its own maximum size. However,
// this setting can lower that pixel size limitation even
// further. To prevent further limiting the pixel size for
// pick activities, set this value to `0`.
// (useful for devices with limited memory)
maxPickPixelSize: 0,
// Reduce the size of images returned by share activities.
// To prevent resizing images that are shared, set this
// value to `0`.
// (useful for devices with limited memory)
maxSharePixelSize: 0
},
spinnerTimeouts: {
takingPicture: 1650,
requestingCamera: 850,
loadingVideo: 100,
resizingImage: 100,
lazyLoading: 300
},
mode: {
options: [
{
key: 'picture'
},
{
key: 'video'
}
],
persistent: false
},
isoModes: {
disabled: false,
options: [
{
key: 'auto'
}
],
selected:'auto'
},
whiteBalance: {
disabled: false,
options: [
{
key: 'auto'
}
],
selected:'auto'
},
cameras: {
options: [
{
key: 'back',
icon: 'toggle-camera-rear',
title: 'toggle-camera-rear'
},
{
key: 'front',
icon: 'toggle-camera-front',
title: 'toggle-camera-front'
}
],
persistent: false
},
pictureSizesFront: {
title: 'camera-resolution',
header: 'camera-resolution-header',
icon: 'picture-size',
options: [],
include: {
aspects: ['4:3'],
},
persistent: true,
optionsLocalizable: false,
},
pictureSizesBack: {
title: 'camera-resolution',
header: 'camera-resolution-header',
icon: 'picture-size',
options: [],
include: {
aspects: ['4:3'],
},
persistent: true,
optionsLocalizable: false,
},
recorderProfilesBack: {
title: 'video-resolution',
header: 'video-resolution-header',
icon: 'video-size',
exclude: ['low', 'high', 'default'],
options: [],
persistent: true,
optionsLocalizable: false,
},
recorderProfilesFront: {
title: 'video-resolution',
header: 'video-resolution-header',
icon: 'video-size',
exclude: ['low', 'high', 'default'],
options: [],
persistent: true,
optionsLocalizable: false,
},
flashModesPicture: {
title: 'flash',
options: [
{
key: 'auto',
icon: 'flash-auto',
title: 'flash-auto'
},
{
key: 'on',
icon: 'flash-on',
title: 'flash-on'
},
{
key: 'off',
icon: 'flash-off',
title: 'flash-off'
}
],
persistent: true
},
flashModesVideo: {
title: 'flash',
options: [
{
key: 'off',
icon: 'flash-off',
title: 'flash-off'
},
{
key: 'torch',
icon: 'flash-on',
title: 'flash-on'
}
],
persistent: true
},
countdown: {
title: 'self-timer',
header: 'self-timer-header',
icon: 'self-timer',
options: [
{
key: 'off',
title: 'self-timer-off',
value: 0
},
{
key: 'secs2',
value: 2,
title: 'self-timer-2-seconds'
},
{
key: 'secs5',
value: 5,
title: 'self-timer-5-seconds'
},
{
key: 'secs10',
value: 10,
title: 'self-timer-10-seconds'
}
],
persistent: false,
},
hdr: {
title: 'hdr',
header: 'hdr-header',
icon: 'hdr-boxed',
disabled: false,
options: [
{
key: 'off',
title: 'hdr-off'
},
{
key: 'on',
title: 'hdr-on'
}
],
persistent: true
},
scene: {
title: 'scene-mode',
header: 'scene-mode-header',
icon: 'scene',
options: [
{
key: 'normal',
title: 'scene-mode-normal'
},
{
key: 'pano',
title: 'scene-mode-panorama'
},
{
key: 'beauty',
title: 'scene-mode-beauty'
}
],
persistent: true,
},
grid: {
title: 'grid',
header: 'grid-header',
icon: 'grid-circular',
options: [
{
key: 'off',
title: 'grid-off'
},
{
key: 'on',
title: 'grid-on'
}
],
selected: 'off',
persistent: true,
notifications: false
},
settingsMenu: {
items: [
// {
// key: 'scene'
// },
{
key: 'hdr'
},
{
key: 'countdown'
},
{
key: 'pictureSizes'
},
{
key: 'recorderProfiles'
},
{
key: 'grid'
}
]
}
};
});

View file

@ -0,0 +1,97 @@
requirejs.config({
baseUrl: '/js',
// 'paths' lets us alias complex
// paths to something simpler.
paths: {
'asyncStorage': '../shared/js/async_storage',
'getVideoRotation': '../shared/js/media/get_video_rotation',
'jpegMetaDataParser': '../shared/js/media/jpeg_metadata_parser',
'downsample': '../shared/js/media/downsample',
'getImageSize': '../shared/js/media/image_size',
'cropResizeRotate': '../shared/js/media/crop_resize_rotate',
'format': '../shared/js/format',
'GestureDetector': '../shared/js/gesture_detector',
'VideoPlayer': '../shared/js/media/video_player',
'MediaFrame': '../shared/js/media/media_frame',
'BlobView': '../shared/js/blobview',
'CustomDialog': '../shared/js/custom_dialog',
'debug': '../bower_components/debug/index',
'attach': '../bower_components/attach/index',
'model': '../bower_components/model/index',
'view': '../bower_components/view/index',
'evt': '../bower_components/evt/index',
'drag': '../bower_components/drag/index',
'device-orientation': '../bower_components/device-orientation/device-orientation',/*jshint ignore:line*/
'stop-recording-event': '../shared/js/stop_recording_event'
},
// If your package uses relative `require()` paths
// internally, then it needs to be defined as
// a 'package' so they are resolved correctly.
packages: [
{
name: 'gaia-header',
location: '../bower_components/gaia-header',
main: 'gaia-header'
},
{
name: 'gaia-icons',
location: '../bower_components/gaia-icons',
main: 'gaia-icons'
},
{
name: 'gaia-component',
location: '../bower_components/gaia-component',
main: 'gaia-component'
},
{
name: 'font-fit',
location: '../bower_components/font-fit',
main: 'font-fit'
}
],
// 'shim' config lets us `require()` packages
// that don't have an AMD define call.
shim: {
'format': {
exports: 'Format'
},
'getVideoRotation': {
deps: ['BlobView'],
exports: 'getVideoRotation'
},
'MediaFrame': {
deps: ['format', 'VideoPlayer', 'downsample'],
exports: 'MediaFrame'
},
'BlobView': {
exports: 'BlobView'
},
'asyncStorage': {
exports: 'asyncStorage'
},
'jpegMetaDataParser': {
deps: ['BlobView'],
exports: 'parseJPEGMetadata'
},
'getImageSize': {
deps: ['BlobView', 'jpegMetaDataParser'],
exports: 'getImageSize'
},
'cropResizeRotate': {
deps: ['BlobView', 'getImageSize', 'jpegMetaDataParser', 'downsample'],
exports: 'cropResizeRotate'
},
'GestureDetector': {
exports: 'GestureDetector'
},
'CustomDialog': {
exports: 'CustomDialog'
},
'stop-recording-event': {
exports: 'StopRecordingEvent'
}
}
});

View file

@ -0,0 +1,308 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:activity');
var bytesToPixels = require('lib/bytes-to-pixels');
var bindAll = require('lib/bind-all');
/**
* Exports
*/
module.exports = function(app) { return new ActivityController(app); };
module.exports.ActivityController = ActivityController;
/**
* Initialize new `ActivityController`
*
* @param {App} app
*/
function ActivityController(app) {
bindAll(this);
this.app = app;
this.win = app.win;
this.settings = app.settings;
this.configure();
this.bindEvents();
debug('initialized');
}
/**
* Supported activity types.
*
* @type {Object}
*/
ActivityController.prototype.types = {
pick: 'pick',
record: 'record'
};
/**
* Initial configuration.
*
* @private
*/
ActivityController.prototype.configure = function() {
this.name = this.getName();
this.app.activity[this.name] = true;
};
/**
* Filter down pictureSizes and
* recorderProfiles to match activity
* parameters each time the settings
* are configured.
*
* @private
*/
ActivityController.prototype.bindEvents = function() {
this.app.on('activitycanceled', this.onActivityCanceled);
this.app.on('confirm:selected', this.onActivityConfirmed);
// If an activity name was found, we must bind
// the listener straight away so we don't miss
// the event, otherwise we can bind more lazily.
if (this.name) { this.setupListener(); }
else { this.app.once('criticalpathdone', this.setupListener); }
};
ActivityController.prototype.setupListener = function() {
debug('setup listener');
navigator.mozSetMessageHandler('activity', this.onMessage);
debug('listener setup');
};
/**
* Get activity name from the hash fragment.
*
* This is used only so that some parts
* of the app can prepare for an incoming
* activity before the message arrives.
*
* @private
*/
ActivityController.prototype.getName = function() {
var hash = this.win.location.hash;
var name = hash && hash.substr(1);
return this.types[name];
};
/**
* Responds to incoming activity message.
*
* Configures the mode then emits an
* event so that other parts of
* the app can update accordingly.
*
* @param {MozActivity} activity
*/
ActivityController.prototype.onMessage = function(activity) {
debug('incoming activity', activity);
var name = activity.source.name;
var supported = this.types[name];
// Exit if this activity isn't supported
if (!supported) { return; }
var data = {
name: name,
maxPixelSize: this.getMaxPixelSize(activity),
maxFileSizeBytes: activity.source.data.maxFileSizeBytes
};
this.activity = activity;
this.configureMode(activity);
this.app.emit('activity', data);
this.app.emit('activity:' + name, data);
};
/**
* Configures the mode setting based
* on the parameters supplied by
* the incoming activity.
*
* @param {MozActivity} activity
* @private
*/
ActivityController.prototype.configureMode = function(activity) {
var type = activity.source.data.type;
var name = activity.source.name;
var modes = (name === 'pick') ?
this.getModesForPick(type) :
this.getModesForRecord(type);
this.settings.mode.filterOptions(modes);
this.settings.mode.select(modes[0]);
debug('configured mode', modes);
};
/**
* Get a max pixel size estimate based
* on the parameters supplied by the
* incoming activity.
*
* Activities don't always supply pixel
* size restrictions.
*
* NOTE: There
*
* @param {MozActivity} activity
* @return {Number|null}
*/
ActivityController.prototype.getMaxPixelSize = function(activity) {
var data = activity.source.data;
var bytes = data.maxFileSizeBytes;
var maxPickPixelSize = this.settings.activity.get('maxPickPixelSize') || 0;
var maxPixelSize;
// If bytes were specified then derive
// a maxPixelSize from that, else we
// calculate the maxPixelSize using
// supplied dimensions.
if (bytes) {
maxPixelSize = bytesToPixels(bytes);
} else if (data.width || data.height) {
maxPixelSize = this.getMaxPixelsFromSize(data);
} else {
maxPixelSize = maxPickPixelSize;
}
// If the Camera app has been configured to have a max pixel size
// for pick activities, ensure we are at or below that value.
if (maxPickPixelSize > 0) {
maxPixelSize = Math.min(maxPixelSize, maxPickPixelSize);
}
debug('maxPixelsSize: %s', maxPixelSize);
return maxPixelSize;
};
ActivityController.prototype.getMaxPixelsFromSize = function(size) {
var scale = this.settings.activity.get('maxPixelSizeScaleFactor');
var aspect = 4 / 3;
// In the event that only one dimension has
// been supplied, calculate the largest the
// other edge could be based on a 4:3 aspect.
var width = size.width || size.height * aspect;
var height = size.height || size.width * aspect;
var pixels = width * height;
// Take the actual total number of
// pixels asked for and bump it by the
// `scale` to cover pictureSizes above
// the pixels asked for. We later resize
// the image post-capture to the exact size
// requested (data.width * data.height).
//
// This is to avoid us picking a pictureSize
// smaller than the number of pixels requested
// and then to scaling up post-capture,
// resulting in a shitty image.
return pixels * scale;
};
/**
* Parse types given by 'pick' activity
* and return a list of modes.
*
* @param {Array|String} types
* @return {Array}
*/
ActivityController.prototype.getModesForPick = function(types) {
types = [].concat(types || []);
var modes = [];
types.forEach(function(item) {
var type = item.split('/')[0];
var mode = type === 'image' ? 'picture' : type;
if (modes.indexOf(mode) === -1) {
modes.push(mode);
}
});
if (modes.length === 0) {
modes = ['picture', 'video'];
}
return modes;
};
/**
* Parse types given by 'record' activity
* and return a list of modes.
*
* @param {String} type
* @return {Array}
*/
ActivityController.prototype.getModesForRecord = function(type) {
return type === 'videos' ?
['video', 'picture'] :
['picture', 'video'];
};
/**
* Respond to activity cancelation
* events and send the error call
* via the original acitity object.
*
* @private
*/
ActivityController.prototype.onActivityCanceled = function() {
if (!this.activity) { return; }
this.activity.postError('pick cancelled');
};
// TODO: Messy, tidy up!
ActivityController.prototype.onActivityConfirmed = function(newMedia) {
var self = this;
var activity = this.activity;
var media = {
blob: newMedia.blob
};
// In low end devices resizing can be slow.
// We display a spinner
this.app.showSpinner();
// Video
if (newMedia.isVideo) {
media.type = 'video/3gpp';
media.poster = newMedia.poster.blob;
activity.postResult(media);
this.app.clearSpinner();
}
// Image
else {
media.type = 'image/jpeg';
// We need to call resizeImageAndSave() unconditionally even if
// the image does not need to be resized because if the camera
// produces images with EXIF orientation, we need to rotate the
// image before returning it from the pick activity. If the image
// does not need to be resized or rotated, then resizeImageAndSave
// will return it unchanged, and will not re-save it.
require(['lib/resize-image-and-save'], function(resizeImageAndSave) {
resizeImageAndSave(
{
blob: newMedia.blob,
width: activity.source.data.width,
height: activity.source.data.height
},
function onImageResized(resizedBlob) {
media.blob = resizedBlob;
activity.postResult(media);
self.app.clearSpinner();
}
);
});
}
};
});

View file

@ -0,0 +1,171 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:battery');
var bindAll = require('lib/bind-all');
var bind = require('lib/bind');
/**
* Exports
*/
module.exports = function(app) { return new BatteryController(app); };
module.exports.BatteryController = BatteryController;
/**
* Initialize a new `BatteryController`
*
* @param {Object} options
*/
function BatteryController(app) {
bindAll(this);
this.app = app;
this.battery = app.battery || navigator.battery || navigator.mozBattery;
if (!this.battery) { this.app.set('batteryStatus', 'unknown'); return; }
this.levels = app.settings.battery.get('levels');
this.notification = app.views.notification;
this.bindEvents();
this.updateStatus();
debug('initialized');
}
/**
* Bind callbacks to required events.
*
* @private
*/
BatteryController.prototype.bindEvents = function() {
bind(this.battery, 'levelchange', this.updateStatus);
bind(this.battery, 'chargingchange', this.updateStatus);
this.app.on('change:batteryStatus', this.onStatusChange);
this.app.on('change:recording', this.updatePowerSave);
var mozSettings = navigator.mozSettings;
mozSettings.addObserver('powersave.enabled', this.onPowerSaveChange);
mozSettings.createLock().get('powersave.enabled').then(
this.onPowerSaveChange);
};
/**
* Callback from settings when the power save state changes.
*
* @private
*/
BatteryController.prototype.onPowerSaveChange = function(values) {
var value;
if (values.settingValue !== undefined) {
value = values.settingValue;
} else {
value = values['powersave.enabled'];
}
this.powerSaveEnabled = value;
this.updatePowerSave();
};
/**
* Emits powersave event if state has changed.
*
* @private
*/
BatteryController.prototype.updatePowerSave = function() {
var state = this.powerSaveEnabled && !this.app.get('recording');
if (this.powerSave === state) {
return;
}
this.powerSave = state;
debug('power save: ' + state);
this.app.emit('battery:powersave', state);
};
/**
* Map of status keys to message.
*
* @type {Object}
* @private
*/
BatteryController.prototype.notifications = {
low: {
text: 'battery-low-text',
attrs: {
'data-icon': 'battery-3',
'data-l10n-id': 'battery-low-indicator'
},
},
verylow: {
text: 'battery-verylow-text',
attrs: {
'data-icon': 'battery-1',
'data-l10n-id': 'battery-verylow-indicator'
}
},
critical: {
text: 'battery-critical-text',
attrs: {
'data-icon': 'battery-1',
'data-l10n-id': 'battery-critical-indicator'
},
persistent: true
}
};
/**
* Updates app `batteryStatus` and
* manages battery notifications.
*
* @private
*/
BatteryController.prototype.updateStatus = function () {
var previous = this.app.get('batteryStatus');
var current = this.getStatus(this.battery);
if (current !== previous) {
this.app.set('batteryStatus', current);
}
};
/**
* Returns a status key derived
* from the given `battery` object.
*
* @param {Battery} battery
* @return {String}
* @private
*/
BatteryController.prototype.getStatus = function(battery) {
var level = Math.round(battery.level * 100);
var levels = this.levels;
if (battery.charging) { return 'charging'; }
else if (level <= levels.shutdown) { return 'shutdown'; }
else if (level <= levels.critical) { return 'critical'; }
else if (level <= levels.verylow) { return 'verylow'; }
else if (level <= levels.low) { return 'low'; }
else { return 'healthy'; }
};
BatteryController.prototype.onStatusChange = function(status) {
this.clearLastNotification();
this.displayNotification(status);
};
BatteryController.prototype.displayNotification = function(status) {
var notification = this.notifications[status];
if (!notification) { return; }
this.lastNotification = this.notification.display({
text: notification.text,
className: notification.className,
attrs: notification.attrs,
persistent: notification.persistent
});
};
BatteryController.prototype.clearLastNotification = function() {
this.notification.clear(this.lastNotification);
};
});

View file

@ -0,0 +1,581 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:camera');
var bindAll = require('lib/bind-all');
/**
* Exports
*/
module.exports = function(app) { return new CameraController(app); };
module.exports.CameraController = CameraController;
/**
* Initialize a new `CameraController`
*
* @param {App} app
*/
function CameraController(app) {
bindAll(this);
this.app = app;
this.camera = app.camera;
this.settings = app.settings;
this.activity = app.activity;
this.hdrDisabled = this.settings.hdr.get('disabled');
this.notification = app.views.notification;
// Wait until we get the battery state before turning
// on the camera
this.lowBattery = this.app.get('batteryStatus') === 'shutdown';
this.configure();
this.bindEvents();
debug('initialized');
}
CameraController.prototype.bindEvents = function() {
var settings = this.settings;
var camera = this.camera;
var app = this.app;
// Relaying camera events means other modules
// don't have to depend directly on camera
camera.on('change:previewActive', this.app.firer('camera:previewactive'));
camera.on('change:videoElapsed', app.firer('camera:recorderTimeUpdate'));
camera.on('autofocuschanged', app.firer('camera:autofocuschanged'));
camera.on('focusconfigured', app.firer('camera:focusconfigured'));
camera.on('change:focus', app.firer('camera:focusstatechanged'));
camera.on('filesizelimitreached', this.onFileSizeLimitReached);
camera.on('facesdetected', app.firer('camera:facesdetected'));
camera.on('willrecord', app.firer('camera:willrecord'));
camera.on('configured', app.firer('camera:configured'));
camera.on('requesting', app.firer('camera:requesting'));
camera.on('change:recording', this.onRecordingChange);
camera.on('newcamera', app.firer('camera:newcamera'));
camera.on('newimage', app.firer('camera:newimage'));
camera.on('newvideo', app.firer('camera:newvideo'));
camera.on('shutter', app.firer('camera:shutter'));
camera.on('loaded', app.firer('camera:loaded'));
camera.on('closed', this.onCameraClosed);
camera.on('error', app.firer('camera:error'));
camera.on('ready', app.firer('ready'));
camera.on('busy', app.firer('busy'));
// App
app.on('viewfinder:focuspointchanged', this.onFocusPointChanged);
app.on('change:batteryStatus', this.onBatteryStatusChange);
app.on('settings:configured', this.onSettingsConfigured);
app.on('previewgallery:opened', this.onGalleryOpened);
app.on('previewgallery:closed', this.onGalleryClosed);
app.on('stoprecording', this.camera.stopRecording);
app.on('storage:volumechanged', this.onStorageVolumeChanged);
app.on('storage:changed', this.onStorageChanged);
app.on('activity:pick', this.onPickActivity);
app.on('keydown:capture', this.onCaptureKey);
app.on('keydown:focus', this.onFocusKey);
app.on('hidden', this.shutdownCamera);
app.on('click', this.clearCountdown);
app.on('visible', this.loadCamera);
app.on('capture', this.capture);
// Settings
settings.recorderProfiles.on('change:selected', this.updateRecorderProfile);
settings.pictureSizes.on('change:selected', this.updatePictureSize);
settings.flashModes.on('change:selected', this.onFlashModeChange);
settings.flashModes.on('change:selected', this.setFlashMode);
settings.cameras.on('change:selected', this.setCamera);
settings.mode.on('change:selected', this.setMode);
settings.hdr.on('change:selected', this.setHDR);
settings.hdr.on('change:selected', this.onHDRChange);
debug('events bound');
};
CameraController.prototype.onRecordingChange = function(recording) {
var active;
if (recording === 'started') {
active = true;
} else if (recording === 'stopped') {
active = false;
} else {
return;
}
this.app.set('recording', active);
};
/**
* Take picture or start/end recording
* when a capture hardware key is invoked.
*
* Calling `.preventDefault()` prevents
* the default system operation
* (eg. changing volume level). We
* only call it when capture request
* succeeds.
*
* We don't want to .preventDefault() when
* the preview-gallery is open as the
* user may want to change the volume
* of a video being played back.
*
* @param {Event} e
* @private
*/
CameraController.prototype.onCaptureKey = function(e) {
debug('on capture key', e);
if (this.capture() !== false) { e.preventDefault(); }
};
/**
* Focus the camera when a focus
* hardware key is invoked.
*
* @param {Event} e
* @private
*/
CameraController.prototype.onFocusKey = function(e) {
debug('on focus key', e);
if (!this.shouldCapture()) { return; }
this.camera.focus.focus();
};
/**
* Configure the 'cameras' setting using the
* `cameraList` data given by the camera hardware
*
* @private
*/
CameraController.prototype.configure = function() {
this.settings.cameras.filterOptions(this.camera.cameraList);
debug('configured');
};
/**
* Once the settings have finished configuring
* we do the final camera configuration.
*
* @private
*/
CameraController.prototype.onSettingsConfigured = function() {
var recorderProfile = this.settings.recorderProfiles.selected('key');
var pictureSize = this.settings.pictureSizes.selected('data');
this.setWhiteBalance();
this.setFlashMode();
this.setISO();
this.setHDR();
this.camera.setRecorderProfile(recorderProfile);
this.camera.setPictureSize(pictureSize);
this.camera.configureZoom();
// Defer this work as it involves
// expensive mozSettings calls
setTimeout(this.updateZoomForMako);
debug('camera configured with final settings');
};
/**
* Updates camera configuration in
* response to pick activity parameters.
*
* @param {Object} data
* @private
*/
CameraController.prototype.onPickActivity = function(data) {
// This is set so that the video recorder can
// automatically stop when video size limit is reached.
this.camera.set('maxFileSizeBytes', data.maxFileSizeBytes);
// Disable camera config caches when in 'pick' activity
// to prevent activity specific configuration persisting.
this.camera.cacheConfig = false;
};
CameraController.prototype.capture = function(options = {}) {
var force = options.force;
if (!this.shouldCapture()) { return false; }
if (!force && this.shouldCountdown()) { return this.startCountdown(); }
if (this.countdown) { return this.clearCountdown(); }
var position = this.app.geolocation.position;
return this.camera.capture({ position: position });
};
CameraController.prototype.shouldCapture = function() {
return !this.app.get('confirmViewVisible') &&
!this.app.hidden &&
!this.app.busy &&
!this.galleryOpen &&
!this.lowBattery;
};
CameraController.prototype.shouldCountdown = function() {
var countdownSet = this.settings.countdown.selected('value');
var recording = this.app.get('recording');
return countdownSet &&
!this.countdown &&
!recording;
};
CameraController.prototype.startCountdown = function() {
if (this.countdown) { return; }
var seconds = this.settings.countdown.selected('value');
var self = this;
if (!seconds) { return; }
this.app.emit('countdown:started', seconds);
(function scheduleTick() {
self.countdown = setTimeout(() => {
if (--seconds <= 0) {
self.clearCountdown();
self.capture({ force: true });
return;
}
self.app.emit('countdown:tick', seconds);
scheduleTick();
}, 1000);
})();
};
CameraController.prototype.clearCountdown = function() {
if (!this.countdown) { return; }
clearTimeout(this.countdown);
this.countdown = null;
this.app.emit('countdown:ended');
};
CameraController.prototype.onFileSizeLimitReached = function() {
this.camera.stopRecording();
this.showSizeLimitAlert();
};
CameraController.prototype.showSizeLimitAlert = function() {
if (this.sizeLimitAlertActive) { return; }
this.sizeLimitAlertActive = true;
var alertL10nId = this.activity.pick ?
'activity-size-limit-reached' :
'storage-size-limit-reached';
document.l10n.formatValue(alertL10nId).then(alert);
this.sizeLimitAlertActive = false;
};
/**
* Set the camera's mode (picture/video).
*
* We send a signal to say that the camera
* 'will change', this allows other parts
* of the app to respond if need be.
*
* We then wait for the viewfinder to be 'hidden'
* before setting the camera to prevent the
* user from seeing the stream flicker/jump.
*
* @param {String} mode ['picture'|'video']
* @private
*/
CameraController.prototype.setMode = function(mode) {
debug('set mode: %s', mode);
var self = this;
// Abort if didn't change.
//
// TODO: Perhaps the `Setting` instance should
// not emit a `change` event if the value did
// not change? This may require some deep checking
// if the value is an object. Quite a risky change
// to make, but would remove the need for us to check
// here and in other change callbacks. Food 4 thought :)
if (this.camera.isMode(mode)) {
debug('mode didn\'t change');
return;
}
var l10nId = mode == 'video' ? 'Video-Mode' : 'Photo-Mode';
this.notification.display({ text: l10nId });
this.setFlashMode();
this.app.emit('camera:willchange');
this.app.once('viewfinder:hidden', function() {
self.camera.setMode(mode);
});
};
/**
* Updates the camera's `pictureSize` to match
* the size set in the app's settings.
*
* When in 'picture' mode we send a signal
* to say that the camera 'will change',
* this allows other parts of the app to
* repsond if need be.
*
* We then wait for the viewfinder to be hidden
* before setting the pictureSize to prevent the
* user from seeing the stream flicker/jump.
*
* @private
*/
CameraController.prototype.updatePictureSize = function() {
debug('update picture-size');
var pictureMode = this.settings.mode.selected('key') === 'picture';
var value = this.settings.pictureSizes.selected('data');
var self = this;
// Don't do anything if the picture size didn't change
if (this.camera.isPictureSize(value)) { return; }
// If not currently in 'picture'
// mode, just configure.
if (!pictureMode) {
this.camera.setPictureSize(value, { configure: false });
return;
}
// Make change once the viewfinder is hidden
this.app.emit('camera:willchange');
this.app.once('viewfinder:hidden', function() {
self.camera.setPictureSize(value);
});
};
/**
* Updates the camera's `recorderProfile` to
* match the size set in the app's settings.
*
* When in 'picture' mode we send a signal
* to say that the camera 'will change',
* this allows other parts of the app to
* repsond if need be.
*
* We then wait for the viewfinder to be hidden
* before setting the pictureSize to prevent the
* user from seeing the stream flicker/jump.
*
* @private
*/
CameraController.prototype.updateRecorderProfile = function() {
debug('update recorder-profile');
var videoMode = this.settings.mode.selected('key') === 'video';
var key = this.settings.recorderProfiles.selected('key');
var self = this;
// Don't do anything if the recorder-profile didn't change
if (this.camera.isRecorderProfile(key)) { return; }
// If not currently in 'video'
// mode, just configure.
if (!videoMode) {
this.camera.setRecorderProfile(key, { configure: false });
return;
}
// Wait for the viewfinder to be hidden
this.app.emit('camera:willchange');
this.app.once('viewfinder:hidden', function() {
self.camera.setRecorderProfile(key);
});
};
/**
* Set the selected camera (front/back).
*
* We send a signal to say that the camera
* 'will change', this allows other parts
* of the app to respond if need be.
*
* We then wait for the viewfinder to be 'hidden'
* before setting the camera to prevent the
* user from seeing the stream flicker/jump.
*
* @param {String} camera ['front'|'back']
* @private
*/
CameraController.prototype.setCamera = function(camera) {
debug('set camera: %s', camera);
var self = this;
this.app.emit('camera:willchange');
this.app.once('viewfinder:hidden', function() {
self.camera.setCamera(camera);
});
};
/**
* Sets the flash mode on the camera
* to match the current flash mode
* in the app's settings.
*
* @private
*/
CameraController.prototype.setFlashMode = function() {
var flashSetting = this.settings.flashModes;
this.camera.setFlashMode(flashSetting.selected('key'));
};
CameraController.prototype.setISO = function() {
if (!this.settings.isoModes.get('disabled')) {
this.camera.setISOMode(this.settings.isoModes.selected('key'));
}
};
CameraController.prototype.setWhiteBalance = function() {
if (!this.settings.whiteBalance.get('disabled')) {
this.camera.setWhiteBalance(this.settings.whiteBalance.selected('key'));
}
};
CameraController.prototype.setHDR = function() {
if (this.hdrDisabled) { return; }
this.camera.setHDR(this.settings.hdr.selected('key'));
};
CameraController.prototype.onFlashModeChange = function(flashModes) {
if (this.hdrDisabled) { return; }
var ishdrOn = this.settings.hdr.selected('key') === 'on';
if (ishdrOn && flashModes !== 'off') {
this.settings.hdr.select('off');
}
};
CameraController.prototype.onHDRChange = function(hdr) {
var flashMode = this.settings.flashModesPicture.selected('key');
var ishdrOn = hdr === 'on';
if (ishdrOn && flashMode !== 'off') {
this.settings.flashModesPicture.select('off');
}
};
CameraController.prototype.onBatteryStatusChange = function(status) {
this.lowBattery = status === 'shutdown';
if (this.lowBattery) { this.shutdownCamera(); }
else { this.loadCamera(); }
};
/**
* Stop recording if storage changes state.
* Examples:
* 'shared' usually due to the device being connected to
* a computer via USB.
* 'unavailable' when the SDCARD is yanked
*
* @private
*/
CameraController.prototype.onStorageChanged = function(state) {
if (state !== 'available') {
this.camera.stopRecording();
}
};
/**
* For instance, when the storage volume changes from to internal memory
* to the SD Card
*
* @private
*/
CameraController.prototype.onStorageVolumeChanged = function(storage) {
this.camera.setStorage(storage);
};
/**
* Updates focus area when the user clicks on the viewfinder
*/
CameraController.prototype.onFocusPointChanged = function(focusPoint) {
if (this.countdown) { return; }
this.camera.updateFocusArea(focusPoint.area);
};
CameraController.prototype.loadCamera = function(showSpinner) {
if (this.lowBattery || this.galleryOpen || this.app.hidden) {
return;
}
if (showSpinner) { this.app.showSpinner(); }
this.camera.load();
};
CameraController.prototype.shutdownCamera = function() {
this.clearCountdown();
this.camera.shutdown();
};
/**
* Camera hardware can be closed after a failure or after app request
* It reboots the application in the case of failure
*
* @private
*/
CameraController.prototype.onCameraClosed = function(reason) {
reason = reason || 'SystemFailure';
if (reason === 'SystemFailure') {
this.app.emit('reboot');
}
};
CameraController.prototype.onGalleryOpened = function() {
this.galleryOpen = true;
this.shutdownCamera();
};
/**
* As the camera is shutdown when the
* preview gallery is opened, we must
* reload it when it is closed.
*
* Although if the app is has been minimised
* we do not want to reload the camera as
* the hardware must be released when the
* app is not visible.
*
* @private
*/
CameraController.prototype.onGalleryClosed = function(reason) {
this.galleryOpen = false;
if (this.app.hidden) { return; }
this.loadCamera(true);
};
/**
* For some reason, the above calculation
* for `maxHardwareZoom` does not work
* properly on Mako (Nexus-4) devices.
*
* Bug 983930 - [B2G][Camera] CameraControl API's
* "zoom" attribute doesn't scale preview properly
*
* @private
*/
CameraController.prototype.updateZoomForMako = function() {
debug('update zoom for mako');
var self = this;
navigator.mozSettings
.createLock()
.get('deviceinfo.hardware')
.onsuccess = onSuccess;
debug('settings request made');
function onSuccess(e) {
var device = e.target.result['deviceinfo.hardware'];
if (device !== 'mako') { return; }
var frontCamera = self.camera.selectedCamera === 'front';
var maxHardwareZoom = frontCamera ? 1 : 1.25;
// Nexus 4 needs zoom preview adjustment since the viewfinder preview
// stream does not automatically reflect the current zoom value.
self.settings.zoom.set('useZoomPreviewAdjustment', true);
self.camera.set('maxHardwareZoom', maxHardwareZoom);
self.camera.emit('zoomconfigured', self.camera.getZoom());
debug('zoom reconfigured for mako');
}
};
});

View file

@ -0,0 +1,127 @@
define(function(require, exports, module) {
/*jshint laxbreak:true*/
'use strict';
/**
* Module Dependencies
*/
var prepareBlob = require('lib/prepare-preview-blob');
var debug = require('debug')('controller:confirm');
var ConfirmView = require('views/confirm');
var bindAll = require('lib/bind-all');
/**
* Exports
*/
module.exports = function(options) { return new ConfirmController(options); };
module.exports.ConfirmController = ConfirmController;
/**
* Initialize a new `ConfirmController`
*
* @param {Object} options
*/
function ConfirmController(app) {
this.app = app;
this.settings = app.settings;
this.activity = app.activity;
this.camera = app.camera;
this.container = app.el;
// Allow these dependencies
// to be injected if need be.
this.ConfirmView = app.ConfirmView || ConfirmView;
this.prepareBlob = app.prepareBlob || prepareBlob;
bindAll(this);
this.bindEvents();
debug('initialized');
}
ConfirmController.prototype.renderView = function() {
if (!this.activity.pick) { return; }
if (!this.view) {
this.view = new this.ConfirmView();
this.view.maxPreviewSize = window.CONFIG_MAX_IMAGE_PIXEL_SIZE;
this.view.render().appendTo(this.container);
// We want to listen to select events exactly once
// The media is selected, the confirm view dismissed
// and the activity returns
this.view.once('click:select', this.onSelectMedia);
this.view.on('click:retake', this.onRetakeMedia);
this.view.on('loadingvideo', this.app.firer('busy'));
this.view.on('playingvideo', this.app.firer('ready'));
}
this.view.show();
this.app.set('confirmViewVisible', true);
};
/**
* Bind callbacks to required events.
*
*/
ConfirmController.prototype.bindEvents = function() {
// Render/Show the view on the `newimage` and `newvideo` events
// since they are fired immediately when tapping 'Capture'/'Stop'.
// This prevents the 'Capture'/'Stop' button from being able to be
// triggered multiple times before the confirm view appears.
this.camera.on('newimage', this.renderView);
this.camera.on('change:recording', this.onRecordingChange);
// Update the MediaFrame contents with the image/video upon
// receiving the `newmedia` event. This event is slightly delayed
// since it waits for the storage callback to complete.
this.app.on('newmedia', this.onNewMedia);
};
ConfirmController.prototype.onRecordingChange = function(recording) {
if (!this.activity.pick) { return; }
if (recording === 'starting') {
this.recording = true;
} else if (recording === 'error') {
this.recording = false;
} else if (recording === 'stopped' && this.recording) {
this.renderView();
}
};
/**
* When inside a 'pick' activity
* will present the user with a
* confirm overlay where they can
* decide to 'select' or 'retake'
* the photo or video
*
* @param {Object} data
*
*/
ConfirmController.prototype.onNewMedia = function(newMedia) {
if (!this.activity.pick) { return; }
this.newMedia = newMedia;
if (newMedia.isVideo) { // Is video
this.view.showVideo(newMedia);
} else { // Is Image
this.prepareBlob(this.newMedia.blob, this.view.showImage);
}
};
ConfirmController.prototype.onSelectMedia = function() {
this.app.emit('confirm:selected', this.newMedia);
};
ConfirmController.prototype.onRetakeMedia = function() {
this.view.hide();
this.view.clearMediaFrame();
this.app.set('confirmViewVisible', false);
};
});

328
gaia/apps/camera/js/controllers/controls.js vendored Executable file
View file

@ -0,0 +1,328 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:controls');
var ControlsView = require('views/controls');
var bindAll = require('lib/bind-all');
/**
* Exports
*/
module.exports = function(app) { return new ControlsController(app); };
module.exports.ControlsController = ControlsController;
/**
* Initialize a new `ControlsController`
*
* @param {App} app
*/
function ControlsController(app) {
bindAll(this);
this.app = app;
this.activity = app.activity;
this.createView();
this.bindEvents();
debug('initialized');
}
/**
* Event bindings.
*
* @private
*/
ControlsController.prototype.bindEvents = function() {
this.app.settings.mode.on('change:selected', this.view.setMode);
this.app.settings.mode.on('change:options', this.configureMode);
// App
this.app.on('change:recording', this.onRecordingChange);
this.app.on('camera:shutter', this.captureHighlightOff);
this.app.on('camera:willchange', this.onCameraWillChange);
this.app.on('camera:configured', this.onCameraConfigured);
this.app.on('newthumbnail', this.onNewThumbnail);
this.app.once('loaded', this.onceAppLoaded);
this.app.on('busy', this.onCameraBusy);
this.app.on('localized', this.view.localize);
// View
this.view.on('modechanged', this.onViewModeChanged);
this.view.on('click:thumbnail', this.app.firer('preview'));
this.view.on('click:cancel', this.onCancelButtonClick);
this.view.on('click:capture', this.onCaptureClick);
// Timer
this.app.on('countdown:started', this.onCountdownStarted);
this.app.on('countdown:ended', this.onCountdownStopped);
// Settings
this.app.on('settings:opened', this.onSettingsOpened);
this.app.on('settings:closed', this.onSettingsClosed);
// Preview gallery
this.app.on('previewgallery:opened', this.view.hide);
this.app.on('previewgallery:closed', this.view.show);
debug('events bound');
};
/**
* Create and configure the view.
*
* @private
*/
ControlsController.prototype.createView = function() {
var initialMode = this.app.settings.mode.selected('key');
var cancellable = !!this.app.activity.pick;
// Create the view (test hook)
this.view = this.app.views.controls || new ControlsView();
// The gallery button should not
// be shown if an activity is pending
// or the application is in 'secure mode'.
this.view.set('cancel', cancellable);
this.view.setMode(initialMode);
// Disable view until camera
// 'ready' enables it.
this.view.disable();
// Put it in the DOM
this.view.appendTo(this.app.el);
debug('cancelable: %s', cancellable);
debug('mode: %s', initialMode);
};
/**
* Disables the switch if there is
* only one modes available.
*
* This is only the case if an activity
* indicated it only supports one mode,
* just 'picture' or 'video'.
*
* @private
*/
ControlsController.prototype.configureMode = function() {
var switchable = this.app.settings.mode.get('options').length > 1;
if (!switchable) { this.view.disable('switch'); }
};
/**
* Once the app is loaded, we can enable
* the controls. We also bind a listener
* that enabled the controls whenever
* the camera becomes 'ready' from
* hereon after.
*
* @private
*/
ControlsController.prototype.onceAppLoaded = function() {
this.app.on('ready', this.restore);
this.view.enable();
};
/**
* Keep capture button pressed and
* fire the `capture` event to allow
* the camera to repond.
*
* When the 'camera:shutter' event fires
* we remove the capture butter pressed
* state so that it times with the
* capture sound effect.
*
* @private
*/
ControlsController.prototype.onCaptureClick = function() {
this.captureHighlightOn();
this.app.emit('capture');
};
/**
* Prevent the mode switch from being changed
* by the user when we are in the process of
* configuring the camera.
*
* @private
*/
ControlsController.prototype.onCameraWillChange = function() {
this.view.suspendModeSwitch(true);
};
ControlsController.prototype.onCameraConfigured = function() {
this.view.suspendModeSwitch(false);
};
/**
* Set the recording attribute on
* the view to allow it to style
* accordingly.
*
* @param {Boolean} recording
* @private
*/
ControlsController.prototype.onRecordingChange = function(recording) {
this.view.set('recording', recording);
if (!recording) { this.onRecordingEnd(); }
// Update capture button label when recording changes.
this.view.setCaptureLabel(recording);
};
/**
* Remove the capture highlight,
* once recording has finished.
*
* @private
*/
ControlsController.prototype.onRecordingEnd = function() {
this.captureHighlightOff();
};
/**
* When the thumbnail changes, update it in the view.
* This method is triggered by the 'newthumbnail' event.
* That event is emitted by the preview gallery controller when the a new
* photo or video is added, or when the preview is closed and the first
* photo or video has changed (because of a file deletion).
*/
ControlsController.prototype.onNewThumbnail = function(thumbnailBlob) {
if (thumbnailBlob) {
this.view.setThumbnail(thumbnailBlob);
} else {
this.view.removeThumbnail();
}
};
/**
* Forces the capture button to
* look pressed while the countdown is
* counting down and hides controls.
*
* @private
*/
ControlsController.prototype.onCountdownStarted = function() {
this.captureHighlightOn();
this.view.set('countdown', 'active');
};
/**
* Forces the capture button to
* look unpressed when the countdown
* stops and shows controls.
*
* @private
*/
ControlsController.prototype.onCountdownStopped = function() {
this.captureHighlightOff();
this.view.set('countdown', 'inactive');
};
/**
* Make controls invisible to the screen reader since they are behind settings
* overlay.
*
* @private
*/
ControlsController.prototype.onSettingsOpened = function() {
this.view.setScreenReaderVisible(false);
};
/**
* Make controls visible to the screen reader again when settings are closed.
*
* @private
*/
ControlsController.prototype.onSettingsClosed = function() {
this.view.setScreenReaderVisible(true);
};
ControlsController.prototype.onCameraBusy = function() {
this.view.disable();
};
/**
* Restores the capture button to its
* unpressed state and re-enables buttons.
*
* @private
*/
ControlsController.prototype.restore = function() {
debug('restore');
this.captureHighlightOff();
this.view.enable();
};
/**
* Make the capture button
* appear pressed.
*
* @private
*/
ControlsController.prototype.captureHighlightOn = function() {
this.view.set('capture-active');
};
/**
* Remove the pressed apperance
* from the capture button.
*
* @private
*/
ControlsController.prototype.captureHighlightOff = function() {
this.view.unset('capture-active');
};
/**
* Switch to the next capture mode:
* 'picture' or 'video', when the
* mode is changed via the view.
*
* Them mode can be changed by either
* tapping or swiping the mode switch.
*
* @private
*/
ControlsController.prototype.onViewModeChanged = function() {
debug('view mode changed');
this.app.settings.mode.next();
};
ControlsController.prototype.onCancelButtonClick = function() {
this.app.emit('activitycanceled');
};
/**
* Open the gallery app when the
* gallery button is pressed.
*
* @private
*/
ControlsController.prototype.onGalleryButtonClick = function(event) {
event.stopPropagation();
var MozActivity = window.MozActivity;
// Can't launch the gallery if the lockscreen is locked.
// The button shouldn't even be visible in this case, but
// let's be really sure here.
if (this.app.inSecureMode) { return; }
// Launch the gallery with an activity
this.mozActivity = new MozActivity({
name: 'browse',
data: { type: 'photos' }
});
// Wait 2000ms before re-enabling the
// Gallery to be launched (Bug 957709)
this.view.disable();
setTimeout(this.view.enable, 2000);
};
});

View file

@ -0,0 +1,66 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:countdown');
var CountdownView = require('views/countdown');
var bindAll = require('lib/bind-all');
/**
* Exports
*/
module.exports = function(app) { return new CountdownController(app); };
module.exports.CountdownController = CountdownController;
/**
* Create a new `CountdownController`
*
* @param {App} app
*/
function CountdownController(app) {
bindAll(this);
this.app = app;
this.view = app.views.countdown || new CountdownView(); // test hook
this.view.appendTo(app.el);
this.bindEvents();
debug('initialized');
}
/**
* Connects the countdown view
* with the app via events.
*
* @private
*/
CountdownController.prototype.bindEvents = function() {
this.app.on('countdown:started', this.start);
this.app.on('countdown:tick', this.update);
this.app.on('countdown:ended', this.clear);
};
CountdownController.prototype.start = function(seconds) {
this.update(seconds);
this.view.show();
debug('started');
};
CountdownController.prototype.update = function(seconds) {
if (seconds < 1) { seconds = ''; }
var immanent = seconds <= 3;
this.view
.set(seconds)
.setImmanent(immanent);
if (immanent && seconds > 0) { this.app.emit('countdown:immanent'); }
};
CountdownController.prototype.clear = function() {
this.view.hide(this.view.reset);
};
});

View file

@ -0,0 +1,181 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:hud');
var debounce = require('lib/debounce');
var bindAll = require('lib/bind-all');
var HudView = require('views/hud');
/**
* Exports
*/
module.exports = function(app) { return new HudController(app); };
module.exports.HudController = HudController;
/**
* Initialize a new `HudController`
*
* @param {AppController} app
* @constructor
*
*/
function HudController(app) {
bindAll(this);
this.app = app;
this.settings = app.settings;
this.notification = app.views.notification;
this.createView();
this.bindEvents();
debug('initialized');
}
/**
* Create and configure the HUD view.
*
* Disable flash button until we know
* whether the hardware has flash.
*
* @private
*/
HudController.prototype.createView = function() {
var hasDualCamera = this.settings.cameras.get('options').length > 1;
this.view = this.app.views.hud || new HudView(); // test hook
this.view.enable('camera', hasDualCamera);
this.view.disable('flash');
this.view.hide();
this.updateCamera();
this.view.appendTo(this.app.el);
};
/**
* Bind callbacks to events.
*
* @return {HudController} for chaining
* @private
*/
HudController.prototype.bindEvents = function() {
// App
this.app.on('change:recording', this.view.setter('recording'));
this.app.on('ready', this.view.setter('camera', 'ready'));
this.app.on('busy', this.view.setter('camera', 'busy'));
// We need the app to be first localized before localizing the hud view.
this.app.on('localized', this.localize);
// Settings
this.app.once('settings:configured', this.view.show);
this.app.on('settings:configured', this.updateFlashSupport);
this.app.settings.flashModes.on('change:selected', this.updateFlashMode);
this.app.settings.mode.on('change:selected', this.updateFlashMode);
this.app.settings.cameras.on('change:selected', this.updateCamera);
// We 'debouce' some UI callbacks to prevent
// thrashing the hardware when a user taps repeatedly.
// This means the first calback will fire instantly but
// subsequent events will be blocked for given time period.
this.view.on('click:camera', debounce(this.onCameraClick, 500, true));
this.view.on('click:settings', this.app.firer('settings:toggle'));
this.view.on('click:flash', this.onFlashClick);
// Countdown
this.app.on('countdown:started', this.view.setter('countdown', 'active'));
this.app.on('countdown:ended', this.view.setter('countdown', 'inactive'));
// Settings
this.app.on('settings:opened', this.view.hide);
this.app.on('settings:closed', this.view.show);
// Preview gallery
this.app.on('previewgallery:opened', this.view.hide);
this.app.on('previewgallery:closed', this.view.show);
};
HudController.prototype.onCameraClick = function() {
debug('camera clicked');
this.clearNotifications();
this.app.settings.cameras.next();
};
HudController.prototype.clearNotifications = function() {
this.notification.clear(this.flashNotification);
};
/**
* Cycle to the next available flash
* option, update the HUD view and
* show a change notification.
*
* @private
*/
HudController.prototype.onFlashClick = function() {
var setting = this.settings.flashModes;
var ishdrOn = this.settings.hdr.selected('key') === 'on';
setting.next();
this.view.set('flashMode' , setting.selected('key'));
this.notify(setting, ishdrOn);
};
/**
* Display a notifcation showing the
* current state of the given setting.
*
* @param {Setting} setting
* @private
*/
HudController.prototype.notify = function(setting, hdrDeactivated) {
var optionTitle = '<span data-l10n-id="' +
setting.selected('title') + '"></span>';
var title = '<span data-l10n-id="' + setting.get('title') +'"></span>';
var html;
// Check if the `hdr` setting is going to be deactivated as part
// of the change in the `flashMode` setting and display a specialized
// notification if that is the case
if (hdrDeactivated) {
html = title + ' ' + optionTitle + '<br/>' +
'<span data-l10n-id="hdr-deactivated"></span>';
} else {
html = title + '<br/>' + optionTitle;
}
this.flashNotification = this.notification.display({ text: {html: html} });
};
/**
* Localize hud view when app is localized or locale updated.
*/
HudController.prototype.localize = function() {
this.view.setFlashModeLabel(this.settings.flashModes.selected());
this.view.setCameraLabel(this.settings.cameras.selected());
this.view.setMenuLabel();
};
HudController.prototype.updateFlashMode = function() {
var selected = this.settings.flashModes.selected();
if (!selected) { return; }
this.view.setFlashMode(selected);
debug('updated flash mode: %s', selected.key);
};
HudController.prototype.updateFlashSupport = function() {
var supported = this.settings.flashModes.supported();
this.view.enable('flash', supported);
this.updateFlashMode();
debug('flash supported: %s', supported);
};
HudController.prototype.updateCamera = function() {
var selected = this.settings.cameras.selected();
if (!selected) { return; }
this.view.setCamera(selected);
debug('updated camera: %s', selected.key);
};
});

View file

@ -0,0 +1,89 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:indicators');
var IndicatorsView = require('views/indicators');
/**
* Exports
*/
module.exports = function(app) { return new IndicatorsController(app); };
module.exports.IndicatorsController = IndicatorsController;
/**
* Initialize a new `IndicatorsController`
*
* @param {Object} options
*/
function IndicatorsController(app) {
this.app = app;
this.settings = app.settings;
this.configure = this.configure.bind(this);
this.createView();
this.configure();
this.bindEvents();
debug('initialized');
}
/**
* Creates and injects the view.
*
* The view is hidden until the
* settings are configured.
*
* @private
*/
IndicatorsController.prototype.createView = function() {
debug('create view');
this.view = this.app.views.indicators || new IndicatorsView();
this.view.appendTo(this.app.el);
debug('view created');
};
/**
* Bind to relevant events.
*
* @public
*/
IndicatorsController.prototype.bindEvents = function() {
this.settings.countdown.on('change:selected', this.view.setter('countdown'));
this.settings.mode.on('change:selected', this.view.setter('mode'));
this.settings.hdr.on('change:selected', this.view.setter('hdr'));
this.app.on('change:batteryStatus', this.view.setter('battery'));
this.app.on('change:recording', this.view.setter('recording'));
this.app.on('settings:configured', this.configure);
// Handle indicators screen reader visibility
// depending on settings visibility. When settings
// are open, though the view is behind an overlay
// and not hidden off screen, it still needs
// to be hidden from the screen reader.
this.app.on('settings:opened', this.view.setter('ariaHidden', true));
this.app.on('settings:closed', this.view.setter('ariaHidden', false));
// Preview gallery
this.app.on('previewgallery:opened', this.view.hide);
this.app.on('previewgallery:closed', this.view.show);
debug('events bound');
};
/**
* Configures the view to match
* current settings state.
*
* @private
*/
IndicatorsController.prototype.configure = function() {
debug('configuring');
this.view.set('hdr', this.settings.hdr.selected('key'));
this.view.set('countdown', this.settings.countdown.selected('key'));
this.view.set('battery', this.app.get('batteryStatus'));
debug('configured');
};
});

View file

@ -0,0 +1,214 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:media');
var bindAll = require('lib/bind-all');
/**
* Exports
*/
module.exports = function(app) { return new MediaController(app); };
module.exports.MediaController = MediaController;
function MediaController(app) {
bindAll(this);
this.app = app;
this.require = app.require;
this.settings = app.settings;
this.bindEvents();
this.configure();
debug('initialized');
}
MediaController.prototype.bindEvents = function() {
this.app.on('storage:itemdeleted', this.onItemDeleted);
this.app.on('previewgallery:deletevideo', this.onItemDeleted);
this.app.on('previewgallery:deletepicture', this.onItemDeleted);
this.app.on('storage:changed', this.onStorageChanged);
this.app.on('newmedia', this.onNewMedia);
this.app.on('hidden', this.onHidden);
this.app.on('lazyloaded', this.configured);
// Preload libraries we may need after capturing to maintain snappiness
var self = this;
this.app.once('capture', function() {
var lib = ['lib/create-thumbnail-image', 'lib/prepare-preview-blob'];
self.require(lib, function() {});
});
debug('events bound');
};
MediaController.prototype.configure = function() {
this.items = []; // All the pictures and videos we know about
this.currentItem = null;
var dpr = window.devicePixelRatio;
this.thumbnailSize = {
width: this.settings.previewGallery.get('thumbnailWidth') * dpr,
height: this.settings.previewGallery.get('thumbnailHeight') * dpr
};
this.configured();
};
MediaController.prototype.configured = function() {
this.app.emit('media:configured', this.items);
};
MediaController.prototype.onNewMedia = function(item) {
// If we're handling a pick activity we don't generate thumbnails
if (this.app.activity.pick) {
return;
}
var self = this;
if (item.isVideo) {
// If the new media is video, use it as-is
addNewMedia(item);
} else {
// If it is a photo, find its EXIF preview first
this.require(['lib/prepare-preview-blob'], function(preparePreview) {
preparePreview(item.blob, function(metadata) {
metadata.blob = item.blob;
metadata.filepath = item.filepath;
addNewMedia(metadata);
});
});
}
function addNewMedia(item) {
self.items.unshift(item);
self.updateThumbnail();
}
};
/**
* Delete all items in the media controller
* when storage becomes unavailable.
*
* @param {String} status
*/
MediaController.prototype.onStorageChanged = function(status) {
if (status === 'unavailable') {
this.configure();
this.updateThumbnail();
}
};
/**
* As a privacy feature, when the camera app is used from the lockscreen
* and the lockscreen is actually locked with a passcode, we don't want
* the camera to retain any state from one use to the next. So if the
* camera is hidden (i.e. if the phone returns to the lockscreen) we
* forget our state. In practice, it appears that the system app actually
* kills the camera when this happens, so this code is redundant.
*/
MediaController.prototype.onHidden = function() {
if (this.app.inSecureMode) {
this.configure(); // Forget all stored images
this.updateThumbnail(); // Get rid of any thumbnail
}
};
MediaController.prototype.updateThumbnail = function() {
// This is the media item that we want to create a thumbnail for.
var media = this.items[0] || null;
if (media === this.currentItem) {
return;
}
if (media === null) {
this.app.emit('newthumbnail', null);
return;
}
// To create a thumbanil, we first need to figure out which image
// blob to use, and what the size and rotation of that image is.
var blob; // The image we'll create the thumbnail from.
var metadata = { // The metadata for that image.
rotation: media.rotation,
mirrored: media.mirrored
};
if (media.isVideo) {
// If it is a video we can create a thumbnail from the poster image
blob = media.poster.blob;
metadata.width = media.poster.width;
metadata.height = media.poster.height;
} else {
// If it is a photo we want to use the EXIF preview rather than
// decoding the whole image if we can.
if (media.preview) {
// The Tarako may produce EXIF previews that have the wrong
// aspect ratio and are distorted. Check for that, and if the
// aspect ratio is not right, then create a thumbnail from the
// full size image
var fullRatio = media.width / media.height;
var previewRatio = media.preview.width / media.preview.height;
// If aspect ratios match, create thumbnail from EXIF preview
if (Math.abs(fullRatio - previewRatio) < 0.01) {
blob = media.blob.slice(media.preview.start, media.preview.end,
'image/jpeg');
metadata.width = media.preview.width;
metadata.height = media.preview.height;
}
}
// If a thumbnail couldn't be obtained from the EXIF preview,
// use the full image
if (!blob) {
blob = media.blob;
metadata.width = media.width;
metadata.height = media.height;
}
}
var self = this;
this.require(['lib/create-thumbnail-image'], function(createThumbnailImage) {
createThumbnailImage(blob, metadata, self.thumbnailSize, (thumb) => {
self.currentItem = media;
self.app.emit('newthumbnail', thumb);
});
});
};
/**
* Delete and update items in the preview gallery
* when images/videos are deleted by others
*
* @param {Object} filepath
*/
MediaController.prototype.onItemDeleted = function(data) {
var deleteIdx = -1;
var deletedFilepath = typeof(data) === 'object' ? data.path : data;
// find the item in items
for (var n = 0; n < this.items.length; n++) {
if (this.items[n].filepath === deletedFilepath) {
deleteIdx = n;
break;
}
}
// Exit when item not found
if (n === this.items.length) { return; }
var item = this.items[deleteIdx];
// Check if this event is being stopped such as in the case
// of resizing an image for a share activity.
if (item.resizing) { return; }
this.items.splice(deleteIdx, 1);
this.updateThumbnail();
this.app.emit('media:deleted');
};
});

View file

@ -0,0 +1,128 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:overlay');
var bindAll = require('lib/bind-all');
/**
* Exports
*/
module.exports = function(app) { return new OverlayController(app); };
module.exports.OverlayController = OverlayController;
/**
* Initialize a new `OverlayController`
*
* @param {App} app
*/
function OverlayController(app) {
bindAll(this);
this.overlays = {};
this.app = app;
this.require = app.require;
this.activity = app.activity;
this.bindEvents();
debug('initialized');
}
OverlayController.prototype.bindEvents = function() {
this.app.on('storage:changed', this.onStorageChanged);
this.app.on('change:batteryStatus', this.onBatteryChanged);
this.app.on('camera:requesting', this.onCameraRequesting);
this.app.on('camera:error', this.onCameraError);
};
/**
* Respond to storage `statechange`
* events by inserting or destroying
* overlays from the app.
*
* @param {String} value ['nospace'|'shared'|'unavailable'|'available']
*/
OverlayController.prototype.onStorageChanged = function(state) {
this.updateOverlay('storage', state !== 'available', state);
};
/**
* Respond to battery `statuschange`
* events by inserting or destroying
* overlays from the app.
*
* @param {String} status ['shutdown'|'critical'|'verylow'|'low']
*/
OverlayController.prototype.onBatteryChanged = function(state) {
this.updateOverlay('battery', state === 'shutdown', state);
};
/**
* Respond to camera `requesting`
* events by destroying overlays
* from the app.
*
* @param {String} state ['start'|'success'|'fail']
*/
OverlayController.prototype.onCameraRequesting = function() {
this.updateOverlay('cameraError', false);
};
/**
* Respond to camera `error`
* events by inserting overlays
* into the app.
*
* @param {String} state ['start'|'success'|'fail']
*/
OverlayController.prototype.onCameraError = function(type) {
this.updateOverlay('cameraError', true, type);
};
OverlayController.prototype.updateOverlay = function(type, enabled, reason) {
debug('\'%s/%s\' overlay %s', type, reason, enabled);
var overlay = this.overlays[type];
if (!overlay) {
overlay = this.overlays[type] = {
id: 0,
};
}
if (overlay.view) {
overlay.view.destroy();
delete overlay.view;
}
var id = ++overlay.id;
if (!enabled) {
return;
}
var self = this;
this.require(['views/overlay'], function(OverlayView) {
if (id !== overlay.id) {
return;
}
var closable = self.activity.pick && type !== 'request-fail';
var view = new OverlayView({
type: reason,
closable: closable,
});
if (!view.rendered()) {
return;
}
overlay.view = view.appendTo(document.body)
.on('click:close-btn', function() {
self.app.emit('activitycanceled');
});
debug('inserted \'%s/%s\' overlay', type, reason);
});
};
});

View file

@ -0,0 +1,305 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:preview-gallery');
var StringUtils = require('lib/string-utils');
var bindAll = require('lib/bind-all');
var PreviewGalleryView = require('views/preview-gallery');
var CustomDialog = require('CustomDialog');
/**
* Exports
*/
module.exports = function(app) { return new PreviewGalleryController(app); };
module.exports.PreviewGalleryController = PreviewGalleryController;
function PreviewGalleryController(app) {
bindAll(this);
this.app = app;
this.require = app.require;
this.settings = app.settings;
this.dialog = app.dialog || CustomDialog; // test hook
this.resizeImageAndSave = app.resizeImageAndSave;
this.currentItemIndex = 0;
this.bindEvents();
debug('initialized');
}
PreviewGalleryController.prototype.bindEvents = function() {
this.app.on('media:configured', this.updatePreviewGallery);
this.app.on('media:deleted', this.updatePreviewGallery);
this.app.on('preview', this.openPreview);
this.app.on('hidden', this.closePreview);
debug('events bound');
};
PreviewGalleryController.prototype.resetPreviewGallery = function(items) {
this.items = items;
};
PreviewGalleryController.prototype.openPreview = function() {
// If we're handling a pick activity the preview gallery is not used
if (this.app.activity.pick || this.view || this.app.hidden) {
return;
}
var maxPreviewSize = window.CONFIG_MAX_IMAGE_PIXEL_SIZE;
this.view = new PreviewGalleryView();
this.view.maxPreviewSize = maxPreviewSize;
this.view.render().appendTo(this.app.el);
this.view.on('click:gallery', this.onGalleryButtonClick);
this.view.on('click:share', this.shareCurrentItem);
this.view.on('click:delete', this.deleteCurrentItem);
this.view.on('click:back', this.closePreview);
this.view.on('swipe', this.handleSwipe);
this.view.on('click:options', this.onOptionsClick);
this.view.on('loadingvideo', this.app.firer('busy'));
this.view.on('playingvideo', this.app.firer('ready'));
// If lockscreen is locked, hide all control buttons
var secureMode = this.app.inSecureMode;
this.view.set('secure-mode', secureMode);
this.view.open();
this.previewItem();
this.app.emit('previewgallery:opened');
};
PreviewGalleryController.prototype.closePreview = function() {
if (this.view) {
this.currentItemIndex = 0;
this.view.close();
this.view.destroy();
this.view = null;
}
this.app.emit('previewgallery:closed');
};
/**
* Open the gallery app when the
* gallery button is pressed.
*
* @private
*/
PreviewGalleryController.prototype.onGalleryButtonClick = function() {
// Can't launch the gallery if the lockscreen is locked.
// The button shouldn't even be visible in this case, but
// let's be really sure here.
if (this.app.inSecureMode) {
return;
}
var MozActivity = window.MozActivity;
// Launch the gallery with an activity
this.mozActivity = new MozActivity({
name: 'browse',
data: { type: 'photos' }
});
};
PreviewGalleryController.prototype.onOptionsClick = function() {
if (this.app.inSecureMode) {
return;
}
this.view.showOptionsMenu();
};
PreviewGalleryController.prototype.shareCurrentItem = function() {
var index = this.currentItemIndex;
var item = this.items[index];
if (this.app.inSecureMode || !item || item.resizing) {
return;
}
var type = item.isVideo ? 'video/*' : 'image/*';
var filename = StringUtils.lastPathComponent(item.filepath);
var launchShareActivity = function(blob) {
var activity = new window.MozActivity({
name: 'share',
data: {
type: type,
number: 1,
blobs: [blob],
filenames: [filename],
filepaths: [item.filepath] /* temporary hack for bluetooth app */
}
});
activity.onerror = function(e) {
console.warn('Share activity error:', activity.error.name);
};
};
if (item.isVideo) {
launchShareActivity(item.blob);
return;
}
var self = this;
item.resizing = true;
this.app.emit('busy', 'resizingImage');
// Resize the image to the maximum pixel size for share activities.
// If no maximum is specified (value is `0`), then simply rotate
// (if needed) and re-save the image prior to launching the activity.
var maxSize = this.settings.activity.get('maxSharePixelSize');
this.require(['lib/resize-image-and-save'], function(resize) {
if (!self.resizeImageAndSave) {
self.resizeImageAndSave = resize;
}
self.resizeImageAndSave({
blob: item.blob,
size: maxSize,
}, function(resizedBlob) {
// Update the cached preview to reflect the new size of the saved
// image; it will also rotate the image based on the EXIF data before
// saving, so we should adjust for that
if (resizedBlob !== item.blob) {
item.blob = resizedBlob;
if (maxSize && maxSize.width && maxSize.height) {
item.width = maxSize.width;
item.height = maxSize.height;
} else if (item.rotation === 90 || item.rotation === 270) {
var tmp = item.width;
item.width = item.height;
item.height = tmp;
}
delete item.rotation;
}
delete item.resizing;
self.app.emit('ready');
launchShareActivity(resizedBlob);
});
});
};
/**
* Delete the current item
* when the delete button is pressed.
*
* @private
*/
PreviewGalleryController.prototype.deleteCurrentItem = function() {
// The button should be gone,but
// hard exit from this function just in case.
if (this.app.inSecureMode) { return; }
var index = this.currentItemIndex;
var item = this.items[index];
var filepath = item.filepath;
var dialog = this.dialog;
var self = this;
var msg;
if (item.isVideo) {
msg = 'delete-video?';
}
else {
msg = 'delete-photo?';
}
dialog.show('', msg, {
title: 'cancel',
callback: closeDialog
}, {
title: 'delete',
callback: deleteItem,
recommend: false
});
function closeDialog() {
dialog.hide();
}
function deleteItem() {
dialog.hide();
// Actually delete the file
if (item.isVideo) {
self.app.emit('previewgallery:deletevideo', filepath);
} else {
self.app.emit('previewgallery:deletepicture', filepath);
}
}
};
/**
* Update the preview with the latest recent item
* after deleting images/videos.
*
* @param {String} index
*/
PreviewGalleryController.prototype.updatePreviewGallery = function(items) {
// Remove the item from the array of items
if (items) {
this.items = items;
}
// If there are no more items, go back to the camera
if (this.items.length === 0) {
this.closePreview();
}
else {
if (this.currentItemIndex === this.items.length) {
this.currentItemIndex = this.items.length - 1;
}
var isOpened = this.view ? true : false;
if (isOpened) {
this.previewItem();
}
}
};
/**
* To Do: Image Swipe Transition
*/
PreviewGalleryController.prototype.handleSwipe = function(direction) {
if (direction === 'left') {
this.next();
}
else if (direction === 'right') {
this.previous();
}
};
PreviewGalleryController.prototype.next = function() {
if (this.currentItemIndex < this.items.length - 1) {
this.currentItemIndex += 1;
this.previewItem();
}
};
PreviewGalleryController.prototype.previous = function() {
if (this.currentItemIndex > 0) {
this.currentItemIndex -= 1;
this.previewItem();
}
};
PreviewGalleryController.prototype.previewItem = function() {
var index = this.currentItemIndex;
var item = this.items[index];
this.view.updateCountText(index + 1, this.items.length);
if (item.isVideo) {
this.view.showVideo(item);
} else {
this.view.showImage(item);
}
};
});

View file

@ -0,0 +1,78 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:recording-timer');
var RecordingTimerView = require('views/recording-timer');
/**
* Exports
*/
module.exports = function(app) { return new RecordingTimerController(app); };
module.exports.RecordingTimerController = RecordingTimerController;
/**
* Initialize a new `RecordingTimerController`
*
* Allow `this.view` to be mocked for testing
*
* @param {App} app
*/
function RecordingTimerController(app) {
this.onRecordingChange = this.onRecordingChange.bind(this);
this.app = app;
this.createView();
this.bindEvents();
debug('initialized');
}
/**
* Inject the view into the app.
*
* @private
*/
RecordingTimerController.prototype.createView = function() {
this.view = this.app.view || new RecordingTimerView();
this.view.appendTo(this.app.el);
};
/**
* Listen to relevant events.
*
* Toggle the visibility when
* recording starts/stops.
*
* Update the time value when camera
* recording time updates.
*
* @private
*/
RecordingTimerController.prototype.bindEvents = function() {
this.app.on('change:recording', this.onRecordingChange);
this.app.on('camera:recorderTimeUpdate', this.view.value);
debug('events bound');
};
/**
* Show the view when recording,
* hide it when not.
*
* @param {Boolean} recording
* @private
*/
RecordingTimerController.prototype.onRecordingChange = function(recording) {
debug('recording: %s', recording);
if (!recording) {
this.view.hide();
return;
}
this.view.value(0);
this.view.show();
};
});

View file

@ -0,0 +1,485 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var formatRecorderProfiles = require('lib/format-recorder-profiles');
var formatPictureSizes = require('lib/format-picture-sizes');
var debug = require('debug')('controller:settings');
var bindAll = require('lib/bind-all');
/**
* Exports
*/
module.exports = function(app) { return new SettingsController(app); };
module.exports.SettingsController = SettingsController;
/**
* Initialize a new `SettingsController`
*
* @constructor
* @param {App} app
*/
function SettingsController(app) {
bindAll(this);
this.app = app;
this.require = app.require;
this.settings = app.settings;
this.activity = app.activity;
this.notification = app.views.notification;
// Provide test hooks
this.nav = app.nav || navigator;
this.SettingsView = app.SettingsView;
this.formatPictureSizes = app.formatPictureSizes || formatPictureSizes;
this.formatRecorderProfiles = app.formatRecorderProfiles ||
formatRecorderProfiles;
this.configure();
this.bindEvents();
debug('initialized');
}
/**
* Registers settings 'aliases' that provide
* a single interface for settings such as
* `flashModes` where we have `flashModesPicture`
* and `flashModesVideo`.
*
* This means that we can just use settings.flashModes,
* and be confident that it will interface with the
* correct setting depending on the value of `mode`.
*
* You can always use the underlying settings
* directly if you need that kind of control.
*/
SettingsController.prototype.configure = function() {
this.setupRecorderProfilesAlias();
this.setupPictureSizesAlias();
this.setupFlashModesAlias();
};
/**
* Creates a SettingAlias that dynamically
* interfaces with the correct recorder
* profile Setting based on which camera
* ('front'/'back') is selected.
*
* @private
*/
SettingsController.prototype.setupRecorderProfilesAlias = function() {
var settings = this.settings;
this.settings.alias({
key: 'recorderProfiles',
settings: {
back: this.settings.recorderProfilesBack,
front: this.settings.recorderProfilesFront
},
get: function() {
var camera = settings.cameras.selected('key');
return this.settings[camera];
}
});
};
/**
* Creates a SettingAlias that dynamically
* interfaces with the correct picture
* size Setting based on which camera
* ('front'/'back') is selected.
*
* @private
*/
SettingsController.prototype.setupPictureSizesAlias = function() {
var settings = this.settings;
this.settings.alias({
key: 'pictureSizes',
settings: {
back: this.settings.pictureSizesBack,
front: this.settings.pictureSizesFront
},
get: function() {
var camera = settings.cameras.selected('key');
return this.settings[camera];
}
});
};
/**
* Creates a SettingAlias that dynamically
* interfaces with the correct flash-modes
* Setting based on which mode ('picture'/
* 'video') is selected.
*
* @private
*/
SettingsController.prototype.setupFlashModesAlias = function() {
var settings = this.settings;
this.settings.alias({
key: 'flashModes',
settings: {
picture: this.settings.flashModesPicture,
video: this.settings.flashModesVideo
},
get: function() {
var mode = settings.mode.selected('key');
return this.settings[mode];
}
});
};
/**
* Bind to app events.
*
* @private
*/
SettingsController.prototype.bindEvents = function() {
this.app.on('localized', this.formatPictureSizeTitles);
this.app.on('settings:toggle', this.toggleSettings);
this.app.on('camera:newcamera', this.onNewCamera);
this.app.on('activity:pick', this.onPickActivity);
};
/**
* Toggle the settings menu open/closed.
*
* @private
*/
SettingsController.prototype.toggleSettings = function() {
if (this.view) { this.closeSettings(); }
else { this.openSettings(); }
};
/**
* Render and display the settings menu.
*
* We use settings.menu() to retrieve
* and ordered list of settings that
* have a `menu` property.
*
* @private
*/
SettingsController.prototype.openSettings = function() {
debug('open settings');
var self = this;
this.app.emit('busy', 'lazyLoading');
this.require(['views/settings'], function(SettingsView) {
self.app.emit('ready');
if (!self.SettingsView) {
self.SettingsView = SettingsView;
}
if (self.view) { return; }
var items = self.menuItems();
self.view = new self.SettingsView({ items: items })
.render()
.appendTo(self.app.el)
.on('click:close', self.closeSettings)
.on('click:option', self.onOptionTap);
// Make sure the view is
// hidden before fading in
self.view.hide();
self.view.fadeIn();
self.app.emit('settings:opened');
debug('settings opened');
});
};
/**
* Destroy the settings menu.
*
* @private
*/
SettingsController.prototype.closeSettings = function(done) {
debug('close settings');
if (!this.view) { return; }
var self = this;
this.view.fadeOut(function() {
self.view.destroy();
self.view = null;
self.app.emit('settings:closed');
debug('settings closed');
if (typeof done === 'function') { done(); }
});
};
/**
* Selects the option that was
* clicked on the setting.
*
* @param {String} key
* @param {Setting} setting
* @private
*/
SettingsController.prototype.onOptionTap = function(key, setting) {
var flashMode = this.settings.flashModesPicture.selected('key');
var ishdrOn = setting.key === 'hdr' && key === 'on';
var flashDeactivated = flashMode !== 'off' && ishdrOn;
var self = this;
self.closeSettings(function() {
setting.select(key);
self.notify(setting, flashDeactivated);
});
};
/**
* Adjusts settings to meet requirements
* on new pick activity.
*
* @param {Object} data
* @private
*/
SettingsController.prototype.onPickActivity = function(data) {
debug('pick activity', data);
var setting;
var options;
var updated = false;
var maxFileSize = data.maxFileSizeBytes;
var maxPixelSize = data.maxPixelSize;
// Settings changes made in 'pick'
// sessions shouldn't persist.
this.settings.dontSave();
if (maxPixelSize) {
setting = this.settings.pictureSizes;
var lastMaxPixelSize = setting.get('maxPixelSize');
this.settings.pictureSizesFront.set('maxPixelSize', maxPixelSize);
this.settings.pictureSizesBack.set('maxPixelSize', maxPixelSize);
debug('set maxPixelSize: %s', maxPixelSize);
if (lastMaxPixelSize !== maxPixelSize) {
options = setting.get('options');
var restricted = [];
if (options && options.length > 0) {
options.forEach(function(option) {
if (option.pixelSize <= maxPixelSize) {
restricted.push(option);
}
});
setting.resetOptions(restricted);
updated = true;
}
}
}
if (maxFileSize) {
setting = this.settings.recorderProfiles;
var lastMaxFileSize = setting.get('maxFileSizeBytes');
this.settings.recorderProfilesFront.set('maxFileSizeBytes', maxFileSize);
this.settings.recorderProfilesBack.set('maxFileSizeBytes', maxFileSize);
debug('set maxFileSize: %s', maxFileSize);
if (lastMaxFileSize !== maxFileSize) {
options = setting.get('options');
if (options && options.length > 1) {
setting.resetOptions([options[options.length - 1]]);
updated = true;
}
}
}
// If the size restrictions come in after the camera was brought
// up, then we must retrigger a configuration event
if (updated) {
this.app.emit('settings:configured');
}
};
/**
* Display a notifcation showing the
* current state of the given setting.
*
* If `notification` is `false in config
* for a setting then we don't show one.
*
* @param {Setting} setting
* @private
*/
SettingsController.prototype.notify = function(setting, flashDeactivated) {
var dontNotify = setting.get('notifications') === false;
if (dontNotify) { return; }
var localizable = setting.get('optionsLocalizable') !== false;
var title = '<span data-l10n-id="' + setting.get('title') + '"></span>';
var html;
// Localize option title only if not specified in the config
var optionTitle = localizable ?
'<span data-l10n-id="' + setting.selected('title') + '"></span>' :
'<span>' + setting.selected('title') + '</span>';
// Check if the `flashMode` setting is going to be deactivated as part
// of the change in the `hdr` setting and display a specialized
// notification if that is the case
if (flashDeactivated) {
html = title + ' ' + optionTitle + '<br/>' +
'<span data-l10n-id="flash-deactivated"></span>';
} else {
html = title + '<br/>' + optionTitle;
}
this.notification.display({ text: { html: html }});
};
/**
* When the hardware capabilities change
* we update the available options for
* each setting to match what is available.
*
* The rest of the app should listen for
* the 'settings:configured' event as an
* indication to update UI etc.
*
* @param {Object} capabilities
*/
SettingsController.prototype.onNewCamera = function(camera) {
debug('new capabilities');
var capabilities = camera.capabilities;
this.settings.hdr.filterOptions(capabilities.hdr);
this.settings.flashModesPicture.filterOptions(capabilities.flashModes);
this.settings.flashModesVideo.filterOptions(capabilities.flashModes);
this.configurePictureSizes(camera);
this.configureRecorderProfiles(camera);
// Let the rest of the app know we're good to go.
this.app.emit('settings:configured');
debug('settings configured to new capabilities');
};
/**
* Formats the raw pictureSizes into
* a format that Setting class can
* work with, then resets the pictureSize
* options.
*
* @param {Array} sizes
*/
SettingsController.prototype.configurePictureSizes = function(camera) {
debug('configuring picture sizes');
var maxPixelSize = window.CONFIG_MAX_IMAGE_PIXEL_SIZE;
var sizes = camera.capabilities.pictureSizes;
var setting = this.settings.pictureSizes;
var currentSize = camera.pictureSize;
var currentSizeKey = currentSize.width + 'x' + currentSize.height;
var options = {
exclude: setting.get('exclude'),
include: setting.get('include'),
maxPixelSize: maxPixelSize
};
var formatted = this.formatPictureSizes(sizes, options);
// A sensor may expose only sizes outside Gaia's original 4:3 preset.
if (!formatted.length && sizes.length) {
formatted = this.formatPictureSizes(sizes, {});
}
setting.resetOptions(formatted);
// If the setting has no stored default
// set the setting to match the current
// mozCamera setting.
if (!setting.current().fetched) {
setting.select(currentSizeKey, { silent: true });
}
this.formatPictureSizeTitles();
debug('configured pictureSizes', setting.selected('key'));
};
/**
* Formats the raw recorderProfiles
* into a format that Setting class can
* work with, then resets the recorderProfile
* options.
*
* @param {Array} sizes
*/
SettingsController.prototype.configureRecorderProfiles = function(camera) {
var sizes = camera.capabilities.recorderProfiles;
var currentProfile = camera.recorderProfile;
var setting = this.settings.recorderProfiles;
var maxFileSize = setting.get('maxFileSizeBytes');
var exclude = setting.get('exclude');
var options = { exclude: exclude };
var items = this.formatRecorderProfiles(sizes, options);
// If a file size limit has been imposed,
// pick the lowest-res (last) profile only.
if (maxFileSize) { items = [items[items.length - 1]]; }
setting.resetOptions(items);
// Set the recorder profile to match
// the currently selected mozCamera profile
if (!maxFileSize && !setting.current().fetched) {
setting.select(currentProfile, { silent: true });
}
};
/**
* Creates a localized `title` property
* on each pictureSize option. This is
* used within the settings-menu.
*
* This is run each time `configurePictureSizes`
* is run and each time the app recieves a
* 'localized' event.
*
* If the app isn't 'localized' yet, we don't do
* anything and wait for the 'localized'
* event binding to run the function.
*
* @private
*/
SettingsController.prototype.formatPictureSizeTitles = function() {
return document.l10n.formatValue('mp').then((value) => {
var options = this.settings.pictureSizes.get('options');
options.forEach(function(size) {
var data = size.data;
var mp = data.mp ? data.mp + value + ' ' : '';
size.title = mp + size.key;
});
debug('picture size titles formatted');
});
};
/**
* Returns a list of settings
* based on the `settingsMenu`
* configuration.
*
* @return {Array}
*/
SettingsController.prototype.menuItems = function() {
var items = this.settings.settingsMenu.get('items');
return items.filter(this.validMenuItem, this)
.map(function(item) { return this.settings[item.key]; }, this);
};
/**
* Tests if the passed `settingsMenu`
* item is allowed in the settings menu.
*
* @param {Object} item
* @return {Boolean}
*/
SettingsController.prototype.validMenuItem = function(item) {
var setting = this.settings[item.key];
return !!setting && setting.supported();
};
});

View file

@ -0,0 +1,49 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:sounds');
var Sounds = require('lib/sounds');
/**
* Exports
*/
module.exports = function(app) { return new SoundsController(app); };
module.exports.SoundsController = SoundsController;
/**
* Initialize a new `SoundsController`
*
* @param {App} app [description]
*/
function SoundsController(app) {
var list = app.settings.sounds.get('list');
this.sounds = new Sounds(list);
this.app = app;
this.bindEvents();
debug('initialized');
}
SoundsController.prototype.bindEvents = function() {
this.app.on('change:recording', this.onRecordingChange.bind(this));
this.app.on('camera:willrecord', this.sounds.player('recordingStart'));
this.app.on('camera:shutter', this.sounds.player('shutter'));
this.app.on('countdown:immanent', this.sounds.player('countdown'));
};
/**
* Plays the start/end recording sound.
*
* @private
*/
SoundsController.prototype.onRecordingChange = function(recording) {
if (!recording) {
this.sounds.play('recordingEnd');
}
};
});

View file

@ -0,0 +1,191 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:storage');
var bindAll = require('lib/bind-all');
var Storage = require('lib/storage');
/**
* Exports
*/
module.exports = function(app) { return new StorageController(app); };
module.exports.StorageController = StorageController;
/**
* Initialize a new `StorageController`
*
* @param {App} app
*/
function StorageController(app) {
bindAll(this);
this.app = app;
this.camera = app.camera;
this.settings = app.settings;
this.storage = app.storage || new Storage();
this.bindEvents();
this.configure();
debug('initialized');
}
/**
* Initial configuration.
*
* Give the camera a way to create video filepaths.
* This is so that the camera can record videos
* directly to the final location without us
* having to move the video file from temporary,
* to final location at recording end.
*
* @private
*/
StorageController.prototype.configure = function() {
this.storage.configure();
this.camera.createVideoFilepath = this.storage.createVideoFilepath;
this.updateMaxFileSize();
};
/**
* Bind to relevant events.
*
* @private
*/
StorageController.prototype.bindEvents = function() {
debug('bind events');
// App
this.settings.pictureSizes.on('change:selected', this.updateMaxFileSize);
this.app.on('previewgallery:deletepicture', this.storage.deletePicture);
this.app.on('previewgallery:deletevideo', this.storage.deleteVideo);
this.app.on('settings:configured', this.updateMaxFileSize);
this.app.on('camera:newimage', this.storePicture);
this.app.on('camera:newvideo', this.storeVideo);
this.app.on('visible', this.storage.check);
// Storage
this.storage.on('volumechanged', this.app.firer('storage:volumechanged'));
this.storage.on('itemdeleted', this.app.firer('storage:itemdeleted'));
this.storage.on('changed', this.onChanged);
this.storage.on('checked', this.onChecked);
debug('events bound');
};
/**
* Relay storage state change events.
*
* @param {String} state
* @private
*/
StorageController.prototype.onChanged = function(state) {
debug('changed: %s', state);
this.app.emit('storage:changed', state);
};
/**
* Emit the outcome of a storage check
* so that other parts of the app
* can respond.
*
* @param {String} value
* @private
*/
StorageController.prototype.onChecked = function(value) {
debug('checked: %s', value);
this.app.emit('storage:checked', value);
this.app.emit('storage:checked:' + value);
};
/**
* Store a picture.
*
* In either case, save the memory-backed
* photo blob to device storage, retrieve
* the resulting File (blob) and pass that
* around instead of the original memory blob.
*
* This is critical for "pick" activity consumers
* where the memory-backed Blob is either highly
* inefficent or will almost-immediately become
* inaccesible, depending on the state of the
* platform. https://bugzil.la/982779
*
* @param {Object} picture
* @private
*/
StorageController.prototype.storePicture = function(picture) {
var memoryBlob = picture.blob;
var self = this;
this.storage.addPicture(
memoryBlob,
function(error, filepath, abspath, fileBlob) {
picture.blob = fileBlob;
picture.filepath = filepath;
debug('stored picture', picture);
self.app.emit('newmedia', picture);
});
};
/**
* Store a video.
*
* We don't store the video blob like
* we do for images, as it is recorded
* directly to the final location.
* This is for memory reason. The poster
* is also automatically created.
*
* @param {Object} video
*/
StorageController.prototype.storeVideo = function(video) {
debug('new video', video);
var poster = video.poster;
var self = this;
// Add the poster image to the image storage
poster.filepath = video.filepath.replace('.3gp', '.jpg');
video.isVideo = true;
this.storage.addPicture(
poster.blob,
{ filepath: poster.filepath },
function(error, path, absolutePath, fileBlob) {
// Replace the memory-backed Blob with the DeviceStorage file-backed File.
// Note that "video" has a reference to "poster", so video previews will
// use this File.
poster.blob = fileBlob;
debug('new video', video);
self.app.emit('newmedia', video);
});
};
/**
* Calculate and set a `maxFileSize`
* value on the storage instance.
*
* This is so that it can alert us when there
* isn't enough space left in storage
* to accomodate a new picture.
*
* It is very unlikely that a JPEG file will have a file size that is
* more than half a byte per pixel. There is some fixed EXIF overhead
* that is the same for small and large pictures, however, so we add
* an additional 25,000 bytes of padding.
*
* @private
*/
StorageController.prototype.updateMaxFileSize = function() {
var pictureSize = this.settings.pictureSizes.selected('data');
/* Depending on when we get loaded, we may not have a picture size yet. */
if (pictureSize) {
var bytes = (pictureSize.width * pictureSize.height / 2) + 25000;
this.storage.setMaxFileSize(bytes);
debug('maxFileSize updated %s', bytes);
}
};
});

View file

@ -0,0 +1,445 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var cameraCoordinates = require('lib/camera-coordinates');
var debug = require('debug')('controller:viewfinder');
var ViewfinderView = require('views/viewfinder');
var FocusView = require('views/focus');
var FacesView = require('views/faces');
var bindAll = require('lib/bind-all');
/**
* Exports
*/
module.exports = function(app) { return new ViewfinderController(app); };
module.exports.ViewfinderController = ViewfinderController;
/**
* Initialize a new `ViewfinderController`
*
* @param {App} app
*/
function ViewfinderController(app) {
bindAll(this);
this.app = app;
this.camera = app.camera;
this.activity = app.activity;
this.settings = app.settings;
this.createViews();
this.bindEvents();
this.configure();
debug('initialized');
}
/**
* Create and inject the views.
*
* @private
*/
ViewfinderController.prototype.createViews = function() {
this.views = {};
this.views.viewfinder = this.app.views.viewfinder || new ViewfinderView();
this.views.focus = this.app.views.focus || new FocusView();
this.views.faces = this.app.views.faces || new FacesView();
this.views.focus.appendTo(this.views.viewfinder.el);
this.views.faces.appendTo(this.views.viewfinder.el);
this.views.viewfinder.appendTo(this.app.el);
};
/**
* Initial configuration.
*
* @private
*/
ViewfinderController.prototype.configure = function() {
var settings = this.app.settings;
var zoomSensitivity = settings.viewfinder.get('zoomGestureSensitivity');
this.sensitivity = zoomSensitivity * window.innerWidth;
this.configureScaleType();
this.configureGrid();
};
/**
* Configure the viewfinder scale type,
* aspect fill/fit, depending on setting.
*
* @private
*/
ViewfinderController.prototype.configureScaleType = function() {
var scaleType = this.app.settings.viewfinder.get('scaleType');
this.views.viewfinder.scaleType = scaleType;
debug('set scale type: %s', scaleType);
};
/**
* Show/hide grid depending on currently
* selected option.
*
* @private
*/
ViewfinderController.prototype.configureGrid = function() {
var grid = this.app.settings.grid.selected('key');
this.views.viewfinder.set('grid', grid);
};
/**
* Hides the viewfinder frame-grid.
*
* @private
*/
ViewfinderController.prototype.hideGrid = function() {
this.views.viewfinder.set('grid', 'off');
};
/**
* Bind to relavant events.
*
* @private
*/
ViewfinderController.prototype.bindEvents = function() {
// View
this.views.viewfinder.on('fadedin', this.app.firer('viewfinder:visible'));
this.views.viewfinder.on('fadedout', this.app.firer('viewfinder:hidden'));
this.views.viewfinder.on('click', this.app.firer('viewfinder:click'));
this.views.viewfinder.on('click', this.onViewfinderClicked);
// Tut tut, we shouldn't have direct coupling here.
// TODO: Camera events should be relayed through the app.
this.camera.on('zoomconfigured', this.onZoomConfigured);
this.camera.on('zoomchanged', this.onZoomChanged);
this.camera.on('preview:started', this.show);
// Camera
this.app.on('camera:autofocuschanged', this.views.focus.showAutoFocusRing);
this.app.on('camera:focusstatechanged', this.views.focus.setFocusState);
this.app.on('camera:focusconfigured', this.onFocusConfigured);
this.app.on('camera:shutter', this.views.viewfinder.shutter);
this.app.on('camera:facesdetected', this.onFacesDetected);
this.app.on('camera:configured', this.onCameraConfigured);
this.app.on('camera:previewactive', this.onPreviewActive);
this.app.on('busy', this.views.viewfinder.disable);
this.app.on('ready', this.views.viewfinder.enable);
this.app.on('camera:willchange', this.hide);
// Preview Gallery
this.app.on('previewgallery:opened', this.onGalleryOpened);
this.app.on('previewgallery:closed', this.onGalleryClosed);
// Settings
this.app.on('settings:closed', this.onSettingsClosed);
this.app.on('settings:opened', this.onSettingsOpened);
this.app.settings.grid.on('change:selected',
this.views.viewfinder.setter('grid'));
// Battery
this.app.on('battery:powersave', this.onPowerSave);
// App
this.app.on('pinch:changed', this.onPinchChanged);
this.app.on('hidden', this.stopStream);
};
/**
* Perform required viewfinder configuration
* once the camera has configured.
*
* @private
*/
ViewfinderController.prototype.onCameraConfigured = function() {
debug('configuring');
this.loadStream();
this.configurePreview();
};
/**
* Show the viewfinder.
*
* If the critical-path is not done we
* fade the viewfinder in straight away
* to make sure we have the quickest
* startup possible.
*
* We have to use a timeout for all other
* viewfinder showing actions conceal a Gecko
* rendering bug whereby the video element has
* not yet 'visually' switched to the new stream
* when we get the preview 'started' event from
* the camera. This means the user sees a flicker
* if we don't give it some time to adjust.
*
* We clear these timeouts to avoid multiple pending
* timeouts, which could cause pain.
*
* https://bugzilla.mozilla.org/show_bug.cgi?id=982230
*
* @private
*/
ViewfinderController.prototype.show = function() {
debug('show');
if (!this.app.criticalPathDone) {
this.views.viewfinder.fadeIn(true);
return;
}
clearTimeout(this.showTimeout);
this.showTimeout = setTimeout(this.views.viewfinder.fadeIn, 280);
debug('schedule delayed fade-in');
};
/**
* Fades the viewfinder in.
*
* We clear any pending timeouts here
* to prevent unusual behaviour ensuing.
*
* @private
*/
ViewfinderController.prototype.hide = function() {
debug('hide');
clearTimeout(this.showTimeout);
this.views.viewfinder.fadeOut();
};
/**
* Sets appropiate flags when the camera focus is configured
*/
ViewfinderController.prototype.onFocusConfigured = function(config) {
this.views.focus.setFocusMode(config.mode);
this.touchFocusEnabled = config.touchFocus;
this.views.faces.clear();
};
ViewfinderController.prototype.onFacesDetected = function(faces) {
var self = this;
var faceCircles = [];
var viewfinderSize = this.views.viewfinder.getSize();
var viewportHeight = viewfinderSize.height;
var viewportWidth = viewfinderSize.width;
var sensorAngle = this.camera.getSensorAngle();
var camera = this.app.settings.cameras.selected('key');
var isFrontCamera = camera === 'front';
faces.forEach(function(face, index) {
// Face comes in camera coordinates from gecko
var faceInPixels = cameraCoordinates.faceToPixels(
face.bounds, viewportWidth, viewportHeight, sensorAngle, isFrontCamera);
var faceCircle = self.calculateFaceCircle(faceInPixels);
faceCircles.push(faceCircle);
});
this.views.faces.show();
this.views.faces.render(faceCircles);
};
ViewfinderController.prototype.calculateFaceCircle = function(face) {
var diameter = Math.max(face.width, face.height);
var radius = diameter / 2;
return {
x: Math.round(face.left + face.width / 2 - radius),
y: Math.round(face.top + face.height / 2 - radius),
diameter: diameter
};
};
/**
* Start the viewfinder stream flowing
* with the current camera configuration.
*
* This indirectly enforces a screen wakelock,
* meaning the device is unable to go to sleep.
*
* We don't want the stream to start flowing if
* the preview-gallery is open, as this prevents
* the device falling asleep.
*
* @private
*/
ViewfinderController.prototype.loadStream = function() {
this.camera.loadStreamInto(this.views.viewfinder.els.video);
debug('stream started');
};
/**
* Stop the preview stream flowing.
*
* This indirectly removes the wakelock
* that is magically enforced by the
* flowing camera stream. Meaning the
* device is able to go to sleep.
*
* @private
*/
ViewfinderController.prototype.stopStream = function() {
this.views.viewfinder.stopStream();
debug('stream stopped');
};
/**
* Power save state changed. If entering power
* save mode, release the screen wake lock to
* allow it to timeout without user interaction,
* otherwise acquire the wake lock.
*
* @private
*/
ViewfinderController.prototype.onPowerSave = function(powerSave) {
this.views.viewfinder.els.video.mozUseScreenWakeLock = !powerSave;
};
/**
* Configure the size and postion
* of the preview video stream.
*
* @private
*/
ViewfinderController.prototype.configurePreview = function() {
var camera = this.app.settings.cameras.selected('key');
var isFrontCamera = camera === 'front';
var sensorAngle = this.camera.getSensorAngle();
var previewSize = this.camera.previewSize();
this.views.viewfinder.updatePreview(previewSize, sensorAngle, isFrontCamera);
};
/**
* Configures the viewfinder
* to the current camera.
*
* @private
*/
ViewfinderController.prototype.onZoomConfigured = function() {
var zoomSupported = this.camera.isZoomSupported();
var zoomEnabled = this.app.settings.zoom.enabled();
var enableZoom = zoomSupported && zoomEnabled;
if (!enableZoom) {
this.views.viewfinder.disableZoom();
return;
}
if (this.app.settings.zoom.get('useZoomPreviewAdjustment')) {
this.views.viewfinder.enableZoomPreviewAdjustment();
} else {
this.views.viewfinder.disableZoomPreviewAdjustment();
}
var minimumZoom = this.camera.getMinimumZoom();
var maximumZoom = this.camera.getMaximumZoom();
this.views.viewfinder.enableZoom(minimumZoom, maximumZoom);
};
/**
* Updates the zoom level on the camera
* when the pinch changes.
*
* @private
*/
ViewfinderController.prototype.onPinchChanged = function(deltaPinch) {
var zoom = this.views.viewfinder._zoom *
(1 + (deltaPinch / this.sensitivity));
this.views.viewfinder.setZoom(zoom);
this.camera.setZoom(zoom);
};
/**
* Responds to changes of the `zoom` value on the Camera to update the
* view's internal state so that the pinch-to-zoom gesture can resume
* zooming from the updated value. Also, updates the CSS scale transform
* on the <video/> tag to compensate for zooming beyond the
* `maxHardwareZoom` value.
*
* @param {Number} zoom
*/
ViewfinderController.prototype.onZoomChanged = function(zoom) {
var zoomPreviewAdjustment = this.camera.getZoomPreviewAdjustment();
this.views.viewfinder.setZoomPreviewAdjustment(zoomPreviewAdjustment);
this.views.viewfinder.setZoom(zoom);
};
ViewfinderController.prototype.onViewfinderClicked = function(e) {
if (!this.touchFocusEnabled) { return; }
this.views.faces.hide();
this.changeFocusPoint(e.pageX, e.pageY);
};
ViewfinderController.prototype.changeFocusPoint = function(x, y) {
var viewfinderSize = this.views.viewfinder.getSize();
var viewportHeight = viewfinderSize.height;
var viewportWidth = viewfinderSize.width;
var sensorAngle = this.camera.getSensorAngle();
var focusAreaSize = 10;
var focusAreaHalfSide = Math.round(focusAreaSize / 2);
// Top Left corner of the area and its size
var focusAreaPixels = {
left: x - focusAreaHalfSide,
top: y - focusAreaHalfSide,
right: x + focusAreaHalfSide,
bottom: y + focusAreaHalfSide,
width: focusAreaSize,
height: focusAreaSize
};
var camera = this.app.settings.cameras.selected('key');
var isFrontCamera = camera === 'front';
var focusArea = cameraCoordinates.faceToCamera(
focusAreaPixels, viewportWidth, viewportHeight, sensorAngle, isFrontCamera);
var focusPoint = {
x: x,
y: y,
area: focusArea
};
this.views.focus.setPosition(x, y);
this.app.emit('viewfinder:focuspointchanged', focusPoint);
};
ViewfinderController.prototype.onSettingsOpened = function() {
this.hideGrid();
// Make viewfinder invisible to the screen reader since it is behind settings
// overlay.
this.views.viewfinder.set('ariaHidden', true);
};
ViewfinderController.prototype.onSettingsClosed = function() {
this.configureGrid();
// Make viewfinder visible to the screen reader again when settings are
// closed.
this.views.viewfinder.set('ariaHidden', false);
};
/**
* Disables the viewfinder stream
* and pinch events.
*
* @private
*/
ViewfinderController.prototype.onGalleryOpened = function() {
this.views.viewfinder.disable();
// Make viewfinder invisible to the screen reader since it is behind gallery
// overlay.
this.views.viewfinder.set('ariaHidden', true);
};
/**
* Enables the viewfinder stream
* and pinch events.
*
* @private
*/
ViewfinderController.prototype.onGalleryClosed = function() {
this.views.viewfinder.enable();
// Make viewfinder visible to the screen reader again when gallery is closed.
this.views.viewfinder.set('ariaHidden', false);
};
ViewfinderController.prototype.onPreviewActive = function(active) {
if (!active) {
this.stopStream();
}
};
});

View file

@ -0,0 +1,84 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('controller:zoom-bar');
var ZoomBar = require('views/zoom-bar');
var bindAll = require('lib/bind-all');
/**
* Exports
*/
module.exports = function(app) { return new ZoomBarController(app); };
module.exports.ZoomBarController = ZoomBarController;
/**
* Initialize a new `ZoomBarController`
*
* @param {App} app
*/
function ZoomBarController(app) {
bindAll(this);
this.app = app;
this.camera = app.camera;
this.createView();
this.bindEvents();
this.configureZoomBar();
debug('initialized');
}
ZoomBarController.prototype.createView = function() {
this.view = this.app.views.zoombar || new ZoomBar();
this.view.hide();
this.view.appendTo(this.app.el);
};
ZoomBarController.prototype.bindEvents = function() {
this.view.on('change', this.onChange);
// TODO: Camera events should be relayed through
// the app, so that controllers dont' have a
// hard dependency on each other.
this.camera.on('zoomconfigured', this.onZoomConfigured);
this.camera.on('zoomchanged', this.setZoom);
};
ZoomBarController.prototype.configureZoomBar = function() {
var zoomSupported = this.camera.isZoomSupported();
var zoomEnabled = this.app.settings.zoom.enabled();
this.enableZoom = zoomSupported && zoomEnabled;
};
ZoomBarController.prototype.onChange = function(value) {
var minimumZoom = this.camera.getMinimumZoom();
var maximumZoom = this.camera.getMaximumZoom();
var range = maximumZoom - minimumZoom;
var zoom = (range * value / 100) + minimumZoom;
this.camera.setZoom(zoom);
};
ZoomBarController.prototype.onZoomConfigured = function(zoom) {
this.configureZoomBar();
this.setZoom(zoom);
this.view.hide();
};
ZoomBarController.prototype.setZoom = function(zoom) {
if (!this.enableZoom) {
return;
}
debug('set zoom');
var minimumZoom = this.camera.getMinimumZoom();
var maximumZoom = this.camera.getMaximumZoom();
var range = maximumZoom - minimumZoom;
var percent = (zoom - minimumZoom) / range * 100;
this.view.setValue(percent);
debug('zoom set');
};
});

View file

@ -0,0 +1,15 @@
define(function(require, exports, module) {
'use strict';
module.exports = function(object) {
var key;
var fn;
for (key in object) {
fn = object[key];
if (typeof fn === 'function') {
object[key] = fn.bind(object);
}
}
};
});

30
gaia/apps/camera/js/lib/bind.js Executable file
View file

@ -0,0 +1,30 @@
define(function(require, exports, module) {
'use strict';
/**
* Exports
*/
exports = module.exports = bind;
/**
* addEventListener shorthand.
* @param {Element} el
* @param {String} name
* @param {Function} fn
*/
function bind(el, name, fn, capture) {
el.addEventListener(name, fn, capture || false);
}
/**
* removeEventListener shorthand.
* @param {Element} el
* @param {String} name
* @param {Function} fn
*/
exports.unbind = function(el, name, fn, capture) {
el.removeEventListener(name, fn, capture || false);
};
});

View file

@ -0,0 +1,11 @@
define(function(require, exports, module) {
'use strict';
module.exports = function(bytes) {
var bytesPerPixel = 3;
var avgJpegCompression = window.CONFIG_AVG_JPEG_COMPRESSION_RATIO || 8;
var uncompressedBytes = bytes * avgJpegCompression;
return Math.round(uncompressedBytes / bytesPerPixel);
};
});

View file

@ -0,0 +1,527 @@
/*
This module contains functions to deal with android camera coordinates.
Example use cases:
1. In the camera application we want to convert to screen coordinates
the top left corner of the faces detected by the camera so we can position a
DOM element to highlight the corresponding area.
2. We have to transform screen to android camera coordinates when
the user touches the screen to focus on a specific area.
In both cases we have to take into account the possible mismatch between the
screen pixels order and the order of the pixels sent by the driver.
ANDROID CAMERA COORDINATES
This documentation contain how android coordinates work and how the screen,
sensor and scene coordinates relate to one another.
The following is *always* true: the active rectangles of
the phone/screen and the sensor always -physically- line up.
It has to be thus, otherwise it would be impossible to
fill a portrait viewfinder with a portrait view, or a
landscape viewfinder with a landscape view.
Visually, this looks like:
phone/screen
+------+
| |
| |
| |
| |
| |
| |
| |
| |
+------+
sensor
+----+
| |
| |
| |
| |
+----+
detected face reported by the camera driver
-------------------------------------------
top
+-----+
| |
left | | right
| |
+-----+
bottom
The driver also reports width and height
If sensor orientation is 0, the sensor and the screen are scanned like this:
phone/screen
+------+
|aabbcc|
|aabbcc|
|ddeeff|
|ddeeff|
|gghhii|
|gghhii|
|jjkkll|
|jjkkll|
+------+
sensor
O---+ O = Origin of sensor coordinates (-1000, -1000)
|abc| X = (1000, 1000)
|def| sensor-to-screen conversion => none (x = x, y = y)
|ghi| face coordinate to convert => top, left
|jkl|
+---X
If there is an arrow pointing right:
o-->
...the sensor sees this arrow as such:
+----+
| |
|o-->|
| |
| |
+----+
...and if the sensor orientation is 0, the
arrow appears on the screen (magnified) like this:
+--------+
| |
| |
| |
|()====>>|
| |
| |
| |
| |
+--------+
If the sensor orientation is, e.g., 90-degrees--that is,
the "top" of the sensor corresponds to the "right" of the
screen--then without applying any rotation to the UI,
the arrow appears on the screen like this:
+--------+
| |
| |
| ^ |
| | |
| O |
| |
| |
| |
+--------+
The arrow is smaller, because the frames need to be
shrunk so that the entire height of the sensor image
fits into the width of the screen.
In this case, rotating the UI by 90-degrees clockwise,
and stretching it to fill the screen, gives:
+--------+
| |
| |
| |
|()====>>|
| |
| |
| |
| |
+--------+
This is because, in the 90-degree sensor
orientation case, the scanning is like this:
sensor
+---O O = Origin of sensor coordinates (-1000, -1000)
|iea| X = (1000, 1000)
|jfb| sensor-to-screen conversion => x = -y, y = x
|kgc| face coordinate to convert => bottom, left
|lhd|
X---+
And without compensating for this rotation,
the image on the screen would appear like this:
+------+
| |
| |
| abcd |
| efgh |
| ijkl |
| |
| |
| |
+------+
Thus sensor orientation has nothing to do with the physical
orientation of the sensor. Rather, it has to do with which
scan-line in the sensor is considered the "top".
Given all this, the coordinates of the "top-left" of
the sensor view, (-1000, -1000), are the coordinates of the
first pixel of the first scan-line. Similarly, the "bottom-right"
of the sensor is the last pixel of the last scan-line,
and has coordinates (1000, 1000).
----- 8< -----
As requested, the 180-degree case is:
phone/screen
+------+
|aabbcc|
|aabbcc|
|ddeeff|
|ddeeff|
|gghhii|
|gghhii|
|jjkkll|
|jjkkll|
+------+
sensor
X---+ O = Origin of sensor coordinates (-1000, -1000)
|jkl| X = (1000, 1000)
|ihg| sensor-to-screen conversion => x = -x, y = -y
|fed| face coordinate to convert => bottom, right
|cba|
+---O
And the 270-degree case is:
phone/screen
+------+
| |
| |
| abcd |
| efgh |
| ijkl |
| |
| |
| |
+------+
sensor
+---X O = Origin of sensor coordinates (-1000, -1000)
|dhl| X = (1000, 1000)
|cgk| sensor-to-screen conversion => x = y, y = -x
|bfj| face coordinate to convert => bottom, right
|aei|
O---+
*/
define(function(require, exports, module) {
/*jshint maxlen:false*/
'use strict';
/**
* It transforms camera coordinates to pixels
*
* @private
*/
function toPixels(x, y, viewportWidth, viewportHeight) {
// In camera coordinate system, (-1000, -1000) represents the
// top-left of the camera field of view, and (1000, 1000) represents
// the bottom-right.
// For convenience we assume that coordinates go from 0-2000
var cameraCoordinatesRange = 2000;
// How many pixels per camera unit?
var pixelsPerCameraUnitWidth = viewportWidth / cameraCoordinatesRange;
var pixelsPerCameraUnitHeight = viewportHeight / cameraCoordinatesRange;
// It makes the faces coordinates to go from 0-2000 for convenience
var xCameraCoordinates = x + 1000;
var yCameraCoordinates = y + 1000;
var xPixelCoordinates = xCameraCoordinates * pixelsPerCameraUnitWidth;
var yPixelCoordinates = yCameraCoordinates * pixelsPerCameraUnitHeight;
return {
x: Math.round(xPixelCoordinates),
y: Math.round(yPixelCoordinates)
};
}
/**
* It transforms pixels to camera coordinates
*
* @private
*/
function toCamera(x, y, viewportWidth, viewportHeight) {
// In camera coordinate system, (-1000, -1000) represents the
// top-left of the camera field of view, and (1000, 1000) represents
// the bottom-right.
var cameraCoordinatesRange = 2000;
// How many camera units per pixel?
var cameraUnitsPerPixelWidth = cameraCoordinatesRange / viewportWidth;
var cameraUnitsPerPixelHeight = cameraCoordinatesRange / viewportHeight;
return {
x: Math.round(x * cameraUnitsPerPixelWidth) - 1000,
y: Math.round(y * cameraUnitsPerPixelHeight) - 1000
};
}
/**
* It rotates camera coordinates given an angle 0, 90, 180, 270
*
* @private
*/
function rotatePoint(x, y, angle) {
// It clamps the angle to +/- 0-270
angle = (Math.round((angle % 360) / 90) % 4) * 90;
switch (angle) {
case 0:
return {
x: x,
y: y
};
case 90:
case -270:
return {
x: -y,
y: x
};
case 180:
case -180:
return {
x: -x,
y: -y
};
case 270:
case -90:
return {
x: y,
y: -x
};
default:
console.error('wrong angle value');
}
}
/**
* The sensor orientation is the orientation of the camera output
* (the order of the pixels sent from the sensor to the preview stream) with
* respect to the device screen in portrait orientation.
* This function matches the orientation of the sensor and screen coordinates
* We're interested on the top left and bottom-rignt corners of the
* area as observed through the lenses (scene coordinates).
* We take into account the orientation of the sensor to pick the correct
* corner that corresponds to the top most, left most, right most and
* bottom most coordinates in the physical/scene space as observed through the camera preview.
*
* @private
*/
function rotateArea(area, sensorOrientation) {
var topLeft = rotatePoint(area.left, area.top, sensorOrientation);
var bottomRight = rotatePoint(area.right, area.bottom, sensorOrientation);
// It picks the top left and bottom right corners on the scene / physical space
return {
top: Math.min(topLeft.y, bottomRight.y),
left: Math.min(topLeft.x, bottomRight.x),
bottom: Math.max(topLeft.y, bottomRight.y),
right: Math.max(topLeft.x, bottomRight.x),
width: Math.abs(topLeft.x - bottomRight.x),
height: Math.abs(topLeft.y - bottomRight.y)
};
}
/**
* It mirrors a given area in camera coordinates
* Use case: Display detected faces by the front camera.
*
* @private
*/
function mirrorAreaCamera(area) {
return {
top: area.top,
left: -area.right,
bottom: area.bottom,
right: -area.left,
width: area.width,
height: area.height
};
}
/**
* It mirrors a given area in camera coordinates
* Use case: Display detected faces by the front camera.
*
* @private
*/
function mirrorAreaPixels(area, viewportWidth) {
return {
top: area.top,
left: viewportWidth - area.right,
bottom: area.bottom,
right: viewportWidth - area.left,
width: area.width,
height: area.height
};
}
/**
* Given a distance in pixels and a screen size it returns
* the distance in camera units.
*
* @private
*/
function sizeToCamera(width, height, viewportWidth, viewportHeight) {
// In camera coordinate system, (-1000, -1000) represents the
// top-left of the camera field of view, and (1000, 1000) represents
// the bottom-right of the field of view.
// There are 2000 units in each axis of the camera coordinates
var cameraCoordinatesRange = 2000;
// How many camera units per pixel?
var cameraUnitsPerPixelWidth = cameraCoordinatesRange/ viewportWidth;
var cameraUnitsPerPixelHeight = cameraCoordinatesRange / viewportHeight;
return {
width: Math.round(width * cameraUnitsPerPixelWidth),
height: Math.round(height * cameraUnitsPerPixelHeight)
};
}
/**
* Given a distance in camera units and a screen size it returns
* the distance in pixels.
*
* @private
*/
function sizeToPixels(width, height, viewportWidth, viewportHeight) {
// In camera coordinate system, (-1000, -1000) represents the
// top-left of the camera field of view, and (1000, 1000) represents
// the bottom-right of the field of view.
// There are 2000 units in each axis of the camera coordinates
var cameraCoordinatesRange = 2000;
// How many pixels per camera coordinates unit?
var pixelsPerCameraUnitWidth = viewportWidth / cameraCoordinatesRange;
var pixelsPerCameraUnitHeight = viewportHeight / cameraCoordinatesRange;
return {
width: Math.round(width * pixelsPerCameraUnitWidth),
height: Math.round(height * pixelsPerCameraUnitHeight)
};
}
/**
* Given an area in screen camera units (-1000, 1000)
* and viewport dimensions it returns the same area
* in pixels
*
* @private
*/
function areaToPixels(area, viewportWidth, viewportHeight) {
// It converts the face from Android camera coordinates to pixels
var areaPixels = toPixels(
area.left, area.top, viewportWidth, viewportHeight);
// It converts the face size from Android camera units to pixels
var areaPixelSize = sizeToPixels(
area.width, area.height,
viewportWidth, viewportHeight);
var width = areaPixelSize.width;
var height = areaPixelSize.height;
return {
top: areaPixels.y,
left: areaPixels.x,
bottom: areaPixels.y + height,
right: areaPixels.x + width,
width: width,
height: height
};
}
/**
* Given an area in screen pixel coordinates
* and viewport dimensions it returns the same area
* in android camera coordinates (-1000, 1000)
*
* @public
*/
function areaToCamera(area, viewportWidth, viewportHeight) {
var topLeft = toCamera(
area.left, area.top, viewportWidth, viewportHeight);
// It converts the size from pixels to camera units
var areaCameraUnits = sizeToCamera(
area.width, area.height,
viewportWidth, viewportHeight);
var width = areaCameraUnits.width;
var height = areaCameraUnits.height;
var areaCamera = {
top: topLeft.y,
left: topLeft.x,
bottom: topLeft.y + height,
right: topLeft.x + width,
height: height,
width: width
};
return areaCamera;
}
/**
* Given face coordinates in pixels
* a viewport width and height it returns the face coordinates
* and area in camera units
*
* @public
*/
function faceToCamera(face, viewportWidth, viewportHeight, sensorOrientation, mirrored) {
if (mirrored) {
face = mirrorAreaPixels(face, viewportWidth);
}
face = areaToCamera(face, viewportWidth, viewportHeight);
// The orientation of the screen and the sensor might not match.
// We rotate the face so it matches screen coordinates orientation
if (sensorOrientation) {
face = rotateArea(face, -sensorOrientation);
}
return face;
}
/**
* Given face coordinates as provided by camera controller and
* a viewport width and height it returns the face coordinates, diameter
* and area in pixel units
*
* @public
*/
function faceToPixels(face, viewportWidth, viewportHeight, sensorOrientation, mirrored) {
// The orientation of the screen and the sensor might not match.
// We rotate the face so it matches screen coordinates orientation
if (sensorOrientation) {
face = rotateArea(face, sensorOrientation);
}
if (mirrored) {
face = mirrorAreaCamera(face);
}
return areaToPixels(face, viewportWidth, viewportHeight);
}
return {
// Face transformations
faceToCamera: faceToCamera,
faceToPixels: faceToPixels,
// Area transformations
private: {
// Area transformations
areaToCamera: areaToCamera,
areaToPixels: areaToPixels,
mirrorAreaCamera: mirrorAreaCamera,
mirrorAreaPixels: mirrorAreaPixels,
rotateArea: rotateArea,
// Size transformations
sizeToCamera: sizeToCamera,
sizeToPixels: sizeToPixels,
// Point transformations
toCamera: toCamera,
toPixels: toPixels,
rotatePoint: rotatePoint
}
};
});

View file

@ -0,0 +1,75 @@
define(function(require) {
/*jshint maxlen:false*/
'use strict';
var CameraUtils = {};
CameraUtils.scaleSizeToFitViewport = function(viewportSize, imageSize) {
var sw = viewportSize.width / imageSize.width,
sh = viewportSize.height / imageSize.height,
scale;
// Select the smaller scale to fit image completely within the viewport
scale = Math.min(sw, sh);
return {
width: imageSize.width * scale,
height: imageSize.height * scale
};
};
CameraUtils.scaleSizeToFillViewport = function(viewportSize, imageSize) {
var sw = viewportSize.width / imageSize.width,
sh = viewportSize.height / imageSize.height,
scale;
// Select the larger scale to fill and overflow viewport with image
scale = Math.max(sw, sh);
return {
width: imageSize.width * scale,
height: imageSize.height * scale
};
};
/**
* Get the maximum preview size (in terms of area) from a list of
* possible preview sizes.
*
* NOTE: If an `aspectRatio` value is provided, the search will be
* constrained to only accept preview sizes matching that aspect
* ratio.
*
* @param {Array} previewSizes
* @param {Number} aspectRatio
* @return {Object}
*/
CameraUtils.getMaximumPreviewSize = function(previewSizes, aspectRatio) {
// Use a very small tolerance because we want an exact match if we are
// constraining to only include specific aspect ratios.
const ASPECT_TOLERANCE = 0.001;
var maximumArea = 0;
var maximumPreviewSize = null;
previewSizes.forEach(function(previewSize) {
var area = previewSize.width * previewSize.height;
if (aspectRatio) {
var ratio = previewSize.width / previewSize.height;
if (Math.abs(ratio - aspectRatio) > ASPECT_TOLERANCE) {
return;
}
}
if (area > maximumArea) {
maximumArea = area;
maximumPreviewSize = previewSize;
}
});
return maximumPreviewSize;
};
return CameraUtils;
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,325 @@
define(function(require, exports, module) {
'use strict';
/**
* Module Dependencies
*/
var bindAll = require('lib/bind-all');
/**
* Exports
*/
module.exports = Focus;
/**
* Options:
*
* - {Boolean} `continousAutoFocus`
* - {Boolean} `touchFocus`
* - {Boolean} `faceDetection`
*/
function Focus(options) {
bindAll(this);
this.userPreferences = options || {};
this.detectedFaces = [];
}
Focus.prototype.configure = function(mozCamera, mode) {
var focusModes = mozCamera.capabilities.focusModes;
var focusMode;
this.mozCamera = mozCamera;
this.configureFocusModes(mode);
// Determines focus mode based on camera mode
// In case of C-AF enabled
if (this.continuousAutoFocus) {
if (mode === 'picture') {
focusMode = 'continuous-picture';
} else if (mode === 'video'){
focusMode = 'continuous-video';
}
}
// auto is the default focus mode
focusMode = focusMode || 'auto';
// If auto or passed mode is not supported we pick the
// first available in the hardware list
if (focusModes.indexOf(focusMode) === -1) {
focusMode = focusModes[0];
}
// If touch-to-focus is in progress we need to ensure
// the correct mode is restored when it is complete
this.suspendedMode = focusMode;
mozCamera.focusMode = focusMode;
this.reboot();
};
Focus.prototype.getMode = function() {
var mozCamera = this.mozCamera;
return this.suspendedMode || mozCamera.focusMode;
};
/**
* Configures focus modes based on user preferences
* and hardware availability
*/
Focus.prototype.configureFocusModes = function(mode) {
var userPreferences = this.userPreferences;
var continuousAutoFocusUserEnabled =
userPreferences.continuousAutoFocus !== false;
var touchFocusUserEnabled = userPreferences.touchFocus;
var touchFocusSupported = this.isTouchFocusSupported();
var faceDetectionUserEnabled = userPreferences.faceDetection;
var faceDetectionSupported = this.isFaceDetectionSupported();
this.continuousAutoFocus = continuousAutoFocusUserEnabled;
// User preferences override defaults
this.touchFocus = touchFocusUserEnabled && touchFocusSupported;
// Face detection only enabled on picture mode (disabled on video)
this.faceDetection =
faceDetectionUserEnabled && faceDetectionSupported && mode === 'picture';
this.mozCamera.addEventListener('focus', this.onAutoFocusStateChange);
};
Focus.prototype.startFaceDetection = function() {
if (!this.faceDetection) { return; }
this.mozCamera.addEventListener('facesdetected',
this.handleFaceDetectionEvent);
this.mozCamera.startFaceDetection();
};
Focus.prototype.stopFaceDetection = function() {
// Clear suspenstion timers
clearTimeout(this.faceDetectionSuspended);
clearTimeout(this.faceDetectionSuspensionTimer);
if (this.mozCamera.stopFaceDetection) {
this.mozCamera.removeEventListener('facesdetected',
this.handleFaceDetectionEvent);
this.mozCamera.stopFaceDetection();
}
this.clearFaceDetection();
};
Focus.prototype.handleFaceDetectionEvent = function(e) {
this.focusOnLargestFace(e.faces);
};
Focus.prototype.clearFaceDetection = function() {
this.focusOnLargestFace([]);
};
Focus.prototype.suspendFaceDetection = function(ms, delay) {
if (!this.faceDetection) {
return;
}
var self = this;
delay = delay || 0;
clearTimeout(this.faceDetectionSuspended);
clearTimeout(this.faceDetectionSuspensionTimer);
this.faceDetectionSuspensionTimer = setTimeout(suspendFaceDetection, delay);
function suspendFaceDetection() {
self.faceDetectionSuspended = setTimeout(clearTimer, ms);
}
function clearTimer() {
self.faceFocused = false;
self.faceDetectionSuspended = undefined;
}
};
Focus.prototype.stopContinuousFocus = function() {
var focusMode = this.mozCamera.focusMode;
// Clear suspension timers
clearTimeout(this.continuousModeTimer);
if (focusMode === 'continuous-picture' || focusMode === 'continuous-video') {
this.suspendedMode = this.mozCamera.focusMode;
this.mozCamera.focusMode = 'auto';
}
};
Focus.prototype.resumeContinuousFocus = function() {
this.mozCamera.focusMode = this.suspendedMode;
this.suspendedMode = null;
this.resetFocusAreas();
this.mozCamera.resumeContinuousFocus();
};
Focus.prototype.suspendContinuousFocus = function(ms) {
clearTimeout(this.continuousModeTimer);
this.stopContinuousFocus();
this.continuousModeTimer = setTimeout(this.resumeContinuousFocus, ms);
};
Focus.prototype.updateFocusState = function(state) {
// Only update if the state has changed, and only transition to focused
// or unfocused if we were previously focusing; this eliminates unfocused
// rings just before a focusing state change.
if (this.focusState !== state &&
(this.focusState === 'focusing' || state === 'focusing')) {
this.focusState = state;
this.onAutoFocusChanged(state);
}
};
Focus.prototype.onAutoFocusStateChange = function(e) {
var state = e.newState;
if (state === 'unfocused') {
state = 'fail';
}
this.updateFocusState(state);
};
Focus.prototype.onAutoFocusChanged = function(state) {
// NO OP by default
};
/**
* Callback invoked when faces are detected
* It's no op by default and it's meant to be overriden by
* the user of the module in order to be informed about detected faces
*/
Focus.prototype.onFacesDetected = function(faces) {
// NO OP by default
};
Focus.prototype.focusOnLargestFace = function(faces) {
if (this.faceDetectionSuspended) {
this.onFacesDetected([]);
return;
}
this.detectedFaces = faces;
this.onFacesDetected(this.detectedFaces);
};
/**
* Focus the camera, invoke the callback asynchronously when done.
*
* If we only have fixed focus, then we call the callback right away
* (but still asynchronously). Otherwise, we call autoFocus to focus
* the camera and call the callback when focus is complete. In C-AF mode
* this process should be fast. In manual AF mode, focusing takes about
* a second and causes a noticeable delay before the picture is taken.
*
* @param {Function} done
* @private
*/
Focus.prototype.focus = function(done) {
if (!this.mozCamera) { return; }
done = done || function() {};
var self = this;
this.suspendContinuousFocus(10000);
if (this.mozCamera.focusMode !== 'auto') {
done();
return;
}
//
// In either focus mode, we call autoFocus() to ensure that the user gets
// a sharp picture. The difference between the two modes is that if
// C-AF is on, it is likely that the camera is already focused, so the
// call to autoFocus() invokes its callback very quickly and we get much
// better response time.
//
// In either case, the callback is passed a boolean specifying whether
// focus was successful or not, and we display a green or red focus ring
// then call the done callback, which takes the picture and clears
// the focus ring.
//
this.updateFocusState('focusing');
this.mozCamera.autoFocus().then(onSuccess, onError);
// If focus fails with an error, we still need to signal the
// caller. Interruptions are a special case, but other errors
// can be treated the same as completing the operation but
// remaining unfocused.
function onError(err) {
self.focused = false;
if (err.name === 'NS_ERROR_IN_PROGRESS') {
done('interrupted');
} else {
done('error');
}
}
// This is fixed focus: there is nothing we can do here so we
// should just call the callback and take the photo. No focus
// happens so we don't display a focus ring.
function onSuccess(success) {
if (success) {
self.focused = true;
done('focused');
} else {
self.focused = false;
done('failed');
}
}
};
/**
* Resets focus regions
*/
Focus.prototype.resetFocusAreas = function() {
if (!this.touchFocus) {
return;
}
this.mozCamera.setFocusAreas([]);
this.mozCamera.setMeteringAreas([]);
};
Focus.prototype.pause = function() {
if (this.paused) { return; }
this.stopContinuousFocus();
this.stopFaceDetection();
this.paused = true;
delete this.focusState;
};
Focus.prototype.resume = function() {
if (!this.paused) { return; }
this.resumeContinuousFocus();
this.startFaceDetection();
this.paused = false;
};
Focus.prototype.reboot = function() {
this.pause();
this.resume();
};
/**
* Check if the hardware supports touch to focus
*/
Focus.prototype.isTouchFocusSupported = function() {
var maxFocusAreas = this.mozCamera.capabilities.maxFocusAreas;
return maxFocusAreas > 0;
};
/**
* Check if hardware supports face detection
*/
Focus.prototype.isFaceDetectionSupported = function() {
var cameraDetectsFaces = this.mozCamera.capabilities.maxDetectedFaces > 0;
var apiAvailable = !!this.mozCamera.startFaceDetection;
this.maxDetectedFaces = this.mozCamera.capabilities.maxDetectedFaces;
return cameraDetectsFaces && apiAvailable;
};
Focus.prototype.updateFocusArea = function(rect, done) {
done = done || function() {};
if (!this.touchFocus) {
done('touchToFocusNotAvailable');
return;
}
this.updateFocusState('focusing');
this.stopContinuousFocus();
this.suspendFaceDetection(10000);
this.mozCamera.setFocusAreas([rect]);
this.mozCamera.setMeteringAreas([rect]);
// Call auto focus to focus on focus area.
this.focus(done);
};
});

View file

@ -0,0 +1,35 @@
define(function(require, exports, module) {
'use strict';
var cropResizeRotate = require('cropResizeRotate');
/**
* Create a thumbnail size canvas,
* copy the <img> or <video> into it
* cropping the edges as needed to
* make it fit, and then extract the
* thumbnail image as a blob and pass
* it to the callback.
*
* @param {Blob} imageBlob [description]
* @param {Object} metadata [description]
* @param {Object} thumbnailSize [description]
* @param {Function} done [description]
*/
module.exports = function(imageBlob, metadata, thumbnailSize, done) {
cropResizeRotate(imageBlob, null, thumbnailSize, null, metadata,
function(error, resizedBlob) {
// If we couldn't resize or rotate it, use the original
if (error) {
console.error('Error while resizing image: ' + error);
done(imageBlob);
return;
}
done(resizedBlob);
}
);
};
});

101
gaia/apps/camera/js/lib/dcf.js Executable file
View file

@ -0,0 +1,101 @@
define(function(require, exports, module) {
'use strict';
// This handles the logic pertaining to the naming of files according
// to the Design rule for Camera File System
// * http://en.wikipedia.org/wiki/Design_rule_for_Camera_File_system
/**
* Dependencies
*/
var asyncStorage = require('asyncStorage');
var debug = require('debug')('dcf');
var format = require('format');
/**
* Locals
*/
var dcfConfigLoaded = false;
var deferredArgs = null;
var defaultSeq = {file: 1, dir: 100};
var dcfConfig = {
key: 'dcf_key',
seq: null,
postFix: 'MZLLA',
prefix: {video: 'VID_', image: 'IMG_'},
ext: {video: '3gp', image: 'jpg'}
};
exports.init = function() {
debug('initializing');
exports.checkFileCounter(function() {
// We have a previous call to createDCFFilename that is waiting for
// a response, fire it again
if (deferredArgs) {
var args = deferredArgs;
exports.createDCFFilename(args.storage, args.type, args.callback);
deferredArgs = null;
}
debug('initialized');
});
};
exports.checkFileCounter = function(callback) {
asyncStorage.getItem(dcfConfig.key, function(value) {
dcfConfigLoaded = true;
dcfConfig.seq = value ? value : defaultSeq;
if (callback) {
callback();
}
});
};
exports.createDCFFilename = function(storage, type, callback) {
// We havent loaded the current counters from indexedDB yet, defer
// the call
if (!dcfConfigLoaded) {
deferredArgs = {storage: storage, type: type, callback: callback};
return;
}
var dir = 'DCIM/' + dcfConfig.seq.dir + dcfConfig.postFix + '/';
var filename = dcfConfig.prefix[type] +
format.padLeft(dcfConfig.seq.file, 4, '0') + '.' +
dcfConfig.ext[type];
var filepath = dir + filename;
// A file with this name may have been written by the user or
// our indexeddb sequence tracker was cleared, check we wont overwrite
// anything
var req = storage.get(filepath);
// A file existed, we bump the directory then try to generate a
// new filename
req.onsuccess = function() {
dcfConfig.seq.file = 1;
dcfConfig.seq.dir += 1;
asyncStorage.setItem(dcfConfig.key, dcfConfig.seq, function() {
exports.createDCFFilename(storage, type, callback);
});
};
// No file existed, we are good to go
req.onerror = function() {
if (dcfConfig.seq.file < 9999) {
dcfConfig.seq.file += 1;
} else {
dcfConfig.seq.file = 1;
dcfConfig.seq.dir += 1;
}
asyncStorage.setItem(dcfConfig.key, dcfConfig.seq, function() {
callback(filepath, filename, dir);
});
};
};
});

View file

@ -0,0 +1,19 @@
define(function(require, exports, module) {
'use strict';
module.exports = function(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) { func.apply(context, args); }
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) { func.apply(context, args); }
};
};
});

View file

@ -0,0 +1,81 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var getAspect = require('./get-aspect');
/**
* Returns a formatted list of picture
* sizes ready to be set as setting options.
*
* Options:
*
* - `maxPixelSize {Number}`
* - `exclude {Array}`
*
* @param {Array} sizes
* @param {Object} options
* @return {Array}
*/
module.exports = function(sizes, options) {
var maxPixelSize = options && options.maxPixelSize;
var exclude = options && options.exclude || {};
var include = options && options.include;
var formatted = [];
var hash = {};
exclude.aspects = exclude.aspects || [];
exclude.keys = exclude.keys || [];
sizes.forEach(function(size) {
var w = size.width;
var h = size.height;
var key = w + 'x' + h;
var pixelSize = w * h;
if (hash[key]) { return; }
size.aspect = getAspect(w, h);
// Don't include pictureSizes above the maxPixelSize limit
if (maxPixelSize && pixelSize > maxPixelSize) { return; }
if (include) {
if (include.keys && !~include.keys.indexOf(key)) { return; }
if (include.aspects && !~include.aspects.indexOf(size.aspect)) { return; }
}
if (exclude.keys.indexOf(key) > -1) { return; }
if (exclude.aspects.indexOf(size.aspect) > -1) { return; }
size.mp = getMP(w, h);
hash[key] = true;
formatted.push({
key: key,
pixelSize: pixelSize,
data: size
});
});
// Sort by pixel size
formatted.sort(function(a, b) { return b.pixelSize - a.pixelSize; });
return formatted;
};
/**
* Returns rounded mega-pixel value.
*
* @param {Number} w
* @param {Number} h
* @return {Number}
*/
function getMP(w, h) {
return Math.round((w * h) / 1000000);
}
});

View file

@ -0,0 +1,59 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var getAspect = require('./get-aspect');
/**
* Returns a formatted list of recorder
* profiles ready to be set as setting options.
*
* Options:
*
* - `exclude {Array}`
*
* @param {Object} profiles
* @param {Object} options
* @return {Array}
*/
module.exports = function(profiles, options) {
var exclude = options && options.exclude || [];
var items = [];
var hash = {};
for (var key in profiles) {
if (!profiles.hasOwnProperty(key)) { continue; } // Bug 1091820
var profile = profiles[key];
var video = profile.video;
var sizeKey = video.width + 'x' + video.height;
// guard against duplicate profiles
if (hash[sizeKey]) { continue; }
// Don't include profile if marked as excluded
if (exclude.indexOf(key) > -1) { continue; }
var pixelSize = video.width * video.height;
var aspect = getAspect(video.width, video.height);
hash[sizeKey] = key;
items.push({
key: key,
title: key + ' ' + sizeKey + ' ' + aspect,
pixelSize: pixelSize,
raw: profile
});
}
// Sort from largest to small but put the default/preferred profile first
items.sort(function(a, b) { return b.pixelSize - a.pixelSize; });
return items;
};
});

View file

@ -0,0 +1,35 @@
define(function(require, exports, module) {
'use strict';
var format = require('format');
/**
* Dependencies
*/
function digits(value) {
return format.padLeft(value, 2, '0');
}
/**
* Exports
*/
module.exports = function(ms) {
var totalSeconds = ms / 1000;
var seconds = Math.round(totalSeconds % 60);
var minutes = Math.floor(totalSeconds / 60);
var hours;
if (minutes < 60) {
return digits(minutes) + ':' + digits(seconds);
} else {
hours = Math.floor(minutes / 60);
minutes = Math.round(minutes % 60);
return hours + ':' + digits(minutes) + ':' + digits(seconds);
}
return '';
};
});

View file

@ -0,0 +1,68 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('geolocation');
/**
* Exports
*/
module.exports = GeoLocation;
/**
* Interface to the
* geolocation API.
*
* @constructor
*/
function GeoLocation() {
this.watcher = null;
this.position = null;
this.setPosition = this.setPosition.bind(this);
this.watch = this.watch.bind(this);
}
/**
* Watches device location.
*
* @public
*/
GeoLocation.prototype.watch = function() {
if (!this.watcher) {
this.watcher = navigator.geolocation.watchPosition(this.setPosition);
debug('started watching');
}
};
/**
* Stops watching
* device location.
*
* @public
*/
GeoLocation.prototype.stopWatching = function() {
navigator.geolocation.clearWatch(this.watcher);
this.watcher = null;
debug('stopped watching');
};
/**
* Updates the stored
* position object.
*
* @private
*/
GeoLocation.prototype.setPosition = function(position) {
this.position = {
timestamp: position.timestamp,
altitude: position.coords.altitude,
latitude: position.coords.latitude,
longitude: position.coords.longitude
};
};
});

View file

@ -0,0 +1,20 @@
define(function(require, exports, module) {
'use strict';
/**
* Returns aspect ratio string.
*
* Makes use of Euclid's GCD algorithm,
* http://en.wikipedia.org/wiki/Euclidean_algorithm
*
* @param {Number} w
* @param {Number} h
* @return {String}
*/
module.exports = function(w, h) {
var gcd = function(a, b) { return (b === 0) ? a : gcd(b, a % b); };
var divisor = gcd(w, h);
return (w / divisor) + ':' + (h / divisor);
};
});

View file

@ -0,0 +1,8 @@
define(function(require, exports, module) {
'use strict';
module.exports = function(a, b) {
for (var key in b) { a[key] = b[key]; }
return a;
};
});

View file

@ -0,0 +1,81 @@
define(function(require, exports, module) {
'use strict';
var listener = require('device-orientation');
var classes = document.body.classList;
var current = 0;
listener.on('orientation', onOrientationChange);
listener.start();
function onOrientationChange(degrees) {
classes.remove('deg' + current);
classes.add('deg' + degrees);
current = degrees;
}
// Camera normally has its orientation locked to portrait mode.
// But we unlock orientation when displaying image and video previews.
// When orientation is unlocked, we call listener.stop().
// We calls call stop() when recording a video, and then restart
// when recording is done. If our app ever changes so that we can call
// unlock while the orientation listener is in the stopped state, then
// we would need to modify the lock() function so that it did not
// restart the listener. That is not needed now, however and is omitted.
function unlock() {
screen.mozUnlockOrientation();
listener.stop();
}
function lock() {
screen.mozLockOrientation('default');
listener.start();
}
function rationalize(sensorAngle, angle) {
if (typeof(angle) === 'undefined') {
angle = current;
}
// The result of this operation is an angle from 0..270 degrees,
// in steps of 90 degrees. Angles are rounded to the nearest
// magnitude, so 45 will be rounded to 90, and -45 will be rounded
// to -90 (not 0).
var r = angle + sensorAngle;
if (r >= 0) {
r += 45;
} else {
r -= 45;
}
r /= 90;
if (r >= 0) {
r = Math.floor(r);
} else {
r = Math.ceil(r);
}
r %= 4;
r *= 90;
if (r < 0) {
r += 360;
}
return r;
}
/**
* Exports
*/
module.exports = {
on: listener.on,
off: listener.off,
start: listener.start,
stop: listener.stop,
unlock: unlock,
lock: lock,
rationalize: rationalize,
get: function() {
return current;
}
};
});

View file

@ -0,0 +1,84 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var GestureDetector = require('GestureDetector');
/**
* Exports
*/
module.exports = addPanAndZoomHandlers;
/*
* This module adds pan-and-zoom capability to images displayed by
* shared/js/media/media_frame.js.
* It is used by preview-gallery.js and confirm.js
*/
function addPanAndZoomHandlers(frame, swipeCallback) {
// frame is the MediaFrame object. container is its DOM element.
var container = frame.container;
// Generate gesture events for the container
var gestureDetector = new GestureDetector(container);
gestureDetector.startDetecting();
// When the user touches the screen and moves their finger left or
// right, they might want to pan within a zoomed-in image, or they
// might want to swipe between multiple items in the camera preview
// gallery. We pass the amount of motion to the MediaFrame pan() method,
// and it returns the amount that cannot be used to pan the displayed
// item. We track this returned amount as how far left or right the
// image has been swiped, and pass the number to the swipeCallback.
var swipeAmount = 0;
// And handle them with these listeners
container.addEventListener('dbltap', handleDoubleTap);
container.addEventListener('transform', handleTransform);
container.addEventListener('pan', handlePan);
if (swipeCallback) {
container.addEventListener('swipe', handleSwipe);
}
function handleDoubleTap(e) {
var scale;
if (frame.fit.scale > frame.fit.baseScale) {
scale = frame.fit.baseScale / frame.fit.scale;
}
else {
scale = 2;
}
frame.zoom(scale, e.detail.clientX, e.detail.clientY, 200);
}
function handleTransform(e) {
frame.zoom(e.detail.relative.scale,
e.detail.midpoint.clientX, e.detail.midpoint.clientY);
}
function handlePan(e) {
var dx = e.detail.relative.dx;
var dy = e.detail.relative.dy;
if (swipeCallback) {
dx += swipeAmount;
swipeAmount = frame.pan(dx, dy);
swipeCallback(swipeAmount);
} else {
frame.pan(dx, dy);
}
}
function handleSwipe(e) {
if (swipeAmount !== 0) {
swipeCallback(swipeAmount, e.detail.vx);
swipeAmount = 0;
}
}
}
});

140
gaia/apps/camera/js/lib/pinch.js Executable file
View file

@ -0,0 +1,140 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var unbind = require('lib/bind').unbind;
var bindAll = require('lib/bind-all');
var bind = require('lib/bind');
var events = require('evt');
/**
* Mixin event emitter
*/
events(Pinch.prototype);
/**
* Exports
*/
module.exports = Pinch;
/**
* Initialize a new `Pinch` interface.
*
* @constructor
*/
function Pinch(el) {
bindAll(this);
this.attach(el);
}
Pinch.prototype.attach = function(el) {
this.el = el;
bind(this.el, 'touchstart', this.onTouchStart);
bind(window, 'touchmove', this.onTouchMove);
bind(window, 'touchend', this.onTouchEnd);
};
Pinch.prototype.detach = function() {
unbind(this.el, 'touchstart', this.onTouchStart);
unbind(window, 'touchmove', this.onTouchMove);
unbind(window, 'touchend', this.onTouchEnd);
this.el = null;
};
Pinch.prototype.enable = function() {
this.disabled = false;
};
Pinch.prototype.disable = function() {
this.disabled = true;
};
Pinch.prototype.onTouchStart = function(evt) {
if (evt.touches.length !== 2 || this.disabled) {
return;
}
this.lastTouchA = evt.touches[0];
this.lastTouchB = evt.touches[1];
this.isPinching = true;
this.emit('started');
};
Pinch.prototype.onTouchMove = function(evt) {
if (!this.isPinching || this.disabled) {
return;
}
var touchA = getNewTouchA(this, evt.touches);
var touchB = getNewTouchB(this, evt.touches);
var deltaPinch = getDeltaPinch(this, touchA, touchB);
this.emit('changed', deltaPinch);
this.lastTouchA = touchA;
this.lastTouchB = touchB;
};
Pinch.prototype.onTouchEnd = function(evt) {
if (!this.isPinching || this.disabled) {
return;
}
if (evt.touches.length < 2) {
this.isPinching = false;
this.emit('ended');
}
};
function getNewTouchA(pinch, touches) {
if (!pinch.lastTouchA) {
return null;
}
for (var i = 0, length = touches.length, touch; i < length; i++) {
touch = touches[i];
if (touch.identifier === pinch.lastTouchA.identifier) {
return touch;
}
}
return null;
}
function getNewTouchB(pinch, touches) {
if (!pinch.lastTouchB) {
return null;
}
for (var i = 0, length = touches.length, touch; i < length; i++) {
touch = touches[i];
if (touch.identifier === pinch.lastTouchB.identifier) {
return touch;
}
}
return null;
}
function getDeltaPinch(pinch, touchA, touchB) {
var lastTouchA = pinch.lastTouchA;
var lastTouchB = pinch.lastTouchB;
if (!touchA || !lastTouchA || !touchB || !lastTouchB) {
return 0;
}
var oldDistance = Math.sqrt(
Math.pow(lastTouchB.pageX - lastTouchA.pageX, 2) +
Math.pow(lastTouchB.pageY - lastTouchA.pageY, 2));
var newDistance = Math.sqrt(
Math.pow(touchB.pageX - touchA.pageX, 2) +
Math.pow(touchB.pageY - touchA.pageY, 2));
return newDistance - oldDistance;
}
});

View file

@ -0,0 +1,53 @@
define(function(require, exports, module) {
'use strict';
/**
* Module Dependencies
*/
var parseJpegMetadata = require('jpegMetaDataParser');
/**
* Exports
*/
module.exports = function(blob, done) {
parseJpegMetadata(blob, onJpegParsed);
function onJpegParsed(metadata) {
metadata.blob = blob;
if (!metadata.preview) {
done(metadata);
return;
}
// If we found an EXIF preview,
// and can determine its size, then
// we can display it instead of the
// big image and save memory and time.
var start = metadata.preview.start;
var end = metadata.preview.end;
var previewBlob = blob.slice(start, end, 'image/jpeg');
parseJpegMetadata(previewBlob, onSuccess, onError);
// If we parsed the preview image, add its
// dimensions to the metadata.preview
// object, and then let the MediaFrame
// object display the preview instead of
// the full-size image.
function onSuccess(previewMetadata) {
metadata.preview.width = previewMetadata.width;
metadata.preview.height = previewMetadata.height;
done(metadata);
}
// If we couldn't parse the preview image,
// just display full-size.
function onError() {
done(metadata);
}
}
};
});

View file

@ -0,0 +1,61 @@
define(function(require, exports, module) {
'use strict';
var Storage = require('lib/storage');
var cropResizeRotate = require('cropResizeRotate');
/**
* Exports
*/
module.exports = function(options, done) {
var blob = options.blob;
var outputSize = options.width && options.height ?
{
width: options.width,
height: options.height
} : options.size || null;
cropResizeRotate(blob, null, outputSize, 'image/jpeg+exif',
function(error, resizedBlob) {
// If we couldn't resize or rotate it, use the original
if (error) {
console.error('Error while resizing image: ' + error);
done(blob);
return;
}
// We need to send a file-backed blob as the result of a pick activity
// (see bug 975599) so we'll overwrite the old blob with the new one.
// This means that the image stored will actually match the one passed
// to the app that initiated the pick activity. We delete the old file,
// then save the new blob with the same name. Then we read the file and
// pass that to the callback.
if (resizedBlob === blob) {
done(blob);
return;
}
var storage = new Storage();
// We first delete the full resolution picture
storage.deletePicture(blob.name, addPicture);
// We save the scaled down image
function addPicture(error) {
if (error) {
done(blob);
return;
}
storage.addPicture(resizedBlob, {
filepath: blob.name
}, onSavedPicture);
}
function onSavedPicture(error, filepath, absolutePath, fileBlob) {
done(fileBlob);
}
});
};
});

View file

@ -0,0 +1,157 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('setting-alias');
/**
* Locals
*/
var forwardMethods = [
'filterOptions',
'resetOptions',
'supported',
'selected',
'select',
'next',
'get',
'set'
];
/**
* Exports
*/
module.exports = SettingAlias;
/**
* Initialize a new `SettingsAlias`
*
* SettingsAlias's are kinda confusing,
* but they simplify our app code massively
* in the long run.
*
* A `SettingAlias` has the same API as a `Setting`
* but behind the scenes provides a single interface
* to several `Setting`s. Depending on the state
* of the app, the `SettingAlias` will point to
* a different settings instance.
*
* Example:
*
* var flashModesPicture = new Setting({ key: 'flashModesPicture' });
* var flashModesVideo = new Setting({ key: 'flashModesVideo' });
* var mode = 'picture';
*
* var flashModes = new SettingAlias({
* key: 'flashModes',
* settings: {
* 'picture': flashModesPicture,
* 'video': flashModesVideo
* },
* get: function() {
* return this.settings[mode];
* }
* });
*
* flashModes.get('key'); //=> 'flashModesPicture'
* mode = 'video';
* flashModes.get('key'); //=> 'flashModesVideo'
*
* This means our app can simply call API on
* `flashModes` setting-alias and not worry
* about which 'mode' the app is in.
*
* Options:
*
* - `key` The name of the setting-alias
* - `settings` Hash of possible `Setting`s
* - `get` Method that returns the current `Setting`
*
* @param {Object} options
*/
function SettingAlias(options) {
debug('initialize');
this.key = options.key;
this.settings = options.settings;
this.current = options.get.bind(this);
forwardMethods.forEach(this.forward, this);
debug('initialized');
}
/**
* Iterates over each setting.
*
* @param {Function} fn
* @private
*/
SettingAlias.prototype.each = function(fn) {
for (var key in this.settings) {
fn(this.settings[key]);
}
};
/**
* States whether the passed key
* is the currently active setting.
*
* @param {String} key
* @return {Boolean}
*/
SettingAlias.prototype.is = function(key) {
return this.get('key') === key;
};
/**
* Attaches a method that forwards
* the call onto the current
* matching setting.
*
* @param {String} method
*/
SettingAlias.prototype.forward = function(method) {
this[method] = function() {
var setting = this.current();
return setting[method].apply(setting, arguments);
};
};
/**
* Add an event callback.
*
* The callback is wrapped in
* a function that first checks that
* the setting is the active setting
* before calling the original callback.
*
* @param {String} name
* @param {Function} fn
*/
SettingAlias.prototype.on = function(name, fn) {
var alias = this;
var wrapped = function() {
if (!alias.is(this.key)) { return; }
fn.apply(this, arguments);
};
this.each(function(setting) { setting.on(name, wrapped); });
fn._settingAliasCallback = wrapped;
};
/**
* Remove an event callback.
*
* @param {String} name
* @param {Function} fn
*/
SettingAlias.prototype.off = function(name, fn) {
var wrapped = fn._settingAliasCallback;
this.each(function(setting) { setting.off(name, wrapped); });
delete fn._settingAliasCallback;
};
});

View file

@ -0,0 +1,251 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('setting');
var model = require('model');
/**
* Exports
*/
module.exports = Setting;
/**
* Mixin `Model` methods
*/
model(Setting.prototype);
/**
* Initialize a new `Setting`.
*
* @param {Object} data
*/
function Setting(data) {
this.key = data.key;
this.storage = data.storage || localStorage;
this.reset(data, { silent: true });
this.select = this.select.bind(this);
this.next = this.next.bind(this);
this.configure(data);
if (data.persistent) { this.on('change:selected', this.save); }
}
Setting.prototype.configure = function(data) {
var optionsDefined = !!(data.options && data.options.length);
this.options = { defined: optionsDefined };
this.resetOptions(data.options);
};
Setting.prototype.resetOptions = function(list) {
list = list || [];
var hash = this.optionsToHash(list);
this.options.all = hash;
this.options.available = hash;
this.set('options', list, { silent: true });
this.updateSelected({ silent: true });
};
Setting.prototype.filterOptions = function(keys, options) {
var available = this.options.available = {};
var hash = this.options.all;
var filtered = [];
(keys || []).forEach(function(key) {
var option = hash[key];
if (option !== undefined) {
filtered.push(option);
available[key] = option;
}
});
this.sortByIndex(filtered);
this.set('options', filtered, options);
this.updateSelected({ silent: true });
};
/**
* Convert this Setting's `options` array as defined
* in config/app.js into an object as key/value pairs.
* This allows us to look up an option by its `key`.
*
* e.g.: [ { key: 'a', title: 'A' }, ... ] ->
* { a: { key: 'a', title: 'A' }, ... }
*
* We use this hash to validate
* incoming options and ot mixin
* config data with dynamic options.
*
* @param {Array} options
* @return {undefined|Object}
*/
Setting.prototype.optionsToHash = function(options) {
var hash = {};
options.forEach(function(option, index) {
option.index = index;
hash[option.key] = option;
});
return hash;
};
/**
* Get the selected option,
* or just a particular key
* if given.
*
* @param {String} key
* @return {Object|*}
*/
Setting.prototype.selected = function(key) {
var hash = this.options.available;
var option = hash[this.get('selected')];
return key ? option && option[key] : option;
};
/**
* Select an option.
*
* Accepts an string key to select,
* or an integer index relating to
* the current options list.
*
* Options:
*
* - {Boolean} silent
*
* @param {String|Number} key
* @param {Object} opts Model#set() options
*/
Setting.prototype.select = function(key, options) {
var isIndex = typeof key === 'number';
var list = this.get('options');
var available = this.options.available;
var selected = isIndex ? list[key] : available[key];
// If there are no options, exit
if (!list.length) { return; }
// If an option was not found,
// default to selecting the first.
if (!selected) { return this.select(0, options); }
// Store the new choice
this.set('selected', selected.key, options);
};
/**
* Sorts the current options list
* by their originally defined
* index in the config JSON.
*
* @private
*/
Setting.prototype.sortByIndex = function(list) {
return list.sort(function(a, b) { return a.index - b.index; });
};
/**
* Silently updates the `selected`
* property of the Setting.
*
* If no selected key is present
* we attempt to select the last
* fetched persited selection.
*
* @private
*/
Setting.prototype.updateSelected = function(options) {
this.select(this.get('selected') || this.fetched, options);
};
/**
* Set the `selected` option to
* the next option in the list.
*
* First option is chosen if
* there is no next option.
*
* @public
*/
Setting.prototype.next = function() {
var options = this.get('options');
var selected = this.selected();
var index = options.indexOf(selected);
var newIndex = (index + 1) % options.length;
this.select(newIndex);
debug('set \'%s\' to index: %s', this.key, newIndex);
};
/**
* Persists the current selection
* to storage for retreval in the
* next session.
*
* We're using localStorage as performance
* vastly outweighed indexedBD (asyncStorage)
* by 1ms/500ms on Hamachi device.
*
* @public
*/
Setting.prototype.save = function() {
var selected = this.get('selected');
debug('saving key: %s, selected: %s', this.key, selected);
this.storage.setItem('setting:' + this.key, selected);
debug('saved key: %s', selected);
};
/**
* Fetches the persisted selection
* from storage, updating the
* `selected` key.
*
* We're using localStorage, as performance
* vastly outweighed indexedBD (asyncStorage)
* by 1ms/500ms on Hamachi device.
*
* Leaving in the `done` callback in-case
* storage goes async again in future.
*
* @param {Function} done
* @public
*/
Setting.prototype.fetch = function() {
if (!this.get('persistent')) { return; }
debug('fetch value key: %s', this.key);
this.fetched = this.storage.getItem('setting:' + this.key);
debug('fetched %s value: %s', this.key, this.fetched);
if (this.fetched) { this.select(this.fetched, { silent: true }); }
};
/**
* States whether this setting
* is currently supported.
*
* 'Supported' means, it's not been
* disabled, and there are options
* to be chosen from.
*
* @return {Boolean}
*/
Setting.prototype.supported = function() {
return this.enabled() && !!this.get('options').length;
};
/**
* Check if this setting is not
* marked as disabled.
*
* @return {Boolean}
*/
Setting.prototype.enabled = function() {
return !this.get('disabled');
};
});

View file

@ -0,0 +1,105 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var SettingAlias = require('./setting-alias');
var debug = require('debug')('settings');
var Setting = require('./setting');
/**
* Exports
*/
module.exports = Settings;
/**
* Initialize a new 'Setting'
*
* @param {Object} items
*/
function Settings(items) {
this.ids = {};
this.items = [];
this.aliases = {};
this.SettingAlias = SettingAlias; // Test hook
this.dontSave = this.dontSave.bind(this);
this.addEach(items);
}
/**
* Add several Settings in one go.
*
* @param {Object} items
*/
Settings.prototype.addEach = function(items) {
if (!items) { return; }
var item;
var key;
for (key in items) {
item = items[key];
item.key = item.key || key;
this.add(items[key]);
}
};
/**
* Add a new Setting to the settings collection.
*
* @param {Object} data
*/
Settings.prototype.add = function(data) {
var setting = new Setting(data);
this.items.push(setting);
this.ids[setting.key] = this[setting.key] = setting;
debug('added setting: %s', setting.key);
};
/**
* Fetch all the Settings previous
* state from storage.
*
* @public
*/
Settings.prototype.fetch = function() {
this.items.forEach(function(setting) { setting.fetch(); });
};
/**
* Prevent all settings from saving
* when they are changed.
*
* This is used in activity sessions
* whereby we don't want any settings
* changes to persist to future camera
* sessions.
*
* @public
*/
Settings.prototype.dontSave = function() {
this.items.forEach(function(setting) {
setting.off('change:selected', setting.save);
});
};
/**
* Add a SettingAlias to the settings.
* (see lib/setting-alias.js for more info)
*
* @param {Object} options
* @public
*/
Settings.prototype.alias = function(options) {
var alias = new this.SettingAlias(options);
this.aliases[options.key] = alias;
this[options.key] = alias;
};
Settings.prototype.removeAlias = function(key) {
// TODO: Implement
};
});

162
gaia/apps/camera/js/lib/sounds.js Executable file
View file

@ -0,0 +1,162 @@
define(function(require, exports, module) {
'use strict';
/**
* Exports
*/
module.exports = Sounds;
/**
* Initialize a new `Sounds` interface.
*
* @constructor
*/
function Sounds(list) {
this.items = {};
(list || []).forEach(this.add, this);
}
/**
* Add a new sound.
*
* Checks if this sound
* is enabled, and sets
* up an observer.
*
* @param {Object} data
*
*/
Sounds.prototype.add = function(data) {
var self = this;
var sound = {
name: data.name,
url: data.url,
setting: data.setting,
enabled: false
};
this.items[data.name] = sound;
this.isEnabled(sound, function(value) {
self.setEnabled(sound, value);
self.observeSetting(sound);
});
};
/**
* Check if a sound is
* enabled inside mozSettings.
*
* This is wrapped in a setTimeout
* to prevent expensive mozSettings
* API from blocking other operations.
*
* @param {Object} sound
* @param {Function} done
*/
Sounds.prototype.isEnabled = function(sound, done) {
setTimeout(function() {
var mozSettings = navigator.mozSettings;
var key = sound.setting;
// Requires navigator.mozSettings
if (!mozSettings) {
return;
}
mozSettings
.createLock()
.get(key)
.onsuccess = onSuccess;
function onSuccess(e) {
var result = e.target.result[key];
done(result);
}
});
};
/**
* Observe mozSettings for changes
* on the given settings key.
*
* @param {Object} sound
*
*/
Sounds.prototype.observeSetting = function(sound) {
var mozSettings = navigator.mozSettings;
var key = sound.setting;
var self = this;
if (mozSettings) {
mozSettings.addObserver(key, function(e) {
self.setEnabled(sound, e.settingValue);
});
}
};
/**
* Set a sounds `enabled` key.
*
* @param {Object} sound
* @param {Boolean} value
*
*/
Sounds.prototype.setEnabled = function(sound, value) {
sound.enabled = value;
};
/**
* Play a sound by name.
*
* @param {String} name
* @public
*/
Sounds.prototype.play = function(name) {
var sound = this.items[name];
if (!sound.audio) {
sound.audio = this.createAudio(sound.url);
}
this.playSound(sound);
};
/**
* Return a function that plays
* the given sound when called.
*
* @param {String} name
* @return {Funciton}
* @public
*/
Sounds.prototype.player = function(name) {
return (function() { this.play(name); }).bind(this);
};
/**
* Play a sound.
*
* @param {Object} sound
*
*/
Sounds.prototype.playSound = function(sound) {
if (sound.enabled) {
// cloneNode() lets us play overlapping sounds and is needed when the
// user takes pictures very rapidly.
// See http://robert.ocallahan.org/2011/11/latency-of-html5-sounds.html
sound.audio.cloneNode(true).play();
}
};
/**
* Create an audio element.
*
* @param {String} url
* @return {HTMLAudioElement}
*/
Sounds.prototype.createAudio = function(url) {
var audio = new Audio(url);
audio.mozAudioChannelType = 'notification';
return audio;
};
});

View file

@ -0,0 +1,410 @@
define(function(require, exports, module) {
'use strict';
/**
* Dependencies
*/
var debug = require('debug')('storage');
var bindAll = require('lib/bind-all');
var events = require('evt');
var storageSingleton;
/**
* Expose `Storage`
*/
module.exports = Storage;
/**
* Mixin event emitter
*/
events(Storage.prototype);
/**
* Initialize a new `Storage`.
*
* @param {Object} options
*/
function Storage(options) {
if (storageSingleton) {
return storageSingleton;
}
storageSingleton = this;
bindAll(this);
this.maxFileSize = 0;
options = options || {};
this.dcf = options.dcf;
if (this.dcf) {
this.dcf.init();
}
this.require = options.require || require;
navigator.mozSettings.addObserver(
'device.storage.writable.name',
this.onStorageVolumeChanged);
this.configure();
debug('initialized');
}
Storage.prototype.createFilename = function(storage, type, done) {
var self = this;
this.require(['asyncStorage', 'lib/dcf'], function(as, dcf) {
if (!self.dcf) {
self.dcf = dcf;
self.dcf.init();
}
self.dcf.createDCFFilename(storage, type, done);
});
};
/**
* Save the image Blob to DeviceStorage
* then lookup the File reference and
* return that in the callback as well
* as the resulting paths.
*
* You always want to forget about the
* Blob you told us about and use the
* File instead since otherwise you
* are wasting precious memory.
*
* @param {Object} [options]
* @param {String} options.filepath
* The path to save the image to.
*/
Storage.prototype.addPicture = function(blob, options, done) {
if (typeof options === 'function') {
done = options;
options = {};
}
done = done || function() {};
var filepath = options && options.filepath;
var self = this;
debug('add picture', filepath);
// Create a filepath if
// one hasn't been given.
if (!filepath) {
debug('creating filename');
this.createFilename(this.picture, 'image', onCreated);
} else {
onCreated(filepath);
}
function onCreated(filepath) {
var req = self.picture.addNamed(blob, filepath);
req.onerror = function() { self.emit('error'); };
req.onsuccess = function(e) {
debug('image stored', filepath);
var absolutePath = e.target.result;
// `addNamed` does not give us a File
// handle so we need to get() it again.
refetchFile(filepath, absolutePath);
};
}
function refetchFile(filepath, absolutePath) {
var req = self.picture.get(filepath);
req.onerror = function() {
self.emit('error');
done('Error adding picture to storage');
};
req.onsuccess = function(e) {
debug('image file blob handle retrieved');
var fileBlob = e.target.result;
done(null, filepath, absolutePath, fileBlob);
};
}
};
/**
* Create a new video filepath.
*
* The CameraControl API will not
* automatically create directories
* for the new file if they do not
* exist.
*
* So we write a dummy file to the
* same directory via DeviceStorage
* to ensure that the directory exists
* before attempting to record to this
* filepath.
*
* If the request errors it can mean that
* there is a file that already exists
* in that name. If so, we attempt to delete
* it and try to create the filepath again.
*
* @param {Function} done
* @public
*/
Storage.prototype.createVideoFilepath = function(done) {
var videoStorage = this.video;
var self = this;
this.createFilename(this.video, 'video', function(filepath) {
var dummyFilepath = getDir(filepath) + '.tmp.3gp';
var blob = new Blob([''], { type: 'video/3gpp' });
var req = videoStorage.addNamed(blob, dummyFilepath);
req.onerror = function(e) {
debug('Failed to add ' + filepath + ' to DeviceStorage', e);
var req = videoStorage.delete(dummyFilepath);
req.onerror = function() { done('Error creating video file path'); };
req.onsuccess = function() { self.createVideoFilepath(done); };
};
req.onsuccess = function(e) {
videoStorage.delete(e.target.result);
done(null, filepath);
};
});
};
Storage.prototype.onStorageChange = function(e) {
debug('state change: %s', e.reason);
var value = e.reason;
switch (value) {
case 'deleted':
// Emit an `itemdeleted` event to
// allow parts of the UI to update.
var filepath = this.checkFilepath(e.path);
this.emit('itemdeleted', { path: filepath });
break;
case 'available':
case 'shared':
case 'unavailable':
this.setState(value);
break;
}
// Check storage
// has spare capacity
this.check();
};
Storage.prototype.configure = function(storageVolumeName) {
var i;
var videosStorages;
var picturesStorages;
// If we had a previous ds for pictures, let's remove the observer
// we had set as well before fetching new ds.
if (this.picture) {
this.picture.removeEventListener('change', this.onStorageChange);
}
if (!storageVolumeName) {
this.video = navigator.getDeviceStorage('videos');
this.picture = navigator.getDeviceStorage('pictures');
} else { // We select the volumes with the passed name
videosStorages = navigator.getDeviceStorages('videos');
this.video = videosStorages[0];
for (i = 0; i < videosStorages.length; ++i) {
if (videosStorages[i].storageName === storageVolumeName) {
this.video = videosStorages[i];
break;
}
}
picturesStorages = navigator.getDeviceStorages('pictures');
this.picture = picturesStorages[0];
for (i = 0; i < picturesStorages.length; ++i) {
if (picturesStorages[i].storageName === storageVolumeName) {
this.picture = picturesStorages[i];
break;
}
}
}
// Shouldn't happen?
if (!this.picture) {
this.setState('unavailable');
return;
}
this.picture.addEventListener('change', this.onStorageChange);
this.emit('volumechanged',{
video: this.video,
picture: this.picture
});
};
Storage.prototype.onStorageVolumeChanged = function(setting) {
debug('default storage volume change: %s', setting.settingValue);
this.configure(setting.settingValue);
};
Storage.prototype.checkFilepath = function(filepath) {
var startString = filepath.indexOf('DCIM/');
if (startString < -1) { return; }
else if (startString > 0) {
filepath = filepath.substr(startString);
}
// Check whether filepath is a video poster image or not. If filepath
// contains 'VID' and ends with '.jpg', consider it a video poster
// image and get the video filepath by changing '.jpg' to '.3gp'
if (filepath.indexOf('VID') != -1 &&
filepath.lastIndexOf('.jpg') === filepath.length - 4) {
filepath = filepath.replace('.jpg', '.3gp');
}
return filepath;
};
Storage.prototype.setState = function(value) {
this.state = value;
debug('set state: %s', value);
this.emit('changed', value);
};
Storage.prototype.setMaxFileSize = function(maxFileSize) {
this.maxFileSize = maxFileSize;
this.check();
debug('max file size set: %d', maxFileSize);
};
/**
* Run a full storage check.
*
* @param {Function} done
* @public
*/
Storage.prototype.check = function(done) {
debug('check');
var self = this;
done = done || function() {};
this.getState(function(result) {
self.setState(result);
if (!self.available()) {
onComplete('unhealthy');
return;
}
// check file counter in dcf if it already exist.
if (self.dcf) {
self.dcf.checkFileCounter();
}
self.isSpace(function(result) {
if (!result) { self.setState('nospace'); }
onComplete('healthy');
});
});
function onComplete(state) {
self.emit('checked', state);
}
};
/**
* Checks if there is enough space to
* accomdate the current `maxFileSize`.
*
* @param {Function} done
*/
Storage.prototype.isSpace = function(done) {
var maxFileSize = this.maxFileSize;
this.picture
.freeSpace()
.onsuccess = function(e) {
var freeSpace = e.target.result;
var result = freeSpace > maxFileSize;
debug('is space: %s', result, freeSpace, maxFileSize);
done(result);
};
};
/**
* Get current storage state.
*
* @param {Function} done
*/
Storage.prototype.getState = function(done) {
if (!this.picture) {
setTimeout(function() {
done('unavailable');
});
return;
}
this.picture
.available()
.onsuccess = function(e) {
done(e.target.result);
};
};
/**
* States if sotrage is available.
*
* @return {Boolean}
*/
Storage.prototype.available = function() {
return this.state === 'available';
};
/**
* Delete a picture.
*
* @param {String} filepath
*/
Storage.prototype.deletePicture = function(filepath, done) {
var req = this.picture.delete(filepath);
req.onerror = function(e) {
var message = 'Failed to delete ' + filepath +
' from DeviceStorage:' + e.target.error;
console.warn(message);
done(message);
};
if (done) {
req.onsuccess = function() {
done(null);
};
}
};
/**
* Delete a video and accompanying
* poster image.
*
* @param {String} filepath
*/
Storage.prototype.deleteVideo = function(filepath) {
var poster = filepath.replace('.3gp', '.jpg');
this.video.delete(filepath).onerror = function(e) {
console.warn(
'Failed to delete', filepath,
'from DeviceStorage:', e.target.error);
};
this.picture.delete(poster).onerror = function(e) {
console.warn(
'Failed to delete poster image', poster,
'for video', filepath,
'from DeviceStorage:', e.target.error);
};
};
/**
* Get the directory from a filepath.
*
* @param {String} filepath
* @return {String}
*/
function getDir(filepath) {
var index = filepath.lastIndexOf('/') + 1;
return index ? filepath.substring(0, index) : '';
}
});

Some files were not shown because too many files have changed in this diff Show more