VulpesOS/gaia/apps/camera/js/app.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

523 lines
13 KiB
JavaScript
Executable file

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