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'; /** * Module Dependencies */ 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'); /** * Mixin `Model` API (inc. events) */ model(Camera.prototype); /** * Exports */ module.exports = Camera; /** * Initialize a new 'Camera' * * Options: * * - {object} `focus` * * @param {Object} options */ function Camera(options) { debug('initializing'); bindAll(this); // Options options = options || {}; // mozCamera config is cached by default this.cacheConfig = options.cacheConfig !== false; // Minimum video duration length for creating a // video that contains at least few samples, see bug 899864. // Empirical testing has shown that it is common to get corrupted // files under 1400ms (e.g. on the nexus 5). this.minRecordingTime = options.minRecordingTime || 1500; // Number of bytes left on disk to let us stop recording. this.recordSpacePadding = options.recordSpacePadding || 1024 * 1024 * 1; // The minimum available disk space to start recording a video. this.recordSpaceMin = options.recordSpaceMin || 1024 * 1024 * 2; // The number of times to attempt // hardware request before giving up this.requestAttempts = options.requestAttempts || 3; // Test hooks this.orientation = options.orientation || orientation; this.configStorage = options.configStorage || localStorage; this.cameraList = navigator.mozCameras.getListOfCameras(); this.mozCamera = null; this.storage = options.storage || {}; // Video config this.video = { filepath: null, minSpace: this.recordSpaceMin, spacePadding : this.recordSpacePadding, poster: { filepath: null, } }; this.focus = new Focus(options.focus); this.suspendedFlashCount = 0; // Always boot in 'picture' mode // with 'back' camera. This may need // to be configurable at some point. this.mode = 'picture'; this.selectedCamera = 'back'; // Allow `configure` to be called multiple // times in the same frame, but only ever run once. this.configure = debounce(this.configure); debug('initialized'); } /** * Loads the currently selected camera. * * There are cases whereby the camera * may still be 'releasing' its hardware. * If this is the case we wait for the * release process to finish, then attempt * to load again. * * @public */ Camera.prototype.load = function() { debug('load camera'); var loadingNewCamera = this.selectedCamera !== this.lastLoadedCamera; var self = this; // If hardware is still being released // we're not allowed to request the camera. if (this.releasing) { debug('wait for camera release'); this.once('released', function() { self.load(); }); return; } // Don't re-load hardware if selected camera is the same. if (this.mozCamera && !loadingNewCamera) { debug('camera not changed'); this.ready(); return; } // If a camera is already loaded, // it must be 'released' first. // We also discard the `mozCameraConfig` // as the previous camera config // won't apply to the new camera. if (this.mozCamera) { this.release(ready); } else { ready(); } // Once ready we request the camera // with the currently `selectedCamera` // and any `mozCameraConfig` that may // be in memory. // // The only time there should be a // valid `mozCameraConfig` in memory // is when the app becomes visible again // after being hidden. and we wish to // request the camera again in exactly // the same state it was previously in. function ready() { self.requestCamera(self.selectedCamera); self.lastLoadedCamera = self.selectedCamera; } }; /** * Requests the mozCamera object, * then configures it. * * @private */ 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; // Indicate 'busy' this.configured = false; this.busy('requestingCamera'); // Make initial request request(); /** * Requests the camera hardware. * * @private */ 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); // release camera when press power key // as soon as you open a camera app 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 the camera was configured in the // `mozCamera.getCamera()` call, we can // fire the 'configured' event now. We // can also start the preview because // gecko ensures that configuration only // completes after the preview has been // started. While we will get a preview // started event after this, we can often // save ourselves getting delayed by an // extra refresh by configuring the // viewfinder now. if (self.configured) { self.emit('configured'); self.onPreviewStateChange({newState: 'started'}); } self.ready(); } /** * Called when the request for camera * hardware fails. * * If the hardware is 'closed' we attempt * to re-request it one second later, until * all our attempts have run out. * * @param {String} err */ 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; }; /** * Configures the newly recieved * mozCamera object. * * Setting the 'cababilities' key * triggers 'change' callback inside * the CameraController that sets the * app up for the new camera. * * @param {MozCamera} mozCamera * @private */ Camera.prototype.setupNewCamera = function(mozCamera) { debug('configuring camera'); var capabilities = mozCamera.capabilities; this.mozCamera = mozCamera; // Bind to some events 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 capablities need to be in * a consistent format. * * We shallow clone to make sure the * app doesnt' make changes to the * original `capabilities` object. * * @param {Object} capabilities * @return {Object} */ Camera.prototype.formatCapabilities = function(capabilities) { var hasHDR = capabilities.sceneModes.indexOf('hdr') > -1; var hdr = hasHDR ? ['on', 'off'] : undefined; return mix({ hdr: hdr }, capabilities); }; /** * Configure the camera hardware * with the current `mode`, `previewSize` * and `recorderProfile`. * * @private */ Camera.prototype.configure = function() { debug('configuring hardware...'); var self = this; // As soon as a request to configure // comes in, the confuguration is now // dirty (out-of-date), and the hardware // must be reconfigured at some point. this.configured = false; // Ensure that any requests that // come in whilst busy get run once // camera is ready again. if (this.isBusy) { debug('defering configuration'); this.once('ready', this.configure); return; } // Exit here if there is no camera if (!this.mozCamera) { debug('no mozCamera'); return; } // In some extreme cases the mode can // get changed and configured whilst // video recording is in progress. this.stopRecording(); // Indicate 'busy' this.busy(); // Create a new `mozCameraConfig` var mozCameraConfig = { mode: this.mode, pictureSize: this.pictureSize, recorderProfile: this.recorderProfile }; // Configure the camera hardware 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); }; /** * Plugs Video Stream into Video Element. * * @param {Elmement} videoElement * @public */ Camera.prototype.loadStreamInto = function(videoElement) { debug('loading stream into element'); if (!this.mozCamera) { debug('error - `mozCamera` is undefined or null'); return; } // REVIEW: Something is wrong if we are // calling this without a video element. if (!videoElement) { debug('error - `videoElement` is undefined or null'); return; } // Don't load the same camera stream again 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'); }; /** * Return available preview sizes. * * @return {Array} * @private */ Camera.prototype.previewSizes = function() { if (!this.mozCamera) { return; } return this.mozCamera.capabilities.previewSizes; }; /** * Return the current optimal preview size. * * @return {Object} * @private */ Camera.prototype.previewSize = function() { return this.givenPreviewSize; }; /** * Get the current recording resolution. * * @return {Object} */ Camera.prototype.resolution = function() { switch (this.mode) { case 'picture': return this.pictureSize; case 'video': return this.getRecorderProfile().video; } }; /** * Set the picture size. * * If the given size is the same as the * currently set pictureSize then no * action is taken. * * The camera is 'configured' a soon as the * pictureSize is changed. `.configure` is * debounced so it will only ever run once * per turn. * * Options: * * - {Boolean} `configure` * * @param {Object} size */ Camera.prototype.setPictureSize = function(size, options) { debug('set picture size', size); if (!size) { return; } // Configure unless `false` var configure = !(options && options.configure === false); // Don't do waste time re-configuring the // hardware if the pictureSize hasn't changed. if (this.isPictureSize(size)) { debug('pictureSize didn\'t change'); return; } this.pictureSize = size; // Configure the hardware only when required 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; }; /** * Set the recorder profile. * * If the given profile is the same as * the current profile, no action is * taken. * * The camera is 'configured' a soon as the * recorderProfile is changed (`.configure()` is * debounced so it will only ever run once * per turn). * * Options: * * - {Boolean} `configure` * * @param {String} key */ Camera.prototype.setRecorderProfile = function(key, options) { debug('set recorderProfile: %s', key); if (!key) { return; } // Configure unless `false` var configure = !(options && options.configure === false); // Exit if not changed 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; }; /** * Returns the full profile of the * currently set recordrProfile. * * @return {Object} */ 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); } }; /** * Sets the current flash mode, * both on the Camera instance * and on the cameraObj hardware. * If flash is suspended, it * updates the cached state that * will be restored. * * @param {String} key */ Camera.prototype.setFlashMode = function(key) { if (this.mozCamera) { // If no key was provided, set it to 'off' which is // a valid flash mode. 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; }; /** * Releases the camera hardware. * * @param {Function} done */ Camera.prototype.release = function(done) { debug('release'); done = done || function() {}; var self = this; // Clear any pending hardware requests clearTimeout(this.cameraRequestTimeout); clearTimeout(this.zoomChangeTimeout); this.zoomChangeTimeout = null; // Ignore if there is no loaded camera 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; // Reset cached parameters 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); } }; // TODO: Perhaps this function should be moved into a separate lib 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; // Coping the array to not modify the original thumbnailSizes = thumbnailSizes.slice(0); if (!thumbnailSizes || !pictureSize) { return; } function imageSizeFillsScreen(pixelsWidth, pixelsHeight) { return ((pixelsWidth >= screenWidth || // portrait pixelsHeight >= screenHeight) && (pixelsWidth >= screenHeight || // landscape pixelsHeight >= screenWidth)); } // Removes the sizes with the wrong aspect ratio 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; } // Sorting the array from smaller to larger sizes 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]; }; /** * Takes a photo, or begins/ends * a video capture session. * * Options: * * - `position` {Object} - geolocation to store in EXIF * * @param {Object} options * @public */ 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; } }; /** * Take a picture. * * Options: * * - {Number} `position` - geolocation to store in EXIF * * @param {Object} options */ 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 has been passed in, // add it to the config object. if (position) { config.position = position; } // Front camera is inverted, so flip rotation config.rotation = selectedCamera === 'front' ? -rotation : rotation; // If the camera focus is 'continuous' or 'infinity' // we can take the picture straight away. 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 taking a picture fails because there's // already a picture being taken we ignore it. 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); } } }; /** * Start/stop recording. * * @param {Object} options */ Camera.prototype.toggleRecording = function(options) { var state = this.get('recording'); if (state && state !== 'stopped') { this.stopRecording(); } else { this.startRecording(options); } }; /** * Seet the storage for video. * * @public */ Camera.prototype.setStorage = function(storage) { this.storage.video = storage.video; }; /** * Start recording a video. * * @public */ 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; // Rotation is flipped for front camera if (frontCamera) { rotation = -rotation; } this.set('recording', 'starting'); this.busy(); // Lock orientation during video recording // // REVIEW: This should *not* be here. This // is an App concern and should live in // the `CameraController`. this.orientation.stop(); var profile = this.resolution(); video.width = profile.width; video.height = profile.height; // Calculate what the configured rotation of the video will be // so that we don't have to load the metadata to find this out 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; // First check if there is enough free space 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; // Don't continue if there // is not enough space if (notEnoughSpace) { self.onStartRecordingError('nospace2'); return; } // TODO: Callee should // pass in orientation 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) { // Regardless of error, if we called out to mozCamera, it may have // created an empty file and we need to clean that up. storage.delete(video.filepath); // IN_PROGRESS means there is already a startRecording call in // progress; we can ignore it because it will have its own duplicated // success/error path. if (err.name === 'NS_ERROR_IN_PROGRESS') { debug('start recording error (in progress)'); return; } // ABORT means a stopRecording call interrupted us. Since it never // transitioned to the Started recording state, we need to stop // explicitly here. if (err.name === 'NS_ERROR_ABORT') { debug('start recording error (abort)'); return self.stoppedRecording(); } // Ignore err as we use our own set of error // codes; instead trigger using the default self.onStartRecordingError(); } function onSuccess() { self.ready(); // User closed app while // recording was trying to start // // TODO: Not sure this should be here if (document.hidden) { self.stopRecording(); } } }; Camera.prototype.startedRecording = function() { debug('started recording'); this.startVideoTimer(); this.set('recording', 'started'); }; /** * Stop recording the video. * * Once we have told the camera to stop recording * the video we attach a 'change' listener to the * video storage and wait. Once the listener fires * we know that the video file has been saved. * * At this point we fetch the file Blob from * storage and then call the `.onNewVideo()` * method to handle the final stages. * * @public */ Camera.prototype.stopRecording = function() { debug('stop recording'); var state = this.get('recording'); // Even if we have requested a recording to stop, that doesn't // mean it has finished yet, as we need to wait for the recorder // state change event. 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'); // Unlock orientation when stopping video recording. // REVIEW:WP This logic is out of scope of the // Camera hardware. Only the App should be // making such high level decicions. this.orientation.start(); var self = this; var video; if (recorded) { video = mix({}, this.video); video.poster = mix({}, video.poster); // Re-fetch the blobs from storage this.storage.video.get(video.filepath).then(function(blob) { video.blob = blob; // Tell the app the new video is ready self.emit('newvideo', video); self.ready(); }, function() { if (self) { self.onStopRecordingError(video); self = null; } }); } }; // TODO: This is UI stuff, so // shouldn't be handled in this file. 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 file may or may not exist, delete it just in case this.storage.video.delete(video.filepath); // If the time between start/stop was really short, suppress the // error dialog to the user -- they wouldn't have expected to // get a recording anyways var elapsedTime = this.get('videoElapsed'); if (elapsedTime < this.minRecordingTime) { this.ready(); } else { this.onRecordingError(); } }; /** * Emit a 'shutter' event so that * app UI can respond with shutter * animations and sounds effects. * * @private */ Camera.prototype.onShutter = function() { this.emit('shutter'); }; /** * Emit a 'closed' event when camera controller * closes * * @private */ Camera.prototype.onClosed = function(e) { this.shutdown(); this.emit('closed', e.reason); }; /** * The preview state change events come * from the camera hardware. If 'stopped' * or 'paused' the camera must not be used. * * @param event with {String} newState ['started', 'stopped', 'paused'] * @private */ 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); }; /** * Emit useful event hook. * * @param {String} msg * @private */ 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') { // The last event to come in is always Stopped; if an asynchronous // error happened (i.e. couldn't create the poster), we need to // report the error to the user. if (this.stopRecordError) { this.onStopRecordingError(this.video); } this.stoppedRecording(!this.stopRecordError); this.stopRecordError = false; } else if(msg === 'PosterFailed' || msg === 'TrackFailed' || msg === 'MediaRecorderFailed' || msg === 'MediaServerFailed') { // TrackFailed is triggered when the video has no samples (i.e. // is too short) and would fail to be loaded by the application this.stopRecordError = true; this.stopRecording(); } }; /** * Requested poster got created while recording a video. * * @private */ Camera.prototype.onPoster = function(e) { debug('poster created'); this.video.poster.blob = e.data; }; /** * Get the number of remaining * bytes in video storage. * * @param {Function} done * @async * @private */ 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'); } }; /** * Get a unique video filepath * to record a new video to. * * Your application can overwrite * this method with something * so that you can record directly * to final location. We do this * in CameraController. * * Callback function signature used * so that an async override can * be used if you wish. * * @param {Function} done */ Camera.prototype.createVideoFilepath = function(done) { done(null, Date.now() + '_tmp.3gp'); }; /** * Resume the preview stream. * * After a photo has been taken the * preview stream freezes on the * taken frame. We call this function * to start the stream flowing again. * * @private */ Camera.prototype.resumePreview = function() { this.mozCamera.resumePreview(); // After calling takePicture(Camera.ShutterCallback, Camera.PictureCallback, // Camera.PictureCallback) or stopPreview(), and then resuming preview with // startPreview(), the apps should call this method again to resume face // detection. See Bug 1070791. this.focus.startFaceDetection(); this.emit('previewresumed'); }; /** * Sets the selected camera to the * given string and then reloads * the camera. * * If the given camera is already * selected, no action is taken. * * @param {String} camera 'front'|'back' * @public */ Camera.prototype.setCamera = function(camera) { debug('set camera: %s', camera); if (this.selectedCamera === camera) { return; } this.selectedCamera = camera; this.load(); }; /** * Toggles between 'picture' * and 'video' capture modes. * * @return {String} * @public */ Camera.prototype.setMode = function(mode) { debug('setting mode to: %s', mode); if (this.isMode(mode)) { return; } this.mode = mode; this.configure(); return this; }; /** * States if the camera is currently * set to the passed mode. * * @param {String} mode ['picture'|'video'] * @return {Boolean} * @public */ Camera.prototype.isMode = function(mode) { return this.mode === mode; }; /** * Sets a start time and begins * updating the elapsed time * every second. * * @private */ Camera.prototype.startVideoTimer = function() { this.set('videoStart', new Date().getTime()); this.videoTimer = setInterval(this.updateVideoElapsed, 1000); this.updateVideoElapsed(); }; /** * Clear the video timer interval. * * @private */ Camera.prototype.stopVideoTimer = function() { clearInterval(this.videoTimer); this.videoTimer = null; this.updateVideoElapsed(); }; /** * Calculates the elapse time * and updateds the 'videoElapsed' * value. * * We listen for the 'change:' * event emitted elsewhere to * update the UI accordingly. * */ Camera.prototype.updateVideoElapsed = function() { var now = new Date().getTime(); var start = this.get('videoStart'); this.set('videoElapsed', (now - start)); }; /** * Set ISO value. * * @param {String} value * @public */ Camera.prototype.setISOMode = function(value) { var isoModes = this.mozCamera.capabilities.isoModes; if (isoModes && isoModes.indexOf(value) > -1) { this.mozCamera.isoMode = value; } }; /** * Set the mozCamera white-balance value. * * @param {String} value * @public */ Camera.prototype.setWhiteBalance = function(value){ var capabilities = this.mozCamera.capabilities; var modes = capabilities.whiteBalanceModes; if (modes && modes.indexOf(value) > -1) { this.mozCamera.whiteBalanceMode = value; } }; /** * Set HDR mode. * * HDR is a scene mode, so we * transform the hdr value to * the appropriate scene value. * * @param {String} value * @public */ Camera.prototype.setHDR = function(value){ debug('set hdr: %s', value); if (!value) { return; } var scene = value === 'on' ? 'hdr' : 'auto'; this.setSceneMode(scene); }; /** * Set scene mode. * * @param {String} value * @public */ Camera.prototype.setSceneMode = function(value){ var modes = this.mozCamera.capabilities.sceneModes; if (modes.indexOf(value) > -1) { this.mozCamera.sceneMode = value; } }; /** * Check if the hardware supports zoom. * * @return {Boolean} */ 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()); // Calculate the maximum amount of zoom that the hardware will // perform. This calculation is determined by taking the maximum // supported preview size *width* and dividing by the current preview // size *width*. 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.0; } return zoomRatios[0]; }; Camera.prototype.getMaximumZoom = function() { var zoomRatios = this.mozCamera.capabilities.zoomRatios; if (zoomRatios.length === 0) { return 1.0; } 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); // Stop here if we're already waiting for // `mozCamera.zoom` to be updated. if (this.zoomChangeTimeout) { return; } var self = this; // Throttle to prevent hammering the Camera API. 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.0; } return zoom / maxHardwareZoom; }; /** * Retrieves the angle of orientation of the camera hardware's * image sensor. This value is calculated as the angle that the * camera image needs to be rotated (clockwise) so that it appears * correctly on the display in the device's natural (portrait) * orientation * * Reference: * http://developer.android.com/reference/android/hardware/Camera.CameraInfo.html#orientation * * @return {Number} * @public */ Camera.prototype.getSensorAngle = function() { return this.mozCamera && this.mozCamera.sensorAngle; }; /** * A central place to indicate * the camera is 'busy'. * * @private */ Camera.prototype.busy = function(type) { debug('busy %s', type || ''); this.isBusy = true; this.emit('busy', type); clearTimeout(this.readyTimeout); }; /** * A central place to indicate * the camera is 'ready'. * * @private */ 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: '' }); }); 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'; /** * Dependencies */ var stopRecordingEvent = require('stop-recording-event'); var NotificationView = require('views/notification'); var LoadingView = require('views/loading-screen'); var orientation = require('lib/orientation'); var bindAll = require('lib/bind-all'); var debug = require('debug')('app'); var Pinch = require('lib/pinch'); var bind = require('lib/bind'); var model = require('model'); /** * Exports */ module.exports = App; /** * Mixin `Model` API */ model(App.prototype); /** * Initialize a new `App` * * Options: * * - `root` The node to inject content into * * @param {Object} options * @constructor */ function App(options) { debug('initialize'); bindAll(this); this.views = {}; this.dynamicLazy = {}; this.el = options.el; this.win = options.win; this.doc = options.doc; this.perf = options.perf || {}; this.pinch = options.pinch || new Pinch(this.el); // Test hook this.require = options.require || window.requirejs; // Test hook this.LoadingView = options.LoadingView || LoadingView; // test hook this.orientation = options.orientation || orientation; // test hook this.inSecureMode = (this.win.location.hash === '#secure'); this.controllers = options.controllers; this.geolocation = options.geolocation; this.settings = options.settings; this.camera = options.camera; this.activity = {}; this.sounds = options.sounds; debug('initialized'); } /** * Runs all the methods to boot the app. * * The loading screen is shown until the * camera is 'ready', then it is taken down. * * @public */ App.prototype.boot = function() { debug('boot'); if (this.booted) { return; } this.showSpinner('requestingCamera'); this.bindEvents(); this.initializeViews(); this.runControllers(); // PERFORMANCE MARKER (1): navigationLoaded // Designates that the app's *core* chrome or navigation interface // exists in the DOM and is marked as ready to be displayed. // PERFORMANCE MARKER (2): navigationInteractive // Designates that the app's *core* chrome or navigation interface // has its events bound and is ready for user interaction. window.performance.mark('navigationLoaded'); window.performance.mark('navigationInteractive'); this.injectViews(); this.booted = true; debug('booted'); }; App.prototype.dispatchEvent = function(name) { this.win.dispatchEvent(new CustomEvent(name)); }; /** * Runs controllers to glue all * the parts of the app together. * * @private */ App.prototype.runControllers = function() { debug('run controllers'); this.controllers.overlay(this); this.controllers.battery(this); this.controllers.settings(this); this.controllers.activity(this); this.controllers.camera(this); this.controllers.viewfinder(this); this.controllers.hud(this); this.controllers.controls(this); this.controllers.storage(this); debug('controllers run'); }; /** * Load an extra lazy controller. Used for * controllers only needed based on configuration. * * @param {String} controller */ App.prototype.loadLazyController = function(controller) { if (!this.criticalPathDone) { this.controllers.lazy.push(controller); return; } if (!this.dynamicLazy[controller]) { this.dynamicLazy[controller] = this.loadLazyControllers([controller]); } return this.dynamicLazy[controller]; }; /** * Load and run all the lazy controllers. * * @param {Function} done */ App.prototype.loadLazyControllers = function(controllers) { debug('load lazy controllers'); var self = this; return new Promise(function(resolve, reject) { self.require(controllers, function() { [].forEach.call(arguments, function(controller) { controller(self); }); debug('controllers loaded'); self.emit('lazyloaded'); resolve(); }); }); }; /** * Initialize views. * * @private */ App.prototype.initializeViews = function() { debug('initializing views'); this.views.notification = new NotificationView(); debug('views initialized'); }; /** * Put views in the DOM. * * @private */ App.prototype.injectViews = function() { debug('injecting views'); this.views.notification.appendTo(this.el); debug('views injected'); }; /** * Attaches event handlers. * * @private */ App.prototype.bindEvents = function() { debug('binding events'); // App this.once('storage:checked:healthy', this.geolocationWatch); this.on('storage:checked', function() { this.storageChecked = true; }.bind(this)); this.once('viewfinder:visible', this.onCriticalPathDone); this.once('camera:error', this.onCriticalPathDone); this.once('activity', this.onActivity); this.once('newthumbnail', this.onNewThumbnail); this.once('preview', this.onPreview); this.once('camera:willchange', this.onWillChange); this.on('camera:willchange', this.firer('busy')); this.on('ready', this.clearSpinner); this.on('visible', this.onVisible); this.on('hidden', this.onHidden); this.on('reboot', this.onReboot); this.on('busy', this.onBusy); // Settings if (this.settings.countdown.selected('key') !== 'off') { this.onCountdown(); } else { this.settings.countdown.once('change:selected', this.onCountdown); } // Pinch this.pinch.on('changed', this.firer('pinch:changed')); this.on('previewgallery:opened', this.pinch.disable); this.on('previewgallery:closed', this.pinch.enable); this.on('settings:opened', this.pinch.disable); this.on('settings:closed', this.pinch.enable); // DOM bind(this.doc, 'visibilitychange', this.onVisibilityChange); // we bind to window.onlocalized in order not to depend // on l10n.js loading (which is lazy). See bug 999132 bind(this.doc, 'DOMRetranslated', this.firer('localized')); bind(this.win, 'beforeunload', this.onBeforeUnload); bind(this.win, 'keydown', this.onKeyDown); bind(this.el, 'click', this.onClick); debug('events bound'); }; /** * Tasks to run when the * app becomes visible. * * Check the storage again as users * may have made changes since the * app was minimised */ App.prototype.onVisible = function() { this.geolocationWatch(); this.orientation.start(); this.orientation.lock(); debug('visible'); }; /** * Tasks to run when the * app is minimised/hidden. * * @private */ App.prototype.onHidden = function() { this.geolocation.stopWatching(); this.orientation.stop(); debug('hidden'); }; /** * Reboots the application * * @private */ App.prototype.onReboot = function() { debug('reboot'); window.location.reload(); }; /** * Emit a click event that other * modules can listen to. * * @private */ App.prototype.onClick = function() { debug('click'); this.emit('click'); }; /** * Log when critical path has completed. * * @private */ App.prototype.onCriticalPathDone = function() { if (this.criticalPathDone) { return; } debug('critical path done'); // PERFORMANCE MARKER (3): visuallyLoaded // Designates that the app is visually loaded (e.g.: all of the // "above-the-fold" content exists in the DOM and is marked as // ready to be displayed). window.performance.mark('visuallyLoaded'); // Load non-critical modules this.listenForStopRecordingEvent(); this.loadLazyModules(); this.perf.criticalPath = Date.now(); this.criticalPathDone = true; this.emit('criticalpathdone'); }; /** * Lazy load activity dependencies. * * @private */ App.prototype.onActivity = function() { this.loadLazyController('controllers/confirm'); }; App.prototype.onCountdown = function() { this.loadLazyController('controllers/countdown'); }; App.prototype.onNewThumbnail = function() { this.loadLazyController('controllers/preview-gallery'); }; App.prototype.onPreview = function() { var self = this; this.emit('busy', 'lazyLoading'); this.loadLazyController('controllers/preview-gallery').then(function() { self.emit('preview'); self.emit('ready'); }); }; App.prototype.onWillChange = function() { this.loadLazyController('controllers/recording-timer'); }; App.prototype.loadLazyModules = function() { debug('load lazy modules'); var self = this; var load = this.loadLazyControllers(this.controllers.lazy); var storage = new Promise(function(resolve, reject) { if (self.storageChecked) { resolve(); } else { self.once('storage:checked', resolve); } }); // All done Promise.all([load, storage]).then(function() { debug('app fully loaded'); // PERFORMANCE MARKER (4): contentInteractive // Designates that the app has its events bound for the minimum // set of functionality to allow the user to interact with the // "above-the-fold" content. window.performance.mark('contentInteractive'); self.loaded = true; self.emit('loaded'); self.perf.loaded = Date.now(); self.logPerf(); // PERFORMANCE MARKER (5): fullyLoaded // Designates that the app is *completely* loaded and all relevant // "below-the-fold" content exists in the DOM, is marked visible, // has its events bound and is ready for user interaction. All // required startup background processing should be complete. window.performance.mark('fullyLoaded'); }); }; App.prototype.logPerf =function() { var timing = window.performance.timing; console.log('first module: %s', this.perf.firstModule - this.perf.jsStarted + 'ms'); console.log('critical-path: %s', this.perf.criticalPath - timing.domLoading + 'ms'); console.log('app-fully-loaded: %s', this.perf.loaded - timing.domLoading + 'ms'); }; /** * Begins watching location if not within * a pending activity and the app isn't * currently hidden. * * Watching is delayed by the `promptDelay` * defined in settings. * * @private */ App.prototype.geolocationWatch = function() { var shouldWatch = !this.activity.pick && !this.hidden; if (shouldWatch) { this.geolocation.watch(); } }; /** * Responds to the `visibilitychange` * event, emitting useful app events * that allow us to perform relate * work elsewhere. * * @private */ App.prototype.onVisibilityChange = function() { this.hidden = this.doc.hidden; this.emit(this.hidden ? 'hidden' : 'visible'); }; /** * Runs just before the * app is destroyed. * * @private */ App.prototype.onBeforeUnload = function() { this.emit('beforeunload'); debug('beforeunload'); }; /** * Shows the loading screen after the * number of ms defined in config.js * * @param {String} type * @private */ App.prototype.showSpinner = function(key) { debug('show loading type: %s', key); this.busy = true; var view = this.views.loading; if (view) { return; } var ms = this.settings.spinnerTimeouts.get(key) || 0; var self = this; clearTimeout(this.spinnerTimeout); this.spinnerTimeout = setTimeout(function() { self.views.loading = new self.LoadingView(); self.views.loading.appendTo(self.el).show(); debug('loading shown'); }, ms); }; /** * Clears the loadings screen, or * any pending loading screen. * * @private */ App.prototype.clearSpinner = function() { debug('clear loading'); this.busy = false; var view = this.views.loading; clearTimeout(this.spinnerTimeout); if (!view) { return; } view.hide(view.destroy); this.views.loading = null; }; /** * When the camera indicates it's busy it * sometimes passes a `type` string. When * this type matches one of our keys in the * `spinnerTimeouts` config, we display the * loading screen passing on the type. * * @param {String} type * @private */ App.prototype.onBusy = function(type) { debug('camera busy, type: %s', type); this.busy = true; var delay = this.settings.spinnerTimeouts.get(type); if (delay) { this.showSpinner(type); } }; /** * If the system app is opening an attention screen (because * of an incoming call or an alarm, e.g.) and if we are * currently recording a video then we need to stop recording * before the ringer or alarm starts sounding. We will be sent * to the background shortly after this and will stop recording * when that happens, but by that time it is too late and we * have already recorded some sound. See bugs 995540 and 1006200. * * Similarly, if the user presses the Home button or switches to * another app while recording, we need to stop recording right away, * even if the system is overloaded and we don't get a visiblitychange * event right away. See bug 1046167. * * To make this work, we rely on shared/js/stop_recording_event.js which * abuses the settings API to allow the system app to broadcast a "you * will soon be hidden" message to any certified apps that care. There * ought to be a better way, but this is a quick way to fix a * last-minute release blocker. * * @private */ App.prototype.listenForStopRecordingEvent = function() { debug('listen for stop recording events'); // Start the module that generates // the stoprecording events and listen // for those custom DOM events and emit // them using our internal event emitter stopRecordingEvent.start(); addEventListener('stoprecording', this.firer('stoprecording')); }; /** * When the device's hardware keys * are pressed we emit a global * app event that other controllers * can respond to. * * TIP: Check config.js for the map * of key names to types. * * @param {Event} e * @private */ App.prototype.onKeyDown = function(e) { var key = e.key.toLowerCase(); var type = this.settings.keyDownEvents.get(key); if (type) { this.emit('keydown:' + type, e); } }; }); define("controllers/overlay",["require", "exports", "module", "debug", "lib/bind-all"],function(require, exports, module) { 'use strict'; /** * Dependencies */ var debug = require('debug')('controller:overlay'); var bindAll = require('lib/bind-all'); /** * Exports */ module.exports = function(app) { return new OverlayController(app); }; module.exports.OverlayController = OverlayController; /** * Initialize a new `OverlayController` * * @param {App} app */ function OverlayController(app) { bindAll(this); this.overlays = {}; this.app = app; this.require = app.require; this.activity = app.activity; this.bindEvents(); debug('initialized'); } OverlayController.prototype.bindEvents = function() { this.app.on('storage:changed', this.onStorageChanged); this.app.on('change:batteryStatus', this.onBatteryChanged); this.app.on('camera:requesting', this.onCameraRequesting); this.app.on('camera:error', this.onCameraError); }; /** * Respond to storage `statechange` * events by inserting or destroying * overlays from the app. * * @param {String} value ['nospace'|'shared'|'unavailable'|'available'] */ OverlayController.prototype.onStorageChanged = function(state) { this.updateOverlay('storage', state !== 'available', state); }; /** * Respond to battery `statuschange` * events by inserting or destroying * overlays from the app. * * @param {String} status ['shutdown'|'critical'|'verylow'|'low'] */ OverlayController.prototype.onBatteryChanged = function(state) { this.updateOverlay('battery', state === 'shutdown', state); }; /** * Respond to camera `requesting` * events by destroying overlays * from the app. * * @param {String} state ['start'|'success'|'fail'] */ OverlayController.prototype.onCameraRequesting = function() { this.updateOverlay('cameraError', false); }; /** * Respond to camera `error` * events by inserting overlays * into the app. * * @param {String} state ['start'|'success'|'fail'] */ OverlayController.prototype.onCameraError = function(type) { this.updateOverlay('cameraError', true, type); }; OverlayController.prototype.updateOverlay = function(type, enabled, reason) { debug('\'%s/%s\' overlay %s', type, reason, enabled); var overlay = this.overlays[type]; if (!overlay) { overlay = this.overlays[type] = { id: 0, }; } if (overlay.view) { overlay.view.destroy(); delete overlay.view; } var id = ++overlay.id; if (!enabled) { return; } var self = this; this.require(['views/overlay'], function(OverlayView) { if (id !== overlay.id) { return; } var closable = self.activity.pick || 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) { 'use strict'; /** * Dependencies */ var debug = require('debug')('controller:battery'); var bindAll = require('lib/bind-all'); var bind = require('lib/bind'); /** * Exports */ module.exports = function(app) { return new BatteryController(app); }; module.exports.BatteryController = BatteryController; /** * Initialize a new `BatteryController` * * @param {Object} options */ function BatteryController(app) { bindAll(this); this.app = app; this.battery = app.battery || navigator.battery || navigator.mozBattery; if (!this.battery) { this.app.set('batteryStatus', 'unknown'); return; } this.levels = app.settings.battery.get('levels'); this.notification = app.views.notification; this.bindEvents(); this.updateStatus(); debug('initialized'); } /** * Bind callbacks to required events. * * @private */ BatteryController.prototype.bindEvents = function() { bind(this.battery, 'levelchange', this.updateStatus); bind(this.battery, 'chargingchange', this.updateStatus); this.app.on('change:batteryStatus', this.onStatusChange); this.app.on('change:recording', this.updatePowerSave); var mozSettings = navigator.mozSettings; mozSettings.addObserver('powersave.enabled', this.onPowerSaveChange); mozSettings.createLock().get('powersave.enabled').then( this.onPowerSaveChange); }; /** * Callback from settings when the power save state changes. * * @private */ BatteryController.prototype.onPowerSaveChange = function(values) { var value; if (values.settingValue !== undefined) { value = values.settingValue; } else { value = values['powersave.enabled']; } this.powerSaveEnabled = value; this.updatePowerSave(); }; /** * Emits powersave event if state has changed. * * @private */ BatteryController.prototype.updatePowerSave = function() { var state = this.powerSaveEnabled && !this.app.get('recording'); if (this.powerSave === state) { return; } this.powerSave = state; debug('power save: ' + state); this.app.emit('battery:powersave', state); }; /** * Map of status keys to message. * * @type {Object} * @private */ BatteryController.prototype.notifications = { low: { text: 'battery-low-text', attrs: { 'data-icon': 'battery-3', 'data-l10n-id': 'battery-low-indicator' }, }, verylow: { text: 'battery-verylow-text', attrs: { 'data-icon': 'battery-1', 'data-l10n-id': 'battery-verylow-indicator' } }, critical: { text: 'battery-critical-text', attrs: { 'data-icon': 'battery-1', 'data-l10n-id': 'battery-critical-indicator' }, persistent: true } }; /** * Updates app `batteryStatus` and * manages battery notifications. * * @private */ BatteryController.prototype.updateStatus = function () { var previous = this.app.get('batteryStatus'); var current = this.getStatus(this.battery); if (current !== previous) { this.app.set('batteryStatus', current); } }; /** * Returns a status key derived * from the given `battery` object. * * @param {Battery} battery * @return {String} * @private */ BatteryController.prototype.getStatus = function(battery) { var level = Math.round(battery.level * 100); var levels = this.levels; if (battery.charging) { return 'charging'; } else if (level <= levels.shutdown) { return 'shutdown'; } else if (level <= levels.critical) { return 'critical'; } else if (level <= levels.verylow) { return 'verylow'; } else if (level <= levels.low) { return 'low'; } else { return 'healthy'; } }; BatteryController.prototype.onStatusChange = function(status) { this.clearLastNotification(); this.displayNotification(status); }; BatteryController.prototype.displayNotification = function(status) { var notification = this.notifications[status]; if (!notification) { return; } this.lastNotification = this.notification.display({ text: notification.text, className: notification.className, attrs: notification.attrs, persistent: notification.persistent }); }; BatteryController.prototype.clearLastNotification = function() { this.notification.clear(this.lastNotification); }; }); 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 '
' + '' + ''; } }); }); 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 = ''; var title = ''; var html; if (hdrDeactivated) { html = title + ' ' + optionTitle + '