Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source. Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
7706 lines
No EOL
269 KiB
JavaScript
7706 lines
No EOL
269 KiB
JavaScript
var requirejs, require, define;
|
|
(function (global) {
|
|
var req, s, head, baseElement, dataMain, src, interactiveScript, currentlyAddingScript, mainScript, subPath, version = '2.1.14', commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/gm, cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g, jsSuffixRegExp = /\.js$/, currDirRegExp = /^\.\//, op = Object.prototype, ostring = op.toString, hasOwn = op.hasOwnProperty, ap = Array.prototype, apsp = ap.splice, isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document), isWebWorker = !isBrowser && typeof importScripts !== 'undefined', readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ? /^complete$/ : /^(complete|loaded)$/, defContextName = '_', isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]', contexts = {}, cfg = {}, globalDefQueue = [], useInteractive = false;
|
|
function isFunction(it) {
|
|
return ostring.call(it) === '[object Function]';
|
|
}
|
|
function isArray(it) {
|
|
return ostring.call(it) === '[object Array]';
|
|
}
|
|
function each(ary, func) {
|
|
if (ary) {
|
|
var i;
|
|
for (i = 0; i < ary.length; i += 1) {
|
|
if (ary[i] && func(ary[i], i, ary)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function eachReverse(ary, func) {
|
|
if (ary) {
|
|
var i;
|
|
for (i = ary.length - 1; i > -1; i -= 1) {
|
|
if (ary[i] && func(ary[i], i, ary)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function hasProp(obj, prop) {
|
|
return hasOwn.call(obj, prop);
|
|
}
|
|
function getOwn(obj, prop) {
|
|
return hasProp(obj, prop) && obj[prop];
|
|
}
|
|
function eachProp(obj, func) {
|
|
var prop;
|
|
for (prop in obj) {
|
|
if (hasProp(obj, prop)) {
|
|
if (func(obj[prop], prop)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function mixin(target, source, force, deepStringMixin) {
|
|
if (source) {
|
|
eachProp(source, function (value, prop) {
|
|
if (force || !hasProp(target, prop)) {
|
|
if (deepStringMixin && typeof value === 'object' && value && !isArray(value) && !isFunction(value) && !(value instanceof RegExp)) {
|
|
if (!target[prop]) {
|
|
target[prop] = {};
|
|
}
|
|
mixin(target[prop], value, force, deepStringMixin);
|
|
} else {
|
|
target[prop] = value;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
return target;
|
|
}
|
|
function bind(obj, fn) {
|
|
return function () {
|
|
return fn.apply(obj, arguments);
|
|
};
|
|
}
|
|
function scripts() {
|
|
return document.getElementsByTagName('script');
|
|
}
|
|
function defaultOnError(err) {
|
|
throw err;
|
|
}
|
|
function getGlobal(value) {
|
|
if (!value) {
|
|
return value;
|
|
}
|
|
var g = global;
|
|
each(value.split('.'), function (part) {
|
|
g = g[part];
|
|
});
|
|
return g;
|
|
}
|
|
function makeError(id, msg, err, requireModules) {
|
|
var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id);
|
|
e.requireType = id;
|
|
e.requireModules = requireModules;
|
|
if (err) {
|
|
e.originalError = err;
|
|
}
|
|
return e;
|
|
}
|
|
if (typeof define !== 'undefined') {
|
|
return;
|
|
}
|
|
if (typeof requirejs !== 'undefined') {
|
|
if (isFunction(requirejs)) {
|
|
return;
|
|
}
|
|
cfg = requirejs;
|
|
requirejs = undefined;
|
|
}
|
|
if (typeof require !== 'undefined' && !isFunction(require)) {
|
|
cfg = require;
|
|
require = undefined;
|
|
}
|
|
function newContext(contextName) {
|
|
var inCheckLoaded, Module, context, handlers, checkLoadedTimeoutId, config = {
|
|
waitSeconds: 7,
|
|
baseUrl: './',
|
|
paths: {},
|
|
bundles: {},
|
|
pkgs: {},
|
|
shim: {},
|
|
config: {}
|
|
}, registry = {}, enabledRegistry = {}, undefEvents = {}, defQueue = [], defined = {}, urlFetched = {}, bundlesMap = {}, requireCounter = 1, unnormalizedCounter = 1;
|
|
function trimDots(ary) {
|
|
var i, part;
|
|
for (i = 0; i < ary.length; i++) {
|
|
part = ary[i];
|
|
if (part === '.') {
|
|
ary.splice(i, 1);
|
|
i -= 1;
|
|
} else if (part === '..') {
|
|
if (i === 0 || i == 1 && ary[2] === '..' || ary[i - 1] === '..') {
|
|
continue;
|
|
} else if (i > 0) {
|
|
ary.splice(i - 1, 2);
|
|
i -= 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function normalize(name, baseName, applyMap) {
|
|
var pkgMain, mapValue, nameParts, i, j, nameSegment, lastIndex, foundMap, foundI, foundStarMap, starI, normalizedBaseParts, baseParts = baseName && baseName.split('/'), map = config.map, starMap = map && map['*'];
|
|
if (name) {
|
|
name = name.split('/');
|
|
lastIndex = name.length - 1;
|
|
if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) {
|
|
name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, '');
|
|
}
|
|
if (name[0].charAt(0) === '.' && baseParts) {
|
|
normalizedBaseParts = baseParts.slice(0, baseParts.length - 1);
|
|
name = normalizedBaseParts.concat(name);
|
|
}
|
|
trimDots(name);
|
|
name = name.join('/');
|
|
}
|
|
if (applyMap && map && (baseParts || starMap)) {
|
|
nameParts = name.split('/');
|
|
outerLoop:
|
|
for (i = nameParts.length; i > 0; i -= 1) {
|
|
nameSegment = nameParts.slice(0, i).join('/');
|
|
if (baseParts) {
|
|
for (j = baseParts.length; j > 0; j -= 1) {
|
|
mapValue = getOwn(map, baseParts.slice(0, j).join('/'));
|
|
if (mapValue) {
|
|
mapValue = getOwn(mapValue, nameSegment);
|
|
if (mapValue) {
|
|
foundMap = mapValue;
|
|
foundI = i;
|
|
break outerLoop;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) {
|
|
foundStarMap = getOwn(starMap, nameSegment);
|
|
starI = i;
|
|
}
|
|
}
|
|
if (!foundMap && foundStarMap) {
|
|
foundMap = foundStarMap;
|
|
foundI = starI;
|
|
}
|
|
if (foundMap) {
|
|
nameParts.splice(0, foundI, foundMap);
|
|
name = nameParts.join('/');
|
|
}
|
|
}
|
|
pkgMain = getOwn(config.pkgs, name);
|
|
return pkgMain ? pkgMain : name;
|
|
}
|
|
function removeScript(name) {
|
|
if (isBrowser) {
|
|
each(scripts(), function (scriptNode) {
|
|
if (scriptNode.getAttribute('data-requiremodule') === name && scriptNode.getAttribute('data-requirecontext') === context.contextName) {
|
|
scriptNode.parentNode.removeChild(scriptNode);
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
function hasPathFallback(id) {
|
|
var pathConfig = getOwn(config.paths, id);
|
|
if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) {
|
|
pathConfig.shift();
|
|
context.require.undef(id);
|
|
context.makeRequire(null, { skipMap: true })([id]);
|
|
return true;
|
|
}
|
|
}
|
|
function splitPrefix(name) {
|
|
var prefix, index = name ? name.indexOf('!') : -1;
|
|
if (index > -1) {
|
|
prefix = name.substring(0, index);
|
|
name = name.substring(index + 1, name.length);
|
|
}
|
|
return [
|
|
prefix,
|
|
name
|
|
];
|
|
}
|
|
function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) {
|
|
var url, pluginModule, suffix, nameParts, prefix = null, parentName = parentModuleMap ? parentModuleMap.name : null, originalName = name, isDefine = true, normalizedName = '';
|
|
if (!name) {
|
|
isDefine = false;
|
|
name = '_@r' + (requireCounter += 1);
|
|
}
|
|
nameParts = splitPrefix(name);
|
|
prefix = nameParts[0];
|
|
name = nameParts[1];
|
|
if (prefix) {
|
|
prefix = normalize(prefix, parentName, applyMap);
|
|
pluginModule = getOwn(defined, prefix);
|
|
}
|
|
if (name) {
|
|
if (prefix) {
|
|
if (pluginModule && pluginModule.normalize) {
|
|
normalizedName = pluginModule.normalize(name, function (name) {
|
|
return normalize(name, parentName, applyMap);
|
|
});
|
|
} else {
|
|
normalizedName = name.indexOf('!') === -1 ? normalize(name, parentName, applyMap) : name;
|
|
}
|
|
} else {
|
|
normalizedName = normalize(name, parentName, applyMap);
|
|
nameParts = splitPrefix(normalizedName);
|
|
prefix = nameParts[0];
|
|
normalizedName = nameParts[1];
|
|
isNormalized = true;
|
|
url = context.nameToUrl(normalizedName);
|
|
}
|
|
}
|
|
suffix = prefix && !pluginModule && !isNormalized ? '_unnormalized' + (unnormalizedCounter += 1) : '';
|
|
return {
|
|
prefix: prefix,
|
|
name: normalizedName,
|
|
parentMap: parentModuleMap,
|
|
unnormalized: !!suffix,
|
|
url: url,
|
|
originalName: originalName,
|
|
isDefine: isDefine,
|
|
id: (prefix ? prefix + '!' + normalizedName : normalizedName) + suffix
|
|
};
|
|
}
|
|
function getModule(depMap) {
|
|
var id = depMap.id, mod = getOwn(registry, id);
|
|
if (!mod) {
|
|
mod = registry[id] = new context.Module(depMap);
|
|
}
|
|
return mod;
|
|
}
|
|
function on(depMap, name, fn) {
|
|
var id = depMap.id, mod = getOwn(registry, id);
|
|
if (hasProp(defined, id) && (!mod || mod.defineEmitComplete)) {
|
|
if (name === 'defined') {
|
|
fn(defined[id]);
|
|
}
|
|
} else {
|
|
mod = getModule(depMap);
|
|
if (mod.error && name === 'error') {
|
|
fn(mod.error);
|
|
} else {
|
|
mod.on(name, fn);
|
|
}
|
|
}
|
|
}
|
|
function onError(err, errback) {
|
|
var ids = err.requireModules, notified = false;
|
|
if (errback) {
|
|
errback(err);
|
|
} else {
|
|
each(ids, function (id) {
|
|
var mod = getOwn(registry, id);
|
|
if (mod) {
|
|
mod.error = err;
|
|
if (mod.events.error) {
|
|
notified = true;
|
|
mod.emit('error', err);
|
|
}
|
|
}
|
|
});
|
|
if (!notified) {
|
|
req.onError(err);
|
|
}
|
|
}
|
|
}
|
|
function takeGlobalQueue() {
|
|
if (globalDefQueue.length) {
|
|
apsp.apply(defQueue, [
|
|
defQueue.length,
|
|
0
|
|
].concat(globalDefQueue));
|
|
globalDefQueue = [];
|
|
}
|
|
}
|
|
handlers = {
|
|
'require': function (mod) {
|
|
if (mod.require) {
|
|
return mod.require;
|
|
} else {
|
|
return mod.require = context.makeRequire(mod.map);
|
|
}
|
|
},
|
|
'exports': function (mod) {
|
|
mod.usingExports = true;
|
|
if (mod.map.isDefine) {
|
|
if (mod.exports) {
|
|
return defined[mod.map.id] = mod.exports;
|
|
} else {
|
|
return mod.exports = defined[mod.map.id] = {};
|
|
}
|
|
}
|
|
},
|
|
'module': function (mod) {
|
|
if (mod.module) {
|
|
return mod.module;
|
|
} else {
|
|
return mod.module = {
|
|
id: mod.map.id,
|
|
uri: mod.map.url,
|
|
config: function () {
|
|
return getOwn(config.config, mod.map.id) || {};
|
|
},
|
|
exports: mod.exports || (mod.exports = {})
|
|
};
|
|
}
|
|
}
|
|
};
|
|
function cleanRegistry(id) {
|
|
delete registry[id];
|
|
delete enabledRegistry[id];
|
|
}
|
|
function breakCycle(mod, traced, processed) {
|
|
var id = mod.map.id;
|
|
if (mod.error) {
|
|
mod.emit('error', mod.error);
|
|
} else {
|
|
traced[id] = true;
|
|
each(mod.depMaps, function (depMap, i) {
|
|
var depId = depMap.id, dep = getOwn(registry, depId);
|
|
if (dep && !mod.depMatched[i] && !processed[depId]) {
|
|
if (getOwn(traced, depId)) {
|
|
mod.defineDep(i, defined[depId]);
|
|
mod.check();
|
|
} else {
|
|
breakCycle(dep, traced, processed);
|
|
}
|
|
}
|
|
});
|
|
processed[id] = true;
|
|
}
|
|
}
|
|
function checkLoaded() {
|
|
var err, usingPathFallback, waitInterval = config.waitSeconds * 1000, expired = waitInterval && context.startTime + waitInterval < new Date().getTime(), noLoads = [], reqCalls = [], stillLoading = false, needCycleCheck = true;
|
|
if (inCheckLoaded) {
|
|
return;
|
|
}
|
|
inCheckLoaded = true;
|
|
eachProp(enabledRegistry, function (mod) {
|
|
var map = mod.map, modId = map.id;
|
|
if (!mod.enabled) {
|
|
return;
|
|
}
|
|
if (!map.isDefine) {
|
|
reqCalls.push(mod);
|
|
}
|
|
if (!mod.error) {
|
|
if (!mod.inited && expired) {
|
|
if (hasPathFallback(modId)) {
|
|
usingPathFallback = true;
|
|
stillLoading = true;
|
|
} else {
|
|
noLoads.push(modId);
|
|
removeScript(modId);
|
|
}
|
|
} else if (!mod.inited && mod.fetched && map.isDefine) {
|
|
stillLoading = true;
|
|
if (!map.prefix) {
|
|
return needCycleCheck = false;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if (expired && noLoads.length) {
|
|
err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads);
|
|
err.contextName = context.contextName;
|
|
return onError(err);
|
|
}
|
|
if (needCycleCheck) {
|
|
each(reqCalls, function (mod) {
|
|
breakCycle(mod, {}, {});
|
|
});
|
|
}
|
|
if ((!expired || usingPathFallback) && stillLoading) {
|
|
if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
|
|
checkLoadedTimeoutId = setTimeout(function () {
|
|
checkLoadedTimeoutId = 0;
|
|
checkLoaded();
|
|
}, 50);
|
|
}
|
|
}
|
|
inCheckLoaded = false;
|
|
}
|
|
Module = function (map) {
|
|
this.events = getOwn(undefEvents, map.id) || {};
|
|
this.map = map;
|
|
this.shim = getOwn(config.shim, map.id);
|
|
this.depExports = [];
|
|
this.depMaps = [];
|
|
this.depMatched = [];
|
|
this.pluginMaps = {};
|
|
this.depCount = 0;
|
|
};
|
|
Module.prototype = {
|
|
init: function (depMaps, factory, errback, options) {
|
|
options = options || {};
|
|
if (this.inited) {
|
|
return;
|
|
}
|
|
this.factory = factory;
|
|
if (errback) {
|
|
this.on('error', errback);
|
|
} else if (this.events.error) {
|
|
errback = bind(this, function (err) {
|
|
this.emit('error', err);
|
|
});
|
|
}
|
|
this.depMaps = depMaps && depMaps.slice(0);
|
|
this.errback = errback;
|
|
this.inited = true;
|
|
this.ignore = options.ignore;
|
|
if (options.enabled || this.enabled) {
|
|
this.enable();
|
|
} else {
|
|
this.check();
|
|
}
|
|
},
|
|
defineDep: function (i, depExports) {
|
|
if (!this.depMatched[i]) {
|
|
this.depMatched[i] = true;
|
|
this.depCount -= 1;
|
|
this.depExports[i] = depExports;
|
|
}
|
|
},
|
|
fetch: function () {
|
|
if (this.fetched) {
|
|
return;
|
|
}
|
|
this.fetched = true;
|
|
context.startTime = new Date().getTime();
|
|
var map = this.map;
|
|
if (this.shim) {
|
|
context.makeRequire(this.map, { enableBuildCallback: true })(this.shim.deps || [], bind(this, function () {
|
|
return map.prefix ? this.callPlugin() : this.load();
|
|
}));
|
|
} else {
|
|
return map.prefix ? this.callPlugin() : this.load();
|
|
}
|
|
},
|
|
load: function () {
|
|
var url = this.map.url;
|
|
if (!urlFetched[url]) {
|
|
urlFetched[url] = true;
|
|
context.load(this.map.id, url);
|
|
}
|
|
},
|
|
check: function () {
|
|
if (!this.enabled || this.enabling) {
|
|
return;
|
|
}
|
|
var err, cjsModule, id = this.map.id, depExports = this.depExports, exports = this.exports, factory = this.factory;
|
|
if (!this.inited) {
|
|
this.fetch();
|
|
} else if (this.error) {
|
|
this.emit('error', this.error);
|
|
} else if (!this.defining) {
|
|
this.defining = true;
|
|
if (this.depCount < 1 && !this.defined) {
|
|
if (isFunction(factory)) {
|
|
if (this.events.error && this.map.isDefine || req.onError !== defaultOnError) {
|
|
try {
|
|
exports = context.execCb(id, factory, depExports, exports);
|
|
} catch (e) {
|
|
err = e;
|
|
}
|
|
} else {
|
|
exports = context.execCb(id, factory, depExports, exports);
|
|
}
|
|
if (this.map.isDefine && exports === undefined) {
|
|
cjsModule = this.module;
|
|
if (cjsModule) {
|
|
exports = cjsModule.exports;
|
|
} else if (this.usingExports) {
|
|
exports = this.exports;
|
|
}
|
|
}
|
|
if (err) {
|
|
err.requireMap = this.map;
|
|
err.requireModules = this.map.isDefine ? [this.map.id] : null;
|
|
err.requireType = this.map.isDefine ? 'define' : 'require';
|
|
return onError(this.error = err);
|
|
}
|
|
} else {
|
|
exports = factory;
|
|
}
|
|
this.exports = exports;
|
|
if (this.map.isDefine && !this.ignore) {
|
|
defined[id] = exports;
|
|
if (req.onResourceLoad) {
|
|
req.onResourceLoad(context, this.map, this.depMaps);
|
|
}
|
|
}
|
|
cleanRegistry(id);
|
|
this.defined = true;
|
|
}
|
|
this.defining = false;
|
|
if (this.defined && !this.defineEmitted) {
|
|
this.defineEmitted = true;
|
|
this.emit('defined', this.exports);
|
|
this.defineEmitComplete = true;
|
|
}
|
|
}
|
|
},
|
|
callPlugin: function () {
|
|
var map = this.map, id = map.id, pluginMap = makeModuleMap(map.prefix);
|
|
this.depMaps.push(pluginMap);
|
|
on(pluginMap, 'defined', bind(this, function (plugin) {
|
|
var load, normalizedMap, normalizedMod, bundleId = getOwn(bundlesMap, this.map.id), name = this.map.name, parentName = this.map.parentMap ? this.map.parentMap.name : null, localRequire = context.makeRequire(map.parentMap, { enableBuildCallback: true });
|
|
if (this.map.unnormalized) {
|
|
if (plugin.normalize) {
|
|
name = plugin.normalize(name, function (name) {
|
|
return normalize(name, parentName, true);
|
|
}) || '';
|
|
}
|
|
normalizedMap = makeModuleMap(map.prefix + '!' + name, this.map.parentMap);
|
|
on(normalizedMap, 'defined', bind(this, function (value) {
|
|
this.init([], function () {
|
|
return value;
|
|
}, null, {
|
|
enabled: true,
|
|
ignore: true
|
|
});
|
|
}));
|
|
normalizedMod = getOwn(registry, normalizedMap.id);
|
|
if (normalizedMod) {
|
|
this.depMaps.push(normalizedMap);
|
|
if (this.events.error) {
|
|
normalizedMod.on('error', bind(this, function (err) {
|
|
this.emit('error', err);
|
|
}));
|
|
}
|
|
normalizedMod.enable();
|
|
}
|
|
return;
|
|
}
|
|
if (bundleId) {
|
|
this.map.url = context.nameToUrl(bundleId);
|
|
this.load();
|
|
return;
|
|
}
|
|
load = bind(this, function (value) {
|
|
this.init([], function () {
|
|
return value;
|
|
}, null, { enabled: true });
|
|
});
|
|
load.error = bind(this, function (err) {
|
|
this.inited = true;
|
|
this.error = err;
|
|
err.requireModules = [id];
|
|
eachProp(registry, function (mod) {
|
|
if (mod.map.id.indexOf(id + '_unnormalized') === 0) {
|
|
cleanRegistry(mod.map.id);
|
|
}
|
|
});
|
|
onError(err);
|
|
});
|
|
load.fromText = bind(this, function (text, textAlt) {
|
|
var moduleName = map.name, moduleMap = makeModuleMap(moduleName), hasInteractive = useInteractive;
|
|
if (textAlt) {
|
|
text = textAlt;
|
|
}
|
|
if (hasInteractive) {
|
|
useInteractive = false;
|
|
}
|
|
getModule(moduleMap);
|
|
if (hasProp(config.config, id)) {
|
|
config.config[moduleName] = config.config[id];
|
|
}
|
|
try {
|
|
req.exec(text);
|
|
} catch (e) {
|
|
return onError(makeError('fromtexteval', 'fromText eval for ' + id + ' failed: ' + e, e, [id]));
|
|
}
|
|
if (hasInteractive) {
|
|
useInteractive = true;
|
|
}
|
|
this.depMaps.push(moduleMap);
|
|
context.completeLoad(moduleName);
|
|
localRequire([moduleName], load);
|
|
});
|
|
plugin.load(map.name, localRequire, load, config);
|
|
}));
|
|
context.enable(pluginMap, this);
|
|
this.pluginMaps[pluginMap.id] = pluginMap;
|
|
},
|
|
enable: function () {
|
|
enabledRegistry[this.map.id] = this;
|
|
this.enabled = true;
|
|
this.enabling = true;
|
|
each(this.depMaps, bind(this, function (depMap, i) {
|
|
var id, mod, handler;
|
|
if (typeof depMap === 'string') {
|
|
depMap = makeModuleMap(depMap, this.map.isDefine ? this.map : this.map.parentMap, false, !this.skipMap);
|
|
this.depMaps[i] = depMap;
|
|
handler = getOwn(handlers, depMap.id);
|
|
if (handler) {
|
|
this.depExports[i] = handler(this);
|
|
return;
|
|
}
|
|
this.depCount += 1;
|
|
on(depMap, 'defined', bind(this, function (depExports) {
|
|
this.defineDep(i, depExports);
|
|
this.check();
|
|
}));
|
|
if (this.errback) {
|
|
on(depMap, 'error', bind(this, this.errback));
|
|
}
|
|
}
|
|
id = depMap.id;
|
|
mod = registry[id];
|
|
if (!hasProp(handlers, id) && mod && !mod.enabled) {
|
|
context.enable(depMap, this);
|
|
}
|
|
}));
|
|
eachProp(this.pluginMaps, bind(this, function (pluginMap) {
|
|
var mod = getOwn(registry, pluginMap.id);
|
|
if (mod && !mod.enabled) {
|
|
context.enable(pluginMap, this);
|
|
}
|
|
}));
|
|
this.enabling = false;
|
|
this.check();
|
|
},
|
|
on: function (name, cb) {
|
|
var cbs = this.events[name];
|
|
if (!cbs) {
|
|
cbs = this.events[name] = [];
|
|
}
|
|
cbs.push(cb);
|
|
},
|
|
emit: function (name, evt) {
|
|
each(this.events[name], function (cb) {
|
|
cb(evt);
|
|
});
|
|
if (name === 'error') {
|
|
delete this.events[name];
|
|
}
|
|
}
|
|
};
|
|
function callGetModule(args) {
|
|
if (!hasProp(defined, args[0])) {
|
|
getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
|
|
}
|
|
}
|
|
function removeListener(node, func, name, ieName) {
|
|
if (node.detachEvent && !isOpera) {
|
|
if (ieName) {
|
|
node.detachEvent(ieName, func);
|
|
}
|
|
} else {
|
|
node.removeEventListener(name, func, false);
|
|
}
|
|
}
|
|
function getScriptData(evt) {
|
|
var node = evt.currentTarget || evt.srcElement;
|
|
removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange');
|
|
removeListener(node, context.onScriptError, 'error');
|
|
return {
|
|
node: node,
|
|
id: node && node.getAttribute('data-requiremodule')
|
|
};
|
|
}
|
|
function intakeDefines() {
|
|
var args;
|
|
takeGlobalQueue();
|
|
while (defQueue.length) {
|
|
args = defQueue.shift();
|
|
if (args[0] === null) {
|
|
return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1]));
|
|
} else {
|
|
callGetModule(args);
|
|
}
|
|
}
|
|
}
|
|
context = {
|
|
config: config,
|
|
contextName: contextName,
|
|
registry: registry,
|
|
defined: defined,
|
|
urlFetched: urlFetched,
|
|
defQueue: defQueue,
|
|
Module: Module,
|
|
makeModuleMap: makeModuleMap,
|
|
nextTick: req.nextTick,
|
|
onError: onError,
|
|
configure: function (cfg) {
|
|
if (cfg.baseUrl) {
|
|
if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') {
|
|
cfg.baseUrl += '/';
|
|
}
|
|
}
|
|
var shim = config.shim, objs = {
|
|
paths: true,
|
|
bundles: true,
|
|
config: true,
|
|
map: true
|
|
};
|
|
eachProp(cfg, function (value, prop) {
|
|
if (objs[prop]) {
|
|
if (!config[prop]) {
|
|
config[prop] = {};
|
|
}
|
|
mixin(config[prop], value, true, true);
|
|
} else {
|
|
config[prop] = value;
|
|
}
|
|
});
|
|
if (cfg.bundles) {
|
|
eachProp(cfg.bundles, function (value, prop) {
|
|
each(value, function (v) {
|
|
if (v !== prop) {
|
|
bundlesMap[v] = prop;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
if (cfg.shim) {
|
|
eachProp(cfg.shim, function (value, id) {
|
|
if (isArray(value)) {
|
|
value = { deps: value };
|
|
}
|
|
if ((value.exports || value.init) && !value.exportsFn) {
|
|
value.exportsFn = context.makeShimExports(value);
|
|
}
|
|
shim[id] = value;
|
|
});
|
|
config.shim = shim;
|
|
}
|
|
if (cfg.packages) {
|
|
each(cfg.packages, function (pkgObj) {
|
|
var location, name;
|
|
pkgObj = typeof pkgObj === 'string' ? { name: pkgObj } : pkgObj;
|
|
name = pkgObj.name;
|
|
location = pkgObj.location;
|
|
if (location) {
|
|
config.paths[name] = pkgObj.location;
|
|
}
|
|
config.pkgs[name] = pkgObj.name + '/' + (pkgObj.main || 'main').replace(currDirRegExp, '').replace(jsSuffixRegExp, '');
|
|
});
|
|
}
|
|
eachProp(registry, function (mod, id) {
|
|
if (!mod.inited && !mod.map.unnormalized) {
|
|
mod.map = makeModuleMap(id);
|
|
}
|
|
});
|
|
if (cfg.deps || cfg.callback) {
|
|
context.require(cfg.deps || [], cfg.callback);
|
|
}
|
|
},
|
|
makeShimExports: function (value) {
|
|
function fn() {
|
|
var ret;
|
|
if (value.init) {
|
|
ret = value.init.apply(global, arguments);
|
|
}
|
|
return ret || value.exports && getGlobal(value.exports);
|
|
}
|
|
return fn;
|
|
},
|
|
makeRequire: function (relMap, options) {
|
|
options = options || {};
|
|
function localRequire(deps, callback, errback) {
|
|
var id, map, requireMod;
|
|
if (options.enableBuildCallback && callback && isFunction(callback)) {
|
|
callback.__requireJsBuild = true;
|
|
}
|
|
if (typeof deps === 'string') {
|
|
if (isFunction(callback)) {
|
|
return onError(makeError('requireargs', 'Invalid require call'), errback);
|
|
}
|
|
if (relMap && hasProp(handlers, deps)) {
|
|
return handlers[deps](registry[relMap.id]);
|
|
}
|
|
if (req.get) {
|
|
return req.get(context, deps, relMap, localRequire);
|
|
}
|
|
map = makeModuleMap(deps, relMap, false, true);
|
|
id = map.id;
|
|
if (!hasProp(defined, id)) {
|
|
return onError(makeError('notloaded', 'Module name "' + id + '" has not been loaded yet for context: ' + contextName + (relMap ? '' : '. Use require([])')));
|
|
}
|
|
return defined[id];
|
|
}
|
|
intakeDefines();
|
|
context.nextTick(function () {
|
|
intakeDefines();
|
|
requireMod = getModule(makeModuleMap(null, relMap));
|
|
requireMod.skipMap = options.skipMap;
|
|
requireMod.init(deps, callback, errback, { enabled: true });
|
|
checkLoaded();
|
|
});
|
|
return localRequire;
|
|
}
|
|
mixin(localRequire, {
|
|
isBrowser: isBrowser,
|
|
toUrl: function (moduleNamePlusExt) {
|
|
var ext, index = moduleNamePlusExt.lastIndexOf('.'), segment = moduleNamePlusExt.split('/')[0], isRelative = segment === '.' || segment === '..';
|
|
if (index !== -1 && (!isRelative || index > 1)) {
|
|
ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length);
|
|
moduleNamePlusExt = moduleNamePlusExt.substring(0, index);
|
|
}
|
|
return context.nameToUrl(normalize(moduleNamePlusExt, relMap && relMap.id, true), ext, true);
|
|
},
|
|
defined: function (id) {
|
|
return hasProp(defined, makeModuleMap(id, relMap, false, true).id);
|
|
},
|
|
specified: function (id) {
|
|
id = makeModuleMap(id, relMap, false, true).id;
|
|
return hasProp(defined, id) || hasProp(registry, id);
|
|
}
|
|
});
|
|
if (!relMap) {
|
|
localRequire.undef = function (id) {
|
|
takeGlobalQueue();
|
|
var map = makeModuleMap(id, relMap, true), mod = getOwn(registry, id);
|
|
removeScript(id);
|
|
delete defined[id];
|
|
delete urlFetched[map.url];
|
|
delete undefEvents[id];
|
|
eachReverse(defQueue, function (args, i) {
|
|
if (args[0] === id) {
|
|
defQueue.splice(i, 1);
|
|
}
|
|
});
|
|
if (mod) {
|
|
if (mod.events.defined) {
|
|
undefEvents[id] = mod.events;
|
|
}
|
|
cleanRegistry(id);
|
|
}
|
|
};
|
|
}
|
|
return localRequire;
|
|
},
|
|
enable: function (depMap) {
|
|
var mod = getOwn(registry, depMap.id);
|
|
if (mod) {
|
|
getModule(depMap).enable();
|
|
}
|
|
},
|
|
completeLoad: function (moduleName) {
|
|
var found, args, mod, shim = getOwn(config.shim, moduleName) || {}, shExports = shim.exports;
|
|
takeGlobalQueue();
|
|
while (defQueue.length) {
|
|
args = defQueue.shift();
|
|
if (args[0] === null) {
|
|
args[0] = moduleName;
|
|
if (found) {
|
|
break;
|
|
}
|
|
found = true;
|
|
} else if (args[0] === moduleName) {
|
|
found = true;
|
|
}
|
|
callGetModule(args);
|
|
}
|
|
mod = getOwn(registry, moduleName);
|
|
if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) {
|
|
if (config.enforceDefine && (!shExports || !getGlobal(shExports))) {
|
|
if (hasPathFallback(moduleName)) {
|
|
return;
|
|
} else {
|
|
return onError(makeError('nodefine', 'No define call for ' + moduleName, null, [moduleName]));
|
|
}
|
|
} else {
|
|
callGetModule([
|
|
moduleName,
|
|
shim.deps || [],
|
|
shim.exportsFn
|
|
]);
|
|
}
|
|
}
|
|
checkLoaded();
|
|
},
|
|
nameToUrl: function (moduleName, ext, skipExt) {
|
|
var paths, syms, i, parentModule, url, parentPath, bundleId, pkgMain = getOwn(config.pkgs, moduleName);
|
|
if (pkgMain) {
|
|
moduleName = pkgMain;
|
|
}
|
|
bundleId = getOwn(bundlesMap, moduleName);
|
|
if (bundleId) {
|
|
return context.nameToUrl(bundleId, ext, skipExt);
|
|
}
|
|
if (req.jsExtRegExp.test(moduleName)) {
|
|
url = moduleName + (ext || '');
|
|
} else {
|
|
paths = config.paths;
|
|
syms = moduleName.split('/');
|
|
for (i = syms.length; i > 0; i -= 1) {
|
|
parentModule = syms.slice(0, i).join('/');
|
|
parentPath = getOwn(paths, parentModule);
|
|
if (parentPath) {
|
|
if (isArray(parentPath)) {
|
|
parentPath = parentPath[0];
|
|
}
|
|
syms.splice(0, i, parentPath);
|
|
break;
|
|
}
|
|
}
|
|
url = syms.join('/');
|
|
url += ext || (/^data\:|\?/.test(url) || skipExt ? '' : '.js');
|
|
url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url;
|
|
}
|
|
return config.urlArgs ? url + ((url.indexOf('?') === -1 ? '?' : '&') + config.urlArgs) : url;
|
|
},
|
|
load: function (id, url) {
|
|
req.load(context, id, url);
|
|
},
|
|
execCb: function (name, callback, args, exports) {
|
|
return callback.apply(exports, args);
|
|
},
|
|
onScriptLoad: function (evt) {
|
|
if (evt.type === 'load' || readyRegExp.test((evt.currentTarget || evt.srcElement).readyState)) {
|
|
interactiveScript = null;
|
|
var data = getScriptData(evt);
|
|
context.completeLoad(data.id);
|
|
}
|
|
},
|
|
onScriptError: function (evt) {
|
|
var data = getScriptData(evt);
|
|
if (!hasPathFallback(data.id)) {
|
|
return onError(makeError('scripterror', 'Script error for: ' + data.id, evt, [data.id]));
|
|
}
|
|
}
|
|
};
|
|
context.require = context.makeRequire();
|
|
return context;
|
|
}
|
|
req = requirejs = function (deps, callback, errback, optional) {
|
|
var context, config, contextName = defContextName;
|
|
if (!isArray(deps) && typeof deps !== 'string') {
|
|
config = deps;
|
|
if (isArray(callback)) {
|
|
deps = callback;
|
|
callback = errback;
|
|
errback = optional;
|
|
} else {
|
|
deps = [];
|
|
}
|
|
}
|
|
if (config && config.context) {
|
|
contextName = config.context;
|
|
}
|
|
context = getOwn(contexts, contextName);
|
|
if (!context) {
|
|
context = contexts[contextName] = req.s.newContext(contextName);
|
|
}
|
|
if (config) {
|
|
context.configure(config);
|
|
}
|
|
return context.require(deps, callback, errback);
|
|
};
|
|
req.config = function (config) {
|
|
return req(config);
|
|
};
|
|
req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) {
|
|
setTimeout(fn, 4);
|
|
} : function (fn) {
|
|
fn();
|
|
};
|
|
if (!require) {
|
|
require = req;
|
|
}
|
|
req.version = version;
|
|
req.jsExtRegExp = /^\/|:|\?|\.js$/;
|
|
req.isBrowser = isBrowser;
|
|
s = req.s = {
|
|
contexts: contexts,
|
|
newContext: newContext
|
|
};
|
|
req({});
|
|
each([
|
|
'toUrl',
|
|
'undef',
|
|
'defined',
|
|
'specified'
|
|
], function (prop) {
|
|
req[prop] = function () {
|
|
var ctx = contexts[defContextName];
|
|
return ctx.require[prop].apply(ctx, arguments);
|
|
};
|
|
});
|
|
if (isBrowser) {
|
|
head = s.head = document.getElementsByTagName('head')[0];
|
|
baseElement = document.getElementsByTagName('base')[0];
|
|
if (baseElement) {
|
|
head = s.head = baseElement.parentNode;
|
|
}
|
|
}
|
|
req.onError = defaultOnError;
|
|
req.createNode = function (config, moduleName, url) {
|
|
var node = config.xhtml ? document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') : document.createElement('script');
|
|
node.type = config.scriptType || 'text/javascript';
|
|
node.charset = 'utf-8';
|
|
node.async = true;
|
|
return node;
|
|
};
|
|
req.load = function (context, moduleName, url) {
|
|
var config = context && context.config || {}, node;
|
|
if (isBrowser) {
|
|
node = req.createNode(config, moduleName, url);
|
|
node.setAttribute('data-requirecontext', context.contextName);
|
|
node.setAttribute('data-requiremodule', moduleName);
|
|
if (node.attachEvent && !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && !isOpera) {
|
|
useInteractive = true;
|
|
node.attachEvent('onreadystatechange', context.onScriptLoad);
|
|
} else {
|
|
node.addEventListener('load', context.onScriptLoad, false);
|
|
node.addEventListener('error', context.onScriptError, false);
|
|
}
|
|
node.src = url;
|
|
currentlyAddingScript = node;
|
|
if (baseElement) {
|
|
head.insertBefore(node, baseElement);
|
|
} else {
|
|
head.appendChild(node);
|
|
}
|
|
currentlyAddingScript = null;
|
|
return node;
|
|
} else if (isWebWorker) {
|
|
try {
|
|
importScripts(url);
|
|
context.completeLoad(moduleName);
|
|
} catch (e) {
|
|
context.onError(makeError('importscripts', 'importScripts failed for ' + moduleName + ' at ' + url, e, [moduleName]));
|
|
}
|
|
}
|
|
};
|
|
function getInteractiveScript() {
|
|
if (interactiveScript && interactiveScript.readyState === 'interactive') {
|
|
return interactiveScript;
|
|
}
|
|
eachReverse(scripts(), function (script) {
|
|
if (script.readyState === 'interactive') {
|
|
return interactiveScript = script;
|
|
}
|
|
});
|
|
return interactiveScript;
|
|
}
|
|
if (isBrowser && !cfg.skipDataMain) {
|
|
eachReverse(scripts(), function (script) {
|
|
if (!head) {
|
|
head = script.parentNode;
|
|
}
|
|
dataMain = script.getAttribute('data-main');
|
|
if (dataMain) {
|
|
mainScript = dataMain;
|
|
if (!cfg.baseUrl) {
|
|
src = mainScript.split('/');
|
|
mainScript = src.pop();
|
|
subPath = src.length ? src.join('/') + '/' : './';
|
|
cfg.baseUrl = subPath;
|
|
}
|
|
mainScript = mainScript.replace(jsSuffixRegExp, '');
|
|
if (req.jsExtRegExp.test(mainScript)) {
|
|
mainScript = dataMain;
|
|
}
|
|
cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript];
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
define = function (name, deps, callback) {
|
|
var node, context;
|
|
if (typeof name !== 'string') {
|
|
callback = deps;
|
|
deps = name;
|
|
name = null;
|
|
}
|
|
if (!isArray(deps)) {
|
|
callback = deps;
|
|
deps = null;
|
|
}
|
|
if (!deps && isFunction(callback)) {
|
|
deps = [];
|
|
if (callback.length) {
|
|
callback.toString().replace(commentRegExp, '').replace(cjsRequireRegExp, function (match, dep) {
|
|
deps.push(dep);
|
|
});
|
|
deps = (callback.length === 1 ? ['require'] : [
|
|
'require',
|
|
'exports',
|
|
'module'
|
|
]).concat(deps);
|
|
}
|
|
}
|
|
if (useInteractive) {
|
|
node = currentlyAddingScript || getInteractiveScript();
|
|
if (node) {
|
|
if (!name) {
|
|
name = node.getAttribute('data-requiremodule');
|
|
}
|
|
context = contexts[node.getAttribute('data-requirecontext')];
|
|
}
|
|
}
|
|
(context ? context.defQueue : globalDefQueue).push([
|
|
name,
|
|
deps,
|
|
callback
|
|
]);
|
|
};
|
|
define.amd = { jQuery: true };
|
|
req.exec = function (text) {
|
|
return eval(text);
|
|
};
|
|
req(cfg);
|
|
}(this));
|
|
define('../bower_components/requirejs/index', function () {
|
|
});
|
|
!function (e) {
|
|
if ('object' == typeof exports && 'undefined' != typeof module)
|
|
module.exports = e();
|
|
else if ('function' == typeof define && define.amd)
|
|
define('debug', [], 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) {
|
|
exports = module.exports = _dereq_('./debug');
|
|
exports.log = log;
|
|
exports.formatArgs = formatArgs;
|
|
exports.save = save;
|
|
exports.load = load;
|
|
exports.useColors = useColors;
|
|
exports.colors = [
|
|
'lightseagreen',
|
|
'forestgreen',
|
|
'goldenrod',
|
|
'dodgerblue',
|
|
'darkorchid',
|
|
'crimson'
|
|
];
|
|
function useColors() {
|
|
return 'WebkitAppearance' in document.documentElement.style || window.console && (console.firebug || console.exception && console.table);
|
|
}
|
|
exports.formatters.j = function (v) {
|
|
return JSON.stringify(v);
|
|
};
|
|
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));
|
|
var index = 0;
|
|
var lastC = 0;
|
|
args[0].replace(/%[a-z%]/g, function (match) {
|
|
if ('%%' === match)
|
|
return;
|
|
index++;
|
|
if ('%c' === match) {
|
|
lastC = index;
|
|
}
|
|
});
|
|
args.splice(lastC, 0, c);
|
|
return args;
|
|
}
|
|
function log() {
|
|
return 'object' == typeof console && 'function' == typeof console.log && Function.prototype.apply.call(console.log, console, arguments);
|
|
}
|
|
function save(namespaces) {
|
|
try {
|
|
if (null == namespaces) {
|
|
localStorage.removeItem('debug');
|
|
} else {
|
|
localStorage.debug = namespaces;
|
|
}
|
|
} catch (e) {
|
|
}
|
|
}
|
|
function load() {
|
|
var r;
|
|
try {
|
|
r = localStorage.debug;
|
|
} catch (e) {
|
|
}
|
|
return r;
|
|
}
|
|
exports.enable(load());
|
|
},
|
|
{ './debug': 2 }
|
|
],
|
|
2: [
|
|
function (_dereq_, module, exports) {
|
|
exports = module.exports = debug;
|
|
exports.coerce = coerce;
|
|
exports.disable = disable;
|
|
exports.enable = enable;
|
|
exports.enabled = enabled;
|
|
exports.humanize = _dereq_('ms');
|
|
exports.names = [];
|
|
exports.skips = [];
|
|
exports.formatters = {};
|
|
var prevColor = 0;
|
|
var prevTime;
|
|
function selectColor() {
|
|
return exports.colors[prevColor++ % exports.colors.length];
|
|
}
|
|
function debug(namespace) {
|
|
function disabled() {
|
|
}
|
|
disabled.enabled = false;
|
|
function enabled() {
|
|
var self = enabled;
|
|
var curr = +new Date();
|
|
var ms = curr - (prevTime || curr);
|
|
self.diff = ms;
|
|
self.prev = prevTime;
|
|
self.curr = curr;
|
|
prevTime = curr;
|
|
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]) {
|
|
args = ['%o'].concat(args);
|
|
}
|
|
var index = 0;
|
|
args[0] = args[0].replace(/%([a-z%])/g, function (match, format) {
|
|
if (match === '%%')
|
|
return match;
|
|
index++;
|
|
var formatter = exports.formatters[format];
|
|
if ('function' === typeof formatter) {
|
|
var val = args[index];
|
|
match = formatter.call(self, val);
|
|
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;
|
|
}
|
|
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;
|
|
namespaces = split[i].replace('*', '.*?');
|
|
if (namespaces[0] === '-') {
|
|
exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$'));
|
|
} else {
|
|
exports.names.push(new RegExp('^' + namespaces + '$'));
|
|
}
|
|
}
|
|
}
|
|
function disable() {
|
|
exports.enable('');
|
|
}
|
|
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;
|
|
}
|
|
function coerce(val) {
|
|
if (val instanceof Error)
|
|
return val.stack || val.message;
|
|
return val;
|
|
}
|
|
},
|
|
{ 'ms': 3 }
|
|
],
|
|
3: [
|
|
function (_dereq_, module, exports) {
|
|
var s = 1000;
|
|
var m = s * 60;
|
|
var h = m * 60;
|
|
var d = h * 24;
|
|
var y = d * 365.25;
|
|
module.exports = function (val, options) {
|
|
options = options || {};
|
|
if ('string' == typeof val)
|
|
return parse(val);
|
|
return options.long ? long(val) : short(val);
|
|
};
|
|
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;
|
|
}
|
|
}
|
|
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';
|
|
}
|
|
function long(ms) {
|
|
return plural(ms, d, 'day') || plural(ms, h, 'hour') || plural(ms, m, 'minute') || plural(ms, s, 'second') || ms + ' ms';
|
|
}
|
|
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);
|
|
});
|
|
define('lib/setting-alias', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'debug'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var debug = require('debug')('setting-alias');
|
|
var forwardMethods = [
|
|
'filterOptions',
|
|
'resetOptions',
|
|
'supported',
|
|
'selected',
|
|
'select',
|
|
'next',
|
|
'get',
|
|
'set'
|
|
];
|
|
module.exports = SettingAlias;
|
|
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');
|
|
}
|
|
SettingAlias.prototype.each = function (fn) {
|
|
for (var key in this.settings) {
|
|
fn(this.settings[key]);
|
|
}
|
|
};
|
|
SettingAlias.prototype.is = function (key) {
|
|
return this.get('key') === key;
|
|
};
|
|
SettingAlias.prototype.forward = function (method) {
|
|
this[method] = function () {
|
|
var setting = this.current();
|
|
return setting[method].apply(setting, arguments);
|
|
};
|
|
};
|
|
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;
|
|
};
|
|
SettingAlias.prototype.off = function (name, fn) {
|
|
var wrapped = fn._settingAliasCallback;
|
|
this.each(function (setting) {
|
|
setting.off(name, wrapped);
|
|
});
|
|
delete fn._settingAliasCallback;
|
|
};
|
|
});
|
|
;
|
|
(function () {
|
|
var proto = Events.prototype;
|
|
var slice = [].slice;
|
|
function Events(obj) {
|
|
if (!(this instanceof Events))
|
|
return new Events(obj);
|
|
if (obj)
|
|
return mixin(obj, proto);
|
|
}
|
|
proto.on = function (name, cb) {
|
|
this._cbs = this._cbs || {};
|
|
(this._cbs[name] || (this._cbs[name] = [])).push(cb);
|
|
return this;
|
|
};
|
|
proto.once = function (name, cb) {
|
|
this.on(name, one);
|
|
function one() {
|
|
cb.apply(this, arguments);
|
|
this.off(name, one);
|
|
}
|
|
};
|
|
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;
|
|
};
|
|
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);
|
|
};
|
|
};
|
|
function mixin(a, b) {
|
|
for (var key in b)
|
|
a[key] = b[key];
|
|
return a;
|
|
}
|
|
if (typeof exports === 'object') {
|
|
module.exports = Events;
|
|
} else if (typeof define === 'function' && define.amd) {
|
|
define('evt', [], function () {
|
|
return Events;
|
|
});
|
|
} else {
|
|
window.evt = Events;
|
|
}
|
|
}());
|
|
(function (define) {
|
|
define('model', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'evt'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var events = require('evt');
|
|
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({
|
|
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)));
|
|
define('lib/setting', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'debug',
|
|
'model'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var debug = require('debug')('setting');
|
|
var model = require('model');
|
|
module.exports = Setting;
|
|
model(Setting.prototype);
|
|
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 });
|
|
};
|
|
Setting.prototype.optionsToHash = function (options) {
|
|
var hash = {};
|
|
options.forEach(function (option, index) {
|
|
option.index = index;
|
|
hash[option.key] = option;
|
|
});
|
|
return hash;
|
|
};
|
|
Setting.prototype.selected = function (key) {
|
|
var hash = this.options.available;
|
|
var option = hash[this.get('selected')];
|
|
return key ? option && option[key] : option;
|
|
};
|
|
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 (!list.length) {
|
|
return;
|
|
}
|
|
if (!selected) {
|
|
return this.select(0, options);
|
|
}
|
|
this.set('selected', selected.key, options);
|
|
};
|
|
Setting.prototype.sortByIndex = function (list) {
|
|
return list.sort(function (a, b) {
|
|
return a.index - b.index;
|
|
});
|
|
};
|
|
Setting.prototype.updateSelected = function (options) {
|
|
this.select(this.get('selected') || this.fetched, options);
|
|
};
|
|
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);
|
|
};
|
|
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);
|
|
};
|
|
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 });
|
|
}
|
|
};
|
|
Setting.prototype.supported = function () {
|
|
return this.enabled() && !!this.get('options').length;
|
|
};
|
|
Setting.prototype.enabled = function () {
|
|
return !this.get('disabled');
|
|
};
|
|
});
|
|
define('lib/settings', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'./setting-alias',
|
|
'debug',
|
|
'./setting'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var SettingAlias = require('./setting-alias');
|
|
var debug = require('debug')('settings');
|
|
var Setting = require('./setting');
|
|
module.exports = Settings;
|
|
function Settings(items) {
|
|
this.ids = {};
|
|
this.items = [];
|
|
this.aliases = {};
|
|
this.SettingAlias = SettingAlias;
|
|
this.dontSave = this.dontSave.bind(this);
|
|
this.addEach(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]);
|
|
}
|
|
};
|
|
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);
|
|
};
|
|
Settings.prototype.fetch = function () {
|
|
this.items.forEach(function (setting) {
|
|
setting.fetch();
|
|
});
|
|
};
|
|
Settings.prototype.dontSave = function () {
|
|
this.items.forEach(function (setting) {
|
|
setting.off('change:selected', setting.save);
|
|
});
|
|
};
|
|
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) {
|
|
};
|
|
});
|
|
define('lib/geo-location', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'debug'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var debug = require('debug')('geolocation');
|
|
module.exports = GeoLocation;
|
|
function GeoLocation() {
|
|
this.watcher = null;
|
|
this.position = null;
|
|
this.setPosition = this.setPosition.bind(this);
|
|
this.watch = this.watch.bind(this);
|
|
}
|
|
GeoLocation.prototype.watch = function () {
|
|
if (!this.watcher) {
|
|
this.watcher = navigator.geolocation.watchPosition(this.setPosition);
|
|
debug('started watching');
|
|
}
|
|
};
|
|
GeoLocation.prototype.stopWatching = function () {
|
|
navigator.geolocation.clearWatch(this.watcher);
|
|
this.watcher = null;
|
|
debug('stopped watching');
|
|
};
|
|
GeoLocation.prototype.setPosition = function (position) {
|
|
this.position = {
|
|
timestamp: position.timestamp,
|
|
altitude: position.coords.altitude,
|
|
latitude: position.coords.latitude,
|
|
longitude: position.coords.longitude
|
|
};
|
|
};
|
|
});
|
|
define('config/config', [
|
|
'require',
|
|
'exports',
|
|
'module'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
module.exports = {
|
|
globals: {
|
|
CONFIG_MAX_IMAGE_PIXEL_SIZE: 24 * 1024 * 1024,
|
|
CONFIG_MAX_SNAPSHOT_PIXEL_SIZE: 24 * 1024 * 1024,
|
|
CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH: 0,
|
|
CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT: 0
|
|
},
|
|
zoom: {
|
|
disabled: false,
|
|
useZoomPreviewAdjustment: false
|
|
},
|
|
focus: {
|
|
continuousAutoFocus: true,
|
|
touchFocus: true,
|
|
faceDetection: true
|
|
},
|
|
previewGallery: {
|
|
thumbnailWidth: 54,
|
|
thumbnailHeight: 54
|
|
},
|
|
viewfinder: {
|
|
scaleType: 'fill',
|
|
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: {
|
|
maxPixelSizeScaleFactor: 2.5,
|
|
maxPickPixelSize: 0,
|
|
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: 'hdr' },
|
|
{ key: 'countdown' },
|
|
{ key: 'pictureSizes' },
|
|
{ key: 'recorderProfiles' },
|
|
{ key: 'grid' }
|
|
]
|
|
}
|
|
};
|
|
});
|
|
define('lib/camera-utils', ['require'], function (require) {
|
|
'use strict';
|
|
var CameraUtils = {};
|
|
CameraUtils.scaleSizeToFitViewport = function (viewportSize, imageSize) {
|
|
var sw = viewportSize.width / imageSize.width, sh = viewportSize.height / imageSize.height, scale;
|
|
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;
|
|
scale = Math.max(sw, sh);
|
|
return {
|
|
width: imageSize.width * scale,
|
|
height: imageSize.height * scale
|
|
};
|
|
};
|
|
CameraUtils.getMaximumPreviewSize = function (previewSizes, aspectRatio) {
|
|
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;
|
|
});
|
|
(function (define) {
|
|
define('device-orientation', [
|
|
'require',
|
|
'exports',
|
|
'module'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
const ORIENTATION_CHANGE_INTERVAL = 300;
|
|
const MAX_MOTION_FILTER_TIME = 1000;
|
|
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;
|
|
if (now > lastMotionData.t + MAX_MOTION_FILTER_TIME) {
|
|
lastMotionData.x = 0;
|
|
lastMotionData.y = 0;
|
|
lastMotionData.z = 0;
|
|
filterReset = true;
|
|
}
|
|
if (lastMotionData.x || lastMotionData.y || lastMotionData.z) {
|
|
var diff = now - lastMotionFilteredTime;
|
|
var alpha = diff / (MOTION_FILTER_TIME_CONSTANT + diff);
|
|
x = alpha * (x - lastMotionData.x) + lastMotionData.x;
|
|
y = alpha * (y - lastMotionData.y) + lastMotionData.y;
|
|
z = alpha * (z - lastMotionData.z) + lastMotionData.z;
|
|
}
|
|
lastMotionData.x = x;
|
|
lastMotionData.y = y;
|
|
lastMotionData.z = z;
|
|
lastMotionData.t = now;
|
|
return filterReset;
|
|
}
|
|
function calcOrientation(x, y) {
|
|
var orientationAngle = Math.atan2(-x, y) * 180 / Math.PI;
|
|
if (orientationAngle < 0) {
|
|
orientationAngle += 360;
|
|
}
|
|
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);
|
|
if (filterReset) {
|
|
return;
|
|
}
|
|
var x = lastMotionData.x;
|
|
var y = lastMotionData.y;
|
|
var z = lastMotionData.z;
|
|
if (x * x + y * y + z * z > 110) {
|
|
return;
|
|
}
|
|
if (z > 9.2 || z < -9.2) {
|
|
return;
|
|
}
|
|
var orientation = calcOrientation(x, y);
|
|
if (orientation === pendingOrientation) {
|
|
return;
|
|
}
|
|
if (orientationChangeTimer) {
|
|
window.clearTimeout(orientationChangeTimer);
|
|
}
|
|
if (pendingOrientation === null) {
|
|
pendingOrientation = orientation;
|
|
fireOrientationChangeEvent(pendingOrientation);
|
|
} else {
|
|
pendingOrientation = orientation;
|
|
orientationChangeTimer = window.setTimeout(function doOrient() {
|
|
fireOrientationChangeEvent(pendingOrientation);
|
|
orientationChangeTimer = 0;
|
|
}, ORIENTATION_CHANGE_INTERVAL);
|
|
}
|
|
}
|
|
function start() {
|
|
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)));
|
|
define('lib/orientation', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'device-orientation'
|
|
], 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;
|
|
}
|
|
function unlock() {
|
|
screen.mozUnlockOrientation();
|
|
listener.stop();
|
|
}
|
|
function lock() {
|
|
screen.mozLockOrientation('default');
|
|
listener.start();
|
|
}
|
|
function rationalize(sensorAngle, angle) {
|
|
if (typeof angle === 'undefined') {
|
|
angle = current;
|
|
}
|
|
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;
|
|
}
|
|
module.exports = {
|
|
on: listener.on,
|
|
off: listener.off,
|
|
start: listener.start,
|
|
stop: listener.stop,
|
|
unlock: unlock,
|
|
lock: lock,
|
|
rationalize: rationalize,
|
|
get: function () {
|
|
return current;
|
|
}
|
|
};
|
|
});
|
|
define('lib/bind-all', [
|
|
'require',
|
|
'exports',
|
|
'module'
|
|
], 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);
|
|
}
|
|
}
|
|
};
|
|
});
|
|
define('lib/camera/focus', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'lib/bind-all'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var bindAll = require('lib/bind-all');
|
|
module.exports = Focus;
|
|
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);
|
|
if (this.continuousAutoFocus) {
|
|
if (mode === 'picture') {
|
|
focusMode = 'continuous-picture';
|
|
} else if (mode === 'video') {
|
|
focusMode = 'continuous-video';
|
|
}
|
|
}
|
|
focusMode = focusMode || 'auto';
|
|
if (focusModes.indexOf(focusMode) === -1) {
|
|
focusMode = focusModes[0];
|
|
}
|
|
this.suspendedMode = focusMode;
|
|
mozCamera.focusMode = focusMode;
|
|
this.reboot();
|
|
};
|
|
Focus.prototype.getMode = function () {
|
|
var mozCamera = this.mozCamera;
|
|
return this.suspendedMode || mozCamera.focusMode;
|
|
};
|
|
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;
|
|
this.touchFocus = touchFocusUserEnabled && touchFocusSupported;
|
|
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 () {
|
|
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;
|
|
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) {
|
|
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) {
|
|
};
|
|
Focus.prototype.onFacesDetected = function (faces) {
|
|
};
|
|
Focus.prototype.focusOnLargestFace = function (faces) {
|
|
if (this.faceDetectionSuspended) {
|
|
this.onFacesDetected([]);
|
|
return;
|
|
}
|
|
this.detectedFaces = faces;
|
|
this.onFacesDetected(this.detectedFaces);
|
|
};
|
|
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;
|
|
}
|
|
this.updateFocusState('focusing');
|
|
this.mozCamera.autoFocus().then(onSuccess, onError);
|
|
function onError(err) {
|
|
self.focused = false;
|
|
if (err.name === 'NS_ERROR_IN_PROGRESS') {
|
|
done('interrupted');
|
|
} else {
|
|
done('error');
|
|
}
|
|
}
|
|
function onSuccess(success) {
|
|
if (success) {
|
|
self.focused = true;
|
|
done('focused');
|
|
} else {
|
|
self.focused = false;
|
|
done('failed');
|
|
}
|
|
}
|
|
};
|
|
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();
|
|
};
|
|
Focus.prototype.isTouchFocusSupported = function () {
|
|
var maxFocusAreas = this.mozCamera.capabilities.maxFocusAreas;
|
|
return maxFocusAreas > 0;
|
|
};
|
|
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]);
|
|
this.focus(done);
|
|
};
|
|
});
|
|
define('lib/debounce', [
|
|
'require',
|
|
'exports',
|
|
'module'
|
|
], 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);
|
|
}
|
|
};
|
|
};
|
|
});
|
|
define('lib/mixin', [
|
|
'require',
|
|
'exports',
|
|
'module'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
module.exports = function (a, b) {
|
|
for (var key in b) {
|
|
a[key] = b[key];
|
|
}
|
|
return a;
|
|
};
|
|
});
|
|
define('lib/camera/camera', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'lib/camera-utils',
|
|
'lib/orientation',
|
|
'lib/camera/focus',
|
|
'debug',
|
|
'lib/debounce',
|
|
'lib/bind-all',
|
|
'lib/mixin',
|
|
'model'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var CameraUtils = require('lib/camera-utils');
|
|
var orientation = require('lib/orientation');
|
|
var Focus = require('lib/camera/focus');
|
|
var debug = require('debug')('camera');
|
|
var debounce = require('lib/debounce');
|
|
var bindAll = require('lib/bind-all');
|
|
var mix = require('lib/mixin');
|
|
var model = require('model');
|
|
model(Camera.prototype);
|
|
module.exports = Camera;
|
|
function Camera(options) {
|
|
debug('initializing');
|
|
bindAll(this);
|
|
options = options || {};
|
|
this.cacheConfig = options.cacheConfig !== false;
|
|
this.minRecordingTime = options.minRecordingTime || 1500;
|
|
this.recordSpacePadding = options.recordSpacePadding || 1024 * 1024 * 1;
|
|
this.recordSpaceMin = options.recordSpaceMin || 1024 * 1024 * 2;
|
|
this.requestAttempts = options.requestAttempts || 3;
|
|
this.orientation = options.orientation || orientation;
|
|
this.configStorage = options.configStorage || localStorage;
|
|
this.cameraList = navigator.mozCameras.getListOfCameras();
|
|
this.mozCamera = null;
|
|
this.storage = options.storage || {};
|
|
this.video = {
|
|
filepath: null,
|
|
minSpace: this.recordSpaceMin,
|
|
spacePadding: this.recordSpacePadding,
|
|
poster: { filepath: null }
|
|
};
|
|
this.focus = new Focus(options.focus);
|
|
this.suspendedFlashCount = 0;
|
|
this.mode = 'picture';
|
|
this.selectedCamera = 'back';
|
|
this.configure = debounce(this.configure);
|
|
debug('initialized');
|
|
}
|
|
Camera.prototype.load = function () {
|
|
debug('load camera');
|
|
var loadingNewCamera = this.selectedCamera !== this.lastLoadedCamera;
|
|
var self = this;
|
|
if (this.releasing) {
|
|
debug('wait for camera release');
|
|
this.once('released', function () {
|
|
self.load();
|
|
});
|
|
return;
|
|
}
|
|
if (this.mozCamera && !loadingNewCamera) {
|
|
debug('camera not changed');
|
|
this.ready();
|
|
return;
|
|
}
|
|
if (this.mozCamera) {
|
|
this.release(ready);
|
|
} else {
|
|
ready();
|
|
}
|
|
function ready() {
|
|
self.requestCamera(self.selectedCamera);
|
|
self.lastLoadedCamera = self.selectedCamera;
|
|
}
|
|
};
|
|
Camera.prototype.requestCamera = function (camera, config) {
|
|
if (!config) {
|
|
config = { mode: this.mode };
|
|
}
|
|
debug('request camera', camera, config);
|
|
if (this.isBusy) {
|
|
return;
|
|
}
|
|
var attempts = this.requestAttempts;
|
|
var self = this;
|
|
this.configured = false;
|
|
this.busy('requestingCamera');
|
|
request();
|
|
function request() {
|
|
navigator.mozCameras.getCamera(camera, config).then(onSuccess, onError);
|
|
self.emit('requesting');
|
|
debug('camera requested', camera, config);
|
|
attempts--;
|
|
}
|
|
function onSuccess(result) {
|
|
debug('successfully got mozCamera', result);
|
|
if (document.hidden) {
|
|
self.mozCamera = result.camera;
|
|
self.release();
|
|
return;
|
|
}
|
|
self.updateConfig(result.configuration);
|
|
self.setupNewCamera(result.camera);
|
|
self.configureFocus();
|
|
self.emit('focusconfigured', {
|
|
mode: self.mozCamera.focusMode,
|
|
touchFocus: self.focus.touchFocus,
|
|
faceDetection: self.focus.faceDetection,
|
|
maxDetectedFaces: self.focus.maxDetectedFaces
|
|
});
|
|
if (self.configured) {
|
|
self.emit('configured');
|
|
self.onPreviewStateChange({ newState: 'started' });
|
|
}
|
|
self.ready();
|
|
}
|
|
function onError(err) {
|
|
debug('error requesting camera', err);
|
|
if (err.name === 'NS_ERROR_NOT_INITIALIZED' && attempts) {
|
|
self.cameraRequestTimeout = setTimeout(request, 1000);
|
|
return;
|
|
}
|
|
self.emit('error', 'request-fail');
|
|
self.ready();
|
|
}
|
|
};
|
|
Camera.prototype.updateConfig = function (config) {
|
|
this.givenPreviewSize = config.previewSize;
|
|
this.pictureSize = config.pictureSize;
|
|
this.recorderProfile = config.recorderProfile;
|
|
this.configured = true;
|
|
};
|
|
Camera.prototype.setupNewCamera = function (mozCamera) {
|
|
debug('configuring camera');
|
|
var capabilities = mozCamera.capabilities;
|
|
this.mozCamera = mozCamera;
|
|
this.mozCamera.addEventListener('shutter', this.onShutter);
|
|
this.mozCamera.addEventListener('close', this.onClosed);
|
|
this.mozCamera.addEventListener('previewstatechange', this.onPreviewStateChange);
|
|
this.mozCamera.addEventListener('recorderstatechange', this.onRecorderStateChange);
|
|
this.mozCamera.addEventListener('poster', this.onPoster);
|
|
this.capabilities = this.formatCapabilities(capabilities);
|
|
this.emit('newcamera', {
|
|
capabilities: this.capabilities,
|
|
pictureSize: this.pictureSize,
|
|
recorderProfile: this.recorderProfile
|
|
});
|
|
debug('configured new camera');
|
|
};
|
|
Camera.prototype.formatCapabilities = function (capabilities) {
|
|
var hasHDR = capabilities.sceneModes.indexOf('hdr') > -1;
|
|
var hdr = hasHDR ? [
|
|
'on',
|
|
'off'
|
|
] : undefined;
|
|
return mix({ hdr: hdr }, capabilities);
|
|
};
|
|
Camera.prototype.configure = function () {
|
|
debug('configuring hardware...');
|
|
var self = this;
|
|
this.configured = false;
|
|
if (this.isBusy) {
|
|
debug('defering configuration');
|
|
this.once('ready', this.configure);
|
|
return;
|
|
}
|
|
if (!this.mozCamera) {
|
|
debug('no mozCamera');
|
|
return;
|
|
}
|
|
this.stopRecording();
|
|
this.busy();
|
|
var mozCameraConfig = {
|
|
mode: this.mode,
|
|
pictureSize: this.pictureSize,
|
|
recorderProfile: this.recorderProfile
|
|
};
|
|
this.mozCamera.setConfiguration(mozCameraConfig).then(onSuccess, onError);
|
|
debug('mozCamera configuring', mozCameraConfig);
|
|
function onSuccess(config) {
|
|
debug('configuration success');
|
|
if (!self.mozCamera) {
|
|
return;
|
|
}
|
|
self.updateConfig(config);
|
|
self.configureFocus();
|
|
self.emit('configured');
|
|
self.ready();
|
|
}
|
|
function onError(err) {
|
|
debug('Error configuring camera');
|
|
self.configured = true;
|
|
self.ready();
|
|
}
|
|
};
|
|
Camera.prototype.configureFocus = function () {
|
|
this.focus.configure(this.mozCamera, this.mode);
|
|
this.focus.onFacesDetected = this.onFacesDetected;
|
|
this.focus.onAutoFocusChanged = this.onAutoFocusChanged;
|
|
};
|
|
Camera.prototype.shutdown = function () {
|
|
this.stopRecording();
|
|
this.set('previewActive', false);
|
|
this.set('focus', 'none');
|
|
this.release();
|
|
};
|
|
Camera.prototype.onAutoFocusChanged = function (state) {
|
|
this.set('focus', state);
|
|
};
|
|
Camera.prototype.onFacesDetected = function (faces) {
|
|
this.emit('facesdetected', faces);
|
|
};
|
|
Camera.prototype.loadStreamInto = function (videoElement) {
|
|
debug('loading stream into element');
|
|
if (!this.mozCamera) {
|
|
debug('error - `mozCamera` is undefined or null');
|
|
return;
|
|
}
|
|
if (!videoElement) {
|
|
debug('error - `videoElement` is undefined or null');
|
|
return;
|
|
}
|
|
var isCurrent = videoElement.mozSrcObject === this.mozCamera;
|
|
if (isCurrent) {
|
|
return debug('camera didn\'t change');
|
|
}
|
|
videoElement.mozSrcObject = this.mozCamera;
|
|
videoElement.play();
|
|
debug('stream loaded into video');
|
|
};
|
|
Camera.prototype.previewSizes = function () {
|
|
if (!this.mozCamera) {
|
|
return;
|
|
}
|
|
return this.mozCamera.capabilities.previewSizes;
|
|
};
|
|
Camera.prototype.previewSize = function () {
|
|
return this.givenPreviewSize;
|
|
};
|
|
Camera.prototype.resolution = function () {
|
|
switch (this.mode) {
|
|
case 'picture':
|
|
return this.pictureSize;
|
|
case 'video':
|
|
return this.getRecorderProfile().video;
|
|
}
|
|
};
|
|
Camera.prototype.setPictureSize = function (size, options) {
|
|
debug('set picture size', size);
|
|
if (!size) {
|
|
return;
|
|
}
|
|
var configure = !(options && options.configure === false);
|
|
if (this.isPictureSize(size)) {
|
|
debug('pictureSize didn\'t change');
|
|
return;
|
|
}
|
|
this.pictureSize = size;
|
|
if (configure) {
|
|
this.configure();
|
|
} else {
|
|
this.mozCamera.setPictureSize(size);
|
|
}
|
|
this.setThumbnailSize();
|
|
debug('pictureSize changed');
|
|
return this;
|
|
};
|
|
Camera.prototype.isPictureSize = function (size) {
|
|
if (!this.pictureSize) {
|
|
return false;
|
|
}
|
|
var sameWidth = size.width === this.pictureSize.width;
|
|
var sameHeight = size.height === this.pictureSize.height;
|
|
return sameWidth && sameHeight;
|
|
};
|
|
Camera.prototype.setRecorderProfile = function (key, options) {
|
|
debug('set recorderProfile: %s', key);
|
|
if (!key) {
|
|
return;
|
|
}
|
|
var configure = !(options && options.configure === false);
|
|
if (this.isRecorderProfile(key)) {
|
|
debug('recorderProfile didn\'t change');
|
|
return;
|
|
}
|
|
this.recorderProfile = key;
|
|
if (configure) {
|
|
this.configure();
|
|
}
|
|
debug('recorderProfile changed: %s', key);
|
|
return this;
|
|
};
|
|
Camera.prototype.isRecorderProfile = function (key) {
|
|
return key === this.recorderProfile;
|
|
};
|
|
Camera.prototype.getRecorderProfile = function () {
|
|
var key = this.recorderProfile;
|
|
return this.mozCamera.capabilities.recorderProfiles[key];
|
|
};
|
|
Camera.prototype.setThumbnailSize = function () {
|
|
var sizes = this.mozCamera.capabilities.thumbnailSizes;
|
|
var pictureSize = this.mozCamera.getPictureSize();
|
|
var picked = this.pickThumbnailSize(sizes, pictureSize);
|
|
if (picked) {
|
|
this.mozCamera.setThumbnailSize(picked);
|
|
}
|
|
};
|
|
Camera.prototype.setFlashMode = function (key) {
|
|
if (this.mozCamera) {
|
|
key = key || 'off';
|
|
if (this.suspendedFlashCount > 0) {
|
|
this.suspendedFlashMode = key;
|
|
debug('flash mode set while suspended: %s', key);
|
|
} else {
|
|
this.mozCamera.flashMode = key;
|
|
debug('flash mode set: %s', key);
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
Camera.prototype.release = function (done) {
|
|
debug('release');
|
|
done = done || function () {
|
|
};
|
|
var self = this;
|
|
clearTimeout(this.cameraRequestTimeout);
|
|
clearTimeout(this.zoomChangeTimeout);
|
|
this.zoomChangeTimeout = null;
|
|
if (!this.mozCamera) {
|
|
done();
|
|
return;
|
|
}
|
|
this.busy();
|
|
this.stopRecording();
|
|
this.set('focus', 'none');
|
|
this.mozCamera.release().then(onSuccess, onError);
|
|
this.releasing = true;
|
|
this.mozCamera = null;
|
|
delete this.pictureSize;
|
|
delete this.recorderProfile;
|
|
delete this.givenPreviewSize;
|
|
function onSuccess() {
|
|
debug('successfully released');
|
|
self.ready();
|
|
self.releasing = false;
|
|
self.emit('released');
|
|
done();
|
|
}
|
|
function onError(err) {
|
|
debug('failed to release hardware');
|
|
self.ready();
|
|
self.releasing = false;
|
|
done(err);
|
|
}
|
|
};
|
|
Camera.prototype.pickThumbnailSize = function (thumbnailSizes, pictureSize) {
|
|
var screenWidth = window.innerWidth * window.devicePixelRatio;
|
|
var screenHeight = window.innerHeight * window.devicePixelRatio;
|
|
var pictureAspectRatio = pictureSize.width / pictureSize.height;
|
|
var currentThumbnailSize;
|
|
var i;
|
|
thumbnailSizes = thumbnailSizes.slice(0);
|
|
if (!thumbnailSizes || !pictureSize) {
|
|
return;
|
|
}
|
|
function imageSizeFillsScreen(pixelsWidth, pixelsHeight) {
|
|
return (pixelsWidth >= screenWidth || pixelsHeight >= screenHeight) && (pixelsWidth >= screenHeight || pixelsHeight >= screenWidth);
|
|
}
|
|
thumbnailSizes = thumbnailSizes.filter(function (thumbnailSize) {
|
|
var thumbnailAspectRatio = thumbnailSize.width / thumbnailSize.height;
|
|
return Math.abs(thumbnailAspectRatio - pictureAspectRatio) < 0.05;
|
|
});
|
|
if (thumbnailSizes.length === 0) {
|
|
console.error('Error while selecting thumbnail size. ' + 'There are no thumbnail sizes that match the ratio of ' + 'the selected picture size: ' + JSON.stringify(pictureSize));
|
|
return;
|
|
}
|
|
thumbnailSizes.sort(function (a, b) {
|
|
return a.width * a.height - b.width * b.height;
|
|
});
|
|
for (i = 0; i < thumbnailSizes.length; ++i) {
|
|
currentThumbnailSize = thumbnailSizes[i];
|
|
if (imageSizeFillsScreen(currentThumbnailSize.width, currentThumbnailSize.height)) {
|
|
return currentThumbnailSize;
|
|
}
|
|
}
|
|
return thumbnailSizes[thumbnailSizes.length - 1];
|
|
};
|
|
Camera.prototype.capture = function (options) {
|
|
if (!this.mozCamera) {
|
|
return false;
|
|
}
|
|
switch (this.mode) {
|
|
case 'picture':
|
|
this.takePicture(options);
|
|
break;
|
|
case 'video':
|
|
this.toggleRecording(options);
|
|
break;
|
|
}
|
|
};
|
|
Camera.prototype.takePicture = function (options) {
|
|
debug('take picture', options);
|
|
this.busy();
|
|
var rotation = this.orientation.get();
|
|
var selectedCamera = this.selectedCamera;
|
|
var self = this;
|
|
var position = options && options.position;
|
|
var config = {
|
|
dateTime: Date.now() / 1000,
|
|
pictureSize: self.pictureSize,
|
|
fileFormat: 'jpeg'
|
|
};
|
|
if (position) {
|
|
config.position = position;
|
|
}
|
|
config.rotation = selectedCamera === 'front' ? -rotation : rotation;
|
|
if (this.focus.getMode() === 'auto') {
|
|
this.focus.focus(onFocused);
|
|
} else {
|
|
takePicture();
|
|
}
|
|
function onFocused(state) {
|
|
takePicture();
|
|
}
|
|
function takePicture() {
|
|
self.busy('takingPicture');
|
|
self.mozCamera.takePicture(config).then(onSuccess, onError);
|
|
}
|
|
function onError(error) {
|
|
if (error.name === 'NS_ERROR_IN_PROGRESS') {
|
|
complete();
|
|
} else {
|
|
document.l10n.formatValue('error-saving').then(value => {
|
|
alert(value);
|
|
complete();
|
|
});
|
|
}
|
|
}
|
|
function onSuccess(blob) {
|
|
var image = { blob: blob };
|
|
self.resumePreview();
|
|
self.set('focus', 'none');
|
|
self.emit('newimage', image);
|
|
debug('success taking picture');
|
|
complete();
|
|
}
|
|
function complete() {
|
|
self.set('focus', 'none');
|
|
self.ready();
|
|
}
|
|
};
|
|
Camera.prototype.updateFocusArea = function (rect, done) {
|
|
this.focus.updateFocusArea(rect, focusDone);
|
|
function focusDone(state) {
|
|
if (done) {
|
|
done(state);
|
|
}
|
|
}
|
|
};
|
|
Camera.prototype.toggleRecording = function (options) {
|
|
var state = this.get('recording');
|
|
if (state && state !== 'stopped') {
|
|
this.stopRecording();
|
|
} else {
|
|
this.startRecording(options);
|
|
}
|
|
};
|
|
Camera.prototype.setStorage = function (storage) {
|
|
this.storage.video = storage.video;
|
|
};
|
|
Camera.prototype.startRecording = function (options) {
|
|
debug('start recording');
|
|
var frontCamera = this.selectedCamera === 'front';
|
|
var rotation = this.orientation.get();
|
|
var storage = this.storage.video;
|
|
var video = this.video;
|
|
var self = this;
|
|
if (frontCamera) {
|
|
rotation = -rotation;
|
|
}
|
|
this.set('recording', 'starting');
|
|
this.busy();
|
|
this.orientation.stop();
|
|
var profile = this.resolution();
|
|
video.width = profile.width;
|
|
video.height = profile.height;
|
|
video.rotation = this.orientation.rationalize(this.getSensorAngle(), rotation);
|
|
var previewSize = this.previewSize();
|
|
video.poster.width = previewSize.width;
|
|
video.poster.height = previewSize.height;
|
|
video.poster.rotation = video.rotation;
|
|
this.getFreeVideoStorageSpace(gotStorageSpace);
|
|
function gotStorageSpace(err, freeBytes) {
|
|
if (self.get('recording') === 'stopping') {
|
|
debug('start recording interrupted (getFreeVideoStorageSpace)');
|
|
return self.stoppedRecording();
|
|
}
|
|
if (err) {
|
|
return self.onStartRecordingError();
|
|
}
|
|
var notEnoughSpace = freeBytes < self.video.minSpace;
|
|
var remaining = freeBytes - self.video.spacePadding;
|
|
var targetFileSize = self.get('maxFileSizeBytes');
|
|
var maxFileSizeBytes = targetFileSize || remaining;
|
|
if (notEnoughSpace) {
|
|
self.onStartRecordingError('nospace2');
|
|
return;
|
|
}
|
|
var config = {
|
|
rotation: rotation,
|
|
maxFileSizeBytes: maxFileSizeBytes,
|
|
createPoster: true
|
|
};
|
|
self.createVideoFilepath(createVideoFilepathDone);
|
|
function createVideoFilepathDone(errorMsg, filepath) {
|
|
if (self.get('recording') === 'stopping') {
|
|
debug('start recording interrupted (createVideoFilepath)');
|
|
return self.stoppedRecording();
|
|
}
|
|
if (typeof filepath === 'undefined') {
|
|
debug(errorMsg);
|
|
return self.onStartRecordingError('error-video-file-path');
|
|
}
|
|
video.filepath = filepath;
|
|
self.emit('willrecord');
|
|
self.mozCamera.startRecording(config, storage, filepath).then(onSuccess, onError);
|
|
}
|
|
}
|
|
function onError(err) {
|
|
storage.delete(video.filepath);
|
|
if (err.name === 'NS_ERROR_IN_PROGRESS') {
|
|
debug('start recording error (in progress)');
|
|
return;
|
|
}
|
|
if (err.name === 'NS_ERROR_ABORT') {
|
|
debug('start recording error (abort)');
|
|
return self.stoppedRecording();
|
|
}
|
|
self.onStartRecordingError();
|
|
}
|
|
function onSuccess() {
|
|
self.ready();
|
|
if (document.hidden) {
|
|
self.stopRecording();
|
|
}
|
|
}
|
|
};
|
|
Camera.prototype.startedRecording = function () {
|
|
debug('started recording');
|
|
this.startVideoTimer();
|
|
this.set('recording', 'started');
|
|
};
|
|
Camera.prototype.stopRecording = function () {
|
|
debug('stop recording');
|
|
var state = this.get('recording');
|
|
if (!state || state === 'stopping' || state === 'stopped' || state === 'error') {
|
|
debug('not recording or stop pending');
|
|
return;
|
|
}
|
|
this.set('recording', 'stopping');
|
|
this.busy();
|
|
this.mozCamera.stopRecording();
|
|
};
|
|
Camera.prototype.stoppedRecording = function (recorded) {
|
|
debug('stopped recording');
|
|
this.stopVideoTimer();
|
|
if (!recorded) {
|
|
this.set('recording', 'error');
|
|
}
|
|
this.set('recording', 'stopped');
|
|
this.orientation.start();
|
|
var self = this;
|
|
var video;
|
|
if (recorded) {
|
|
video = mix({}, this.video);
|
|
video.poster = mix({}, video.poster);
|
|
this.storage.video.get(video.filepath).then(function (blob) {
|
|
video.blob = blob;
|
|
self.emit('newvideo', video);
|
|
self.ready();
|
|
}, function () {
|
|
if (self) {
|
|
self.onStopRecordingError(video);
|
|
self = null;
|
|
}
|
|
});
|
|
}
|
|
};
|
|
Camera.prototype.onRecordingError = function (id) {
|
|
if (id) {
|
|
document.l10n.formatValue(id).then(value => {
|
|
alert(value);
|
|
});
|
|
}
|
|
this.ready();
|
|
};
|
|
Camera.prototype.onStartRecordingError = function (id) {
|
|
debug('start record error');
|
|
this.stoppedRecording();
|
|
this.onRecordingError(id);
|
|
};
|
|
Camera.prototype.onStopRecordingError = function (video) {
|
|
debug('stop record error');
|
|
this.storage.video.delete(video.filepath);
|
|
var elapsedTime = this.get('videoElapsed');
|
|
if (elapsedTime < this.minRecordingTime) {
|
|
this.ready();
|
|
} else {
|
|
this.onRecordingError();
|
|
}
|
|
};
|
|
Camera.prototype.onShutter = function () {
|
|
this.emit('shutter');
|
|
};
|
|
Camera.prototype.onClosed = function (e) {
|
|
this.shutdown();
|
|
this.emit('closed', e.reason);
|
|
};
|
|
Camera.prototype.onPreviewStateChange = function (e) {
|
|
var state = e.newState;
|
|
debug('preview state change: %s', state);
|
|
if (state === this.previewState) {
|
|
return;
|
|
}
|
|
this.previewState = state;
|
|
this.emit('preview:' + state);
|
|
};
|
|
Camera.prototype.onRecorderStateChange = function (e) {
|
|
var msg = e.newState;
|
|
debug('recorder state change: %s', msg);
|
|
if (msg === 'FileSizeLimitReached') {
|
|
this.emit('filesizelimitreached');
|
|
} else if (msg === 'Started') {
|
|
this.startedRecording();
|
|
} else if (msg === 'Stopped') {
|
|
if (this.stopRecordError) {
|
|
this.onStopRecordingError(this.video);
|
|
}
|
|
this.stoppedRecording(!this.stopRecordError);
|
|
this.stopRecordError = false;
|
|
} else if (msg === 'PosterFailed' || msg === 'TrackFailed' || msg === 'MediaRecorderFailed' || msg === 'MediaServerFailed') {
|
|
this.stopRecordError = true;
|
|
this.stopRecording();
|
|
}
|
|
};
|
|
Camera.prototype.onPoster = function (e) {
|
|
debug('poster created');
|
|
this.video.poster.blob = e.data;
|
|
};
|
|
Camera.prototype.getFreeVideoStorageSpace = function (done) {
|
|
debug('get free storage space');
|
|
var storage = this.storage.video;
|
|
var req = storage.freeSpace();
|
|
req.onerror = onError;
|
|
req.onsuccess = onSuccess;
|
|
function onSuccess() {
|
|
var freeBytes = req.result;
|
|
debug('%d free space found', freeBytes);
|
|
done(null, freeBytes);
|
|
}
|
|
function onError() {
|
|
done('error');
|
|
}
|
|
};
|
|
Camera.prototype.createVideoFilepath = function (done) {
|
|
done(null, Date.now() + '_tmp.3gp');
|
|
};
|
|
Camera.prototype.resumePreview = function () {
|
|
this.mozCamera.resumePreview();
|
|
this.focus.startFaceDetection();
|
|
this.emit('previewresumed');
|
|
};
|
|
Camera.prototype.setCamera = function (camera) {
|
|
debug('set camera: %s', camera);
|
|
if (this.selectedCamera === camera) {
|
|
return;
|
|
}
|
|
this.selectedCamera = camera;
|
|
this.load();
|
|
};
|
|
Camera.prototype.setMode = function (mode) {
|
|
debug('setting mode to: %s', mode);
|
|
if (this.isMode(mode)) {
|
|
return;
|
|
}
|
|
this.mode = mode;
|
|
this.configure();
|
|
return this;
|
|
};
|
|
Camera.prototype.isMode = function (mode) {
|
|
return this.mode === mode;
|
|
};
|
|
Camera.prototype.startVideoTimer = function () {
|
|
this.set('videoStart', new Date().getTime());
|
|
this.videoTimer = setInterval(this.updateVideoElapsed, 1000);
|
|
this.updateVideoElapsed();
|
|
};
|
|
Camera.prototype.stopVideoTimer = function () {
|
|
clearInterval(this.videoTimer);
|
|
this.videoTimer = null;
|
|
this.updateVideoElapsed();
|
|
};
|
|
Camera.prototype.updateVideoElapsed = function () {
|
|
var now = new Date().getTime();
|
|
var start = this.get('videoStart');
|
|
this.set('videoElapsed', now - start);
|
|
};
|
|
Camera.prototype.setISOMode = function (value) {
|
|
var isoModes = this.mozCamera.capabilities.isoModes;
|
|
if (isoModes && isoModes.indexOf(value) > -1) {
|
|
this.mozCamera.isoMode = value;
|
|
}
|
|
};
|
|
Camera.prototype.setWhiteBalance = function (value) {
|
|
var capabilities = this.mozCamera.capabilities;
|
|
var modes = capabilities.whiteBalanceModes;
|
|
if (modes && modes.indexOf(value) > -1) {
|
|
this.mozCamera.whiteBalanceMode = value;
|
|
}
|
|
};
|
|
Camera.prototype.setHDR = function (value) {
|
|
debug('set hdr: %s', value);
|
|
if (!value) {
|
|
return;
|
|
}
|
|
var scene = value === 'on' ? 'hdr' : 'auto';
|
|
this.setSceneMode(scene);
|
|
};
|
|
Camera.prototype.setSceneMode = function (value) {
|
|
var modes = this.mozCamera.capabilities.sceneModes;
|
|
if (modes.indexOf(value) > -1) {
|
|
this.mozCamera.sceneMode = value;
|
|
}
|
|
};
|
|
Camera.prototype.isZoomSupported = function () {
|
|
if (!this.mozCamera) {
|
|
return false;
|
|
}
|
|
return this.mozCamera.capabilities.zoomRatios.length > 1;
|
|
};
|
|
Camera.prototype.configureZoom = function () {
|
|
var previewSize = this.previewSize();
|
|
var maxPreviewSize = CameraUtils.getMaximumPreviewSize(this.previewSizes());
|
|
var maxHardwareZoom = maxPreviewSize.width / previewSize.width;
|
|
this.set('maxHardwareZoom', maxHardwareZoom);
|
|
this.setZoom(this.getMinimumZoom());
|
|
this.emit('zoomconfigured', this.getZoom());
|
|
return this;
|
|
};
|
|
Camera.prototype.getMinimumZoom = function () {
|
|
var zoomRatios = this.mozCamera.capabilities.zoomRatios;
|
|
if (zoomRatios.length === 0) {
|
|
return 1;
|
|
}
|
|
return zoomRatios[0];
|
|
};
|
|
Camera.prototype.getMaximumZoom = function () {
|
|
var zoomRatios = this.mozCamera.capabilities.zoomRatios;
|
|
if (zoomRatios.length === 0) {
|
|
return 1;
|
|
}
|
|
return zoomRatios[zoomRatios.length - 1];
|
|
};
|
|
Camera.prototype.getZoom = function () {
|
|
return this.mozCamera.zoom;
|
|
};
|
|
Camera.prototype.setZoom = function (zoom) {
|
|
this.zoom = zoom;
|
|
this.emit('zoomchanged', this.zoom);
|
|
if (this.zoomChangeTimeout) {
|
|
return;
|
|
}
|
|
var self = this;
|
|
this.zoomChangeTimeout = window.setTimeout(function () {
|
|
self.zoomChangeTimeout = null;
|
|
self.mozCamera.zoom = self.zoom;
|
|
}, 150);
|
|
};
|
|
Camera.prototype.getZoomPreviewAdjustment = function () {
|
|
var zoom = this.mozCamera.zoom;
|
|
var maxHardwareZoom = this.get('maxHardwareZoom');
|
|
if (zoom <= maxHardwareZoom) {
|
|
return 1;
|
|
}
|
|
return zoom / maxHardwareZoom;
|
|
};
|
|
Camera.prototype.getSensorAngle = function () {
|
|
return this.mozCamera && this.mozCamera.sensorAngle;
|
|
};
|
|
Camera.prototype.busy = function (type) {
|
|
debug('busy %s', type || '');
|
|
this.isBusy = true;
|
|
this.emit('busy', type);
|
|
clearTimeout(this.readyTimeout);
|
|
};
|
|
Camera.prototype.ready = function () {
|
|
var self = this;
|
|
this.isBusy = false;
|
|
clearTimeout(this.readyTimeout);
|
|
this.readyTimeout = setTimeout(function () {
|
|
debug('ready');
|
|
self.emit('ready');
|
|
}, 150);
|
|
};
|
|
});
|
|
(function (exports) {
|
|
'use strict';
|
|
const stopRecordingKey = 'private.broadcast.stop_recording';
|
|
const attentionScreenKey = 'private.broadcast.attention_screen_opening';
|
|
function start() {
|
|
if (!document.hidden) {
|
|
listen();
|
|
}
|
|
window.addEventListener('visibilitychange', visibilityChangeHandler);
|
|
}
|
|
function stop() {
|
|
window.removeEventListener('visibilitychange', visibilityChangeHandler);
|
|
unlisten();
|
|
}
|
|
function visibilityChangeHandler() {
|
|
if (document.hidden) {
|
|
unlisten();
|
|
} else {
|
|
listen();
|
|
}
|
|
}
|
|
function listen() {
|
|
navigator.mozSettings.addObserver(stopRecordingKey, stopRecordingObserver);
|
|
navigator.mozSettings.addObserver(attentionScreenKey, stopRecordingObserver);
|
|
}
|
|
function unlisten() {
|
|
navigator.mozSettings.removeObserver(stopRecordingKey, stopRecordingObserver);
|
|
navigator.mozSettings.removeObserver(attentionScreenKey, stopRecordingObserver);
|
|
}
|
|
function stopRecordingObserver(event) {
|
|
if (event.settingValue) {
|
|
window.dispatchEvent(new CustomEvent('stoprecording'));
|
|
}
|
|
}
|
|
exports.StopRecordingEvent = {
|
|
start: start,
|
|
stop: stop
|
|
};
|
|
}(window));
|
|
define('stop-recording-event', function (global) {
|
|
return function () {
|
|
var ret, fn;
|
|
return ret || global.StopRecordingEvent;
|
|
};
|
|
}(this));
|
|
(function (define) {
|
|
define('view', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'evt'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var events = require('evt');
|
|
var counter = 1;
|
|
var noop = function () {
|
|
};
|
|
module.exports = View;
|
|
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);
|
|
}
|
|
events(View.prototype);
|
|
View.prototype.fire = View.prototype.fire || View.prototype.emit;
|
|
View.prototype.tag = 'div';
|
|
View.prototype.name = 'noname';
|
|
View.prototype.appendTo = function (parent) {
|
|
if (!parent)
|
|
return this;
|
|
parent.appendChild(this.el);
|
|
this.fire('inserted');
|
|
return this;
|
|
};
|
|
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;
|
|
};
|
|
View.prototype.find = function (query) {
|
|
return this.el.querySelector(query);
|
|
};
|
|
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);
|
|
};
|
|
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);
|
|
};
|
|
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]';
|
|
};
|
|
View.prototype.initialize = noop;
|
|
View.prototype.template = function () {
|
|
return '';
|
|
};
|
|
View.extend = function (props) {
|
|
var Parent = this;
|
|
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)));
|
|
define('views/notification', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'lib/mixin',
|
|
'view'
|
|
], function (require, exports, module) {
|
|
var mix = require('lib/mixin');
|
|
var View = require('view');
|
|
module.exports = View.extend({
|
|
name: 'notification',
|
|
tag: 'ul',
|
|
time: 3000,
|
|
initialize: function () {
|
|
this.counter = 0;
|
|
this.hash = {};
|
|
this.el.setAttribute('role', 'presentation');
|
|
},
|
|
display: function (options) {
|
|
var item = mix({}, options);
|
|
var id = ++this.counter;
|
|
var self = this;
|
|
item.el = document.createElement('li');
|
|
item.el.className = options.className || '';
|
|
var span = document.createElement('span');
|
|
if (typeof options.text === 'string') {
|
|
span.setAttribute('data-l10n-id', options.text);
|
|
} else {
|
|
if (options.text.html) {
|
|
span.innerHTML = options.text.html;
|
|
}
|
|
}
|
|
item.el.appendChild(span);
|
|
if (options.attrs) {
|
|
this.setAttributes(item.el, options.attrs);
|
|
}
|
|
item.el.setAttribute('role', 'status');
|
|
item.el.setAttribute('aria-live', 'assertive');
|
|
this.el.appendChild(item.el);
|
|
this.clear(this.temporary);
|
|
if (!item.persistent) {
|
|
this.temporary = id;
|
|
this.hide(this.persistent);
|
|
item.clearTimeout = setTimeout(function () {
|
|
self.clear(id);
|
|
}, this.time);
|
|
}
|
|
if (item.persistent) {
|
|
this.clear(this.persistent);
|
|
this.persistent = id;
|
|
}
|
|
this.hash[id] = item;
|
|
return id;
|
|
},
|
|
setAttributes: function (el, attrs) {
|
|
for (var key in attrs) {
|
|
el.setAttribute(key, attrs[key]);
|
|
}
|
|
},
|
|
clear: function (id) {
|
|
var item = this.hash[id];
|
|
if (!item || item.cleared) {
|
|
return;
|
|
}
|
|
this.el.removeChild(item.el);
|
|
clearTimeout(item.clearTimeout);
|
|
item.cleared = true;
|
|
if (item === this.temporary) {
|
|
this.temporary = null;
|
|
}
|
|
if (item === this.persistent) {
|
|
this.persistent = null;
|
|
}
|
|
delete this.hash[id];
|
|
this.show(this.persistent);
|
|
},
|
|
hide: function (id) {
|
|
var item = this.hash[id];
|
|
if (!item) {
|
|
return;
|
|
}
|
|
item.el.classList.add('hidden');
|
|
},
|
|
show: function (id) {
|
|
var item = this.hash[id];
|
|
if (!item) {
|
|
return;
|
|
}
|
|
item.el.classList.remove('hidden');
|
|
}
|
|
});
|
|
});
|
|
define('views/loading-screen', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'debug',
|
|
'view'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var debug = require('debug')('view:loading-screen');
|
|
var View = require('view');
|
|
module.exports = View.extend({
|
|
name: 'loading-screen',
|
|
fadeTime: 300,
|
|
initialize: function () {
|
|
this.render();
|
|
},
|
|
render: function () {
|
|
this.el.innerHTML = this.template;
|
|
delete this.template;
|
|
debug('rendered');
|
|
return this;
|
|
},
|
|
show: function (done) {
|
|
this.reflow = this.el.offsetTop;
|
|
View.prototype.show.call(this);
|
|
},
|
|
hide: function (done) {
|
|
View.prototype.hide.call(this);
|
|
if (done) {
|
|
setTimeout(done, this.fadeTime);
|
|
}
|
|
},
|
|
template: '<progress></progress>'
|
|
});
|
|
});
|
|
define('lib/bind', [
|
|
'require',
|
|
'exports',
|
|
'module'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
exports = module.exports = bind;
|
|
function bind(el, name, fn, capture) {
|
|
el.addEventListener(name, fn, capture || false);
|
|
}
|
|
exports.unbind = function (el, name, fn, capture) {
|
|
el.removeEventListener(name, fn, capture || false);
|
|
};
|
|
});
|
|
define('lib/pinch', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'lib/bind',
|
|
'lib/bind-all',
|
|
'lib/bind',
|
|
'evt'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var unbind = require('lib/bind').unbind;
|
|
var bindAll = require('lib/bind-all');
|
|
var bind = require('lib/bind');
|
|
var events = require('evt');
|
|
events(Pinch.prototype);
|
|
module.exports = Pinch;
|
|
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;
|
|
}
|
|
});
|
|
define('app', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'stop-recording-event',
|
|
'views/notification',
|
|
'views/loading-screen',
|
|
'lib/orientation',
|
|
'lib/bind-all',
|
|
'debug',
|
|
'lib/pinch',
|
|
'lib/bind',
|
|
'model'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
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');
|
|
module.exports = App;
|
|
model(App.prototype);
|
|
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);
|
|
this.require = options.require || window.requirejs;
|
|
this.LoadingView = options.LoadingView || LoadingView;
|
|
this.orientation = options.orientation || orientation;
|
|
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');
|
|
}
|
|
App.prototype.boot = function () {
|
|
debug('boot');
|
|
if (this.booted) {
|
|
return;
|
|
}
|
|
this.showSpinner('requestingCamera');
|
|
this.bindEvents();
|
|
this.initializeViews();
|
|
this.runControllers();
|
|
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));
|
|
};
|
|
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');
|
|
};
|
|
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];
|
|
};
|
|
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();
|
|
});
|
|
});
|
|
};
|
|
App.prototype.initializeViews = function () {
|
|
debug('initializing views');
|
|
this.views.notification = new NotificationView();
|
|
debug('views initialized');
|
|
};
|
|
App.prototype.injectViews = function () {
|
|
debug('injecting views');
|
|
this.views.notification.appendTo(this.el);
|
|
debug('views injected');
|
|
};
|
|
App.prototype.bindEvents = function () {
|
|
debug('binding events');
|
|
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);
|
|
if (this.settings.countdown.selected('key') !== 'off') {
|
|
this.onCountdown();
|
|
} else {
|
|
this.settings.countdown.once('change:selected', this.onCountdown);
|
|
}
|
|
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);
|
|
bind(this.doc, 'visibilitychange', this.onVisibilityChange);
|
|
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');
|
|
};
|
|
App.prototype.onVisible = function () {
|
|
this.geolocationWatch();
|
|
this.orientation.start();
|
|
this.orientation.lock();
|
|
debug('visible');
|
|
};
|
|
App.prototype.onHidden = function () {
|
|
this.geolocation.stopWatching();
|
|
this.orientation.stop();
|
|
debug('hidden');
|
|
};
|
|
App.prototype.onReboot = function () {
|
|
debug('reboot');
|
|
window.location.reload();
|
|
};
|
|
App.prototype.onClick = function () {
|
|
debug('click');
|
|
this.emit('click');
|
|
};
|
|
App.prototype.onCriticalPathDone = function () {
|
|
if (this.criticalPathDone) {
|
|
return;
|
|
}
|
|
debug('critical path done');
|
|
window.performance.mark('visuallyLoaded');
|
|
this.listenForStopRecordingEvent();
|
|
this.loadLazyModules();
|
|
this.perf.criticalPath = Date.now();
|
|
this.criticalPathDone = true;
|
|
this.emit('criticalpathdone');
|
|
};
|
|
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); }
|
|
});
|
|
Promise.all([
|
|
load,
|
|
storage
|
|
]).then(function () {
|
|
debug('app fully loaded');
|
|
window.performance.mark('contentInteractive');
|
|
self.loaded = true;
|
|
self.emit('loaded');
|
|
self.perf.loaded = Date.now();
|
|
self.logPerf();
|
|
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');
|
|
};
|
|
App.prototype.geolocationWatch = function () {
|
|
var shouldWatch = !this.activity.pick && !this.hidden;
|
|
if (shouldWatch) {
|
|
this.geolocation.watch();
|
|
}
|
|
};
|
|
App.prototype.onVisibilityChange = function () {
|
|
this.hidden = this.doc.hidden;
|
|
this.emit(this.hidden ? 'hidden' : 'visible');
|
|
};
|
|
App.prototype.onBeforeUnload = function () {
|
|
this.emit('beforeunload');
|
|
debug('beforeunload');
|
|
};
|
|
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);
|
|
};
|
|
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;
|
|
};
|
|
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);
|
|
}
|
|
};
|
|
App.prototype.listenForStopRecordingEvent = function () {
|
|
debug('listen for stop recording events');
|
|
stopRecordingEvent.start();
|
|
addEventListener('stoprecording', this.firer('stoprecording'));
|
|
};
|
|
App.prototype.onKeyDown = function (e) {
|
|
var key = e.key.toLowerCase();
|
|
var type = this.settings.keyDownEvents.get(key);
|
|
if (type) {
|
|
this.emit('keydown:' + type, e);
|
|
}
|
|
};
|
|
});
|
|
define('controllers/overlay', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'debug',
|
|
'lib/bind-all'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var debug = require('debug')('controller:overlay');
|
|
var bindAll = require('lib/bind-all');
|
|
module.exports = function (app) {
|
|
return new OverlayController(app);
|
|
};
|
|
module.exports.OverlayController = OverlayController;
|
|
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);
|
|
};
|
|
OverlayController.prototype.onStorageChanged = function (state) {
|
|
this.updateOverlay('storage', state !== 'available', state);
|
|
};
|
|
OverlayController.prototype.onBatteryChanged = function (state) {
|
|
this.updateOverlay('battery', state === 'shutdown', state);
|
|
};
|
|
OverlayController.prototype.onCameraRequesting = function () {
|
|
this.updateOverlay('cameraError', false);
|
|
};
|
|
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 || reason === '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 () {
|
|
if (reason === 'request-fail' && !self.activity.pick) {
|
|
self.updateOverlay(type, false);
|
|
return;
|
|
}
|
|
self.app.emit('activitycanceled');
|
|
});
|
|
debug('inserted \'%s/%s\' overlay', type, reason);
|
|
});
|
|
};
|
|
});
|
|
define('controllers/battery', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'debug',
|
|
'lib/bind-all',
|
|
'lib/bind'
|
|
], function (require, exports, module) {
|
|
var debug = require('debug')('controller:battery');
|
|
var bindAll = require('lib/bind-all');
|
|
var bind = require('lib/bind');
|
|
module.exports = function (app) {
|
|
return new BatteryController(app);
|
|
};
|
|
module.exports.BatteryController = BatteryController;
|
|
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');
|
|
}
|
|
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);
|
|
};
|
|
BatteryController.prototype.onPowerSaveChange = function (values) {
|
|
var value;
|
|
if (values.settingValue !== undefined) {
|
|
value = values.settingValue;
|
|
} else {
|
|
value = values['powersave.enabled'];
|
|
}
|
|
this.powerSaveEnabled = value;
|
|
this.updatePowerSave();
|
|
};
|
|
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);
|
|
};
|
|
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
|
|
}
|
|
};
|
|
BatteryController.prototype.updateStatus = function () {
|
|
var previous = this.app.get('batteryStatus');
|
|
var current = this.getStatus(this.battery);
|
|
if (current !== previous) {
|
|
this.app.set('batteryStatus', current);
|
|
}
|
|
};
|
|
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);
|
|
};
|
|
});
|
|
define('views/hud', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'debug',
|
|
'lib/bind',
|
|
'view'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var debug = require('debug')('view:hud');
|
|
var bind = require('lib/bind');
|
|
var View = require('view');
|
|
module.exports = View.extend({
|
|
name: 'hud',
|
|
initialize: function () {
|
|
this.render();
|
|
},
|
|
render: function () {
|
|
this.el.innerHTML = this.template();
|
|
this.els.flash = this.find('.js-flash');
|
|
this.els.camera = this.find('.js-camera');
|
|
this.els.settings = this.find('.js-settings');
|
|
delete this.template;
|
|
debug('rendered');
|
|
return this.bindEvents();
|
|
},
|
|
bindEvents: function () {
|
|
bind(this.els.flash, 'click', this.onFlashClick);
|
|
bind(this.els.camera, 'click', this.onCameraClick);
|
|
bind(this.els.settings, 'click', this.onSettingsClick, true);
|
|
return this;
|
|
},
|
|
_setLabel: function (element, mode) {
|
|
if (mode) {
|
|
this.els[element].setAttribute('data-l10n-id', mode.title + '-button');
|
|
} else {
|
|
this.els[element].removeAttribute('data-l10n-id');
|
|
this.els[element].removeAttribute('aria-label');
|
|
}
|
|
},
|
|
setFlashModeLabel: function (mode) {
|
|
this._setLabel('flash', mode);
|
|
},
|
|
setFlashMode: function (mode) {
|
|
if (!mode) {
|
|
return;
|
|
}
|
|
this.els.flash.dataset.icon = mode.icon;
|
|
this.setFlashModeLabel(mode);
|
|
},
|
|
setCameraLabel: function (camera) {
|
|
this._setLabel('camera', camera);
|
|
},
|
|
setCamera: function (camera) {
|
|
if (!camera) {
|
|
return;
|
|
}
|
|
this.els.camera.dataset.icon = camera.icon;
|
|
this.setCameraLabel(camera);
|
|
},
|
|
setMenuLabel: function () {
|
|
this._setLabel('settings', { title: 'menu' });
|
|
},
|
|
onFlashClick: function (event) {
|
|
event.stopPropagation();
|
|
this.emit('click:flash');
|
|
},
|
|
onCameraClick: function (event) {
|
|
event.stopPropagation();
|
|
this.emit('click:camera');
|
|
},
|
|
onSettingsClick: function (event) {
|
|
event.stopPropagation();
|
|
this.emit('click:settings');
|
|
},
|
|
template: function () {
|
|
return '<div role="button" class="hud_btn hud_camera rotates ' + 'test-camera-toggle js-camera"></div>' + '<div role="button" class="hud_btn hud_flash rotates test-flash-button ' + 'js-flash"></div>' + '<div role="button" class="hud_btn hud_settings rotates ' + 'test-settings-toggle js-settings" data-icon="menu" ' + 'data-l10n-id="menu-button"></div>';
|
|
}
|
|
});
|
|
});
|
|
define('controllers/hud', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'debug',
|
|
'lib/debounce',
|
|
'lib/bind-all',
|
|
'views/hud'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var debug = require('debug')('controller:hud');
|
|
var debounce = require('lib/debounce');
|
|
var bindAll = require('lib/bind-all');
|
|
var HudView = require('views/hud');
|
|
module.exports = function (app) {
|
|
return new HudController(app);
|
|
};
|
|
module.exports.HudController = HudController;
|
|
function HudController(app) {
|
|
bindAll(this);
|
|
this.app = app;
|
|
this.settings = app.settings;
|
|
this.notification = app.views.notification;
|
|
this.createView();
|
|
this.bindEvents();
|
|
debug('initialized');
|
|
}
|
|
HudController.prototype.createView = function () {
|
|
var hasDualCamera = this.settings.cameras.get('options').length > 1;
|
|
this.view = this.app.views.hud || new HudView();
|
|
this.view.enable('camera', hasDualCamera);
|
|
this.view.disable('flash');
|
|
this.view.hide();
|
|
this.updateCamera();
|
|
this.view.appendTo(this.app.el);
|
|
};
|
|
HudController.prototype.bindEvents = function () {
|
|
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'));
|
|
this.app.on('localized', this.localize);
|
|
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);
|
|
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);
|
|
this.app.on('countdown:started', this.view.setter('countdown', 'active'));
|
|
this.app.on('countdown:ended', this.view.setter('countdown', 'inactive'));
|
|
this.app.on('settings:opened', this.view.hide);
|
|
this.app.on('settings:closed', this.view.show);
|
|
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);
|
|
};
|
|
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);
|
|
};
|
|
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;
|
|
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 } });
|
|
};
|
|
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);
|
|
};
|
|
});
|
|
(function (define) {
|
|
'use strict';
|
|
define('drag', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'evt'
|
|
], function (require, exports, module) {
|
|
var events = require('evt');
|
|
module.exports = Drag;
|
|
events(Drag.prototype);
|
|
var pointer = [
|
|
{
|
|
down: 'touchstart',
|
|
up: 'touchend',
|
|
move: 'touchmove'
|
|
},
|
|
{
|
|
down: 'mousedown',
|
|
up: 'mouseup',
|
|
move: 'mousemove'
|
|
}
|
|
]['ontouchstart' in window ? 0 : 1];
|
|
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);
|
|
this.handle.el.style.transform = translate;
|
|
this.handle.position = position;
|
|
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))
|
|
};
|
|
};
|
|
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)));
|
|
define('views/controls', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'debug',
|
|
'lib/debounce',
|
|
'lib/bind',
|
|
'view',
|
|
'drag'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var debug = require('debug')('view:controls');
|
|
var debounce = require('lib/debounce');
|
|
var bind = require('lib/bind');
|
|
var View = require('view');
|
|
var Drag = require('drag');
|
|
module.exports = View.extend({
|
|
name: 'controls',
|
|
className: 'test-controls',
|
|
initialize: function (options) {
|
|
this.drag = options && options.drag;
|
|
this.once('inserted', this.setupSwitch);
|
|
this.render();
|
|
},
|
|
switchPositions: {
|
|
left: 'picture',
|
|
right: 'video',
|
|
picture: 'left',
|
|
video: 'right'
|
|
},
|
|
elsL10n: {
|
|
cancel: 'controls-button-close',
|
|
thumbnail: 'preview-button',
|
|
capture: 'capture-button'
|
|
},
|
|
render: function () {
|
|
this.el.innerHTML = this.template();
|
|
this.els.switchHandle = this.find('.js-switch-handle');
|
|
this.els.thumbnail = this.find('.js-thumbnail');
|
|
this.els.capture = this.find('.js-capture');
|
|
this.els.cancel = this.find('.js-cancel');
|
|
this.els.switch = this.find('.js-switch');
|
|
this.els.icons = {
|
|
camera: this.find('.js-icon-camera'),
|
|
video: this.find('.js-icon-video')
|
|
};
|
|
delete this.template;
|
|
debug('rendered');
|
|
return this.bindEvents();
|
|
},
|
|
bindEvents: function () {
|
|
this.onButtonClick = debounce(this.onButtonClick, 300, true);
|
|
bind(this.els.thumbnail, 'click', this.onButtonClick);
|
|
bind(this.els.capture, 'click', this.onButtonClick);
|
|
bind(this.els.cancel, 'click', this.onButtonClick);
|
|
return this;
|
|
},
|
|
setupSwitch: function () {
|
|
debug('setup dragger');
|
|
if (document.readyState !== 'complete') {
|
|
window.addEventListener('load', this.setupSwitch);
|
|
debug('deferred switch setup till after load');
|
|
return;
|
|
}
|
|
this.drag = this.drag || new Drag({
|
|
handle: this.els.switchHandle,
|
|
container: this.els.switch
|
|
});
|
|
this.drag.on('tapped', debounce(this.onSwitchTapped, 300, true));
|
|
this.drag.on('ended', this.drag.snapToClosestEdge);
|
|
this.drag.on('translate', this.onSwitchTranslate);
|
|
this.drag.on('snapped', this.onSwitchSnapped);
|
|
this.drag.updateDimensions();
|
|
this.updateSwitchPosition();
|
|
window.removeEventListener('load', this.setupSwitch);
|
|
},
|
|
setCaptureLabel: function (recording) {
|
|
this.els.capture.setAttribute('data-l10n-id', recording ? 'stop-capture-button' : 'capture-button');
|
|
},
|
|
onSwitchSnapped: function (edges) {
|
|
var mode = this.switchPositions[edges.x];
|
|
var changed = mode !== this.get('mode');
|
|
if (changed) {
|
|
this.onSwitchChanged();
|
|
}
|
|
},
|
|
onSwitchChanged: function () {
|
|
this.emit('modechanged');
|
|
},
|
|
onSwitchTapped: function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
debug('switch tapped');
|
|
this.onSwitchChanged();
|
|
},
|
|
onSwitchTranslate: function (e) {
|
|
this.setSwitchIcon(e.position.ratio.x);
|
|
},
|
|
setSwitchIcon: function (ratio) {
|
|
var skew = 2;
|
|
var ratioSkewed = ratio * skew;
|
|
var camera = Math.max(0, 1 - ratioSkewed);
|
|
var video = Math.max(0, -1 + ratioSkewed);
|
|
this.els.icons.camera.style.opacity = camera;
|
|
this.els.icons.video.style.opacity = video;
|
|
debug('set switch icon camera: %s, video: %s', camera, video);
|
|
},
|
|
setScreenReaderVisible: function (visible) {
|
|
this.el.setAttribute('aria-hidden', !visible);
|
|
},
|
|
onButtonClick: function (e) {
|
|
e.stopPropagation();
|
|
debug('button click');
|
|
var name = e.currentTarget.getAttribute('name');
|
|
this.emit('click:' + name, e);
|
|
},
|
|
suspendModeSwitch: function (suspended) {
|
|
if (suspended) {
|
|
this.set('switch-toggle-disabled');
|
|
} else {
|
|
this.unset('switch-toggle-disabled');
|
|
}
|
|
},
|
|
setMode: function (mode) {
|
|
debug('set mode: %s', mode);
|
|
this.set('mode', mode);
|
|
this.switchPosition = this.switchPositions[mode];
|
|
var ratio = {
|
|
left: 0,
|
|
right: 1
|
|
}[this.switchPosition];
|
|
this.updateSwitchPosition();
|
|
this.setSwitchIcon(ratio);
|
|
this.els.switch.setAttribute('data-l10n-id', mode + '-mode-button');
|
|
debug('mode set pos: %s', this.switchPosition);
|
|
},
|
|
updateSwitchPosition: function () {
|
|
debug('updateSwitchPosition');
|
|
if (!this.drag) {
|
|
return;
|
|
}
|
|
this.drag.set({ x: this.switchPosition });
|
|
debug('updated switch position: %s', this.switchPosition);
|
|
},
|
|
setThumbnail: function (blob) {
|
|
if (!this.els.image) {
|
|
this.els.image = new Image();
|
|
this.els.image.classList.add('test-thumbnail');
|
|
this.els.thumbnail.appendChild(this.els.image);
|
|
this.set('thumbnail', true);
|
|
} else {
|
|
window.URL.revokeObjectURL(this.els.image.src);
|
|
}
|
|
this.els.image.src = window.URL.createObjectURL(blob);
|
|
debug('thumbnail set');
|
|
},
|
|
removeThumbnail: function () {
|
|
if (this.els.image) {
|
|
this.els.thumbnail.removeChild(this.els.image);
|
|
window.URL.revokeObjectURL(this.els.image.src);
|
|
this.els.image = null;
|
|
}
|
|
this.set('thumbnail', false);
|
|
},
|
|
set: function (key, value) {
|
|
if (typeof key !== 'string') {
|
|
return;
|
|
}
|
|
if (arguments.length === 1) {
|
|
value = true;
|
|
}
|
|
if (!value) {
|
|
return this.unset(key);
|
|
}
|
|
var attr = 'data-' + key;
|
|
var oldValue = this.el.getAttribute(attr);
|
|
var oldClass = oldValue && classFrom(key, oldValue);
|
|
var newClass = classFrom(key, value);
|
|
if (oldClass) {
|
|
this.el.classList.remove(oldClass);
|
|
}
|
|
if (newClass) {
|
|
this.el.classList.add(newClass);
|
|
}
|
|
this.el.setAttribute(attr, value);
|
|
debug('remove: %s, add: %s', oldClass, newClass);
|
|
debug('attr key: %s, value: %s', attr, value);
|
|
},
|
|
get: function (key) {
|
|
var attr = 'data-' + key;
|
|
return this.el.getAttribute(attr);
|
|
},
|
|
unset: function (key) {
|
|
var attr = 'data-' + key;
|
|
var value = this.el.getAttribute(attr);
|
|
this.el.classList.remove(classFrom(key, value));
|
|
this.el.removeAttribute(attr);
|
|
},
|
|
enable: function (key) {
|
|
this.set(key ? key + '-enabled' : 'enabled');
|
|
this.unset(key ? key + '-disabled' : 'disabled');
|
|
},
|
|
disable: function (key) {
|
|
this.set(key ? key + '-disabled' : 'disabled');
|
|
this.unset(key ? key + '-enabled' : 'enabled');
|
|
},
|
|
localize: function () {
|
|
var mode = this.get('mode') || 'picture';
|
|
this.els.switch.setAttribute('data-l10n-id', mode + '-mode-button');
|
|
},
|
|
template: function () {
|
|
return '<div class="controls-left">' + '<div class="controls-button controls-thumbnail-button test-thumbnail js-thumbnail rotates" ' + 'name="thumbnail" role="button" data-l10n-id="preview-button"></div>' + '<div class="controls-button controls-cancel-pick-button test-cancel-pick rotates js-cancel" ' + 'name="cancel" data-icon="close" role="button" data-l10n-id="controls-button-close"></div>' + '</div>' + '<div class="controls-middle">' + '<div class="capture-button test-capture rotates js-capture" name="capture" ' + 'data-l10n-id="capture-button" role="button">' + '<div class="circle outer-circle"></div>' + '<div class="circle inner-circle"></div>' + '<div class="center" data-icon="camera" aria-hidden="true"></div>' + '</div>' + '</div>' + '<div class="controls-right">' + '<div class="mode-switch test-switch" name="switch">' + '<div class="inner js-switch" role="button">' + '<div class="mode-switch_bg-icon rotates" data-icon="camera" aria-hidden="true"></div>' + '<div class="mode-switch_bg-icon rotates" data-icon="video" aria-hidden="true"></div>' + '<div class="mode-switch_handle js-switch-handle" aria-hidden="true">' + '<div class="mode-switch_current-icon camera rotates js-icon-camera" data-icon="camera" aria-hidden="true"></div>' + '<div class="mode-switch_current-icon video rotates js-icon-video" data-icon="video" aria-hidden="true"></div>' + '</div>' + '</div>' + '</div>' + '</div>';
|
|
}
|
|
});
|
|
function classFrom(key, value) {
|
|
value = detectBooleans(value);
|
|
if (typeof value === 'boolean') {
|
|
return value ? key : '';
|
|
} else if (value) {
|
|
return key + '-' + value;
|
|
} else {
|
|
return key;
|
|
}
|
|
}
|
|
function detectBooleans(value) {
|
|
if (typeof value === 'boolean') {
|
|
return value;
|
|
} else if (value === 'true') {
|
|
return true;
|
|
} else if (value === 'false') {
|
|
return false;
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
});
|
|
define('controllers/controls', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'debug',
|
|
'views/controls',
|
|
'lib/bind-all'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var debug = require('debug')('controller:controls');
|
|
var ControlsView = require('views/controls');
|
|
var bindAll = require('lib/bind-all');
|
|
module.exports = function (app) {
|
|
return new ControlsController(app);
|
|
};
|
|
module.exports.ControlsController = ControlsController;
|
|
function ControlsController(app) {
|
|
bindAll(this);
|
|
this.app = app;
|
|
this.activity = app.activity;
|
|
this.createView();
|
|
this.bindEvents();
|
|
debug('initialized');
|
|
}
|
|
ControlsController.prototype.bindEvents = function () {
|
|
this.app.settings.mode.on('change:selected', this.view.setMode);
|
|
this.app.settings.mode.on('change:options', this.configureMode);
|
|
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);
|
|
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);
|
|
this.app.on('countdown:started', this.onCountdownStarted);
|
|
this.app.on('countdown:ended', this.onCountdownStopped);
|
|
this.app.on('settings:opened', this.onSettingsOpened);
|
|
this.app.on('settings:closed', this.onSettingsClosed);
|
|
this.app.on('previewgallery:opened', this.view.hide);
|
|
this.app.on('previewgallery:closed', this.view.show);
|
|
debug('events bound');
|
|
};
|
|
ControlsController.prototype.createView = function () {
|
|
var initialMode = this.app.settings.mode.selected('key');
|
|
var cancellable = !!this.app.activity.pick;
|
|
this.view = this.app.views.controls || new ControlsView();
|
|
this.view.set('cancel', cancellable);
|
|
this.view.setMode(initialMode);
|
|
this.view.disable();
|
|
this.view.appendTo(this.app.el);
|
|
debug('cancelable: %s', cancellable);
|
|
debug('mode: %s', initialMode);
|
|
};
|
|
ControlsController.prototype.configureMode = function () {
|
|
var switchable = this.app.settings.mode.get('options').length > 1;
|
|
if (!switchable) {
|
|
this.view.disable('switch');
|
|
}
|
|
};
|
|
ControlsController.prototype.onceAppLoaded = function () {
|
|
this.app.on('ready', this.restore);
|
|
this.view.enable();
|
|
};
|
|
ControlsController.prototype.onCaptureClick = function () {
|
|
this.captureHighlightOn();
|
|
this.app.emit('capture');
|
|
};
|
|
ControlsController.prototype.onCameraWillChange = function () {
|
|
this.view.suspendModeSwitch(true);
|
|
};
|
|
ControlsController.prototype.onCameraConfigured = function () {
|
|
this.view.suspendModeSwitch(false);
|
|
};
|
|
ControlsController.prototype.onRecordingChange = function (recording) {
|
|
this.view.set('recording', recording);
|
|
if (!recording) {
|
|
this.onRecordingEnd();
|
|
}
|
|
this.view.setCaptureLabel(recording);
|
|
};
|
|
ControlsController.prototype.onRecordingEnd = function () {
|
|
this.captureHighlightOff();
|
|
};
|
|
ControlsController.prototype.onNewThumbnail = function (thumbnailBlob) {
|
|
if (thumbnailBlob) {
|
|
this.view.setThumbnail(thumbnailBlob);
|
|
} else {
|
|
this.view.removeThumbnail();
|
|
}
|
|
};
|
|
ControlsController.prototype.onCountdownStarted = function () {
|
|
this.captureHighlightOn();
|
|
this.view.set('countdown', 'active');
|
|
};
|
|
ControlsController.prototype.onCountdownStopped = function () {
|
|
this.captureHighlightOff();
|
|
this.view.set('countdown', 'inactive');
|
|
};
|
|
ControlsController.prototype.onSettingsOpened = function () {
|
|
this.view.setScreenReaderVisible(false);
|
|
};
|
|
ControlsController.prototype.onSettingsClosed = function () {
|
|
this.view.setScreenReaderVisible(true);
|
|
};
|
|
ControlsController.prototype.onCameraBusy = function () {
|
|
this.view.disable();
|
|
};
|
|
ControlsController.prototype.restore = function () {
|
|
debug('restore');
|
|
this.captureHighlightOff();
|
|
this.view.enable();
|
|
};
|
|
ControlsController.prototype.captureHighlightOn = function () {
|
|
this.view.set('capture-active');
|
|
};
|
|
ControlsController.prototype.captureHighlightOff = function () {
|
|
this.view.unset('capture-active');
|
|
};
|
|
ControlsController.prototype.onViewModeChanged = function () {
|
|
debug('view mode changed');
|
|
this.app.settings.mode.next();
|
|
};
|
|
ControlsController.prototype.onCancelButtonClick = function () {
|
|
this.app.emit('activitycanceled');
|
|
};
|
|
ControlsController.prototype.onGalleryButtonClick = function (event) {
|
|
event.stopPropagation();
|
|
var MozActivity = window.MozActivity;
|
|
if (this.app.inSecureMode) {
|
|
return;
|
|
}
|
|
this.mozActivity = new MozActivity({
|
|
name: 'browse',
|
|
data: { type: 'photos' }
|
|
});
|
|
this.view.disable();
|
|
setTimeout(this.view.enable, 2000);
|
|
};
|
|
});
|
|
define('lib/camera-coordinates', [
|
|
'require',
|
|
'exports',
|
|
'module'
|
|
], function (require, exports, module) {
|
|
function toPixels(x, y, viewportWidth, viewportHeight) {
|
|
var cameraCoordinatesRange = 2000;
|
|
var pixelsPerCameraUnitWidth = viewportWidth / cameraCoordinatesRange;
|
|
var pixelsPerCameraUnitHeight = viewportHeight / cameraCoordinatesRange;
|
|
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)
|
|
};
|
|
}
|
|
function toCamera(x, y, viewportWidth, viewportHeight) {
|
|
var cameraCoordinatesRange = 2000;
|
|
var cameraUnitsPerPixelWidth = cameraCoordinatesRange / viewportWidth;
|
|
var cameraUnitsPerPixelHeight = cameraCoordinatesRange / viewportHeight;
|
|
return {
|
|
x: Math.round(x * cameraUnitsPerPixelWidth) - 1000,
|
|
y: Math.round(y * cameraUnitsPerPixelHeight) - 1000
|
|
};
|
|
}
|
|
function rotatePoint(x, y, angle) {
|
|
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');
|
|
}
|
|
}
|
|
function rotateArea(area, sensorOrientation) {
|
|
var topLeft = rotatePoint(area.left, area.top, sensorOrientation);
|
|
var bottomRight = rotatePoint(area.right, area.bottom, sensorOrientation);
|
|
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)
|
|
};
|
|
}
|
|
function mirrorAreaCamera(area) {
|
|
return {
|
|
top: area.top,
|
|
left: -area.right,
|
|
bottom: area.bottom,
|
|
right: -area.left,
|
|
width: area.width,
|
|
height: area.height
|
|
};
|
|
}
|
|
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
|
|
};
|
|
}
|
|
function sizeToCamera(width, height, viewportWidth, viewportHeight) {
|
|
var cameraCoordinatesRange = 2000;
|
|
var cameraUnitsPerPixelWidth = cameraCoordinatesRange / viewportWidth;
|
|
var cameraUnitsPerPixelHeight = cameraCoordinatesRange / viewportHeight;
|
|
return {
|
|
width: Math.round(width * cameraUnitsPerPixelWidth),
|
|
height: Math.round(height * cameraUnitsPerPixelHeight)
|
|
};
|
|
}
|
|
function sizeToPixels(width, height, viewportWidth, viewportHeight) {
|
|
var cameraCoordinatesRange = 2000;
|
|
var pixelsPerCameraUnitWidth = viewportWidth / cameraCoordinatesRange;
|
|
var pixelsPerCameraUnitHeight = viewportHeight / cameraCoordinatesRange;
|
|
return {
|
|
width: Math.round(width * pixelsPerCameraUnitWidth),
|
|
height: Math.round(height * pixelsPerCameraUnitHeight)
|
|
};
|
|
}
|
|
function areaToPixels(area, viewportWidth, viewportHeight) {
|
|
var areaPixels = toPixels(area.left, area.top, viewportWidth, viewportHeight);
|
|
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
|
|
};
|
|
}
|
|
function areaToCamera(area, viewportWidth, viewportHeight) {
|
|
var topLeft = toCamera(area.left, area.top, viewportWidth, viewportHeight);
|
|
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;
|
|
}
|
|
function faceToCamera(face, viewportWidth, viewportHeight, sensorOrientation, mirrored) {
|
|
if (mirrored) {
|
|
face = mirrorAreaPixels(face, viewportWidth);
|
|
}
|
|
face = areaToCamera(face, viewportWidth, viewportHeight);
|
|
if (sensorOrientation) {
|
|
face = rotateArea(face, -sensorOrientation);
|
|
}
|
|
return face;
|
|
}
|
|
function faceToPixels(face, viewportWidth, viewportHeight, sensorOrientation, mirrored) {
|
|
if (sensorOrientation) {
|
|
face = rotateArea(face, sensorOrientation);
|
|
}
|
|
if (mirrored) {
|
|
face = mirrorAreaCamera(face);
|
|
}
|
|
return areaToPixels(face, viewportWidth, viewportHeight);
|
|
}
|
|
return {
|
|
faceToCamera: faceToCamera,
|
|
faceToPixels: faceToPixels,
|
|
private: {
|
|
areaToCamera: areaToCamera,
|
|
areaToPixels: areaToPixels,
|
|
mirrorAreaCamera: mirrorAreaCamera,
|
|
mirrorAreaPixels: mirrorAreaPixels,
|
|
rotateArea: rotateArea,
|
|
sizeToCamera: sizeToCamera,
|
|
sizeToPixels: sizeToPixels,
|
|
toCamera: toCamera,
|
|
toPixels: toPixels,
|
|
rotatePoint: rotatePoint
|
|
}
|
|
};
|
|
});
|
|
define('views/viewfinder', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'debug',
|
|
'lib/bind',
|
|
'lib/camera-utils',
|
|
'view'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var debug = require('debug')('view:viewfinder');
|
|
var bind = require('lib/bind');
|
|
var CameraUtils = require('lib/camera-utils');
|
|
var View = require('view');
|
|
var isZoomEnabled = false;
|
|
var scaleSizeTo = {
|
|
fill: CameraUtils.scaleSizeToFillViewport,
|
|
fit: CameraUtils.scaleSizeToFitViewport
|
|
};
|
|
var clamp = function (value, minimum, maximum) {
|
|
return Math.min(Math.max(value, minimum), maximum);
|
|
};
|
|
module.exports = View.extend({
|
|
name: 'viewfinder',
|
|
className: 'js-viewfinder',
|
|
fadeTime: 360,
|
|
initialize: function () {
|
|
this.render();
|
|
this.getSize();
|
|
},
|
|
render: function () {
|
|
this.el.innerHTML = this.template();
|
|
this.els.frame = this.find('.js-frame');
|
|
this.els.video = this.find('.js-video');
|
|
this.els.videoContainer = this.find('.js-video-container');
|
|
delete this.template;
|
|
debug('rendered');
|
|
return this.bindEvents();
|
|
},
|
|
bindEvents: function () {
|
|
bind(this.el, 'click', this.onClick);
|
|
bind(this.el, 'animationend', this.onShutterEnd);
|
|
return this;
|
|
},
|
|
getSize: function () {
|
|
this.width = window.innerWidth;
|
|
this.height = window.innerHeight;
|
|
return {
|
|
width: this.width,
|
|
height: this.height
|
|
};
|
|
},
|
|
onClick: function (e) {
|
|
this.emit('click', e);
|
|
},
|
|
enableZoom: function (minimumZoom, maximumZoom) {
|
|
if (minimumZoom) {
|
|
this._minimumZoom = minimumZoom;
|
|
}
|
|
if (maximumZoom) {
|
|
this._maximumZoom = maximumZoom;
|
|
}
|
|
isZoomEnabled = true;
|
|
},
|
|
disableZoom: function () {
|
|
this._minimumZoom = 1;
|
|
this._maximumZoom = 1;
|
|
this.setZoom(1);
|
|
isZoomEnabled = false;
|
|
},
|
|
_minimumZoom: 1,
|
|
setMinimumZoom: function (minimumZoom) {
|
|
this._minimumZoom = minimumZoom;
|
|
},
|
|
_maximumZoom: 1,
|
|
setMaximumZoom: function (maximumZoom) {
|
|
this._maximumZoom = maximumZoom;
|
|
},
|
|
_zoom: 1,
|
|
setZoom: function (zoom) {
|
|
if (!isZoomEnabled) {
|
|
return;
|
|
}
|
|
this._zoom = clamp(zoom, this._minimumZoom, this._maximumZoom);
|
|
},
|
|
_useZoomPreviewAdjustment: false,
|
|
enableZoomPreviewAdjustment: function () {
|
|
this._useZoomPreviewAdjustment = true;
|
|
},
|
|
disableZoomPreviewAdjustment: function () {
|
|
this._useZoomPreviewAdjustment = false;
|
|
},
|
|
setZoomPreviewAdjustment: function (zoomPreviewAdjustment) {
|
|
if (this._useZoomPreviewAdjustment) {
|
|
this.els.video.style.transform = 'scale(' + zoomPreviewAdjustment + ')';
|
|
}
|
|
},
|
|
stopStream: function () {
|
|
this.els.video.mozSrcObject = null;
|
|
},
|
|
fadeOut: function () {
|
|
debug('fade-out');
|
|
var self = this;
|
|
this.hide();
|
|
clearTimeout(this.fadeTimeout);
|
|
this.fadeTimeout = setTimeout(function () {
|
|
self.emit('fadedout');
|
|
}, this.fadeTime);
|
|
},
|
|
fadeIn: function (firstRun) {
|
|
debug('fade-in');
|
|
this.show();
|
|
if (firstRun) {
|
|
this.emit('fadedin');
|
|
requestAnimationFrame(() => {
|
|
this.el.style.transitionDuration = this.fadeTime + 'ms';
|
|
});
|
|
} else {
|
|
this.fadeTimeout = setTimeout(this.firer('fadedin'), this.fadeTime);
|
|
}
|
|
},
|
|
shutter: function () {
|
|
this.el.classList.add('shutter');
|
|
},
|
|
onShutterEnd: function () {
|
|
this.reflow = this.el.offsetTop;
|
|
this.el.classList.remove('shutter');
|
|
},
|
|
updatePreview: function (preview, sensorAngle, mirrored) {
|
|
if (!preview) {
|
|
return;
|
|
}
|
|
// Recompute when the embedded app becomes visible or rotates.
|
|
this.getSize();
|
|
if (!this.width || !this.height) { return; }
|
|
var aspect;
|
|
if (sensorAngle % 180 === 0) {
|
|
this.container = {
|
|
width: this.width,
|
|
height: this.height,
|
|
aspect: this.width / this.height
|
|
};
|
|
aspect = preview.height / preview.width;
|
|
} else {
|
|
this.container = {
|
|
width: this.height,
|
|
height: this.width,
|
|
aspect: this.height / this.width
|
|
};
|
|
aspect = preview.width / preview.height;
|
|
}
|
|
var shouldFill = aspect > this.container.aspect;
|
|
var scaleType = this.scaleType || (shouldFill ? 'fill' : 'fit');
|
|
this.updatePreviewMetrics(preview, sensorAngle, mirrored, scaleType);
|
|
},
|
|
updatePreviewMetrics: function (preview, sensorAngle, mirrored, scaleType) {
|
|
debug('update preview scaleType: %s', scaleType, preview);
|
|
var landscape = scaleSizeTo[scaleType](this.container, preview);
|
|
var portrait = {
|
|
width: landscape.height,
|
|
height: landscape.width
|
|
};
|
|
this.els.frame.style.width = portrait.width + 'px';
|
|
this.els.frame.style.height = portrait.height + 'px';
|
|
var transform = '';
|
|
if (mirrored) {
|
|
transform += 'scale(-1, 1) ';
|
|
}
|
|
transform += 'rotate(' + sensorAngle + 'deg)';
|
|
this.els.videoContainer.style.width = landscape.width + 'px';
|
|
this.els.videoContainer.style.height = landscape.height + 'px';
|
|
this.els.videoContainer.style.transform = transform;
|
|
this.set('scaleType', scaleType);
|
|
debug('updated preview size/position', landscape);
|
|
},
|
|
template: function () {
|
|
return '<div class="viewfinder-frame js-frame">' + '<div class="viewfinder-video-container js-video-container" ' + 'aria-hidden="true">' + '<video class="viewfinder-video js-video"></video>' + '</div>' + '<div class="viewfinder-grid">' + '<div class="row"></div>' + '<div class="row middle"></div>' + '<div class="row"></div>' + '<div class="column left">' + '<div class="cell top"></div>' + '<div class="cell middle"></div>' + '<div class="cell bottom"></div>' + '</div>' + '<div class="column middle">' + '<div class="cell top"></div>' + '<div class="cell middle"></div>' + '<div class="cell bottom"></div>' + '</div>' + '<div class="column right">' + '<div class="cell top"></div>' + '<div class="cell middle"></div>' + '<div class="cell bottom"></div>' + '</div>' + '</div>' + '</div>' + '</div>';
|
|
}
|
|
});
|
|
});
|
|
define('views/focus', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'debug',
|
|
'view'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var debug = require('debug')('view:focus');
|
|
var View = require('view');
|
|
module.exports = View.extend({
|
|
name: 'focus',
|
|
fadeTime: 500,
|
|
initialize: function () {
|
|
this.render();
|
|
this.setFocusState('none');
|
|
},
|
|
render: function () {
|
|
this.el.innerHTML = this.template();
|
|
delete this.template;
|
|
debug('rendered');
|
|
return this;
|
|
},
|
|
setFocusState: function (state) {
|
|
this.set('state', state);
|
|
if (state !== 'focusing') {
|
|
this.fadeOut();
|
|
}
|
|
},
|
|
setFocusMode: function (mode) {
|
|
this.reset();
|
|
this.set('mode', mode);
|
|
},
|
|
setPosition: function (x, y) {
|
|
if (this.fadeOutTimer) {
|
|
clearTimeout(this.fadeOutTimer);
|
|
}
|
|
this.el.style.left = x + 'px';
|
|
this.el.style.top = y + 'px';
|
|
},
|
|
reset: function () {
|
|
this.el.style.left = '50%';
|
|
this.el.style.top = '50%';
|
|
this.set('state', 'none');
|
|
},
|
|
fadeOut: function () {
|
|
var self = this;
|
|
this.fadeOutTimer = setTimeout(hide, this.fadeTime);
|
|
function hide() {
|
|
self.reset();
|
|
}
|
|
},
|
|
template: function () {
|
|
return '<div class="focus_locking" data-icon="focus-locking" ' + 'aria-hidden="true"></div>' + '<div class="focus_locked" data-icon="focus-locked" aria-hidden="true">' + '</div>';
|
|
}
|
|
});
|
|
});
|
|
define('views/face', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'view'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var View = require('view');
|
|
module.exports = View.extend({
|
|
name: 'face',
|
|
initialize: function () {
|
|
this.render();
|
|
},
|
|
render: function () {
|
|
this.el.innerHTML = this.template();
|
|
this.el.classList.add('js-face');
|
|
},
|
|
setPosition: function (x, y) {
|
|
this.el.style.left = x + 'px';
|
|
this.el.style.top = y + 'px';
|
|
},
|
|
setDiameter: function (diameter) {
|
|
this.el.style.width = diameter + 'px';
|
|
this.el.style.height = diameter + 'px';
|
|
}
|
|
});
|
|
});
|
|
define('views/faces', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'views/face',
|
|
'view'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var FaceView = require('views/face');
|
|
var View = require('view');
|
|
module.exports = View.extend({
|
|
name: 'faces',
|
|
faces: [],
|
|
initialize: function (options) {
|
|
options = options || {};
|
|
this.el.innerHTML = this.template();
|
|
this.FaceView = options.FaceView || FaceView;
|
|
},
|
|
createFaces: function (maxNumberFaces) {
|
|
var faceView;
|
|
var i;
|
|
for (i = 0; i < maxNumberFaces; ++i) {
|
|
faceView = new this.FaceView();
|
|
faceView.hide();
|
|
this.faces.push(faceView);
|
|
faceView.appendTo(this.el);
|
|
}
|
|
},
|
|
render: function (faces) {
|
|
var self = this;
|
|
this.hideFaces();
|
|
if (faces.length > this.faces.length) {
|
|
this.createFaces(faces.length - this.faces.length);
|
|
}
|
|
faces.forEach(function (face, index) {
|
|
var faceView = self.faces[index];
|
|
self.renderFace(face, faceView);
|
|
});
|
|
},
|
|
renderFace: function (face, faceView) {
|
|
var diameter = Math.min(300, face.diameter);
|
|
faceView.setPosition(face.x, face.y);
|
|
faceView.setDiameter(diameter);
|
|
faceView.show();
|
|
},
|
|
hideFaces: function () {
|
|
this.faces.forEach(function (faceView) {
|
|
faceView.hide();
|
|
});
|
|
},
|
|
clear: function () {
|
|
var self = this;
|
|
this.faces.forEach(function (faceView) {
|
|
self.el.removeChild(faceView.el);
|
|
});
|
|
this.faces = [];
|
|
}
|
|
});
|
|
});
|
|
define('controllers/viewfinder', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'lib/camera-coordinates',
|
|
'debug',
|
|
'views/viewfinder',
|
|
'views/focus',
|
|
'views/faces',
|
|
'lib/bind-all'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
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');
|
|
module.exports = function (app) {
|
|
return new ViewfinderController(app);
|
|
};
|
|
module.exports.ViewfinderController = ViewfinderController;
|
|
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');
|
|
}
|
|
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);
|
|
};
|
|
ViewfinderController.prototype.configure = function () {
|
|
var settings = this.app.settings;
|
|
var zoomSensitivity = settings.viewfinder.get('zoomGestureSensitivity');
|
|
this.sensitivity = zoomSensitivity * window.innerWidth;
|
|
this.configureScaleType();
|
|
this.configureGrid();
|
|
};
|
|
ViewfinderController.prototype.configureScaleType = function () {
|
|
var scaleType = this.app.settings.viewfinder.get('scaleType');
|
|
this.views.viewfinder.scaleType = scaleType;
|
|
debug('set scale type: %s', scaleType);
|
|
};
|
|
ViewfinderController.prototype.configureGrid = function () {
|
|
var grid = this.app.settings.grid.selected('key');
|
|
this.views.viewfinder.set('grid', grid);
|
|
};
|
|
ViewfinderController.prototype.hideGrid = function () {
|
|
this.views.viewfinder.set('grid', 'off');
|
|
};
|
|
ViewfinderController.prototype.bindEvents = function () {
|
|
window.addEventListener('resize', this.configurePreview);
|
|
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);
|
|
this.camera.on('zoomconfigured', this.onZoomConfigured);
|
|
this.camera.on('zoomchanged', this.onZoomChanged);
|
|
this.camera.on('preview:started', this.show);
|
|
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);
|
|
this.app.on('previewgallery:opened', this.onGalleryOpened);
|
|
this.app.on('previewgallery:closed', this.onGalleryClosed);
|
|
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'));
|
|
this.app.on('battery:powersave', this.onPowerSave);
|
|
this.app.on('pinch:changed', this.onPinchChanged);
|
|
this.app.on('hidden', this.stopStream);
|
|
};
|
|
ViewfinderController.prototype.onCameraConfigured = function () {
|
|
debug('configuring');
|
|
this.loadStream();
|
|
this.configurePreview();
|
|
};
|
|
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');
|
|
};
|
|
ViewfinderController.prototype.hide = function () {
|
|
debug('hide');
|
|
clearTimeout(this.showTimeout);
|
|
this.views.viewfinder.fadeOut();
|
|
};
|
|
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) {
|
|
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
|
|
};
|
|
};
|
|
ViewfinderController.prototype.loadStream = function () {
|
|
this.camera.loadStreamInto(this.views.viewfinder.els.video);
|
|
debug('stream started');
|
|
};
|
|
ViewfinderController.prototype.stopStream = function () {
|
|
this.views.viewfinder.stopStream();
|
|
debug('stream stopped');
|
|
};
|
|
ViewfinderController.prototype.onPowerSave = function (powerSave) {
|
|
this.views.viewfinder.els.video.mozUseScreenWakeLock = !powerSave;
|
|
};
|
|
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);
|
|
};
|
|
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);
|
|
};
|
|
ViewfinderController.prototype.onPinchChanged = function (deltaPinch) {
|
|
var zoom = this.views.viewfinder._zoom * (1 + deltaPinch / this.sensitivity);
|
|
this.views.viewfinder.setZoom(zoom);
|
|
this.camera.setZoom(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);
|
|
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();
|
|
this.views.viewfinder.set('ariaHidden', true);
|
|
};
|
|
ViewfinderController.prototype.onSettingsClosed = function () {
|
|
this.configureGrid();
|
|
this.views.viewfinder.set('ariaHidden', false);
|
|
};
|
|
ViewfinderController.prototype.onGalleryOpened = function () {
|
|
this.views.viewfinder.disable();
|
|
this.views.viewfinder.set('ariaHidden', true);
|
|
};
|
|
ViewfinderController.prototype.onGalleryClosed = function () {
|
|
this.views.viewfinder.enable();
|
|
this.views.viewfinder.set('ariaHidden', false);
|
|
};
|
|
ViewfinderController.prototype.onPreviewActive = function (active) {
|
|
if (!active) {
|
|
this.stopStream();
|
|
}
|
|
};
|
|
});
|
|
define('lib/get-aspect', [
|
|
'require',
|
|
'exports',
|
|
'module'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
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;
|
|
};
|
|
});
|
|
define('lib/format-recorder-profiles', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'./get-aspect'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var getAspect = require('./get-aspect');
|
|
module.exports = function (profiles, options) {
|
|
var exclude = options && options.exclude || [];
|
|
var items = [];
|
|
var hash = {};
|
|
for (var key in profiles) {
|
|
if (!profiles.hasOwnProperty(key)) {
|
|
continue;
|
|
}
|
|
var profile = profiles[key];
|
|
var video = profile.video;
|
|
var sizeKey = video.width + 'x' + video.height;
|
|
if (hash[sizeKey]) {
|
|
continue;
|
|
}
|
|
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
|
|
});
|
|
}
|
|
items.sort(function (a, b) {
|
|
return b.pixelSize - a.pixelSize;
|
|
});
|
|
return items;
|
|
};
|
|
});
|
|
define('lib/format-picture-sizes', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'./get-aspect'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var getAspect = require('./get-aspect');
|
|
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);
|
|
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
|
|
});
|
|
});
|
|
formatted.sort(function (a, b) {
|
|
return b.pixelSize - a.pixelSize;
|
|
});
|
|
return formatted;
|
|
};
|
|
function getMP(w, h) {
|
|
return Math.round(w * h / 1000000);
|
|
}
|
|
});
|
|
define('controllers/settings', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'lib/format-recorder-profiles',
|
|
'lib/format-picture-sizes',
|
|
'debug',
|
|
'lib/bind-all'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
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');
|
|
module.exports = function (app) {
|
|
return new SettingsController(app);
|
|
};
|
|
module.exports.SettingsController = SettingsController;
|
|
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;
|
|
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');
|
|
}
|
|
SettingsController.prototype.configure = function () {
|
|
this.setupRecorderProfilesAlias();
|
|
this.setupPictureSizesAlias();
|
|
this.setupFlashModesAlias();
|
|
};
|
|
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];
|
|
}
|
|
});
|
|
};
|
|
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];
|
|
}
|
|
});
|
|
};
|
|
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];
|
|
}
|
|
});
|
|
};
|
|
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);
|
|
};
|
|
SettingsController.prototype.toggleSettings = function () {
|
|
if (this.view) {
|
|
this.closeSettings();
|
|
} else {
|
|
this.openSettings();
|
|
}
|
|
};
|
|
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);
|
|
self.view.hide();
|
|
self.view.fadeIn();
|
|
self.app.emit('settings:opened');
|
|
debug('settings opened');
|
|
});
|
|
};
|
|
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();
|
|
}
|
|
});
|
|
};
|
|
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);
|
|
});
|
|
};
|
|
SettingsController.prototype.onPickActivity = function (data) {
|
|
debug('pick activity', data);
|
|
var setting;
|
|
var options;
|
|
var updated = false;
|
|
var maxFileSize = data.maxFileSizeBytes;
|
|
var maxPixelSize = data.maxPixelSize;
|
|
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 (updated) {
|
|
this.app.emit('settings:configured');
|
|
}
|
|
};
|
|
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;
|
|
var optionTitle = localizable ? '<span data-l10n-id="' + setting.selected('title') + '"></span>' : '<span>' + setting.selected('title') + '</span>';
|
|
if (flashDeactivated) {
|
|
html = title + ' ' + optionTitle + '<br/>' + '<span data-l10n-id="flash-deactivated"></span>';
|
|
} else {
|
|
html = title + '<br/>' + optionTitle;
|
|
}
|
|
this.notification.display({ text: { html: html } });
|
|
};
|
|
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);
|
|
this.app.emit('settings:configured');
|
|
debug('settings configured to new capabilities');
|
|
};
|
|
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);
|
|
// Desktop and Android sensors may only expose a size outside Gaia's 4:3 preset.
|
|
if (!formatted.length && sizes.length) {
|
|
formatted = this.formatPictureSizes(sizes, {});
|
|
}
|
|
setting.resetOptions(formatted);
|
|
if (!setting.current().fetched) {
|
|
setting.select(currentSizeKey, { silent: true });
|
|
}
|
|
this.formatPictureSizeTitles();
|
|
debug('configured pictureSizes', setting.selected('key'));
|
|
};
|
|
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 (maxFileSize) {
|
|
items = [items[items.length - 1]];
|
|
}
|
|
setting.resetOptions(items);
|
|
if (!maxFileSize && !setting.current().fetched) {
|
|
setting.select(currentProfile, { silent: true });
|
|
}
|
|
};
|
|
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');
|
|
});
|
|
};
|
|
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);
|
|
};
|
|
SettingsController.prototype.validMenuItem = function (item) {
|
|
var setting = this.settings[item.key];
|
|
return !!setting && setting.supported();
|
|
};
|
|
});
|
|
define('lib/bytes-to-pixels', [
|
|
'require',
|
|
'exports',
|
|
'module'
|
|
], 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);
|
|
};
|
|
});
|
|
define("controllers/activity",["require", "exports", "module", "debug", "lib/bytes-to-pixels", "lib/bind-all"],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 = media.blob.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();
|
|
}
|
|
);
|
|
});
|
|
}
|
|
};
|
|
|
|
});
|
|
define('controllers/camera', [
|
|
'require',
|
|
'exports',
|
|
'module',
|
|
'debug',
|
|
'lib/bind-all'
|
|
], function (require, exports, module) {
|
|
'use strict';
|
|
var debug = require('debug')('controller:camera');
|
|
var bindAll = require('lib/bind-all');
|
|
module.exports = function (app) {
|
|
return new CameraController(app);
|
|
};
|
|
module.exports.CameraController = CameraController;
|
|
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;
|
|
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;
|
|
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.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.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);
|
|
};
|
|
CameraController.prototype.onCaptureKey = function (e) {
|
|
debug('on capture key', e);
|
|
if (this.capture() !== false) {
|
|
e.preventDefault();
|
|
}
|
|
};
|
|
CameraController.prototype.onFocusKey = function (e) {
|
|
debug('on focus key', e);
|
|
if (!this.shouldCapture()) {
|
|
return;
|
|
}
|
|
this.camera.focus.focus();
|
|
};
|
|
CameraController.prototype.configure = function () {
|
|
this.settings.cameras.filterOptions(this.camera.cameraList);
|
|
debug('configured');
|
|
};
|
|
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();
|
|
setTimeout(this.updateZoomForMako);
|
|
debug('camera configured with final settings');
|
|
};
|
|
CameraController.prototype.onPickActivity = function (data) {
|
|
this.camera.set('maxFileSizeBytes', data.maxFileSizeBytes);
|
|
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;
|
|
};
|
|
CameraController.prototype.setMode = function (mode) {
|
|
debug('set mode: %s', mode);
|
|
var self = this;
|
|
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);
|
|
});
|
|
};
|
|
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;
|
|
if (this.camera.isPictureSize(value)) {
|
|
return;
|
|
}
|
|
if (!pictureMode) {
|
|
this.camera.setPictureSize(value, { configure: false });
|
|
return;
|
|
}
|
|
this.app.emit('camera:willchange');
|
|
this.app.once('viewfinder:hidden', function () {
|
|
self.camera.setPictureSize(value);
|
|
});
|
|
};
|
|
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;
|
|
if (this.camera.isRecorderProfile(key)) {
|
|
return;
|
|
}
|
|
if (!videoMode) {
|
|
this.camera.setRecorderProfile(key, { configure: false });
|
|
return;
|
|
}
|
|
this.app.emit('camera:willchange');
|
|
this.app.once('viewfinder:hidden', function () {
|
|
self.camera.setRecorderProfile(key);
|
|
});
|
|
};
|
|
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);
|
|
});
|
|
};
|
|
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();
|
|
}
|
|
};
|
|
CameraController.prototype.onStorageChanged = function (state) {
|
|
if (state !== 'available') {
|
|
this.camera.stopRecording();
|
|
}
|
|
};
|
|
CameraController.prototype.onStorageVolumeChanged = function (storage) {
|
|
this.camera.setStorage(storage);
|
|
};
|
|
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();
|
|
};
|
|
CameraController.prototype.onCameraClosed = function (reason) {
|
|
reason = reason || 'SystemFailure';
|
|
if (reason === 'SystemFailure') {
|
|
this.app.emit('reboot');
|
|
}
|
|
};
|
|
CameraController.prototype.onGalleryOpened = function () {
|
|
this.galleryOpen = true;
|
|
this.shutdownCamera();
|
|
};
|
|
CameraController.prototype.onGalleryClosed = function (reason) {
|
|
this.galleryOpen = false;
|
|
if (this.app.hidden) {
|
|
return;
|
|
}
|
|
this.loadCamera(true);
|
|
};
|
|
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;
|
|
self.settings.zoom.set('useZoomPreviewAdjustment', true);
|
|
self.camera.set('maxHardwareZoom', maxHardwareZoom);
|
|
self.camera.emit('zoomconfigured', self.camera.getZoom());
|
|
debug('zoom reconfigured for mako');
|
|
}
|
|
};
|
|
});
|
|
define("lib/storage",["require", "exports", "module", "debug", "lib/bind-all", "evt"],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', '.' + (window.VulpesVideoExtension || '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|mp4|webm)$/, '.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) : '';
|
|
}
|
|
|
|
});
|
|
define("controllers/storage",["require", "exports", "module", "debug", "lib/bind-all", "lib/storage"],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|mp4|webm)$/, '.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);
|
|
}
|
|
};
|
|
|
|
});
|
|
window.jsStarted = Date.now();
|
|
define('main', [
|
|
'require',
|
|
'lib/settings',
|
|
'lib/geo-location',
|
|
'config/config',
|
|
'lib/camera/camera',
|
|
'app',
|
|
'controllers/overlay',
|
|
'controllers/battery',
|
|
'controllers/hud',
|
|
'controllers/controls',
|
|
'controllers/viewfinder',
|
|
'controllers/settings',
|
|
'controllers/activity',
|
|
'controllers/camera',
|
|
'controllers/storage'
|
|
], function (require) {
|
|
var perf = {
|
|
jsStarted: window.jsStarted,
|
|
firstModule: Date.now()
|
|
};
|
|
var Settings = require('lib/settings');
|
|
var GeoLocation = require('lib/geo-location');
|
|
var settingsData = require('config/config');
|
|
var settings = new Settings(settingsData);
|
|
var Camera = require('lib/camera/camera');
|
|
var App = require('app');
|
|
var key;
|
|
if (settingsData.globals) {
|
|
for (key in settingsData.globals) {
|
|
window[key] = settingsData.globals[key];
|
|
}
|
|
}
|
|
var app = window.app = new App({
|
|
settings: settings,
|
|
geolocation: new GeoLocation(),
|
|
el: document.body,
|
|
doc: document,
|
|
win: window,
|
|
perf: perf,
|
|
camera: new Camera({ focus: settingsData.focus }),
|
|
controllers: {
|
|
overlay: require('controllers/overlay'),
|
|
battery: require('controllers/battery'),
|
|
hud: require('controllers/hud'),
|
|
controls: require('controllers/controls'),
|
|
viewfinder: require('controllers/viewfinder'),
|
|
settings: require('controllers/settings'),
|
|
activity: require('controllers/activity'),
|
|
camera: require('controllers/camera'),
|
|
storage: require('controllers/storage'),
|
|
lazy: [
|
|
'controllers/zoom-bar',
|
|
'controllers/indicators',
|
|
'controllers/media',
|
|
'controllers/sounds'
|
|
]
|
|
}
|
|
});
|
|
app.camera.load();
|
|
app.settings.fetch();
|
|
app.boot();
|
|
for (key in settingsData) {
|
|
delete settingsData[key];
|
|
}
|
|
});
|
|
requirejs.config({
|
|
baseUrl: '/js',
|
|
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',
|
|
'stop-recording-event': '../shared/js/stop_recording_event'
|
|
},
|
|
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: {
|
|
'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' }
|
|
}
|
|
});
|
|
define('config/require', function () {
|
|
});
|
|
if (!window.VulpesCompat || navigator.mozCameras) require(['main']); |