Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
523
gaia/apps/camera/js/app.js
Executable file
523
gaia/apps/camera/js/app.js
Executable file
|
|
@ -0,0 +1,523 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var stopRecordingEvent = require('stop-recording-event');
|
||||
var NotificationView = require('views/notification');
|
||||
var LoadingView = require('views/loading-screen');
|
||||
var orientation = require('lib/orientation');
|
||||
var bindAll = require('lib/bind-all');
|
||||
var debug = require('debug')('app');
|
||||
var Pinch = require('lib/pinch');
|
||||
var bind = require('lib/bind');
|
||||
var model = require('model');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = App;
|
||||
|
||||
/**
|
||||
* Mixin `Model` API
|
||||
*/
|
||||
|
||||
model(App.prototype);
|
||||
|
||||
/**
|
||||
* Initialize a new `App`
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - `root` The node to inject content into
|
||||
*
|
||||
* @param {Object} options
|
||||
* @constructor
|
||||
*/
|
||||
function App(options) {
|
||||
debug('initialize');
|
||||
bindAll(this);
|
||||
this.views = {};
|
||||
this.dynamicLazy = {};
|
||||
this.el = options.el;
|
||||
this.win = options.win;
|
||||
this.doc = options.doc;
|
||||
this.perf = options.perf || {};
|
||||
this.pinch = options.pinch || new Pinch(this.el); // Test hook
|
||||
this.require = options.require || window.requirejs; // Test hook
|
||||
this.LoadingView = options.LoadingView || LoadingView; // test hook
|
||||
this.orientation = options.orientation || orientation; // test hook
|
||||
this.inSecureMode = (this.win.location.hash === '#secure');
|
||||
this.controllers = options.controllers;
|
||||
this.geolocation = options.geolocation;
|
||||
this.settings = options.settings;
|
||||
this.camera = options.camera;
|
||||
this.activity = {};
|
||||
this.sounds = options.sounds;
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs all the methods to boot the app.
|
||||
*
|
||||
* The loading screen is shown until the
|
||||
* camera is 'ready', then it is taken down.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
App.prototype.boot = function() {
|
||||
debug('boot');
|
||||
if (this.booted) { return; }
|
||||
this.showSpinner('requestingCamera');
|
||||
this.bindEvents();
|
||||
this.initializeViews();
|
||||
this.runControllers();
|
||||
|
||||
// PERFORMANCE MARKER (1): navigationLoaded
|
||||
// Designates that the app's *core* chrome or navigation interface
|
||||
// exists in the DOM and is marked as ready to be displayed.
|
||||
// PERFORMANCE MARKER (2): navigationInteractive
|
||||
// Designates that the app's *core* chrome or navigation interface
|
||||
// has its events bound and is ready for user interaction.
|
||||
window.performance.mark('navigationLoaded');
|
||||
window.performance.mark('navigationInteractive');
|
||||
|
||||
this.injectViews();
|
||||
this.booted = true;
|
||||
debug('booted');
|
||||
};
|
||||
|
||||
App.prototype.dispatchEvent = function(name) {
|
||||
this.win.dispatchEvent(new CustomEvent(name));
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs controllers to glue all
|
||||
* the parts of the app together.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.runControllers = function() {
|
||||
debug('run controllers');
|
||||
this.controllers.overlay(this);
|
||||
this.controllers.battery(this);
|
||||
this.controllers.settings(this);
|
||||
this.controllers.activity(this);
|
||||
this.controllers.camera(this);
|
||||
this.controllers.viewfinder(this);
|
||||
this.controllers.hud(this);
|
||||
this.controllers.controls(this);
|
||||
this.controllers.storage(this);
|
||||
debug('controllers run');
|
||||
};
|
||||
|
||||
/**
|
||||
* Load an extra lazy controller. Used for
|
||||
* controllers only needed based on configuration.
|
||||
*
|
||||
* @param {String} controller
|
||||
*/
|
||||
App.prototype.loadLazyController = function(controller) {
|
||||
if (!this.criticalPathDone) {
|
||||
this.controllers.lazy.push(controller);
|
||||
return;
|
||||
}
|
||||
if (!this.dynamicLazy[controller]) {
|
||||
this.dynamicLazy[controller] = this.loadLazyControllers([controller]);
|
||||
}
|
||||
return this.dynamicLazy[controller];
|
||||
};
|
||||
|
||||
/**
|
||||
* Load and run all the lazy controllers.
|
||||
*
|
||||
* @param {Function} done
|
||||
*/
|
||||
App.prototype.loadLazyControllers = function(controllers) {
|
||||
debug('load lazy controllers');
|
||||
var self = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
self.require(controllers, function() {
|
||||
[].forEach.call(arguments, function(controller) { controller(self); });
|
||||
debug('controllers loaded');
|
||||
self.emit('lazyloaded');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize views.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.initializeViews = function() {
|
||||
debug('initializing views');
|
||||
this.views.notification = new NotificationView();
|
||||
debug('views initialized');
|
||||
};
|
||||
|
||||
/**
|
||||
* Put views in the DOM.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.injectViews = function() {
|
||||
debug('injecting views');
|
||||
this.views.notification.appendTo(this.el);
|
||||
debug('views injected');
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches event handlers.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.bindEvents = function() {
|
||||
debug('binding events');
|
||||
|
||||
// App
|
||||
this.once('storage:checked:healthy', this.geolocationWatch);
|
||||
this.on('storage:checked', function() { this.storageChecked = true; }.bind(this));
|
||||
this.once('viewfinder:visible', this.onCriticalPathDone);
|
||||
this.once('camera:error', this.onCriticalPathDone);
|
||||
this.once('activity', this.onActivity);
|
||||
this.once('newthumbnail', this.onNewThumbnail);
|
||||
this.once('preview', this.onPreview);
|
||||
this.once('camera:willchange', this.onWillChange);
|
||||
this.on('camera:willchange', this.firer('busy'));
|
||||
this.on('ready', this.clearSpinner);
|
||||
this.on('visible', this.onVisible);
|
||||
this.on('hidden', this.onHidden);
|
||||
this.on('reboot', this.onReboot);
|
||||
this.on('busy', this.onBusy);
|
||||
|
||||
// Settings
|
||||
if (this.settings.countdown.selected('key') !== 'off') {
|
||||
this.onCountdown();
|
||||
} else {
|
||||
this.settings.countdown.once('change:selected', this.onCountdown);
|
||||
}
|
||||
|
||||
// Pinch
|
||||
this.pinch.on('changed', this.firer('pinch:changed'));
|
||||
this.on('previewgallery:opened', this.pinch.disable);
|
||||
this.on('previewgallery:closed', this.pinch.enable);
|
||||
this.on('settings:opened', this.pinch.disable);
|
||||
this.on('settings:closed', this.pinch.enable);
|
||||
|
||||
// DOM
|
||||
bind(this.doc, 'visibilitychange', this.onVisibilityChange);
|
||||
|
||||
// we bind to window.onlocalized in order not to depend
|
||||
// on l10n.js loading (which is lazy). See bug 999132
|
||||
bind(this.doc, 'DOMRetranslated', this.firer('localized'));
|
||||
|
||||
bind(this.win, 'beforeunload', this.onBeforeUnload);
|
||||
bind(this.win, 'keydown', this.onKeyDown);
|
||||
bind(this.el, 'click', this.onClick);
|
||||
debug('events bound');
|
||||
};
|
||||
|
||||
/**
|
||||
* Tasks to run when the
|
||||
* app becomes visible.
|
||||
*
|
||||
* Check the storage again as users
|
||||
* may have made changes since the
|
||||
* app was minimised
|
||||
*/
|
||||
App.prototype.onVisible = function() {
|
||||
this.geolocationWatch();
|
||||
this.orientation.start();
|
||||
this.orientation.lock();
|
||||
debug('visible');
|
||||
};
|
||||
|
||||
/**
|
||||
* Tasks to run when the
|
||||
* app is minimised/hidden.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.onHidden = function() {
|
||||
this.geolocation.stopWatching();
|
||||
this.orientation.stop();
|
||||
debug('hidden');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Reboots the application
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.onReboot = function() {
|
||||
debug('reboot');
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Emit a click event that other
|
||||
* modules can listen to.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.onClick = function() {
|
||||
debug('click');
|
||||
this.emit('click');
|
||||
};
|
||||
|
||||
/**
|
||||
* Log when critical path has completed.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.onCriticalPathDone = function() {
|
||||
if (this.criticalPathDone) { return; }
|
||||
debug('critical path done');
|
||||
// PERFORMANCE MARKER (3): visuallyLoaded
|
||||
// Designates that the app is visually loaded (e.g.: all of the
|
||||
// "above-the-fold" content exists in the DOM and is marked as
|
||||
// ready to be displayed).
|
||||
window.performance.mark('visuallyLoaded');
|
||||
|
||||
// Load non-critical modules
|
||||
this.listenForStopRecordingEvent();
|
||||
this.loadLazyModules();
|
||||
this.perf.criticalPath = Date.now();
|
||||
this.criticalPathDone = true;
|
||||
this.emit('criticalpathdone');
|
||||
};
|
||||
|
||||
/**
|
||||
* Lazy load activity dependencies.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.onActivity = function() {
|
||||
this.loadLazyController('controllers/confirm');
|
||||
};
|
||||
|
||||
App.prototype.onCountdown = function() {
|
||||
this.loadLazyController('controllers/countdown');
|
||||
};
|
||||
|
||||
App.prototype.onNewThumbnail = function() {
|
||||
this.loadLazyController('controllers/preview-gallery');
|
||||
};
|
||||
|
||||
App.prototype.onPreview = function() {
|
||||
var self = this;
|
||||
this.emit('busy', 'lazyLoading');
|
||||
this.loadLazyController('controllers/preview-gallery').then(function() {
|
||||
self.emit('preview');
|
||||
self.emit('ready');
|
||||
});
|
||||
};
|
||||
|
||||
App.prototype.onWillChange = function() {
|
||||
this.loadLazyController('controllers/recording-timer');
|
||||
};
|
||||
|
||||
App.prototype.loadLazyModules = function() {
|
||||
debug('load lazy modules');
|
||||
var self = this;
|
||||
|
||||
var load = this.loadLazyControllers(this.controllers.lazy);
|
||||
var storage = new Promise(function(resolve, reject) {
|
||||
if (self.storageChecked) { resolve(); }
|
||||
else { self.once('storage:checked', resolve); }
|
||||
});
|
||||
|
||||
// All done
|
||||
Promise.all([load, storage]).then(function() {
|
||||
debug('app fully loaded');
|
||||
|
||||
// PERFORMANCE MARKER (4): contentInteractive
|
||||
// Designates that the app has its events bound for the minimum
|
||||
// set of functionality to allow the user to interact with the
|
||||
// "above-the-fold" content.
|
||||
window.performance.mark('contentInteractive');
|
||||
|
||||
self.loaded = true;
|
||||
self.emit('loaded');
|
||||
self.perf.loaded = Date.now();
|
||||
self.logPerf();
|
||||
|
||||
// PERFORMANCE MARKER (5): fullyLoaded
|
||||
// Designates that the app is *completely* loaded and all relevant
|
||||
// "below-the-fold" content exists in the DOM, is marked visible,
|
||||
// has its events bound and is ready for user interaction. All
|
||||
// required startup background processing should be complete.
|
||||
window.performance.mark('fullyLoaded');
|
||||
});
|
||||
};
|
||||
|
||||
App.prototype.logPerf =function() {
|
||||
var timing = window.performance.timing;
|
||||
console.log('first module: %s',
|
||||
this.perf.firstModule - this.perf.jsStarted + 'ms');
|
||||
console.log('critical-path: %s',
|
||||
this.perf.criticalPath - timing.domLoading + 'ms');
|
||||
console.log('app-fully-loaded: %s',
|
||||
this.perf.loaded - timing.domLoading + 'ms');
|
||||
};
|
||||
|
||||
/**
|
||||
* Begins watching location if not within
|
||||
* a pending activity and the app isn't
|
||||
* currently hidden.
|
||||
*
|
||||
* Watching is delayed by the `promptDelay`
|
||||
* defined in settings.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.geolocationWatch = function() {
|
||||
var shouldWatch = !this.activity.pick && !this.hidden;
|
||||
if (shouldWatch) { this.geolocation.watch(); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Responds to the `visibilitychange`
|
||||
* event, emitting useful app events
|
||||
* that allow us to perform relate
|
||||
* work elsewhere.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.onVisibilityChange = function() {
|
||||
this.hidden = this.doc.hidden;
|
||||
this.emit(this.hidden ? 'hidden' : 'visible');
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs just before the
|
||||
* app is destroyed.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.onBeforeUnload = function() {
|
||||
this.emit('beforeunload');
|
||||
debug('beforeunload');
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows the loading screen after the
|
||||
* number of ms defined in config.js
|
||||
*
|
||||
* @param {String} type
|
||||
* @private
|
||||
*/
|
||||
App.prototype.showSpinner = function(key) {
|
||||
debug('show loading type: %s', key);
|
||||
this.busy = true;
|
||||
|
||||
var view = this.views.loading;
|
||||
if (view) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ms = this.settings.spinnerTimeouts.get(key) || 0;
|
||||
var self = this;
|
||||
|
||||
clearTimeout(this.spinnerTimeout);
|
||||
this.spinnerTimeout = setTimeout(function() {
|
||||
self.views.loading = new self.LoadingView();
|
||||
self.views.loading.appendTo(self.el).show();
|
||||
debug('loading shown');
|
||||
}, ms);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the loadings screen, or
|
||||
* any pending loading screen.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.clearSpinner = function() {
|
||||
debug('clear loading');
|
||||
this.busy = false;
|
||||
var view = this.views.loading;
|
||||
clearTimeout(this.spinnerTimeout);
|
||||
if (!view) { return; }
|
||||
view.hide(view.destroy);
|
||||
this.views.loading = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* When the camera indicates it's busy it
|
||||
* sometimes passes a `type` string. When
|
||||
* this type matches one of our keys in the
|
||||
* `spinnerTimeouts` config, we display the
|
||||
* loading screen passing on the type.
|
||||
*
|
||||
* @param {String} type
|
||||
* @private
|
||||
*/
|
||||
App.prototype.onBusy = function(type) {
|
||||
debug('camera busy, type: %s', type);
|
||||
this.busy = true;
|
||||
var delay = this.settings.spinnerTimeouts.get(type);
|
||||
if (delay) { this.showSpinner(type); }
|
||||
};
|
||||
|
||||
/**
|
||||
* If the system app is opening an attention screen (because
|
||||
* of an incoming call or an alarm, e.g.) and if we are
|
||||
* currently recording a video then we need to stop recording
|
||||
* before the ringer or alarm starts sounding. We will be sent
|
||||
* to the background shortly after this and will stop recording
|
||||
* when that happens, but by that time it is too late and we
|
||||
* have already recorded some sound. See bugs 995540 and 1006200.
|
||||
*
|
||||
* Similarly, if the user presses the Home button or switches to
|
||||
* another app while recording, we need to stop recording right away,
|
||||
* even if the system is overloaded and we don't get a visiblitychange
|
||||
* event right away. See bug 1046167.
|
||||
*
|
||||
* To make this work, we rely on shared/js/stop_recording_event.js which
|
||||
* abuses the settings API to allow the system app to broadcast a "you
|
||||
* will soon be hidden" message to any certified apps that care. There
|
||||
* ought to be a better way, but this is a quick way to fix a
|
||||
* last-minute release blocker.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
App.prototype.listenForStopRecordingEvent = function() {
|
||||
debug('listen for stop recording events');
|
||||
|
||||
// Start the module that generates
|
||||
// the stoprecording events and listen
|
||||
// for those custom DOM events and emit
|
||||
// them using our internal event emitter
|
||||
stopRecordingEvent.start();
|
||||
addEventListener('stoprecording', this.firer('stoprecording'));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* When the device's hardware keys
|
||||
* are pressed we emit a global
|
||||
* app event that other controllers
|
||||
* can respond to.
|
||||
*
|
||||
* TIP: Check config.js for the map
|
||||
* of key names to types.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @private
|
||||
*/
|
||||
App.prototype.onKeyDown = function(e) {
|
||||
var key = e.key.toLowerCase();
|
||||
var type = this.settings.keyDownEvents.get(key);
|
||||
if (type) { this.emit('keydown:' + type, e); }
|
||||
};
|
||||
|
||||
|
||||
});
|
||||
377
gaia/apps/camera/js/config/config.js
Executable file
377
gaia/apps/camera/js/config/config.js
Executable file
|
|
@ -0,0 +1,377 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
// This remaining globals are required by external dependencies of camera.
|
||||
// shared/js/media/jpeg_metadata_parser.js
|
||||
// shared/js/media/media_frame.js
|
||||
globals : {
|
||||
// The maximum picture size that camera is allowed to take
|
||||
CONFIG_MAX_IMAGE_PIXEL_SIZE: 24 * 1024 * 1024,
|
||||
CONFIG_MAX_SNAPSHOT_PIXEL_SIZE: 24 * 1024 * 1024,
|
||||
|
||||
// Size of the exif preview embeded in images taken by camera
|
||||
CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH: 0,
|
||||
CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT: 0
|
||||
},
|
||||
|
||||
zoom: {
|
||||
disabled: false,
|
||||
|
||||
// The viewfinder preview stream should automatically
|
||||
// reflect the current zoom value. However, on some
|
||||
// devices, the viewfinder needs to be scaled by the
|
||||
// application. Set this flag if the preview stream
|
||||
// does not visually reflect the zoom value properly.
|
||||
useZoomPreviewAdjustment: false
|
||||
},
|
||||
|
||||
focus: {
|
||||
// Set this properties to false if you
|
||||
// want to disable focus modes
|
||||
// even on hardware that supports them
|
||||
// -----------------------------------
|
||||
// The camera will be continously focusing
|
||||
// on a point of the scene. It is the center of the image
|
||||
// unless touch to focus is enabled and the user selects a
|
||||
// different region.
|
||||
continuousAutoFocus: true,
|
||||
// The user can select the area of the image
|
||||
// where the camera is going to try to focus the scene.
|
||||
touchFocus: true,
|
||||
// The camera detects faces and tries to focus
|
||||
// on them.
|
||||
faceDetection: true
|
||||
},
|
||||
|
||||
previewGallery: {
|
||||
// Dimensions for thumbnail image (will automatically be
|
||||
// multiplied by the devicePixelRatio)
|
||||
thumbnailWidth: 54,
|
||||
thumbnailHeight: 54
|
||||
},
|
||||
|
||||
viewfinder: {
|
||||
scaleType: 'fill',
|
||||
// Used to adjust sensitivity for pinch-to-zoom gesture
|
||||
// (smaller values = more sensitivity)
|
||||
zoomGestureSensitivity: 0.425
|
||||
},
|
||||
|
||||
battery: {
|
||||
levels: {
|
||||
low: 15,
|
||||
verylow: 10,
|
||||
critical: 6,
|
||||
shutdown: 5,
|
||||
healthy: 100
|
||||
}
|
||||
},
|
||||
|
||||
sounds: {
|
||||
list: [
|
||||
{
|
||||
name: 'shutter',
|
||||
url: './resources/sounds/shutter.opus',
|
||||
setting: 'camera.sound.enabled'
|
||||
},
|
||||
{
|
||||
name: 'countdown',
|
||||
url: './resources/sounds/countdown.opus',
|
||||
setting: 'camera.sound.enabled'
|
||||
},
|
||||
{
|
||||
name: 'recordingStart',
|
||||
url: './resources/sounds/camcorder_start.opus',
|
||||
setting: 'camera.sound.enabled'
|
||||
},
|
||||
{
|
||||
name: 'recordingEnd',
|
||||
url: './resources/sounds/camcorder_end.opus',
|
||||
setting: 'camera.sound.enabled'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
keyDownEvents: {
|
||||
camera: 'capture',
|
||||
volumeup: 'capture',
|
||||
volumedown: 'capture',
|
||||
mozcamerafocusadjust: 'focus',
|
||||
},
|
||||
|
||||
activity: {
|
||||
|
||||
// The amount to scale pixelSize derived from
|
||||
// 'pick' activities that define `width` or `height`
|
||||
// parameters. The larger the scale factor, the larger
|
||||
// the activity `maxPixelSize` icreasing the probability
|
||||
// that a larger pictureSize will be chosen for the activity.
|
||||
maxPixelSizeScaleFactor: 2.5,
|
||||
|
||||
// Reduce the size of images returned by pick activities.
|
||||
// A pick activity can specify its own maximum size. However,
|
||||
// this setting can lower that pixel size limitation even
|
||||
// further. To prevent further limiting the pixel size for
|
||||
// pick activities, set this value to `0`.
|
||||
// (useful for devices with limited memory)
|
||||
maxPickPixelSize: 0,
|
||||
|
||||
// Reduce the size of images returned by share activities.
|
||||
// To prevent resizing images that are shared, set this
|
||||
// value to `0`.
|
||||
// (useful for devices with limited memory)
|
||||
maxSharePixelSize: 0
|
||||
},
|
||||
|
||||
spinnerTimeouts: {
|
||||
takingPicture: 1650,
|
||||
requestingCamera: 850,
|
||||
loadingVideo: 100,
|
||||
resizingImage: 100,
|
||||
lazyLoading: 300
|
||||
},
|
||||
|
||||
mode: {
|
||||
options: [
|
||||
{
|
||||
key: 'picture'
|
||||
},
|
||||
{
|
||||
key: 'video'
|
||||
}
|
||||
],
|
||||
persistent: false
|
||||
},
|
||||
|
||||
isoModes: {
|
||||
disabled: false,
|
||||
options: [
|
||||
{
|
||||
key: 'auto'
|
||||
}
|
||||
],
|
||||
selected:'auto'
|
||||
},
|
||||
|
||||
whiteBalance: {
|
||||
disabled: false,
|
||||
options: [
|
||||
{
|
||||
key: 'auto'
|
||||
}
|
||||
],
|
||||
selected:'auto'
|
||||
},
|
||||
|
||||
cameras: {
|
||||
options: [
|
||||
{
|
||||
key: 'back',
|
||||
icon: 'toggle-camera-rear',
|
||||
title: 'toggle-camera-rear'
|
||||
},
|
||||
{
|
||||
key: 'front',
|
||||
icon: 'toggle-camera-front',
|
||||
title: 'toggle-camera-front'
|
||||
}
|
||||
],
|
||||
persistent: false
|
||||
},
|
||||
|
||||
pictureSizesFront: {
|
||||
title: 'camera-resolution',
|
||||
header: 'camera-resolution-header',
|
||||
icon: 'picture-size',
|
||||
options: [],
|
||||
include: {
|
||||
aspects: ['4:3'],
|
||||
},
|
||||
persistent: true,
|
||||
optionsLocalizable: false,
|
||||
},
|
||||
|
||||
pictureSizesBack: {
|
||||
title: 'camera-resolution',
|
||||
header: 'camera-resolution-header',
|
||||
icon: 'picture-size',
|
||||
options: [],
|
||||
include: {
|
||||
aspects: ['4:3'],
|
||||
},
|
||||
persistent: true,
|
||||
optionsLocalizable: false,
|
||||
},
|
||||
|
||||
recorderProfilesBack: {
|
||||
title: 'video-resolution',
|
||||
header: 'video-resolution-header',
|
||||
icon: 'video-size',
|
||||
exclude: ['low', 'high', 'default'],
|
||||
options: [],
|
||||
persistent: true,
|
||||
optionsLocalizable: false,
|
||||
},
|
||||
|
||||
recorderProfilesFront: {
|
||||
title: 'video-resolution',
|
||||
header: 'video-resolution-header',
|
||||
icon: 'video-size',
|
||||
exclude: ['low', 'high', 'default'],
|
||||
options: [],
|
||||
persistent: true,
|
||||
optionsLocalizable: false,
|
||||
},
|
||||
|
||||
flashModesPicture: {
|
||||
title: 'flash',
|
||||
options: [
|
||||
{
|
||||
key: 'auto',
|
||||
icon: 'flash-auto',
|
||||
title: 'flash-auto'
|
||||
},
|
||||
{
|
||||
key: 'on',
|
||||
icon: 'flash-on',
|
||||
title: 'flash-on'
|
||||
},
|
||||
{
|
||||
key: 'off',
|
||||
icon: 'flash-off',
|
||||
title: 'flash-off'
|
||||
}
|
||||
],
|
||||
persistent: true
|
||||
},
|
||||
|
||||
flashModesVideo: {
|
||||
title: 'flash',
|
||||
options: [
|
||||
{
|
||||
key: 'off',
|
||||
icon: 'flash-off',
|
||||
title: 'flash-off'
|
||||
},
|
||||
{
|
||||
key: 'torch',
|
||||
icon: 'flash-on',
|
||||
title: 'flash-on'
|
||||
}
|
||||
],
|
||||
persistent: true
|
||||
},
|
||||
|
||||
countdown: {
|
||||
title: 'self-timer',
|
||||
header: 'self-timer-header',
|
||||
icon: 'self-timer',
|
||||
options: [
|
||||
{
|
||||
key: 'off',
|
||||
title: 'self-timer-off',
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
key: 'secs2',
|
||||
value: 2,
|
||||
title: 'self-timer-2-seconds'
|
||||
},
|
||||
{
|
||||
key: 'secs5',
|
||||
value: 5,
|
||||
title: 'self-timer-5-seconds'
|
||||
},
|
||||
{
|
||||
key: 'secs10',
|
||||
value: 10,
|
||||
title: 'self-timer-10-seconds'
|
||||
}
|
||||
],
|
||||
persistent: false,
|
||||
},
|
||||
|
||||
hdr: {
|
||||
title: 'hdr',
|
||||
header: 'hdr-header',
|
||||
icon: 'hdr-boxed',
|
||||
disabled: false,
|
||||
options: [
|
||||
{
|
||||
key: 'off',
|
||||
title: 'hdr-off'
|
||||
},
|
||||
{
|
||||
key: 'on',
|
||||
title: 'hdr-on'
|
||||
}
|
||||
],
|
||||
persistent: true
|
||||
},
|
||||
|
||||
scene: {
|
||||
title: 'scene-mode',
|
||||
header: 'scene-mode-header',
|
||||
icon: 'scene',
|
||||
options: [
|
||||
{
|
||||
key: 'normal',
|
||||
title: 'scene-mode-normal'
|
||||
},
|
||||
{
|
||||
key: 'pano',
|
||||
title: 'scene-mode-panorama'
|
||||
},
|
||||
{
|
||||
key: 'beauty',
|
||||
title: 'scene-mode-beauty'
|
||||
}
|
||||
],
|
||||
persistent: true,
|
||||
},
|
||||
|
||||
grid: {
|
||||
title: 'grid',
|
||||
header: 'grid-header',
|
||||
icon: 'grid-circular',
|
||||
options: [
|
||||
{
|
||||
key: 'off',
|
||||
title: 'grid-off'
|
||||
},
|
||||
{
|
||||
key: 'on',
|
||||
title: 'grid-on'
|
||||
}
|
||||
],
|
||||
selected: 'off',
|
||||
persistent: true,
|
||||
notifications: false
|
||||
},
|
||||
|
||||
settingsMenu: {
|
||||
items: [
|
||||
// {
|
||||
// key: 'scene'
|
||||
// },
|
||||
{
|
||||
key: 'hdr'
|
||||
},
|
||||
{
|
||||
key: 'countdown'
|
||||
},
|
||||
{
|
||||
key: 'pictureSizes'
|
||||
},
|
||||
{
|
||||
key: 'recorderProfiles'
|
||||
},
|
||||
{
|
||||
key: 'grid'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
97
gaia/apps/camera/js/config/require.js
Executable file
97
gaia/apps/camera/js/config/require.js
Executable file
|
|
@ -0,0 +1,97 @@
|
|||
requirejs.config({
|
||||
baseUrl: '/js',
|
||||
|
||||
// 'paths' lets us alias complex
|
||||
// paths to something simpler.
|
||||
paths: {
|
||||
'asyncStorage': '../shared/js/async_storage',
|
||||
'getVideoRotation': '../shared/js/media/get_video_rotation',
|
||||
'jpegMetaDataParser': '../shared/js/media/jpeg_metadata_parser',
|
||||
'downsample': '../shared/js/media/downsample',
|
||||
'getImageSize': '../shared/js/media/image_size',
|
||||
'cropResizeRotate': '../shared/js/media/crop_resize_rotate',
|
||||
'format': '../shared/js/format',
|
||||
'GestureDetector': '../shared/js/gesture_detector',
|
||||
'VideoPlayer': '../shared/js/media/video_player',
|
||||
'MediaFrame': '../shared/js/media/media_frame',
|
||||
'BlobView': '../shared/js/blobview',
|
||||
'CustomDialog': '../shared/js/custom_dialog',
|
||||
'debug': '../bower_components/debug/index',
|
||||
'attach': '../bower_components/attach/index',
|
||||
'model': '../bower_components/model/index',
|
||||
'view': '../bower_components/view/index',
|
||||
'evt': '../bower_components/evt/index',
|
||||
'drag': '../bower_components/drag/index',
|
||||
'device-orientation': '../bower_components/device-orientation/device-orientation',/*jshint ignore:line*/
|
||||
'stop-recording-event': '../shared/js/stop_recording_event'
|
||||
},
|
||||
|
||||
// If your package uses relative `require()` paths
|
||||
// internally, then it needs to be defined as
|
||||
// a 'package' so they are resolved correctly.
|
||||
packages: [
|
||||
{
|
||||
name: 'gaia-header',
|
||||
location: '../bower_components/gaia-header',
|
||||
main: 'gaia-header'
|
||||
},
|
||||
{
|
||||
name: 'gaia-icons',
|
||||
location: '../bower_components/gaia-icons',
|
||||
main: 'gaia-icons'
|
||||
},
|
||||
{
|
||||
name: 'gaia-component',
|
||||
location: '../bower_components/gaia-component',
|
||||
main: 'gaia-component'
|
||||
},
|
||||
{
|
||||
name: 'font-fit',
|
||||
location: '../bower_components/font-fit',
|
||||
main: 'font-fit'
|
||||
}
|
||||
],
|
||||
|
||||
// 'shim' config lets us `require()` packages
|
||||
// that don't have an AMD define call.
|
||||
shim: {
|
||||
'format': {
|
||||
exports: 'Format'
|
||||
},
|
||||
'getVideoRotation': {
|
||||
deps: ['BlobView'],
|
||||
exports: 'getVideoRotation'
|
||||
},
|
||||
'MediaFrame': {
|
||||
deps: ['format', 'VideoPlayer', 'downsample'],
|
||||
exports: 'MediaFrame'
|
||||
},
|
||||
'BlobView': {
|
||||
exports: 'BlobView'
|
||||
},
|
||||
'asyncStorage': {
|
||||
exports: 'asyncStorage'
|
||||
},
|
||||
'jpegMetaDataParser': {
|
||||
deps: ['BlobView'],
|
||||
exports: 'parseJPEGMetadata'
|
||||
},
|
||||
'getImageSize': {
|
||||
deps: ['BlobView', 'jpegMetaDataParser'],
|
||||
exports: 'getImageSize'
|
||||
},
|
||||
'cropResizeRotate': {
|
||||
deps: ['BlobView', 'getImageSize', 'jpegMetaDataParser', 'downsample'],
|
||||
exports: 'cropResizeRotate'
|
||||
},
|
||||
'GestureDetector': {
|
||||
exports: 'GestureDetector'
|
||||
},
|
||||
'CustomDialog': {
|
||||
exports: 'CustomDialog'
|
||||
},
|
||||
'stop-recording-event': {
|
||||
exports: 'StopRecordingEvent'
|
||||
}
|
||||
}
|
||||
});
|
||||
308
gaia/apps/camera/js/controllers/activity.js
Executable file
308
gaia/apps/camera/js/controllers/activity.js
Executable file
|
|
@ -0,0 +1,308 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:activity');
|
||||
var bytesToPixels = require('lib/bytes-to-pixels');
|
||||
var bindAll = require('lib/bind-all');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new ActivityController(app); };
|
||||
module.exports.ActivityController = ActivityController;
|
||||
|
||||
/**
|
||||
* Initialize new `ActivityController`
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
function ActivityController(app) {
|
||||
bindAll(this);
|
||||
this.app = app;
|
||||
this.win = app.win;
|
||||
this.settings = app.settings;
|
||||
this.configure();
|
||||
this.bindEvents();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Supported activity types.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
ActivityController.prototype.types = {
|
||||
pick: 'pick',
|
||||
record: 'record'
|
||||
};
|
||||
|
||||
/**
|
||||
* Initial configuration.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ActivityController.prototype.configure = function() {
|
||||
this.name = this.getName();
|
||||
this.app.activity[this.name] = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter down pictureSizes and
|
||||
* recorderProfiles to match activity
|
||||
* parameters each time the settings
|
||||
* are configured.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ActivityController.prototype.bindEvents = function() {
|
||||
this.app.on('activitycanceled', this.onActivityCanceled);
|
||||
this.app.on('confirm:selected', this.onActivityConfirmed);
|
||||
|
||||
// If an activity name was found, we must bind
|
||||
// the listener straight away so we don't miss
|
||||
// the event, otherwise we can bind more lazily.
|
||||
if (this.name) { this.setupListener(); }
|
||||
else { this.app.once('criticalpathdone', this.setupListener); }
|
||||
};
|
||||
|
||||
ActivityController.prototype.setupListener = function() {
|
||||
debug('setup listener');
|
||||
navigator.mozSetMessageHandler('activity', this.onMessage);
|
||||
debug('listener setup');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get activity name from the hash fragment.
|
||||
*
|
||||
* This is used only so that some parts
|
||||
* of the app can prepare for an incoming
|
||||
* activity before the message arrives.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ActivityController.prototype.getName = function() {
|
||||
var hash = this.win.location.hash;
|
||||
var name = hash && hash.substr(1);
|
||||
return this.types[name];
|
||||
};
|
||||
|
||||
/**
|
||||
* Responds to incoming activity message.
|
||||
*
|
||||
* Configures the mode then emits an
|
||||
* event so that other parts of
|
||||
* the app can update accordingly.
|
||||
*
|
||||
* @param {MozActivity} activity
|
||||
*/
|
||||
ActivityController.prototype.onMessage = function(activity) {
|
||||
debug('incoming activity', activity);
|
||||
var name = activity.source.name;
|
||||
var supported = this.types[name];
|
||||
|
||||
// Exit if this activity isn't supported
|
||||
if (!supported) { return; }
|
||||
|
||||
var data = {
|
||||
name: name,
|
||||
maxPixelSize: this.getMaxPixelSize(activity),
|
||||
maxFileSizeBytes: activity.source.data.maxFileSizeBytes
|
||||
};
|
||||
|
||||
this.activity = activity;
|
||||
this.configureMode(activity);
|
||||
this.app.emit('activity', data);
|
||||
this.app.emit('activity:' + name, data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Configures the mode setting based
|
||||
* on the parameters supplied by
|
||||
* the incoming activity.
|
||||
*
|
||||
* @param {MozActivity} activity
|
||||
* @private
|
||||
*/
|
||||
ActivityController.prototype.configureMode = function(activity) {
|
||||
var type = activity.source.data.type;
|
||||
var name = activity.source.name;
|
||||
var modes = (name === 'pick') ?
|
||||
this.getModesForPick(type) :
|
||||
this.getModesForRecord(type);
|
||||
|
||||
this.settings.mode.filterOptions(modes);
|
||||
this.settings.mode.select(modes[0]);
|
||||
debug('configured mode', modes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a max pixel size estimate based
|
||||
* on the parameters supplied by the
|
||||
* incoming activity.
|
||||
*
|
||||
* Activities don't always supply pixel
|
||||
* size restrictions.
|
||||
*
|
||||
* NOTE: There
|
||||
*
|
||||
* @param {MozActivity} activity
|
||||
* @return {Number|null}
|
||||
*/
|
||||
ActivityController.prototype.getMaxPixelSize = function(activity) {
|
||||
var data = activity.source.data;
|
||||
var bytes = data.maxFileSizeBytes;
|
||||
var maxPickPixelSize = this.settings.activity.get('maxPickPixelSize') || 0;
|
||||
var maxPixelSize;
|
||||
|
||||
// If bytes were specified then derive
|
||||
// a maxPixelSize from that, else we
|
||||
// calculate the maxPixelSize using
|
||||
// supplied dimensions.
|
||||
if (bytes) {
|
||||
maxPixelSize = bytesToPixels(bytes);
|
||||
} else if (data.width || data.height) {
|
||||
maxPixelSize = this.getMaxPixelsFromSize(data);
|
||||
} else {
|
||||
maxPixelSize = maxPickPixelSize;
|
||||
}
|
||||
|
||||
// If the Camera app has been configured to have a max pixel size
|
||||
// for pick activities, ensure we are at or below that value.
|
||||
if (maxPickPixelSize > 0) {
|
||||
maxPixelSize = Math.min(maxPixelSize, maxPickPixelSize);
|
||||
}
|
||||
|
||||
debug('maxPixelsSize: %s', maxPixelSize);
|
||||
return maxPixelSize;
|
||||
};
|
||||
|
||||
ActivityController.prototype.getMaxPixelsFromSize = function(size) {
|
||||
var scale = this.settings.activity.get('maxPixelSizeScaleFactor');
|
||||
var aspect = 4 / 3;
|
||||
|
||||
// In the event that only one dimension has
|
||||
// been supplied, calculate the largest the
|
||||
// other edge could be based on a 4:3 aspect.
|
||||
var width = size.width || size.height * aspect;
|
||||
var height = size.height || size.width * aspect;
|
||||
var pixels = width * height;
|
||||
|
||||
// Take the actual total number of
|
||||
// pixels asked for and bump it by the
|
||||
// `scale` to cover pictureSizes above
|
||||
// the pixels asked for. We later resize
|
||||
// the image post-capture to the exact size
|
||||
// requested (data.width * data.height).
|
||||
//
|
||||
// This is to avoid us picking a pictureSize
|
||||
// smaller than the number of pixels requested
|
||||
// and then to scaling up post-capture,
|
||||
// resulting in a shitty image.
|
||||
return pixels * scale;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse types given by 'pick' activity
|
||||
* and return a list of modes.
|
||||
*
|
||||
* @param {Array|String} types
|
||||
* @return {Array}
|
||||
*/
|
||||
ActivityController.prototype.getModesForPick = function(types) {
|
||||
types = [].concat(types || []);
|
||||
var modes = [];
|
||||
|
||||
types.forEach(function(item) {
|
||||
var type = item.split('/')[0];
|
||||
var mode = type === 'image' ? 'picture' : type;
|
||||
|
||||
if (modes.indexOf(mode) === -1) {
|
||||
modes.push(mode);
|
||||
}
|
||||
});
|
||||
|
||||
if (modes.length === 0) {
|
||||
modes = ['picture', 'video'];
|
||||
}
|
||||
|
||||
return modes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse types given by 'record' activity
|
||||
* and return a list of modes.
|
||||
*
|
||||
* @param {String} type
|
||||
* @return {Array}
|
||||
*/
|
||||
ActivityController.prototype.getModesForRecord = function(type) {
|
||||
return type === 'videos' ?
|
||||
['video', 'picture'] :
|
||||
['picture', 'video'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Respond to activity cancelation
|
||||
* events and send the error call
|
||||
* via the original acitity object.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ActivityController.prototype.onActivityCanceled = function() {
|
||||
if (!this.activity) { return; }
|
||||
this.activity.postError('pick cancelled');
|
||||
};
|
||||
|
||||
// TODO: Messy, tidy up!
|
||||
ActivityController.prototype.onActivityConfirmed = function(newMedia) {
|
||||
var self = this;
|
||||
var activity = this.activity;
|
||||
var media = {
|
||||
blob: newMedia.blob
|
||||
};
|
||||
|
||||
// In low end devices resizing can be slow.
|
||||
// We display a spinner
|
||||
this.app.showSpinner();
|
||||
|
||||
// Video
|
||||
if (newMedia.isVideo) {
|
||||
media.type = 'video/3gpp';
|
||||
media.poster = newMedia.poster.blob;
|
||||
activity.postResult(media);
|
||||
this.app.clearSpinner();
|
||||
}
|
||||
|
||||
// Image
|
||||
else {
|
||||
media.type = 'image/jpeg';
|
||||
|
||||
// We need to call resizeImageAndSave() unconditionally even if
|
||||
// the image does not need to be resized because if the camera
|
||||
// produces images with EXIF orientation, we need to rotate the
|
||||
// image before returning it from the pick activity. If the image
|
||||
// does not need to be resized or rotated, then resizeImageAndSave
|
||||
// will return it unchanged, and will not re-save it.
|
||||
require(['lib/resize-image-and-save'], function(resizeImageAndSave) {
|
||||
resizeImageAndSave(
|
||||
{
|
||||
blob: newMedia.blob,
|
||||
width: activity.source.data.width,
|
||||
height: activity.source.data.height
|
||||
},
|
||||
function onImageResized(resizedBlob) {
|
||||
media.blob = resizedBlob;
|
||||
activity.postResult(media);
|
||||
self.app.clearSpinner();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
171
gaia/apps/camera/js/controllers/battery.js
Executable file
171
gaia/apps/camera/js/controllers/battery.js
Executable file
|
|
@ -0,0 +1,171 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:battery');
|
||||
var bindAll = require('lib/bind-all');
|
||||
var bind = require('lib/bind');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new BatteryController(app); };
|
||||
module.exports.BatteryController = BatteryController;
|
||||
|
||||
/**
|
||||
* Initialize a new `BatteryController`
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
function BatteryController(app) {
|
||||
bindAll(this);
|
||||
this.app = app;
|
||||
this.battery = app.battery || navigator.battery || navigator.mozBattery;
|
||||
if (!this.battery) { this.app.set('batteryStatus', 'unknown'); return; }
|
||||
this.levels = app.settings.battery.get('levels');
|
||||
this.notification = app.views.notification;
|
||||
this.bindEvents();
|
||||
this.updateStatus();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind callbacks to required events.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
BatteryController.prototype.bindEvents = function() {
|
||||
bind(this.battery, 'levelchange', this.updateStatus);
|
||||
bind(this.battery, 'chargingchange', this.updateStatus);
|
||||
this.app.on('change:batteryStatus', this.onStatusChange);
|
||||
this.app.on('change:recording', this.updatePowerSave);
|
||||
|
||||
var mozSettings = navigator.mozSettings;
|
||||
mozSettings.addObserver('powersave.enabled', this.onPowerSaveChange);
|
||||
mozSettings.createLock().get('powersave.enabled').then(
|
||||
this.onPowerSaveChange);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback from settings when the power save state changes.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
BatteryController.prototype.onPowerSaveChange = function(values) {
|
||||
var value;
|
||||
if (values.settingValue !== undefined) {
|
||||
value = values.settingValue;
|
||||
} else {
|
||||
value = values['powersave.enabled'];
|
||||
}
|
||||
this.powerSaveEnabled = value;
|
||||
this.updatePowerSave();
|
||||
};
|
||||
|
||||
/**
|
||||
* Emits powersave event if state has changed.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
BatteryController.prototype.updatePowerSave = function() {
|
||||
var state = this.powerSaveEnabled && !this.app.get('recording');
|
||||
if (this.powerSave === state) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.powerSave = state;
|
||||
debug('power save: ' + state);
|
||||
this.app.emit('battery:powersave', state);
|
||||
};
|
||||
|
||||
/**
|
||||
* Map of status keys to message.
|
||||
*
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
BatteryController.prototype.notifications = {
|
||||
low: {
|
||||
text: 'battery-low-text',
|
||||
attrs: {
|
||||
'data-icon': 'battery-3',
|
||||
'data-l10n-id': 'battery-low-indicator'
|
||||
},
|
||||
},
|
||||
verylow: {
|
||||
text: 'battery-verylow-text',
|
||||
attrs: {
|
||||
'data-icon': 'battery-1',
|
||||
'data-l10n-id': 'battery-verylow-indicator'
|
||||
}
|
||||
},
|
||||
critical: {
|
||||
text: 'battery-critical-text',
|
||||
attrs: {
|
||||
'data-icon': 'battery-1',
|
||||
'data-l10n-id': 'battery-critical-indicator'
|
||||
},
|
||||
persistent: true
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates app `batteryStatus` and
|
||||
* manages battery notifications.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
BatteryController.prototype.updateStatus = function () {
|
||||
var previous = this.app.get('batteryStatus');
|
||||
var current = this.getStatus(this.battery);
|
||||
if (current !== previous) {
|
||||
this.app.set('batteryStatus', current);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a status key derived
|
||||
* from the given `battery` object.
|
||||
*
|
||||
* @param {Battery} battery
|
||||
* @return {String}
|
||||
* @private
|
||||
*/
|
||||
BatteryController.prototype.getStatus = function(battery) {
|
||||
var level = Math.round(battery.level * 100);
|
||||
var levels = this.levels;
|
||||
|
||||
if (battery.charging) { return 'charging'; }
|
||||
else if (level <= levels.shutdown) { return 'shutdown'; }
|
||||
else if (level <= levels.critical) { return 'critical'; }
|
||||
else if (level <= levels.verylow) { return 'verylow'; }
|
||||
else if (level <= levels.low) { return 'low'; }
|
||||
else { return 'healthy'; }
|
||||
};
|
||||
|
||||
BatteryController.prototype.onStatusChange = function(status) {
|
||||
this.clearLastNotification();
|
||||
this.displayNotification(status);
|
||||
};
|
||||
|
||||
BatteryController.prototype.displayNotification = function(status) {
|
||||
var notification = this.notifications[status];
|
||||
if (!notification) { return; }
|
||||
|
||||
this.lastNotification = this.notification.display({
|
||||
text: notification.text,
|
||||
className: notification.className,
|
||||
attrs: notification.attrs,
|
||||
persistent: notification.persistent
|
||||
});
|
||||
};
|
||||
|
||||
BatteryController.prototype.clearLastNotification = function() {
|
||||
this.notification.clear(this.lastNotification);
|
||||
};
|
||||
|
||||
});
|
||||
581
gaia/apps/camera/js/controllers/camera.js
Executable file
581
gaia/apps/camera/js/controllers/camera.js
Executable file
|
|
@ -0,0 +1,581 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:camera');
|
||||
var bindAll = require('lib/bind-all');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new CameraController(app); };
|
||||
module.exports.CameraController = CameraController;
|
||||
|
||||
/**
|
||||
* Initialize a new `CameraController`
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
function CameraController(app) {
|
||||
bindAll(this);
|
||||
this.app = app;
|
||||
this.camera = app.camera;
|
||||
this.settings = app.settings;
|
||||
this.activity = app.activity;
|
||||
this.hdrDisabled = this.settings.hdr.get('disabled');
|
||||
this.notification = app.views.notification;
|
||||
|
||||
// Wait until we get the battery state before turning
|
||||
// on the camera
|
||||
this.lowBattery = this.app.get('batteryStatus') === 'shutdown';
|
||||
|
||||
this.configure();
|
||||
this.bindEvents();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
CameraController.prototype.bindEvents = function() {
|
||||
var settings = this.settings;
|
||||
var camera = this.camera;
|
||||
var app = this.app;
|
||||
|
||||
// Relaying camera events means other modules
|
||||
// don't have to depend directly on camera
|
||||
camera.on('change:previewActive', this.app.firer('camera:previewactive'));
|
||||
camera.on('change:videoElapsed', app.firer('camera:recorderTimeUpdate'));
|
||||
camera.on('autofocuschanged', app.firer('camera:autofocuschanged'));
|
||||
camera.on('focusconfigured', app.firer('camera:focusconfigured'));
|
||||
camera.on('change:focus', app.firer('camera:focusstatechanged'));
|
||||
camera.on('filesizelimitreached', this.onFileSizeLimitReached);
|
||||
camera.on('facesdetected', app.firer('camera:facesdetected'));
|
||||
camera.on('willrecord', app.firer('camera:willrecord'));
|
||||
camera.on('configured', app.firer('camera:configured'));
|
||||
camera.on('requesting', app.firer('camera:requesting'));
|
||||
camera.on('change:recording', this.onRecordingChange);
|
||||
camera.on('newcamera', app.firer('camera:newcamera'));
|
||||
camera.on('newimage', app.firer('camera:newimage'));
|
||||
camera.on('newvideo', app.firer('camera:newvideo'));
|
||||
camera.on('shutter', app.firer('camera:shutter'));
|
||||
camera.on('loaded', app.firer('camera:loaded'));
|
||||
camera.on('closed', this.onCameraClosed);
|
||||
camera.on('error', app.firer('camera:error'));
|
||||
camera.on('ready', app.firer('ready'));
|
||||
camera.on('busy', app.firer('busy'));
|
||||
|
||||
// App
|
||||
app.on('viewfinder:focuspointchanged', this.onFocusPointChanged);
|
||||
app.on('change:batteryStatus', this.onBatteryStatusChange);
|
||||
app.on('settings:configured', this.onSettingsConfigured);
|
||||
app.on('previewgallery:opened', this.onGalleryOpened);
|
||||
app.on('previewgallery:closed', this.onGalleryClosed);
|
||||
app.on('stoprecording', this.camera.stopRecording);
|
||||
app.on('storage:volumechanged', this.onStorageVolumeChanged);
|
||||
app.on('storage:changed', this.onStorageChanged);
|
||||
app.on('activity:pick', this.onPickActivity);
|
||||
app.on('keydown:capture', this.onCaptureKey);
|
||||
app.on('keydown:focus', this.onFocusKey);
|
||||
app.on('hidden', this.shutdownCamera);
|
||||
app.on('click', this.clearCountdown);
|
||||
app.on('visible', this.loadCamera);
|
||||
app.on('capture', this.capture);
|
||||
|
||||
// Settings
|
||||
settings.recorderProfiles.on('change:selected', this.updateRecorderProfile);
|
||||
settings.pictureSizes.on('change:selected', this.updatePictureSize);
|
||||
settings.flashModes.on('change:selected', this.onFlashModeChange);
|
||||
settings.flashModes.on('change:selected', this.setFlashMode);
|
||||
settings.cameras.on('change:selected', this.setCamera);
|
||||
settings.mode.on('change:selected', this.setMode);
|
||||
settings.hdr.on('change:selected', this.setHDR);
|
||||
settings.hdr.on('change:selected', this.onHDRChange);
|
||||
|
||||
debug('events bound');
|
||||
};
|
||||
|
||||
CameraController.prototype.onRecordingChange = function(recording) {
|
||||
var active;
|
||||
if (recording === 'started') {
|
||||
active = true;
|
||||
} else if (recording === 'stopped') {
|
||||
active = false;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
this.app.set('recording', active);
|
||||
};
|
||||
|
||||
/**
|
||||
* Take picture or start/end recording
|
||||
* when a capture hardware key is invoked.
|
||||
*
|
||||
* Calling `.preventDefault()` prevents
|
||||
* the default system operation
|
||||
* (eg. changing volume level). We
|
||||
* only call it when capture request
|
||||
* succeeds.
|
||||
*
|
||||
* We don't want to .preventDefault() when
|
||||
* the preview-gallery is open as the
|
||||
* user may want to change the volume
|
||||
* of a video being played back.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.onCaptureKey = function(e) {
|
||||
debug('on capture key', e);
|
||||
if (this.capture() !== false) { e.preventDefault(); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Focus the camera when a focus
|
||||
* hardware key is invoked.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.onFocusKey = function(e) {
|
||||
debug('on focus key', e);
|
||||
if (!this.shouldCapture()) { return; }
|
||||
this.camera.focus.focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure the 'cameras' setting using the
|
||||
* `cameraList` data given by the camera hardware
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.configure = function() {
|
||||
this.settings.cameras.filterOptions(this.camera.cameraList);
|
||||
debug('configured');
|
||||
};
|
||||
|
||||
/**
|
||||
* Once the settings have finished configuring
|
||||
* we do the final camera configuration.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.onSettingsConfigured = function() {
|
||||
var recorderProfile = this.settings.recorderProfiles.selected('key');
|
||||
var pictureSize = this.settings.pictureSizes.selected('data');
|
||||
|
||||
this.setWhiteBalance();
|
||||
this.setFlashMode();
|
||||
this.setISO();
|
||||
this.setHDR();
|
||||
|
||||
this.camera.setRecorderProfile(recorderProfile);
|
||||
this.camera.setPictureSize(pictureSize);
|
||||
this.camera.configureZoom();
|
||||
|
||||
// Defer this work as it involves
|
||||
// expensive mozSettings calls
|
||||
setTimeout(this.updateZoomForMako);
|
||||
|
||||
debug('camera configured with final settings');
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates camera configuration in
|
||||
* response to pick activity parameters.
|
||||
*
|
||||
* @param {Object} data
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.onPickActivity = function(data) {
|
||||
|
||||
// This is set so that the video recorder can
|
||||
// automatically stop when video size limit is reached.
|
||||
this.camera.set('maxFileSizeBytes', data.maxFileSizeBytes);
|
||||
|
||||
// Disable camera config caches when in 'pick' activity
|
||||
// to prevent activity specific configuration persisting.
|
||||
this.camera.cacheConfig = false;
|
||||
};
|
||||
|
||||
CameraController.prototype.capture = function(options = {}) {
|
||||
var force = options.force;
|
||||
if (!this.shouldCapture()) { return false; }
|
||||
if (!force && this.shouldCountdown()) { return this.startCountdown(); }
|
||||
if (this.countdown) { return this.clearCountdown(); }
|
||||
var position = this.app.geolocation.position;
|
||||
return this.camera.capture({ position: position });
|
||||
};
|
||||
|
||||
CameraController.prototype.shouldCapture = function() {
|
||||
return !this.app.get('confirmViewVisible') &&
|
||||
!this.app.hidden &&
|
||||
!this.app.busy &&
|
||||
!this.galleryOpen &&
|
||||
!this.lowBattery;
|
||||
};
|
||||
|
||||
CameraController.prototype.shouldCountdown = function() {
|
||||
var countdownSet = this.settings.countdown.selected('value');
|
||||
var recording = this.app.get('recording');
|
||||
return countdownSet &&
|
||||
!this.countdown &&
|
||||
!recording;
|
||||
};
|
||||
|
||||
CameraController.prototype.startCountdown = function() {
|
||||
if (this.countdown) { return; }
|
||||
var seconds = this.settings.countdown.selected('value');
|
||||
var self = this;
|
||||
|
||||
if (!seconds) { return; }
|
||||
this.app.emit('countdown:started', seconds);
|
||||
|
||||
(function scheduleTick() {
|
||||
self.countdown = setTimeout(() => {
|
||||
if (--seconds <= 0) {
|
||||
self.clearCountdown();
|
||||
self.capture({ force: true });
|
||||
return;
|
||||
}
|
||||
|
||||
self.app.emit('countdown:tick', seconds);
|
||||
scheduleTick();
|
||||
}, 1000);
|
||||
})();
|
||||
};
|
||||
|
||||
CameraController.prototype.clearCountdown = function() {
|
||||
if (!this.countdown) { return; }
|
||||
clearTimeout(this.countdown);
|
||||
this.countdown = null;
|
||||
this.app.emit('countdown:ended');
|
||||
};
|
||||
|
||||
CameraController.prototype.onFileSizeLimitReached = function() {
|
||||
this.camera.stopRecording();
|
||||
this.showSizeLimitAlert();
|
||||
};
|
||||
|
||||
CameraController.prototype.showSizeLimitAlert = function() {
|
||||
if (this.sizeLimitAlertActive) { return; }
|
||||
this.sizeLimitAlertActive = true;
|
||||
var alertL10nId = this.activity.pick ?
|
||||
'activity-size-limit-reached' :
|
||||
'storage-size-limit-reached';
|
||||
document.l10n.formatValue(alertL10nId).then(alert);
|
||||
this.sizeLimitAlertActive = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the camera's mode (picture/video).
|
||||
*
|
||||
* We send a signal to say that the camera
|
||||
* 'will change', this allows other parts
|
||||
* of the app to respond if need be.
|
||||
*
|
||||
* We then wait for the viewfinder to be 'hidden'
|
||||
* before setting the camera to prevent the
|
||||
* user from seeing the stream flicker/jump.
|
||||
*
|
||||
* @param {String} mode ['picture'|'video']
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.setMode = function(mode) {
|
||||
debug('set mode: %s', mode);
|
||||
var self = this;
|
||||
|
||||
// Abort if didn't change.
|
||||
//
|
||||
// TODO: Perhaps the `Setting` instance should
|
||||
// not emit a `change` event if the value did
|
||||
// not change? This may require some deep checking
|
||||
// if the value is an object. Quite a risky change
|
||||
// to make, but would remove the need for us to check
|
||||
// here and in other change callbacks. Food 4 thought :)
|
||||
if (this.camera.isMode(mode)) {
|
||||
debug('mode didn\'t change');
|
||||
return;
|
||||
}
|
||||
|
||||
var l10nId = mode == 'video' ? 'Video-Mode' : 'Photo-Mode';
|
||||
this.notification.display({ text: l10nId });
|
||||
|
||||
this.setFlashMode();
|
||||
this.app.emit('camera:willchange');
|
||||
this.app.once('viewfinder:hidden', function() {
|
||||
self.camera.setMode(mode);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the camera's `pictureSize` to match
|
||||
* the size set in the app's settings.
|
||||
*
|
||||
* When in 'picture' mode we send a signal
|
||||
* to say that the camera 'will change',
|
||||
* this allows other parts of the app to
|
||||
* repsond if need be.
|
||||
*
|
||||
* We then wait for the viewfinder to be hidden
|
||||
* before setting the pictureSize to prevent the
|
||||
* user from seeing the stream flicker/jump.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.updatePictureSize = function() {
|
||||
debug('update picture-size');
|
||||
var pictureMode = this.settings.mode.selected('key') === 'picture';
|
||||
var value = this.settings.pictureSizes.selected('data');
|
||||
var self = this;
|
||||
|
||||
// Don't do anything if the picture size didn't change
|
||||
if (this.camera.isPictureSize(value)) { return; }
|
||||
|
||||
// If not currently in 'picture'
|
||||
// mode, just configure.
|
||||
if (!pictureMode) {
|
||||
this.camera.setPictureSize(value, { configure: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// Make change once the viewfinder is hidden
|
||||
this.app.emit('camera:willchange');
|
||||
this.app.once('viewfinder:hidden', function() {
|
||||
self.camera.setPictureSize(value);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the camera's `recorderProfile` to
|
||||
* match the size set in the app's settings.
|
||||
*
|
||||
* When in 'picture' mode we send a signal
|
||||
* to say that the camera 'will change',
|
||||
* this allows other parts of the app to
|
||||
* repsond if need be.
|
||||
*
|
||||
* We then wait for the viewfinder to be hidden
|
||||
* before setting the pictureSize to prevent the
|
||||
* user from seeing the stream flicker/jump.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.updateRecorderProfile = function() {
|
||||
debug('update recorder-profile');
|
||||
var videoMode = this.settings.mode.selected('key') === 'video';
|
||||
var key = this.settings.recorderProfiles.selected('key');
|
||||
var self = this;
|
||||
|
||||
// Don't do anything if the recorder-profile didn't change
|
||||
if (this.camera.isRecorderProfile(key)) { return; }
|
||||
|
||||
// If not currently in 'video'
|
||||
// mode, just configure.
|
||||
if (!videoMode) {
|
||||
this.camera.setRecorderProfile(key, { configure: false });
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for the viewfinder to be hidden
|
||||
this.app.emit('camera:willchange');
|
||||
this.app.once('viewfinder:hidden', function() {
|
||||
self.camera.setRecorderProfile(key);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the selected camera (front/back).
|
||||
*
|
||||
* We send a signal to say that the camera
|
||||
* 'will change', this allows other parts
|
||||
* of the app to respond if need be.
|
||||
*
|
||||
* We then wait for the viewfinder to be 'hidden'
|
||||
* before setting the camera to prevent the
|
||||
* user from seeing the stream flicker/jump.
|
||||
*
|
||||
* @param {String} camera ['front'|'back']
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.setCamera = function(camera) {
|
||||
debug('set camera: %s', camera);
|
||||
var self = this;
|
||||
this.app.emit('camera:willchange');
|
||||
this.app.once('viewfinder:hidden', function() {
|
||||
self.camera.setCamera(camera);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the flash mode on the camera
|
||||
* to match the current flash mode
|
||||
* in the app's settings.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.setFlashMode = function() {
|
||||
var flashSetting = this.settings.flashModes;
|
||||
this.camera.setFlashMode(flashSetting.selected('key'));
|
||||
};
|
||||
|
||||
CameraController.prototype.setISO = function() {
|
||||
if (!this.settings.isoModes.get('disabled')) {
|
||||
this.camera.setISOMode(this.settings.isoModes.selected('key'));
|
||||
}
|
||||
};
|
||||
|
||||
CameraController.prototype.setWhiteBalance = function() {
|
||||
if (!this.settings.whiteBalance.get('disabled')) {
|
||||
this.camera.setWhiteBalance(this.settings.whiteBalance.selected('key'));
|
||||
}
|
||||
};
|
||||
|
||||
CameraController.prototype.setHDR = function() {
|
||||
if (this.hdrDisabled) { return; }
|
||||
this.camera.setHDR(this.settings.hdr.selected('key'));
|
||||
};
|
||||
|
||||
CameraController.prototype.onFlashModeChange = function(flashModes) {
|
||||
if (this.hdrDisabled) { return; }
|
||||
var ishdrOn = this.settings.hdr.selected('key') === 'on';
|
||||
if (ishdrOn && flashModes !== 'off') {
|
||||
this.settings.hdr.select('off');
|
||||
}
|
||||
};
|
||||
|
||||
CameraController.prototype.onHDRChange = function(hdr) {
|
||||
var flashMode = this.settings.flashModesPicture.selected('key');
|
||||
var ishdrOn = hdr === 'on';
|
||||
if (ishdrOn && flashMode !== 'off') {
|
||||
this.settings.flashModesPicture.select('off');
|
||||
}
|
||||
};
|
||||
|
||||
CameraController.prototype.onBatteryStatusChange = function(status) {
|
||||
this.lowBattery = status === 'shutdown';
|
||||
if (this.lowBattery) { this.shutdownCamera(); }
|
||||
else { this.loadCamera(); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop recording if storage changes state.
|
||||
* Examples:
|
||||
* 'shared' usually due to the device being connected to
|
||||
* a computer via USB.
|
||||
* 'unavailable' when the SDCARD is yanked
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.onStorageChanged = function(state) {
|
||||
if (state !== 'available') {
|
||||
this.camera.stopRecording();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* For instance, when the storage volume changes from to internal memory
|
||||
* to the SD Card
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.onStorageVolumeChanged = function(storage) {
|
||||
this.camera.setStorage(storage);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates focus area when the user clicks on the viewfinder
|
||||
*/
|
||||
CameraController.prototype.onFocusPointChanged = function(focusPoint) {
|
||||
if (this.countdown) { return; }
|
||||
this.camera.updateFocusArea(focusPoint.area);
|
||||
};
|
||||
|
||||
CameraController.prototype.loadCamera = function(showSpinner) {
|
||||
if (this.lowBattery || this.galleryOpen || this.app.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (showSpinner) { this.app.showSpinner(); }
|
||||
this.camera.load();
|
||||
};
|
||||
|
||||
CameraController.prototype.shutdownCamera = function() {
|
||||
this.clearCountdown();
|
||||
this.camera.shutdown();
|
||||
};
|
||||
|
||||
/**
|
||||
* Camera hardware can be closed after a failure or after app request
|
||||
* It reboots the application in the case of failure
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.onCameraClosed = function(reason) {
|
||||
reason = reason || 'SystemFailure';
|
||||
if (reason === 'SystemFailure') {
|
||||
this.app.emit('reboot');
|
||||
}
|
||||
};
|
||||
|
||||
CameraController.prototype.onGalleryOpened = function() {
|
||||
this.galleryOpen = true;
|
||||
this.shutdownCamera();
|
||||
};
|
||||
|
||||
/**
|
||||
* As the camera is shutdown when the
|
||||
* preview gallery is opened, we must
|
||||
* reload it when it is closed.
|
||||
*
|
||||
* Although if the app is has been minimised
|
||||
* we do not want to reload the camera as
|
||||
* the hardware must be released when the
|
||||
* app is not visible.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.onGalleryClosed = function(reason) {
|
||||
this.galleryOpen = false;
|
||||
if (this.app.hidden) { return; }
|
||||
this.loadCamera(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* For some reason, the above calculation
|
||||
* for `maxHardwareZoom` does not work
|
||||
* properly on Mako (Nexus-4) devices.
|
||||
*
|
||||
* Bug 983930 - [B2G][Camera] CameraControl API's
|
||||
* "zoom" attribute doesn't scale preview properly
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
CameraController.prototype.updateZoomForMako = function() {
|
||||
debug('update zoom for mako');
|
||||
|
||||
var self = this;
|
||||
navigator.mozSettings
|
||||
.createLock()
|
||||
.get('deviceinfo.hardware')
|
||||
.onsuccess = onSuccess;
|
||||
|
||||
debug('settings request made');
|
||||
function onSuccess(e) {
|
||||
var device = e.target.result['deviceinfo.hardware'];
|
||||
if (device !== 'mako') { return; }
|
||||
|
||||
var frontCamera = self.camera.selectedCamera === 'front';
|
||||
var maxHardwareZoom = frontCamera ? 1 : 1.25;
|
||||
|
||||
// Nexus 4 needs zoom preview adjustment since the viewfinder preview
|
||||
// stream does not automatically reflect the current zoom value.
|
||||
self.settings.zoom.set('useZoomPreviewAdjustment', true);
|
||||
self.camera.set('maxHardwareZoom', maxHardwareZoom);
|
||||
self.camera.emit('zoomconfigured', self.camera.getZoom());
|
||||
debug('zoom reconfigured for mako');
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
127
gaia/apps/camera/js/controllers/confirm.js
Executable file
127
gaia/apps/camera/js/controllers/confirm.js
Executable file
|
|
@ -0,0 +1,127 @@
|
|||
define(function(require, exports, module) {
|
||||
/*jshint laxbreak:true*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Module Dependencies
|
||||
*/
|
||||
|
||||
var prepareBlob = require('lib/prepare-preview-blob');
|
||||
var debug = require('debug')('controller:confirm');
|
||||
var ConfirmView = require('views/confirm');
|
||||
var bindAll = require('lib/bind-all');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(options) { return new ConfirmController(options); };
|
||||
module.exports.ConfirmController = ConfirmController;
|
||||
|
||||
/**
|
||||
* Initialize a new `ConfirmController`
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
function ConfirmController(app) {
|
||||
this.app = app;
|
||||
this.settings = app.settings;
|
||||
this.activity = app.activity;
|
||||
this.camera = app.camera;
|
||||
this.container = app.el;
|
||||
|
||||
// Allow these dependencies
|
||||
// to be injected if need be.
|
||||
this.ConfirmView = app.ConfirmView || ConfirmView;
|
||||
this.prepareBlob = app.prepareBlob || prepareBlob;
|
||||
|
||||
bindAll(this);
|
||||
this.bindEvents();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
ConfirmController.prototype.renderView = function() {
|
||||
if (!this.activity.pick) { return; }
|
||||
|
||||
if (!this.view) {
|
||||
this.view = new this.ConfirmView();
|
||||
this.view.maxPreviewSize = window.CONFIG_MAX_IMAGE_PIXEL_SIZE;
|
||||
this.view.render().appendTo(this.container);
|
||||
|
||||
// We want to listen to select events exactly once
|
||||
// The media is selected, the confirm view dismissed
|
||||
// and the activity returns
|
||||
this.view.once('click:select', this.onSelectMedia);
|
||||
this.view.on('click:retake', this.onRetakeMedia);
|
||||
this.view.on('loadingvideo', this.app.firer('busy'));
|
||||
this.view.on('playingvideo', this.app.firer('ready'));
|
||||
}
|
||||
|
||||
this.view.show();
|
||||
this.app.set('confirmViewVisible', true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Bind callbacks to required events.
|
||||
*
|
||||
*/
|
||||
ConfirmController.prototype.bindEvents = function() {
|
||||
|
||||
// Render/Show the view on the `newimage` and `newvideo` events
|
||||
// since they are fired immediately when tapping 'Capture'/'Stop'.
|
||||
// This prevents the 'Capture'/'Stop' button from being able to be
|
||||
// triggered multiple times before the confirm view appears.
|
||||
this.camera.on('newimage', this.renderView);
|
||||
this.camera.on('change:recording', this.onRecordingChange);
|
||||
|
||||
// Update the MediaFrame contents with the image/video upon
|
||||
// receiving the `newmedia` event. This event is slightly delayed
|
||||
// since it waits for the storage callback to complete.
|
||||
this.app.on('newmedia', this.onNewMedia);
|
||||
};
|
||||
|
||||
ConfirmController.prototype.onRecordingChange = function(recording) {
|
||||
if (!this.activity.pick) { return; }
|
||||
|
||||
if (recording === 'starting') {
|
||||
this.recording = true;
|
||||
} else if (recording === 'error') {
|
||||
this.recording = false;
|
||||
} else if (recording === 'stopped' && this.recording) {
|
||||
this.renderView();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When inside a 'pick' activity
|
||||
* will present the user with a
|
||||
* confirm overlay where they can
|
||||
* decide to 'select' or 'retake'
|
||||
* the photo or video
|
||||
*
|
||||
* @param {Object} data
|
||||
*
|
||||
*/
|
||||
ConfirmController.prototype.onNewMedia = function(newMedia) {
|
||||
if (!this.activity.pick) { return; }
|
||||
|
||||
this.newMedia = newMedia;
|
||||
if (newMedia.isVideo) { // Is video
|
||||
this.view.showVideo(newMedia);
|
||||
} else { // Is Image
|
||||
this.prepareBlob(this.newMedia.blob, this.view.showImage);
|
||||
}
|
||||
};
|
||||
|
||||
ConfirmController.prototype.onSelectMedia = function() {
|
||||
this.app.emit('confirm:selected', this.newMedia);
|
||||
};
|
||||
|
||||
ConfirmController.prototype.onRetakeMedia = function() {
|
||||
this.view.hide();
|
||||
this.view.clearMediaFrame();
|
||||
this.app.set('confirmViewVisible', false);
|
||||
};
|
||||
|
||||
});
|
||||
328
gaia/apps/camera/js/controllers/controls.js
vendored
Executable file
328
gaia/apps/camera/js/controllers/controls.js
vendored
Executable file
|
|
@ -0,0 +1,328 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:controls');
|
||||
var ControlsView = require('views/controls');
|
||||
var bindAll = require('lib/bind-all');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new ControlsController(app); };
|
||||
module.exports.ControlsController = ControlsController;
|
||||
|
||||
/**
|
||||
* Initialize a new `ControlsController`
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
function ControlsController(app) {
|
||||
bindAll(this);
|
||||
this.app = app;
|
||||
this.activity = app.activity;
|
||||
this.createView();
|
||||
this.bindEvents();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Event bindings.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.bindEvents = function() {
|
||||
this.app.settings.mode.on('change:selected', this.view.setMode);
|
||||
this.app.settings.mode.on('change:options', this.configureMode);
|
||||
|
||||
// App
|
||||
this.app.on('change:recording', this.onRecordingChange);
|
||||
this.app.on('camera:shutter', this.captureHighlightOff);
|
||||
this.app.on('camera:willchange', this.onCameraWillChange);
|
||||
this.app.on('camera:configured', this.onCameraConfigured);
|
||||
this.app.on('newthumbnail', this.onNewThumbnail);
|
||||
this.app.once('loaded', this.onceAppLoaded);
|
||||
this.app.on('busy', this.onCameraBusy);
|
||||
this.app.on('localized', this.view.localize);
|
||||
|
||||
// View
|
||||
this.view.on('modechanged', this.onViewModeChanged);
|
||||
this.view.on('click:thumbnail', this.app.firer('preview'));
|
||||
this.view.on('click:cancel', this.onCancelButtonClick);
|
||||
this.view.on('click:capture', this.onCaptureClick);
|
||||
|
||||
// Timer
|
||||
this.app.on('countdown:started', this.onCountdownStarted);
|
||||
this.app.on('countdown:ended', this.onCountdownStopped);
|
||||
|
||||
// Settings
|
||||
this.app.on('settings:opened', this.onSettingsOpened);
|
||||
this.app.on('settings:closed', this.onSettingsClosed);
|
||||
|
||||
// Preview gallery
|
||||
this.app.on('previewgallery:opened', this.view.hide);
|
||||
this.app.on('previewgallery:closed', this.view.show);
|
||||
|
||||
debug('events bound');
|
||||
};
|
||||
|
||||
/**
|
||||
* Create and configure the view.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.createView = function() {
|
||||
var initialMode = this.app.settings.mode.selected('key');
|
||||
var cancellable = !!this.app.activity.pick;
|
||||
|
||||
// Create the view (test hook)
|
||||
this.view = this.app.views.controls || new ControlsView();
|
||||
|
||||
// The gallery button should not
|
||||
// be shown if an activity is pending
|
||||
// or the application is in 'secure mode'.
|
||||
this.view.set('cancel', cancellable);
|
||||
this.view.setMode(initialMode);
|
||||
|
||||
// Disable view until camera
|
||||
// 'ready' enables it.
|
||||
this.view.disable();
|
||||
|
||||
// Put it in the DOM
|
||||
this.view.appendTo(this.app.el);
|
||||
|
||||
debug('cancelable: %s', cancellable);
|
||||
debug('mode: %s', initialMode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Disables the switch if there is
|
||||
* only one modes available.
|
||||
*
|
||||
* This is only the case if an activity
|
||||
* indicated it only supports one mode,
|
||||
* just 'picture' or 'video'.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.configureMode = function() {
|
||||
var switchable = this.app.settings.mode.get('options').length > 1;
|
||||
if (!switchable) { this.view.disable('switch'); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Once the app is loaded, we can enable
|
||||
* the controls. We also bind a listener
|
||||
* that enabled the controls whenever
|
||||
* the camera becomes 'ready' from
|
||||
* hereon after.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.onceAppLoaded = function() {
|
||||
this.app.on('ready', this.restore);
|
||||
this.view.enable();
|
||||
};
|
||||
|
||||
/**
|
||||
* Keep capture button pressed and
|
||||
* fire the `capture` event to allow
|
||||
* the camera to repond.
|
||||
*
|
||||
* When the 'camera:shutter' event fires
|
||||
* we remove the capture butter pressed
|
||||
* state so that it times with the
|
||||
* capture sound effect.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.onCaptureClick = function() {
|
||||
this.captureHighlightOn();
|
||||
this.app.emit('capture');
|
||||
};
|
||||
|
||||
/**
|
||||
* Prevent the mode switch from being changed
|
||||
* by the user when we are in the process of
|
||||
* configuring the camera.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.onCameraWillChange = function() {
|
||||
this.view.suspendModeSwitch(true);
|
||||
};
|
||||
|
||||
ControlsController.prototype.onCameraConfigured = function() {
|
||||
this.view.suspendModeSwitch(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the recording attribute on
|
||||
* the view to allow it to style
|
||||
* accordingly.
|
||||
*
|
||||
* @param {Boolean} recording
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.onRecordingChange = function(recording) {
|
||||
this.view.set('recording', recording);
|
||||
if (!recording) { this.onRecordingEnd(); }
|
||||
// Update capture button label when recording changes.
|
||||
this.view.setCaptureLabel(recording);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the capture highlight,
|
||||
* once recording has finished.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.onRecordingEnd = function() {
|
||||
this.captureHighlightOff();
|
||||
};
|
||||
|
||||
/**
|
||||
* When the thumbnail changes, update it in the view.
|
||||
* This method is triggered by the 'newthumbnail' event.
|
||||
* That event is emitted by the preview gallery controller when the a new
|
||||
* photo or video is added, or when the preview is closed and the first
|
||||
* photo or video has changed (because of a file deletion).
|
||||
*/
|
||||
ControlsController.prototype.onNewThumbnail = function(thumbnailBlob) {
|
||||
if (thumbnailBlob) {
|
||||
this.view.setThumbnail(thumbnailBlob);
|
||||
} else {
|
||||
this.view.removeThumbnail();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Forces the capture button to
|
||||
* look pressed while the countdown is
|
||||
* counting down and hides controls.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.onCountdownStarted = function() {
|
||||
this.captureHighlightOn();
|
||||
this.view.set('countdown', 'active');
|
||||
};
|
||||
|
||||
/**
|
||||
* Forces the capture button to
|
||||
* look unpressed when the countdown
|
||||
* stops and shows controls.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.onCountdownStopped = function() {
|
||||
this.captureHighlightOff();
|
||||
this.view.set('countdown', 'inactive');
|
||||
};
|
||||
|
||||
/**
|
||||
* Make controls invisible to the screen reader since they are behind settings
|
||||
* overlay.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.onSettingsOpened = function() {
|
||||
this.view.setScreenReaderVisible(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make controls visible to the screen reader again when settings are closed.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.onSettingsClosed = function() {
|
||||
this.view.setScreenReaderVisible(true);
|
||||
};
|
||||
|
||||
ControlsController.prototype.onCameraBusy = function() {
|
||||
this.view.disable();
|
||||
};
|
||||
|
||||
/**
|
||||
* Restores the capture button to its
|
||||
* unpressed state and re-enables buttons.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.restore = function() {
|
||||
debug('restore');
|
||||
this.captureHighlightOff();
|
||||
this.view.enable();
|
||||
};
|
||||
|
||||
/**
|
||||
* Make the capture button
|
||||
* appear pressed.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.captureHighlightOn = function() {
|
||||
this.view.set('capture-active');
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the pressed apperance
|
||||
* from the capture button.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.captureHighlightOff = function() {
|
||||
this.view.unset('capture-active');
|
||||
};
|
||||
|
||||
/**
|
||||
* Switch to the next capture mode:
|
||||
* 'picture' or 'video', when the
|
||||
* mode is changed via the view.
|
||||
*
|
||||
* Them mode can be changed by either
|
||||
* tapping or swiping the mode switch.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.onViewModeChanged = function() {
|
||||
debug('view mode changed');
|
||||
this.app.settings.mode.next();
|
||||
};
|
||||
|
||||
ControlsController.prototype.onCancelButtonClick = function() {
|
||||
this.app.emit('activitycanceled');
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the gallery app when the
|
||||
* gallery button is pressed.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ControlsController.prototype.onGalleryButtonClick = function(event) {
|
||||
event.stopPropagation();
|
||||
var MozActivity = window.MozActivity;
|
||||
|
||||
// Can't launch the gallery if the lockscreen is locked.
|
||||
// The button shouldn't even be visible in this case, but
|
||||
// let's be really sure here.
|
||||
if (this.app.inSecureMode) { return; }
|
||||
|
||||
// Launch the gallery with an activity
|
||||
this.mozActivity = new MozActivity({
|
||||
name: 'browse',
|
||||
data: { type: 'photos' }
|
||||
});
|
||||
|
||||
// Wait 2000ms before re-enabling the
|
||||
// Gallery to be launched (Bug 957709)
|
||||
this.view.disable();
|
||||
setTimeout(this.view.enable, 2000);
|
||||
};
|
||||
|
||||
});
|
||||
66
gaia/apps/camera/js/controllers/countdown.js
Executable file
66
gaia/apps/camera/js/controllers/countdown.js
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:countdown');
|
||||
var CountdownView = require('views/countdown');
|
||||
var bindAll = require('lib/bind-all');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new CountdownController(app); };
|
||||
module.exports.CountdownController = CountdownController;
|
||||
|
||||
/**
|
||||
* Create a new `CountdownController`
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
function CountdownController(app) {
|
||||
bindAll(this);
|
||||
this.app = app;
|
||||
this.view = app.views.countdown || new CountdownView(); // test hook
|
||||
this.view.appendTo(app.el);
|
||||
this.bindEvents();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects the countdown view
|
||||
* with the app via events.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
CountdownController.prototype.bindEvents = function() {
|
||||
this.app.on('countdown:started', this.start);
|
||||
this.app.on('countdown:tick', this.update);
|
||||
this.app.on('countdown:ended', this.clear);
|
||||
};
|
||||
|
||||
CountdownController.prototype.start = function(seconds) {
|
||||
this.update(seconds);
|
||||
this.view.show();
|
||||
debug('started');
|
||||
};
|
||||
|
||||
CountdownController.prototype.update = function(seconds) {
|
||||
if (seconds < 1) { seconds = ''; }
|
||||
var immanent = seconds <= 3;
|
||||
|
||||
this.view
|
||||
.set(seconds)
|
||||
.setImmanent(immanent);
|
||||
|
||||
if (immanent && seconds > 0) { this.app.emit('countdown:immanent'); }
|
||||
};
|
||||
|
||||
CountdownController.prototype.clear = function() {
|
||||
this.view.hide(this.view.reset);
|
||||
};
|
||||
|
||||
});
|
||||
181
gaia/apps/camera/js/controllers/hud.js
Executable file
181
gaia/apps/camera/js/controllers/hud.js
Executable file
|
|
@ -0,0 +1,181 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:hud');
|
||||
var debounce = require('lib/debounce');
|
||||
var bindAll = require('lib/bind-all');
|
||||
var HudView = require('views/hud');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new HudController(app); };
|
||||
module.exports.HudController = HudController;
|
||||
|
||||
/**
|
||||
* Initialize a new `HudController`
|
||||
*
|
||||
* @param {AppController} app
|
||||
* @constructor
|
||||
*
|
||||
*/
|
||||
function HudController(app) {
|
||||
bindAll(this);
|
||||
this.app = app;
|
||||
this.settings = app.settings;
|
||||
this.notification = app.views.notification;
|
||||
this.createView();
|
||||
this.bindEvents();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and configure the HUD view.
|
||||
*
|
||||
* Disable flash button until we know
|
||||
* whether the hardware has flash.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
HudController.prototype.createView = function() {
|
||||
var hasDualCamera = this.settings.cameras.get('options').length > 1;
|
||||
this.view = this.app.views.hud || new HudView(); // test hook
|
||||
this.view.enable('camera', hasDualCamera);
|
||||
this.view.disable('flash');
|
||||
this.view.hide();
|
||||
this.updateCamera();
|
||||
this.view.appendTo(this.app.el);
|
||||
};
|
||||
|
||||
/**
|
||||
* Bind callbacks to events.
|
||||
*
|
||||
* @return {HudController} for chaining
|
||||
* @private
|
||||
*/
|
||||
HudController.prototype.bindEvents = function() {
|
||||
|
||||
// App
|
||||
this.app.on('change:recording', this.view.setter('recording'));
|
||||
this.app.on('ready', this.view.setter('camera', 'ready'));
|
||||
this.app.on('busy', this.view.setter('camera', 'busy'));
|
||||
|
||||
// We need the app to be first localized before localizing the hud view.
|
||||
this.app.on('localized', this.localize);
|
||||
|
||||
// Settings
|
||||
this.app.once('settings:configured', this.view.show);
|
||||
this.app.on('settings:configured', this.updateFlashSupport);
|
||||
this.app.settings.flashModes.on('change:selected', this.updateFlashMode);
|
||||
this.app.settings.mode.on('change:selected', this.updateFlashMode);
|
||||
this.app.settings.cameras.on('change:selected', this.updateCamera);
|
||||
|
||||
// We 'debouce' some UI callbacks to prevent
|
||||
// thrashing the hardware when a user taps repeatedly.
|
||||
// This means the first calback will fire instantly but
|
||||
// subsequent events will be blocked for given time period.
|
||||
this.view.on('click:camera', debounce(this.onCameraClick, 500, true));
|
||||
this.view.on('click:settings', this.app.firer('settings:toggle'));
|
||||
this.view.on('click:flash', this.onFlashClick);
|
||||
|
||||
// Countdown
|
||||
this.app.on('countdown:started', this.view.setter('countdown', 'active'));
|
||||
this.app.on('countdown:ended', this.view.setter('countdown', 'inactive'));
|
||||
|
||||
// Settings
|
||||
this.app.on('settings:opened', this.view.hide);
|
||||
this.app.on('settings:closed', this.view.show);
|
||||
|
||||
// Preview gallery
|
||||
this.app.on('previewgallery:opened', this.view.hide);
|
||||
this.app.on('previewgallery:closed', this.view.show);
|
||||
};
|
||||
|
||||
HudController.prototype.onCameraClick = function() {
|
||||
debug('camera clicked');
|
||||
this.clearNotifications();
|
||||
this.app.settings.cameras.next();
|
||||
};
|
||||
|
||||
HudController.prototype.clearNotifications = function() {
|
||||
this.notification.clear(this.flashNotification);
|
||||
};
|
||||
|
||||
/**
|
||||
* Cycle to the next available flash
|
||||
* option, update the HUD view and
|
||||
* show a change notification.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
HudController.prototype.onFlashClick = function() {
|
||||
var setting = this.settings.flashModes;
|
||||
var ishdrOn = this.settings.hdr.selected('key') === 'on';
|
||||
|
||||
setting.next();
|
||||
this.view.set('flashMode' , setting.selected('key'));
|
||||
this.notify(setting, ishdrOn);
|
||||
};
|
||||
|
||||
/**
|
||||
* Display a notifcation showing the
|
||||
* current state of the given setting.
|
||||
*
|
||||
* @param {Setting} setting
|
||||
* @private
|
||||
*/
|
||||
HudController.prototype.notify = function(setting, hdrDeactivated) {
|
||||
var optionTitle = '<span data-l10n-id="' +
|
||||
setting.selected('title') + '"></span>';
|
||||
var title = '<span data-l10n-id="' + setting.get('title') +'"></span>';
|
||||
var html;
|
||||
|
||||
// Check if the `hdr` setting is going to be deactivated as part
|
||||
// of the change in the `flashMode` setting and display a specialized
|
||||
// notification if that is the case
|
||||
if (hdrDeactivated) {
|
||||
html = title + ' ' + optionTitle + '<br/>' +
|
||||
'<span data-l10n-id="hdr-deactivated"></span>';
|
||||
} else {
|
||||
html = title + '<br/>' + optionTitle;
|
||||
}
|
||||
|
||||
this.flashNotification = this.notification.display({ text: {html: html} });
|
||||
};
|
||||
|
||||
/**
|
||||
* Localize hud view when app is localized or locale updated.
|
||||
*/
|
||||
HudController.prototype.localize = function() {
|
||||
this.view.setFlashModeLabel(this.settings.flashModes.selected());
|
||||
this.view.setCameraLabel(this.settings.cameras.selected());
|
||||
this.view.setMenuLabel();
|
||||
};
|
||||
|
||||
HudController.prototype.updateFlashMode = function() {
|
||||
var selected = this.settings.flashModes.selected();
|
||||
if (!selected) { return; }
|
||||
this.view.setFlashMode(selected);
|
||||
debug('updated flash mode: %s', selected.key);
|
||||
};
|
||||
|
||||
HudController.prototype.updateFlashSupport = function() {
|
||||
var supported = this.settings.flashModes.supported();
|
||||
this.view.enable('flash', supported);
|
||||
this.updateFlashMode();
|
||||
debug('flash supported: %s', supported);
|
||||
};
|
||||
|
||||
HudController.prototype.updateCamera = function() {
|
||||
var selected = this.settings.cameras.selected();
|
||||
if (!selected) { return; }
|
||||
this.view.setCamera(selected);
|
||||
debug('updated camera: %s', selected.key);
|
||||
};
|
||||
|
||||
});
|
||||
89
gaia/apps/camera/js/controllers/indicators.js
Executable file
89
gaia/apps/camera/js/controllers/indicators.js
Executable file
|
|
@ -0,0 +1,89 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:indicators');
|
||||
var IndicatorsView = require('views/indicators');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new IndicatorsController(app); };
|
||||
module.exports.IndicatorsController = IndicatorsController;
|
||||
|
||||
/**
|
||||
* Initialize a new `IndicatorsController`
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
function IndicatorsController(app) {
|
||||
this.app = app;
|
||||
this.settings = app.settings;
|
||||
this.configure = this.configure.bind(this);
|
||||
this.createView();
|
||||
this.configure();
|
||||
this.bindEvents();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and injects the view.
|
||||
*
|
||||
* The view is hidden until the
|
||||
* settings are configured.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
IndicatorsController.prototype.createView = function() {
|
||||
debug('create view');
|
||||
this.view = this.app.views.indicators || new IndicatorsView();
|
||||
this.view.appendTo(this.app.el);
|
||||
debug('view created');
|
||||
};
|
||||
|
||||
/**
|
||||
* Bind to relevant events.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
IndicatorsController.prototype.bindEvents = function() {
|
||||
this.settings.countdown.on('change:selected', this.view.setter('countdown'));
|
||||
this.settings.mode.on('change:selected', this.view.setter('mode'));
|
||||
this.settings.hdr.on('change:selected', this.view.setter('hdr'));
|
||||
this.app.on('change:batteryStatus', this.view.setter('battery'));
|
||||
this.app.on('change:recording', this.view.setter('recording'));
|
||||
this.app.on('settings:configured', this.configure);
|
||||
|
||||
// Handle indicators screen reader visibility
|
||||
// depending on settings visibility. When settings
|
||||
// are open, though the view is behind an overlay
|
||||
// and not hidden off screen, it still needs
|
||||
// to be hidden from the screen reader.
|
||||
this.app.on('settings:opened', this.view.setter('ariaHidden', true));
|
||||
this.app.on('settings:closed', this.view.setter('ariaHidden', false));
|
||||
|
||||
// Preview gallery
|
||||
this.app.on('previewgallery:opened', this.view.hide);
|
||||
this.app.on('previewgallery:closed', this.view.show);
|
||||
debug('events bound');
|
||||
};
|
||||
|
||||
/**
|
||||
* Configures the view to match
|
||||
* current settings state.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
IndicatorsController.prototype.configure = function() {
|
||||
debug('configuring');
|
||||
this.view.set('hdr', this.settings.hdr.selected('key'));
|
||||
this.view.set('countdown', this.settings.countdown.selected('key'));
|
||||
this.view.set('battery', this.app.get('batteryStatus'));
|
||||
debug('configured');
|
||||
};
|
||||
|
||||
});
|
||||
214
gaia/apps/camera/js/controllers/media.js
Executable file
214
gaia/apps/camera/js/controllers/media.js
Executable file
|
|
@ -0,0 +1,214 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:media');
|
||||
var bindAll = require('lib/bind-all');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new MediaController(app); };
|
||||
module.exports.MediaController = MediaController;
|
||||
|
||||
function MediaController(app) {
|
||||
bindAll(this);
|
||||
this.app = app;
|
||||
this.require = app.require;
|
||||
this.settings = app.settings;
|
||||
this.bindEvents();
|
||||
this.configure();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
MediaController.prototype.bindEvents = function() {
|
||||
this.app.on('storage:itemdeleted', this.onItemDeleted);
|
||||
this.app.on('previewgallery:deletevideo', this.onItemDeleted);
|
||||
this.app.on('previewgallery:deletepicture', this.onItemDeleted);
|
||||
this.app.on('storage:changed', this.onStorageChanged);
|
||||
this.app.on('newmedia', this.onNewMedia);
|
||||
this.app.on('hidden', this.onHidden);
|
||||
this.app.on('lazyloaded', this.configured);
|
||||
|
||||
// Preload libraries we may need after capturing to maintain snappiness
|
||||
var self = this;
|
||||
this.app.once('capture', function() {
|
||||
var lib = ['lib/create-thumbnail-image', 'lib/prepare-preview-blob'];
|
||||
self.require(lib, function() {});
|
||||
});
|
||||
|
||||
debug('events bound');
|
||||
};
|
||||
|
||||
MediaController.prototype.configure = function() {
|
||||
this.items = []; // All the pictures and videos we know about
|
||||
this.currentItem = null;
|
||||
|
||||
var dpr = window.devicePixelRatio;
|
||||
this.thumbnailSize = {
|
||||
width: this.settings.previewGallery.get('thumbnailWidth') * dpr,
|
||||
height: this.settings.previewGallery.get('thumbnailHeight') * dpr
|
||||
};
|
||||
|
||||
this.configured();
|
||||
};
|
||||
|
||||
MediaController.prototype.configured = function() {
|
||||
this.app.emit('media:configured', this.items);
|
||||
};
|
||||
|
||||
MediaController.prototype.onNewMedia = function(item) {
|
||||
// If we're handling a pick activity we don't generate thumbnails
|
||||
if (this.app.activity.pick) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
if (item.isVideo) {
|
||||
// If the new media is video, use it as-is
|
||||
addNewMedia(item);
|
||||
} else {
|
||||
// If it is a photo, find its EXIF preview first
|
||||
this.require(['lib/prepare-preview-blob'], function(preparePreview) {
|
||||
preparePreview(item.blob, function(metadata) {
|
||||
metadata.blob = item.blob;
|
||||
metadata.filepath = item.filepath;
|
||||
addNewMedia(metadata);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addNewMedia(item) {
|
||||
self.items.unshift(item);
|
||||
self.updateThumbnail();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete all items in the media controller
|
||||
* when storage becomes unavailable.
|
||||
*
|
||||
* @param {String} status
|
||||
*/
|
||||
MediaController.prototype.onStorageChanged = function(status) {
|
||||
if (status === 'unavailable') {
|
||||
this.configure();
|
||||
this.updateThumbnail();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* As a privacy feature, when the camera app is used from the lockscreen
|
||||
* and the lockscreen is actually locked with a passcode, we don't want
|
||||
* the camera to retain any state from one use to the next. So if the
|
||||
* camera is hidden (i.e. if the phone returns to the lockscreen) we
|
||||
* forget our state. In practice, it appears that the system app actually
|
||||
* kills the camera when this happens, so this code is redundant.
|
||||
*/
|
||||
MediaController.prototype.onHidden = function() {
|
||||
if (this.app.inSecureMode) {
|
||||
this.configure(); // Forget all stored images
|
||||
this.updateThumbnail(); // Get rid of any thumbnail
|
||||
}
|
||||
};
|
||||
|
||||
MediaController.prototype.updateThumbnail = function() {
|
||||
// This is the media item that we want to create a thumbnail for.
|
||||
var media = this.items[0] || null;
|
||||
if (media === this.currentItem) {
|
||||
return;
|
||||
}
|
||||
if (media === null) {
|
||||
this.app.emit('newthumbnail', null);
|
||||
return;
|
||||
}
|
||||
|
||||
// To create a thumbanil, we first need to figure out which image
|
||||
// blob to use, and what the size and rotation of that image is.
|
||||
var blob; // The image we'll create the thumbnail from.
|
||||
var metadata = { // The metadata for that image.
|
||||
rotation: media.rotation,
|
||||
mirrored: media.mirrored
|
||||
};
|
||||
|
||||
if (media.isVideo) {
|
||||
// If it is a video we can create a thumbnail from the poster image
|
||||
blob = media.poster.blob;
|
||||
metadata.width = media.poster.width;
|
||||
metadata.height = media.poster.height;
|
||||
} else {
|
||||
// If it is a photo we want to use the EXIF preview rather than
|
||||
// decoding the whole image if we can.
|
||||
if (media.preview) {
|
||||
|
||||
// The Tarako may produce EXIF previews that have the wrong
|
||||
// aspect ratio and are distorted. Check for that, and if the
|
||||
// aspect ratio is not right, then create a thumbnail from the
|
||||
// full size image
|
||||
var fullRatio = media.width / media.height;
|
||||
var previewRatio = media.preview.width / media.preview.height;
|
||||
|
||||
// If aspect ratios match, create thumbnail from EXIF preview
|
||||
if (Math.abs(fullRatio - previewRatio) < 0.01) {
|
||||
blob = media.blob.slice(media.preview.start, media.preview.end,
|
||||
'image/jpeg');
|
||||
metadata.width = media.preview.width;
|
||||
metadata.height = media.preview.height;
|
||||
}
|
||||
}
|
||||
|
||||
// If a thumbnail couldn't be obtained from the EXIF preview,
|
||||
// use the full image
|
||||
if (!blob) {
|
||||
blob = media.blob;
|
||||
metadata.width = media.width;
|
||||
metadata.height = media.height;
|
||||
}
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.require(['lib/create-thumbnail-image'], function(createThumbnailImage) {
|
||||
createThumbnailImage(blob, metadata, self.thumbnailSize, (thumb) => {
|
||||
self.currentItem = media;
|
||||
self.app.emit('newthumbnail', thumb);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete and update items in the preview gallery
|
||||
* when images/videos are deleted by others
|
||||
*
|
||||
* @param {Object} filepath
|
||||
*/
|
||||
MediaController.prototype.onItemDeleted = function(data) {
|
||||
var deleteIdx = -1;
|
||||
var deletedFilepath = typeof(data) === 'object' ? data.path : data;
|
||||
|
||||
// find the item in items
|
||||
for (var n = 0; n < this.items.length; n++) {
|
||||
if (this.items[n].filepath === deletedFilepath) {
|
||||
deleteIdx = n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Exit when item not found
|
||||
if (n === this.items.length) { return; }
|
||||
|
||||
var item = this.items[deleteIdx];
|
||||
|
||||
// Check if this event is being stopped such as in the case
|
||||
// of resizing an image for a share activity.
|
||||
if (item.resizing) { return; }
|
||||
this.items.splice(deleteIdx, 1);
|
||||
this.updateThumbnail();
|
||||
this.app.emit('media:deleted');
|
||||
};
|
||||
|
||||
});
|
||||
128
gaia/apps/camera/js/controllers/overlay.js
Executable file
128
gaia/apps/camera/js/controllers/overlay.js
Executable file
|
|
@ -0,0 +1,128 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:overlay');
|
||||
var bindAll = require('lib/bind-all');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new OverlayController(app); };
|
||||
module.exports.OverlayController = OverlayController;
|
||||
|
||||
/**
|
||||
* Initialize a new `OverlayController`
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
function OverlayController(app) {
|
||||
bindAll(this);
|
||||
this.overlays = {};
|
||||
this.app = app;
|
||||
this.require = app.require;
|
||||
this.activity = app.activity;
|
||||
this.bindEvents();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
OverlayController.prototype.bindEvents = function() {
|
||||
this.app.on('storage:changed', this.onStorageChanged);
|
||||
this.app.on('change:batteryStatus', this.onBatteryChanged);
|
||||
this.app.on('camera:requesting', this.onCameraRequesting);
|
||||
this.app.on('camera:error', this.onCameraError);
|
||||
};
|
||||
|
||||
/**
|
||||
* Respond to storage `statechange`
|
||||
* events by inserting or destroying
|
||||
* overlays from the app.
|
||||
*
|
||||
* @param {String} value ['nospace'|'shared'|'unavailable'|'available']
|
||||
*/
|
||||
OverlayController.prototype.onStorageChanged = function(state) {
|
||||
this.updateOverlay('storage', state !== 'available', state);
|
||||
};
|
||||
|
||||
/**
|
||||
* Respond to battery `statuschange`
|
||||
* events by inserting or destroying
|
||||
* overlays from the app.
|
||||
*
|
||||
* @param {String} status ['shutdown'|'critical'|'verylow'|'low']
|
||||
*/
|
||||
OverlayController.prototype.onBatteryChanged = function(state) {
|
||||
this.updateOverlay('battery', state === 'shutdown', state);
|
||||
};
|
||||
|
||||
/**
|
||||
* Respond to camera `requesting`
|
||||
* events by destroying overlays
|
||||
* from the app.
|
||||
*
|
||||
* @param {String} state ['start'|'success'|'fail']
|
||||
*/
|
||||
OverlayController.prototype.onCameraRequesting = function() {
|
||||
this.updateOverlay('cameraError', false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Respond to camera `error`
|
||||
* events by inserting overlays
|
||||
* into the app.
|
||||
*
|
||||
* @param {String} state ['start'|'success'|'fail']
|
||||
*/
|
||||
OverlayController.prototype.onCameraError = function(type) {
|
||||
this.updateOverlay('cameraError', true, type);
|
||||
};
|
||||
|
||||
OverlayController.prototype.updateOverlay = function(type, enabled, reason) {
|
||||
debug('\'%s/%s\' overlay %s', type, reason, enabled);
|
||||
|
||||
var overlay = this.overlays[type];
|
||||
if (!overlay) {
|
||||
overlay = this.overlays[type] = {
|
||||
id: 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (overlay.view) {
|
||||
overlay.view.destroy();
|
||||
delete overlay.view;
|
||||
}
|
||||
|
||||
var id = ++overlay.id;
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.require(['views/overlay'], function(OverlayView) {
|
||||
if (id !== overlay.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
var closable = self.activity.pick && type !== 'request-fail';
|
||||
var view = new OverlayView({
|
||||
type: reason,
|
||||
closable: closable,
|
||||
});
|
||||
if (!view.rendered()) {
|
||||
return;
|
||||
}
|
||||
|
||||
overlay.view = view.appendTo(document.body)
|
||||
.on('click:close-btn', function() {
|
||||
self.app.emit('activitycanceled');
|
||||
});
|
||||
|
||||
debug('inserted \'%s/%s\' overlay', type, reason);
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
305
gaia/apps/camera/js/controllers/preview-gallery.js
Executable file
305
gaia/apps/camera/js/controllers/preview-gallery.js
Executable file
|
|
@ -0,0 +1,305 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:preview-gallery');
|
||||
var StringUtils = require('lib/string-utils');
|
||||
var bindAll = require('lib/bind-all');
|
||||
var PreviewGalleryView = require('views/preview-gallery');
|
||||
var CustomDialog = require('CustomDialog');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new PreviewGalleryController(app); };
|
||||
module.exports.PreviewGalleryController = PreviewGalleryController;
|
||||
|
||||
function PreviewGalleryController(app) {
|
||||
bindAll(this);
|
||||
this.app = app;
|
||||
this.require = app.require;
|
||||
this.settings = app.settings;
|
||||
this.dialog = app.dialog || CustomDialog; // test hook
|
||||
this.resizeImageAndSave = app.resizeImageAndSave;
|
||||
this.currentItemIndex = 0;
|
||||
this.bindEvents();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
PreviewGalleryController.prototype.bindEvents = function() {
|
||||
this.app.on('media:configured', this.updatePreviewGallery);
|
||||
this.app.on('media:deleted', this.updatePreviewGallery);
|
||||
this.app.on('preview', this.openPreview);
|
||||
this.app.on('hidden', this.closePreview);
|
||||
|
||||
debug('events bound');
|
||||
};
|
||||
|
||||
PreviewGalleryController.prototype.resetPreviewGallery = function(items) {
|
||||
this.items = items;
|
||||
};
|
||||
|
||||
PreviewGalleryController.prototype.openPreview = function() {
|
||||
// If we're handling a pick activity the preview gallery is not used
|
||||
if (this.app.activity.pick || this.view || this.app.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
var maxPreviewSize = window.CONFIG_MAX_IMAGE_PIXEL_SIZE;
|
||||
|
||||
this.view = new PreviewGalleryView();
|
||||
this.view.maxPreviewSize = maxPreviewSize;
|
||||
this.view.render().appendTo(this.app.el);
|
||||
|
||||
this.view.on('click:gallery', this.onGalleryButtonClick);
|
||||
this.view.on('click:share', this.shareCurrentItem);
|
||||
this.view.on('click:delete', this.deleteCurrentItem);
|
||||
this.view.on('click:back', this.closePreview);
|
||||
this.view.on('swipe', this.handleSwipe);
|
||||
this.view.on('click:options', this.onOptionsClick);
|
||||
this.view.on('loadingvideo', this.app.firer('busy'));
|
||||
this.view.on('playingvideo', this.app.firer('ready'));
|
||||
|
||||
// If lockscreen is locked, hide all control buttons
|
||||
var secureMode = this.app.inSecureMode;
|
||||
this.view.set('secure-mode', secureMode);
|
||||
this.view.open();
|
||||
|
||||
this.previewItem();
|
||||
this.app.emit('previewgallery:opened');
|
||||
};
|
||||
|
||||
PreviewGalleryController.prototype.closePreview = function() {
|
||||
if (this.view) {
|
||||
this.currentItemIndex = 0;
|
||||
this.view.close();
|
||||
this.view.destroy();
|
||||
this.view = null;
|
||||
}
|
||||
|
||||
this.app.emit('previewgallery:closed');
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the gallery app when the
|
||||
* gallery button is pressed.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
PreviewGalleryController.prototype.onGalleryButtonClick = function() {
|
||||
// Can't launch the gallery if the lockscreen is locked.
|
||||
// The button shouldn't even be visible in this case, but
|
||||
// let's be really sure here.
|
||||
if (this.app.inSecureMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
var MozActivity = window.MozActivity;
|
||||
|
||||
// Launch the gallery with an activity
|
||||
this.mozActivity = new MozActivity({
|
||||
name: 'browse',
|
||||
data: { type: 'photos' }
|
||||
});
|
||||
};
|
||||
|
||||
PreviewGalleryController.prototype.onOptionsClick = function() {
|
||||
if (this.app.inSecureMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.view.showOptionsMenu();
|
||||
};
|
||||
|
||||
PreviewGalleryController.prototype.shareCurrentItem = function() {
|
||||
var index = this.currentItemIndex;
|
||||
var item = this.items[index];
|
||||
|
||||
if (this.app.inSecureMode || !item || item.resizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
var type = item.isVideo ? 'video/*' : 'image/*';
|
||||
var filename = StringUtils.lastPathComponent(item.filepath);
|
||||
|
||||
var launchShareActivity = function(blob) {
|
||||
var activity = new window.MozActivity({
|
||||
name: 'share',
|
||||
data: {
|
||||
type: type,
|
||||
number: 1,
|
||||
blobs: [blob],
|
||||
filenames: [filename],
|
||||
filepaths: [item.filepath] /* temporary hack for bluetooth app */
|
||||
}
|
||||
});
|
||||
activity.onerror = function(e) {
|
||||
console.warn('Share activity error:', activity.error.name);
|
||||
};
|
||||
};
|
||||
|
||||
if (item.isVideo) {
|
||||
launchShareActivity(item.blob);
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
item.resizing = true;
|
||||
this.app.emit('busy', 'resizingImage');
|
||||
|
||||
// Resize the image to the maximum pixel size for share activities.
|
||||
// If no maximum is specified (value is `0`), then simply rotate
|
||||
// (if needed) and re-save the image prior to launching the activity.
|
||||
var maxSize = this.settings.activity.get('maxSharePixelSize');
|
||||
this.require(['lib/resize-image-and-save'], function(resize) {
|
||||
if (!self.resizeImageAndSave) {
|
||||
self.resizeImageAndSave = resize;
|
||||
}
|
||||
self.resizeImageAndSave({
|
||||
blob: item.blob,
|
||||
size: maxSize,
|
||||
}, function(resizedBlob) {
|
||||
// Update the cached preview to reflect the new size of the saved
|
||||
// image; it will also rotate the image based on the EXIF data before
|
||||
// saving, so we should adjust for that
|
||||
if (resizedBlob !== item.blob) {
|
||||
item.blob = resizedBlob;
|
||||
if (maxSize && maxSize.width && maxSize.height) {
|
||||
item.width = maxSize.width;
|
||||
item.height = maxSize.height;
|
||||
} else if (item.rotation === 90 || item.rotation === 270) {
|
||||
var tmp = item.width;
|
||||
item.width = item.height;
|
||||
item.height = tmp;
|
||||
}
|
||||
delete item.rotation;
|
||||
}
|
||||
delete item.resizing;
|
||||
self.app.emit('ready');
|
||||
launchShareActivity(resizedBlob);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete the current item
|
||||
* when the delete button is pressed.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
PreviewGalleryController.prototype.deleteCurrentItem = function() {
|
||||
// The button should be gone,but
|
||||
// hard exit from this function just in case.
|
||||
if (this.app.inSecureMode) { return; }
|
||||
|
||||
var index = this.currentItemIndex;
|
||||
var item = this.items[index];
|
||||
var filepath = item.filepath;
|
||||
var dialog = this.dialog;
|
||||
var self = this;
|
||||
var msg;
|
||||
|
||||
if (item.isVideo) {
|
||||
msg = 'delete-video?';
|
||||
}
|
||||
else {
|
||||
msg = 'delete-photo?';
|
||||
}
|
||||
|
||||
dialog.show('', msg, {
|
||||
title: 'cancel',
|
||||
callback: closeDialog
|
||||
}, {
|
||||
title: 'delete',
|
||||
callback: deleteItem,
|
||||
recommend: false
|
||||
});
|
||||
|
||||
function closeDialog() {
|
||||
dialog.hide();
|
||||
}
|
||||
|
||||
function deleteItem() {
|
||||
dialog.hide();
|
||||
|
||||
// Actually delete the file
|
||||
if (item.isVideo) {
|
||||
self.app.emit('previewgallery:deletevideo', filepath);
|
||||
} else {
|
||||
self.app.emit('previewgallery:deletepicture', filepath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the preview with the latest recent item
|
||||
* after deleting images/videos.
|
||||
*
|
||||
* @param {String} index
|
||||
*/
|
||||
PreviewGalleryController.prototype.updatePreviewGallery = function(items) {
|
||||
// Remove the item from the array of items
|
||||
if (items) {
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
// If there are no more items, go back to the camera
|
||||
if (this.items.length === 0) {
|
||||
this.closePreview();
|
||||
}
|
||||
else {
|
||||
if (this.currentItemIndex === this.items.length) {
|
||||
this.currentItemIndex = this.items.length - 1;
|
||||
}
|
||||
|
||||
var isOpened = this.view ? true : false;
|
||||
if (isOpened) {
|
||||
this.previewItem();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* To Do: Image Swipe Transition
|
||||
*/
|
||||
PreviewGalleryController.prototype.handleSwipe = function(direction) {
|
||||
if (direction === 'left') {
|
||||
this.next();
|
||||
}
|
||||
else if (direction === 'right') {
|
||||
this.previous();
|
||||
}
|
||||
};
|
||||
|
||||
PreviewGalleryController.prototype.next = function() {
|
||||
if (this.currentItemIndex < this.items.length - 1) {
|
||||
this.currentItemIndex += 1;
|
||||
this.previewItem();
|
||||
}
|
||||
};
|
||||
|
||||
PreviewGalleryController.prototype.previous = function() {
|
||||
if (this.currentItemIndex > 0) {
|
||||
this.currentItemIndex -= 1;
|
||||
this.previewItem();
|
||||
}
|
||||
};
|
||||
|
||||
PreviewGalleryController.prototype.previewItem = function() {
|
||||
var index = this.currentItemIndex;
|
||||
var item = this.items[index];
|
||||
this.view.updateCountText(index + 1, this.items.length);
|
||||
|
||||
if (item.isVideo) {
|
||||
this.view.showVideo(item);
|
||||
} else {
|
||||
this.view.showImage(item);
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
78
gaia/apps/camera/js/controllers/recording-timer.js
Executable file
78
gaia/apps/camera/js/controllers/recording-timer.js
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:recording-timer');
|
||||
var RecordingTimerView = require('views/recording-timer');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new RecordingTimerController(app); };
|
||||
module.exports.RecordingTimerController = RecordingTimerController;
|
||||
|
||||
/**
|
||||
* Initialize a new `RecordingTimerController`
|
||||
*
|
||||
* Allow `this.view` to be mocked for testing
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
function RecordingTimerController(app) {
|
||||
this.onRecordingChange = this.onRecordingChange.bind(this);
|
||||
this.app = app;
|
||||
this.createView();
|
||||
this.bindEvents();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject the view into the app.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
RecordingTimerController.prototype.createView = function() {
|
||||
this.view = this.app.view || new RecordingTimerView();
|
||||
this.view.appendTo(this.app.el);
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen to relevant events.
|
||||
*
|
||||
* Toggle the visibility when
|
||||
* recording starts/stops.
|
||||
*
|
||||
* Update the time value when camera
|
||||
* recording time updates.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
RecordingTimerController.prototype.bindEvents = function() {
|
||||
this.app.on('change:recording', this.onRecordingChange);
|
||||
this.app.on('camera:recorderTimeUpdate', this.view.value);
|
||||
debug('events bound');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the view when recording,
|
||||
* hide it when not.
|
||||
*
|
||||
* @param {Boolean} recording
|
||||
* @private
|
||||
*/
|
||||
RecordingTimerController.prototype.onRecordingChange = function(recording) {
|
||||
debug('recording: %s', recording);
|
||||
if (!recording) {
|
||||
this.view.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
this.view.value(0);
|
||||
this.view.show();
|
||||
};
|
||||
|
||||
});
|
||||
485
gaia/apps/camera/js/controllers/settings.js
Executable file
485
gaia/apps/camera/js/controllers/settings.js
Executable file
|
|
@ -0,0 +1,485 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var formatRecorderProfiles = require('lib/format-recorder-profiles');
|
||||
var formatPictureSizes = require('lib/format-picture-sizes');
|
||||
var debug = require('debug')('controller:settings');
|
||||
var bindAll = require('lib/bind-all');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new SettingsController(app); };
|
||||
module.exports.SettingsController = SettingsController;
|
||||
|
||||
/**
|
||||
* Initialize a new `SettingsController`
|
||||
*
|
||||
* @constructor
|
||||
* @param {App} app
|
||||
*/
|
||||
function SettingsController(app) {
|
||||
bindAll(this);
|
||||
this.app = app;
|
||||
this.require = app.require;
|
||||
this.settings = app.settings;
|
||||
this.activity = app.activity;
|
||||
this.notification = app.views.notification;
|
||||
|
||||
// Provide test hooks
|
||||
this.nav = app.nav || navigator;
|
||||
this.SettingsView = app.SettingsView;
|
||||
this.formatPictureSizes = app.formatPictureSizes || formatPictureSizes;
|
||||
this.formatRecorderProfiles = app.formatRecorderProfiles ||
|
||||
formatRecorderProfiles;
|
||||
|
||||
this.configure();
|
||||
this.bindEvents();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers settings 'aliases' that provide
|
||||
* a single interface for settings such as
|
||||
* `flashModes` where we have `flashModesPicture`
|
||||
* and `flashModesVideo`.
|
||||
*
|
||||
* This means that we can just use settings.flashModes,
|
||||
* and be confident that it will interface with the
|
||||
* correct setting depending on the value of `mode`.
|
||||
*
|
||||
* You can always use the underlying settings
|
||||
* directly if you need that kind of control.
|
||||
*/
|
||||
SettingsController.prototype.configure = function() {
|
||||
this.setupRecorderProfilesAlias();
|
||||
this.setupPictureSizesAlias();
|
||||
this.setupFlashModesAlias();
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a SettingAlias that dynamically
|
||||
* interfaces with the correct recorder
|
||||
* profile Setting based on which camera
|
||||
* ('front'/'back') is selected.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SettingsController.prototype.setupRecorderProfilesAlias = function() {
|
||||
var settings = this.settings;
|
||||
this.settings.alias({
|
||||
key: 'recorderProfiles',
|
||||
settings: {
|
||||
back: this.settings.recorderProfilesBack,
|
||||
front: this.settings.recorderProfilesFront
|
||||
},
|
||||
get: function() {
|
||||
var camera = settings.cameras.selected('key');
|
||||
return this.settings[camera];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a SettingAlias that dynamically
|
||||
* interfaces with the correct picture
|
||||
* size Setting based on which camera
|
||||
* ('front'/'back') is selected.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SettingsController.prototype.setupPictureSizesAlias = function() {
|
||||
var settings = this.settings;
|
||||
this.settings.alias({
|
||||
key: 'pictureSizes',
|
||||
settings: {
|
||||
back: this.settings.pictureSizesBack,
|
||||
front: this.settings.pictureSizesFront
|
||||
},
|
||||
get: function() {
|
||||
var camera = settings.cameras.selected('key');
|
||||
return this.settings[camera];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a SettingAlias that dynamically
|
||||
* interfaces with the correct flash-modes
|
||||
* Setting based on which mode ('picture'/
|
||||
* 'video') is selected.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SettingsController.prototype.setupFlashModesAlias = function() {
|
||||
var settings = this.settings;
|
||||
this.settings.alias({
|
||||
key: 'flashModes',
|
||||
settings: {
|
||||
picture: this.settings.flashModesPicture,
|
||||
video: this.settings.flashModesVideo
|
||||
},
|
||||
get: function() {
|
||||
var mode = settings.mode.selected('key');
|
||||
return this.settings[mode];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Bind to app events.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SettingsController.prototype.bindEvents = function() {
|
||||
this.app.on('localized', this.formatPictureSizeTitles);
|
||||
this.app.on('settings:toggle', this.toggleSettings);
|
||||
this.app.on('camera:newcamera', this.onNewCamera);
|
||||
this.app.on('activity:pick', this.onPickActivity);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the settings menu open/closed.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SettingsController.prototype.toggleSettings = function() {
|
||||
if (this.view) { this.closeSettings(); }
|
||||
else { this.openSettings(); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Render and display the settings menu.
|
||||
*
|
||||
* We use settings.menu() to retrieve
|
||||
* and ordered list of settings that
|
||||
* have a `menu` property.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SettingsController.prototype.openSettings = function() {
|
||||
debug('open settings');
|
||||
var self = this;
|
||||
this.app.emit('busy', 'lazyLoading');
|
||||
this.require(['views/settings'], function(SettingsView) {
|
||||
self.app.emit('ready');
|
||||
|
||||
if (!self.SettingsView) {
|
||||
self.SettingsView = SettingsView;
|
||||
}
|
||||
if (self.view) { return; }
|
||||
|
||||
var items = self.menuItems();
|
||||
self.view = new self.SettingsView({ items: items })
|
||||
.render()
|
||||
.appendTo(self.app.el)
|
||||
.on('click:close', self.closeSettings)
|
||||
.on('click:option', self.onOptionTap);
|
||||
|
||||
// Make sure the view is
|
||||
// hidden before fading in
|
||||
self.view.hide();
|
||||
self.view.fadeIn();
|
||||
|
||||
self.app.emit('settings:opened');
|
||||
debug('settings opened');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Destroy the settings menu.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SettingsController.prototype.closeSettings = function(done) {
|
||||
debug('close settings');
|
||||
if (!this.view) { return; }
|
||||
var self = this;
|
||||
this.view.fadeOut(function() {
|
||||
self.view.destroy();
|
||||
self.view = null;
|
||||
self.app.emit('settings:closed');
|
||||
debug('settings closed');
|
||||
if (typeof done === 'function') { done(); }
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects the option that was
|
||||
* clicked on the setting.
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {Setting} setting
|
||||
* @private
|
||||
*/
|
||||
SettingsController.prototype.onOptionTap = function(key, setting) {
|
||||
var flashMode = this.settings.flashModesPicture.selected('key');
|
||||
var ishdrOn = setting.key === 'hdr' && key === 'on';
|
||||
var flashDeactivated = flashMode !== 'off' && ishdrOn;
|
||||
var self = this;
|
||||
|
||||
self.closeSettings(function() {
|
||||
setting.select(key);
|
||||
self.notify(setting, flashDeactivated);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Adjusts settings to meet requirements
|
||||
* on new pick activity.
|
||||
*
|
||||
* @param {Object} data
|
||||
* @private
|
||||
*/
|
||||
SettingsController.prototype.onPickActivity = function(data) {
|
||||
debug('pick activity', data);
|
||||
|
||||
var setting;
|
||||
var options;
|
||||
var updated = false;
|
||||
var maxFileSize = data.maxFileSizeBytes;
|
||||
var maxPixelSize = data.maxPixelSize;
|
||||
|
||||
// Settings changes made in 'pick'
|
||||
// sessions shouldn't persist.
|
||||
this.settings.dontSave();
|
||||
|
||||
if (maxPixelSize) {
|
||||
setting = this.settings.pictureSizes;
|
||||
var lastMaxPixelSize = setting.get('maxPixelSize');
|
||||
|
||||
this.settings.pictureSizesFront.set('maxPixelSize', maxPixelSize);
|
||||
this.settings.pictureSizesBack.set('maxPixelSize', maxPixelSize);
|
||||
debug('set maxPixelSize: %s', maxPixelSize);
|
||||
|
||||
if (lastMaxPixelSize !== maxPixelSize) {
|
||||
options = setting.get('options');
|
||||
var restricted = [];
|
||||
if (options && options.length > 0) {
|
||||
options.forEach(function(option) {
|
||||
if (option.pixelSize <= maxPixelSize) {
|
||||
restricted.push(option);
|
||||
}
|
||||
});
|
||||
setting.resetOptions(restricted);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (maxFileSize) {
|
||||
setting = this.settings.recorderProfiles;
|
||||
var lastMaxFileSize = setting.get('maxFileSizeBytes');
|
||||
|
||||
this.settings.recorderProfilesFront.set('maxFileSizeBytes', maxFileSize);
|
||||
this.settings.recorderProfilesBack.set('maxFileSizeBytes', maxFileSize);
|
||||
debug('set maxFileSize: %s', maxFileSize);
|
||||
|
||||
if (lastMaxFileSize !== maxFileSize) {
|
||||
options = setting.get('options');
|
||||
if (options && options.length > 1) {
|
||||
setting.resetOptions([options[options.length - 1]]);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the size restrictions come in after the camera was brought
|
||||
// up, then we must retrigger a configuration event
|
||||
if (updated) {
|
||||
this.app.emit('settings:configured');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Display a notifcation showing the
|
||||
* current state of the given setting.
|
||||
*
|
||||
* If `notification` is `false in config
|
||||
* for a setting then we don't show one.
|
||||
*
|
||||
* @param {Setting} setting
|
||||
* @private
|
||||
*/
|
||||
SettingsController.prototype.notify = function(setting, flashDeactivated) {
|
||||
var dontNotify = setting.get('notifications') === false;
|
||||
if (dontNotify) { return; }
|
||||
|
||||
var localizable = setting.get('optionsLocalizable') !== false;
|
||||
var title = '<span data-l10n-id="' + setting.get('title') + '"></span>';
|
||||
var html;
|
||||
|
||||
// Localize option title only if not specified in the config
|
||||
var optionTitle = localizable ?
|
||||
'<span data-l10n-id="' + setting.selected('title') + '"></span>' :
|
||||
'<span>' + setting.selected('title') + '</span>';
|
||||
|
||||
// Check if the `flashMode` setting is going to be deactivated as part
|
||||
// of the change in the `hdr` setting and display a specialized
|
||||
// notification if that is the case
|
||||
if (flashDeactivated) {
|
||||
html = title + ' ' + optionTitle + '<br/>' +
|
||||
'<span data-l10n-id="flash-deactivated"></span>';
|
||||
} else {
|
||||
html = title + '<br/>' + optionTitle;
|
||||
}
|
||||
|
||||
this.notification.display({ text: { html: html }});
|
||||
};
|
||||
|
||||
/**
|
||||
* When the hardware capabilities change
|
||||
* we update the available options for
|
||||
* each setting to match what is available.
|
||||
*
|
||||
* The rest of the app should listen for
|
||||
* the 'settings:configured' event as an
|
||||
* indication to update UI etc.
|
||||
*
|
||||
* @param {Object} capabilities
|
||||
*/
|
||||
SettingsController.prototype.onNewCamera = function(camera) {
|
||||
debug('new capabilities');
|
||||
var capabilities = camera.capabilities;
|
||||
|
||||
this.settings.hdr.filterOptions(capabilities.hdr);
|
||||
this.settings.flashModesPicture.filterOptions(capabilities.flashModes);
|
||||
this.settings.flashModesVideo.filterOptions(capabilities.flashModes);
|
||||
|
||||
this.configurePictureSizes(camera);
|
||||
this.configureRecorderProfiles(camera);
|
||||
|
||||
// Let the rest of the app know we're good to go.
|
||||
this.app.emit('settings:configured');
|
||||
debug('settings configured to new capabilities');
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the raw pictureSizes into
|
||||
* a format that Setting class can
|
||||
* work with, then resets the pictureSize
|
||||
* options.
|
||||
*
|
||||
* @param {Array} sizes
|
||||
*/
|
||||
SettingsController.prototype.configurePictureSizes = function(camera) {
|
||||
debug('configuring picture sizes');
|
||||
var maxPixelSize = window.CONFIG_MAX_IMAGE_PIXEL_SIZE;
|
||||
var sizes = camera.capabilities.pictureSizes;
|
||||
var setting = this.settings.pictureSizes;
|
||||
var currentSize = camera.pictureSize;
|
||||
var currentSizeKey = currentSize.width + 'x' + currentSize.height;
|
||||
var options = {
|
||||
exclude: setting.get('exclude'),
|
||||
include: setting.get('include'),
|
||||
maxPixelSize: maxPixelSize
|
||||
};
|
||||
|
||||
var formatted = this.formatPictureSizes(sizes, options);
|
||||
// A sensor may expose only sizes outside Gaia's original 4:3 preset.
|
||||
if (!formatted.length && sizes.length) {
|
||||
formatted = this.formatPictureSizes(sizes, {});
|
||||
}
|
||||
setting.resetOptions(formatted);
|
||||
|
||||
// If the setting has no stored default
|
||||
// set the setting to match the current
|
||||
// mozCamera setting.
|
||||
if (!setting.current().fetched) {
|
||||
setting.select(currentSizeKey, { silent: true });
|
||||
}
|
||||
|
||||
this.formatPictureSizeTitles();
|
||||
|
||||
debug('configured pictureSizes', setting.selected('key'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the raw recorderProfiles
|
||||
* into a format that Setting class can
|
||||
* work with, then resets the recorderProfile
|
||||
* options.
|
||||
*
|
||||
* @param {Array} sizes
|
||||
*/
|
||||
SettingsController.prototype.configureRecorderProfiles = function(camera) {
|
||||
var sizes = camera.capabilities.recorderProfiles;
|
||||
var currentProfile = camera.recorderProfile;
|
||||
var setting = this.settings.recorderProfiles;
|
||||
var maxFileSize = setting.get('maxFileSizeBytes');
|
||||
var exclude = setting.get('exclude');
|
||||
var options = { exclude: exclude };
|
||||
var items = this.formatRecorderProfiles(sizes, options);
|
||||
|
||||
// If a file size limit has been imposed,
|
||||
// pick the lowest-res (last) profile only.
|
||||
if (maxFileSize) { items = [items[items.length - 1]]; }
|
||||
|
||||
setting.resetOptions(items);
|
||||
|
||||
// Set the recorder profile to match
|
||||
// the currently selected mozCamera profile
|
||||
if (!maxFileSize && !setting.current().fetched) {
|
||||
setting.select(currentProfile, { silent: true });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a localized `title` property
|
||||
* on each pictureSize option. This is
|
||||
* used within the settings-menu.
|
||||
*
|
||||
* This is run each time `configurePictureSizes`
|
||||
* is run and each time the app recieves a
|
||||
* 'localized' event.
|
||||
*
|
||||
* If the app isn't 'localized' yet, we don't do
|
||||
* anything and wait for the 'localized'
|
||||
* event binding to run the function.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SettingsController.prototype.formatPictureSizeTitles = function() {
|
||||
return document.l10n.formatValue('mp').then((value) => {
|
||||
var options = this.settings.pictureSizes.get('options');
|
||||
options.forEach(function(size) {
|
||||
var data = size.data;
|
||||
var mp = data.mp ? data.mp + value + ' ' : '';
|
||||
size.title = mp + size.key;
|
||||
});
|
||||
|
||||
debug('picture size titles formatted');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a list of settings
|
||||
* based on the `settingsMenu`
|
||||
* configuration.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
SettingsController.prototype.menuItems = function() {
|
||||
var items = this.settings.settingsMenu.get('items');
|
||||
return items.filter(this.validMenuItem, this)
|
||||
.map(function(item) { return this.settings[item.key]; }, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests if the passed `settingsMenu`
|
||||
* item is allowed in the settings menu.
|
||||
*
|
||||
* @param {Object} item
|
||||
* @return {Boolean}
|
||||
*/
|
||||
SettingsController.prototype.validMenuItem = function(item) {
|
||||
var setting = this.settings[item.key];
|
||||
return !!setting && setting.supported();
|
||||
};
|
||||
|
||||
});
|
||||
49
gaia/apps/camera/js/controllers/sounds.js
Executable file
49
gaia/apps/camera/js/controllers/sounds.js
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:sounds');
|
||||
var Sounds = require('lib/sounds');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new SoundsController(app); };
|
||||
module.exports.SoundsController = SoundsController;
|
||||
|
||||
/**
|
||||
* Initialize a new `SoundsController`
|
||||
*
|
||||
* @param {App} app [description]
|
||||
*/
|
||||
function SoundsController(app) {
|
||||
var list = app.settings.sounds.get('list');
|
||||
this.sounds = new Sounds(list);
|
||||
this.app = app;
|
||||
this.bindEvents();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
SoundsController.prototype.bindEvents = function() {
|
||||
this.app.on('change:recording', this.onRecordingChange.bind(this));
|
||||
this.app.on('camera:willrecord', this.sounds.player('recordingStart'));
|
||||
this.app.on('camera:shutter', this.sounds.player('shutter'));
|
||||
this.app.on('countdown:immanent', this.sounds.player('countdown'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Plays the start/end recording sound.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
SoundsController.prototype.onRecordingChange = function(recording) {
|
||||
if (!recording) {
|
||||
this.sounds.play('recordingEnd');
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
191
gaia/apps/camera/js/controllers/storage.js
Executable file
191
gaia/apps/camera/js/controllers/storage.js
Executable file
|
|
@ -0,0 +1,191 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:storage');
|
||||
var bindAll = require('lib/bind-all');
|
||||
var Storage = require('lib/storage');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new StorageController(app); };
|
||||
module.exports.StorageController = StorageController;
|
||||
|
||||
/**
|
||||
* Initialize a new `StorageController`
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
function StorageController(app) {
|
||||
bindAll(this);
|
||||
this.app = app;
|
||||
this.camera = app.camera;
|
||||
this.settings = app.settings;
|
||||
this.storage = app.storage || new Storage();
|
||||
this.bindEvents();
|
||||
this.configure();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial configuration.
|
||||
*
|
||||
* Give the camera a way to create video filepaths.
|
||||
* This is so that the camera can record videos
|
||||
* directly to the final location without us
|
||||
* having to move the video file from temporary,
|
||||
* to final location at recording end.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
StorageController.prototype.configure = function() {
|
||||
this.storage.configure();
|
||||
this.camera.createVideoFilepath = this.storage.createVideoFilepath;
|
||||
this.updateMaxFileSize();
|
||||
};
|
||||
|
||||
/**
|
||||
* Bind to relevant events.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
StorageController.prototype.bindEvents = function() {
|
||||
debug('bind events');
|
||||
|
||||
// App
|
||||
this.settings.pictureSizes.on('change:selected', this.updateMaxFileSize);
|
||||
this.app.on('previewgallery:deletepicture', this.storage.deletePicture);
|
||||
this.app.on('previewgallery:deletevideo', this.storage.deleteVideo);
|
||||
this.app.on('settings:configured', this.updateMaxFileSize);
|
||||
this.app.on('camera:newimage', this.storePicture);
|
||||
this.app.on('camera:newvideo', this.storeVideo);
|
||||
this.app.on('visible', this.storage.check);
|
||||
|
||||
// Storage
|
||||
this.storage.on('volumechanged', this.app.firer('storage:volumechanged'));
|
||||
this.storage.on('itemdeleted', this.app.firer('storage:itemdeleted'));
|
||||
this.storage.on('changed', this.onChanged);
|
||||
this.storage.on('checked', this.onChecked);
|
||||
debug('events bound');
|
||||
};
|
||||
|
||||
/**
|
||||
* Relay storage state change events.
|
||||
*
|
||||
* @param {String} state
|
||||
* @private
|
||||
*/
|
||||
StorageController.prototype.onChanged = function(state) {
|
||||
debug('changed: %s', state);
|
||||
this.app.emit('storage:changed', state);
|
||||
};
|
||||
|
||||
/**
|
||||
* Emit the outcome of a storage check
|
||||
* so that other parts of the app
|
||||
* can respond.
|
||||
*
|
||||
* @param {String} value
|
||||
* @private
|
||||
*/
|
||||
StorageController.prototype.onChecked = function(value) {
|
||||
debug('checked: %s', value);
|
||||
this.app.emit('storage:checked', value);
|
||||
this.app.emit('storage:checked:' + value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Store a picture.
|
||||
*
|
||||
* In either case, save the memory-backed
|
||||
* photo blob to device storage, retrieve
|
||||
* the resulting File (blob) and pass that
|
||||
* around instead of the original memory blob.
|
||||
*
|
||||
* This is critical for "pick" activity consumers
|
||||
* where the memory-backed Blob is either highly
|
||||
* inefficent or will almost-immediately become
|
||||
* inaccesible, depending on the state of the
|
||||
* platform. https://bugzil.la/982779
|
||||
*
|
||||
* @param {Object} picture
|
||||
* @private
|
||||
*/
|
||||
StorageController.prototype.storePicture = function(picture) {
|
||||
var memoryBlob = picture.blob;
|
||||
var self = this;
|
||||
|
||||
this.storage.addPicture(
|
||||
memoryBlob,
|
||||
function(error, filepath, abspath, fileBlob) {
|
||||
picture.blob = fileBlob;
|
||||
picture.filepath = filepath;
|
||||
debug('stored picture', picture);
|
||||
self.app.emit('newmedia', picture);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Store a video.
|
||||
*
|
||||
* We don't store the video blob like
|
||||
* we do for images, as it is recorded
|
||||
* directly to the final location.
|
||||
* This is for memory reason. The poster
|
||||
* is also automatically created.
|
||||
*
|
||||
* @param {Object} video
|
||||
*/
|
||||
StorageController.prototype.storeVideo = function(video) {
|
||||
debug('new video', video);
|
||||
var poster = video.poster;
|
||||
var self = this;
|
||||
|
||||
// Add the poster image to the image storage
|
||||
poster.filepath = video.filepath.replace('.3gp', '.jpg');
|
||||
video.isVideo = true;
|
||||
|
||||
this.storage.addPicture(
|
||||
poster.blob,
|
||||
{ filepath: poster.filepath },
|
||||
function(error, path, absolutePath, fileBlob) {
|
||||
// Replace the memory-backed Blob with the DeviceStorage file-backed File.
|
||||
// Note that "video" has a reference to "poster", so video previews will
|
||||
// use this File.
|
||||
poster.blob = fileBlob;
|
||||
debug('new video', video);
|
||||
self.app.emit('newmedia', video);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate and set a `maxFileSize`
|
||||
* value on the storage instance.
|
||||
*
|
||||
* This is so that it can alert us when there
|
||||
* isn't enough space left in storage
|
||||
* to accomodate a new picture.
|
||||
*
|
||||
* It is very unlikely that a JPEG file will have a file size that is
|
||||
* more than half a byte per pixel. There is some fixed EXIF overhead
|
||||
* that is the same for small and large pictures, however, so we add
|
||||
* an additional 25,000 bytes of padding.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
StorageController.prototype.updateMaxFileSize = function() {
|
||||
var pictureSize = this.settings.pictureSizes.selected('data');
|
||||
/* Depending on when we get loaded, we may not have a picture size yet. */
|
||||
if (pictureSize) {
|
||||
var bytes = (pictureSize.width * pictureSize.height / 2) + 25000;
|
||||
this.storage.setMaxFileSize(bytes);
|
||||
debug('maxFileSize updated %s', bytes);
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
445
gaia/apps/camera/js/controllers/viewfinder.js
Executable file
445
gaia/apps/camera/js/controllers/viewfinder.js
Executable file
|
|
@ -0,0 +1,445 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var cameraCoordinates = require('lib/camera-coordinates');
|
||||
var debug = require('debug')('controller:viewfinder');
|
||||
var ViewfinderView = require('views/viewfinder');
|
||||
var FocusView = require('views/focus');
|
||||
var FacesView = require('views/faces');
|
||||
var bindAll = require('lib/bind-all');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new ViewfinderController(app); };
|
||||
module.exports.ViewfinderController = ViewfinderController;
|
||||
|
||||
/**
|
||||
* Initialize a new `ViewfinderController`
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
function ViewfinderController(app) {
|
||||
bindAll(this);
|
||||
this.app = app;
|
||||
this.camera = app.camera;
|
||||
this.activity = app.activity;
|
||||
this.settings = app.settings;
|
||||
this.createViews();
|
||||
this.bindEvents();
|
||||
this.configure();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and inject the views.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.createViews = function() {
|
||||
this.views = {};
|
||||
this.views.viewfinder = this.app.views.viewfinder || new ViewfinderView();
|
||||
this.views.focus = this.app.views.focus || new FocusView();
|
||||
this.views.faces = this.app.views.faces || new FacesView();
|
||||
this.views.focus.appendTo(this.views.viewfinder.el);
|
||||
this.views.faces.appendTo(this.views.viewfinder.el);
|
||||
this.views.viewfinder.appendTo(this.app.el);
|
||||
};
|
||||
|
||||
/**
|
||||
* Initial configuration.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.configure = function() {
|
||||
var settings = this.app.settings;
|
||||
var zoomSensitivity = settings.viewfinder.get('zoomGestureSensitivity');
|
||||
this.sensitivity = zoomSensitivity * window.innerWidth;
|
||||
this.configureScaleType();
|
||||
this.configureGrid();
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure the viewfinder scale type,
|
||||
* aspect fill/fit, depending on setting.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.configureScaleType = function() {
|
||||
var scaleType = this.app.settings.viewfinder.get('scaleType');
|
||||
this.views.viewfinder.scaleType = scaleType;
|
||||
debug('set scale type: %s', scaleType);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide grid depending on currently
|
||||
* selected option.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.configureGrid = function() {
|
||||
var grid = this.app.settings.grid.selected('key');
|
||||
this.views.viewfinder.set('grid', grid);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides the viewfinder frame-grid.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.hideGrid = function() {
|
||||
this.views.viewfinder.set('grid', 'off');
|
||||
};
|
||||
|
||||
/**
|
||||
* Bind to relavant events.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.bindEvents = function() {
|
||||
|
||||
// View
|
||||
this.views.viewfinder.on('fadedin', this.app.firer('viewfinder:visible'));
|
||||
this.views.viewfinder.on('fadedout', this.app.firer('viewfinder:hidden'));
|
||||
this.views.viewfinder.on('click', this.app.firer('viewfinder:click'));
|
||||
this.views.viewfinder.on('click', this.onViewfinderClicked);
|
||||
|
||||
// Tut tut, we shouldn't have direct coupling here.
|
||||
// TODO: Camera events should be relayed through the app.
|
||||
this.camera.on('zoomconfigured', this.onZoomConfigured);
|
||||
this.camera.on('zoomchanged', this.onZoomChanged);
|
||||
this.camera.on('preview:started', this.show);
|
||||
|
||||
// Camera
|
||||
this.app.on('camera:autofocuschanged', this.views.focus.showAutoFocusRing);
|
||||
this.app.on('camera:focusstatechanged', this.views.focus.setFocusState);
|
||||
this.app.on('camera:focusconfigured', this.onFocusConfigured);
|
||||
this.app.on('camera:shutter', this.views.viewfinder.shutter);
|
||||
this.app.on('camera:facesdetected', this.onFacesDetected);
|
||||
this.app.on('camera:configured', this.onCameraConfigured);
|
||||
this.app.on('camera:previewactive', this.onPreviewActive);
|
||||
this.app.on('busy', this.views.viewfinder.disable);
|
||||
this.app.on('ready', this.views.viewfinder.enable);
|
||||
this.app.on('camera:willchange', this.hide);
|
||||
|
||||
// Preview Gallery
|
||||
this.app.on('previewgallery:opened', this.onGalleryOpened);
|
||||
this.app.on('previewgallery:closed', this.onGalleryClosed);
|
||||
|
||||
// Settings
|
||||
this.app.on('settings:closed', this.onSettingsClosed);
|
||||
this.app.on('settings:opened', this.onSettingsOpened);
|
||||
this.app.settings.grid.on('change:selected',
|
||||
this.views.viewfinder.setter('grid'));
|
||||
|
||||
// Battery
|
||||
this.app.on('battery:powersave', this.onPowerSave);
|
||||
|
||||
// App
|
||||
this.app.on('pinch:changed', this.onPinchChanged);
|
||||
this.app.on('hidden', this.stopStream);
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform required viewfinder configuration
|
||||
* once the camera has configured.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.onCameraConfigured = function() {
|
||||
debug('configuring');
|
||||
this.loadStream();
|
||||
this.configurePreview();
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the viewfinder.
|
||||
*
|
||||
* If the critical-path is not done we
|
||||
* fade the viewfinder in straight away
|
||||
* to make sure we have the quickest
|
||||
* startup possible.
|
||||
*
|
||||
* We have to use a timeout for all other
|
||||
* viewfinder showing actions conceal a Gecko
|
||||
* rendering bug whereby the video element has
|
||||
* not yet 'visually' switched to the new stream
|
||||
* when we get the preview 'started' event from
|
||||
* the camera. This means the user sees a flicker
|
||||
* if we don't give it some time to adjust.
|
||||
*
|
||||
* We clear these timeouts to avoid multiple pending
|
||||
* timeouts, which could cause pain.
|
||||
*
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=982230
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.show = function() {
|
||||
debug('show');
|
||||
if (!this.app.criticalPathDone) {
|
||||
this.views.viewfinder.fadeIn(true);
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.showTimeout);
|
||||
this.showTimeout = setTimeout(this.views.viewfinder.fadeIn, 280);
|
||||
debug('schedule delayed fade-in');
|
||||
};
|
||||
|
||||
/**
|
||||
* Fades the viewfinder in.
|
||||
*
|
||||
* We clear any pending timeouts here
|
||||
* to prevent unusual behaviour ensuing.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.hide = function() {
|
||||
debug('hide');
|
||||
clearTimeout(this.showTimeout);
|
||||
this.views.viewfinder.fadeOut();
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets appropiate flags when the camera focus is configured
|
||||
*/
|
||||
ViewfinderController.prototype.onFocusConfigured = function(config) {
|
||||
this.views.focus.setFocusMode(config.mode);
|
||||
this.touchFocusEnabled = config.touchFocus;
|
||||
this.views.faces.clear();
|
||||
};
|
||||
|
||||
ViewfinderController.prototype.onFacesDetected = function(faces) {
|
||||
var self = this;
|
||||
var faceCircles = [];
|
||||
var viewfinderSize = this.views.viewfinder.getSize();
|
||||
var viewportHeight = viewfinderSize.height;
|
||||
var viewportWidth = viewfinderSize.width;
|
||||
var sensorAngle = this.camera.getSensorAngle();
|
||||
var camera = this.app.settings.cameras.selected('key');
|
||||
var isFrontCamera = camera === 'front';
|
||||
|
||||
faces.forEach(function(face, index) {
|
||||
// Face comes in camera coordinates from gecko
|
||||
var faceInPixels = cameraCoordinates.faceToPixels(
|
||||
face.bounds, viewportWidth, viewportHeight, sensorAngle, isFrontCamera);
|
||||
var faceCircle = self.calculateFaceCircle(faceInPixels);
|
||||
faceCircles.push(faceCircle);
|
||||
});
|
||||
this.views.faces.show();
|
||||
this.views.faces.render(faceCircles);
|
||||
};
|
||||
|
||||
ViewfinderController.prototype.calculateFaceCircle = function(face) {
|
||||
var diameter = Math.max(face.width, face.height);
|
||||
var radius = diameter / 2;
|
||||
return {
|
||||
x: Math.round(face.left + face.width / 2 - radius),
|
||||
y: Math.round(face.top + face.height / 2 - radius),
|
||||
diameter: diameter
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Start the viewfinder stream flowing
|
||||
* with the current camera configuration.
|
||||
*
|
||||
* This indirectly enforces a screen wakelock,
|
||||
* meaning the device is unable to go to sleep.
|
||||
*
|
||||
* We don't want the stream to start flowing if
|
||||
* the preview-gallery is open, as this prevents
|
||||
* the device falling asleep.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.loadStream = function() {
|
||||
this.camera.loadStreamInto(this.views.viewfinder.els.video);
|
||||
debug('stream started');
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop the preview stream flowing.
|
||||
*
|
||||
* This indirectly removes the wakelock
|
||||
* that is magically enforced by the
|
||||
* flowing camera stream. Meaning the
|
||||
* device is able to go to sleep.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.stopStream = function() {
|
||||
this.views.viewfinder.stopStream();
|
||||
debug('stream stopped');
|
||||
};
|
||||
|
||||
/**
|
||||
* Power save state changed. If entering power
|
||||
* save mode, release the screen wake lock to
|
||||
* allow it to timeout without user interaction,
|
||||
* otherwise acquire the wake lock.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.onPowerSave = function(powerSave) {
|
||||
this.views.viewfinder.els.video.mozUseScreenWakeLock = !powerSave;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure the size and postion
|
||||
* of the preview video stream.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.configurePreview = function() {
|
||||
var camera = this.app.settings.cameras.selected('key');
|
||||
var isFrontCamera = camera === 'front';
|
||||
var sensorAngle = this.camera.getSensorAngle();
|
||||
var previewSize = this.camera.previewSize();
|
||||
|
||||
this.views.viewfinder.updatePreview(previewSize, sensorAngle, isFrontCamera);
|
||||
};
|
||||
|
||||
/**
|
||||
* Configures the viewfinder
|
||||
* to the current camera.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.onZoomConfigured = function() {
|
||||
var zoomSupported = this.camera.isZoomSupported();
|
||||
var zoomEnabled = this.app.settings.zoom.enabled();
|
||||
var enableZoom = zoomSupported && zoomEnabled;
|
||||
|
||||
if (!enableZoom) {
|
||||
this.views.viewfinder.disableZoom();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.app.settings.zoom.get('useZoomPreviewAdjustment')) {
|
||||
this.views.viewfinder.enableZoomPreviewAdjustment();
|
||||
} else {
|
||||
this.views.viewfinder.disableZoomPreviewAdjustment();
|
||||
}
|
||||
|
||||
var minimumZoom = this.camera.getMinimumZoom();
|
||||
var maximumZoom = this.camera.getMaximumZoom();
|
||||
|
||||
this.views.viewfinder.enableZoom(minimumZoom, maximumZoom);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the zoom level on the camera
|
||||
* when the pinch changes.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.onPinchChanged = function(deltaPinch) {
|
||||
var zoom = this.views.viewfinder._zoom *
|
||||
(1 + (deltaPinch / this.sensitivity));
|
||||
this.views.viewfinder.setZoom(zoom);
|
||||
this.camera.setZoom(zoom);
|
||||
};
|
||||
|
||||
/**
|
||||
* Responds to changes of the `zoom` value on the Camera to update the
|
||||
* view's internal state so that the pinch-to-zoom gesture can resume
|
||||
* zooming from the updated value. Also, updates the CSS scale transform
|
||||
* on the <video/> tag to compensate for zooming beyond the
|
||||
* `maxHardwareZoom` value.
|
||||
*
|
||||
* @param {Number} zoom
|
||||
*/
|
||||
ViewfinderController.prototype.onZoomChanged = function(zoom) {
|
||||
var zoomPreviewAdjustment = this.camera.getZoomPreviewAdjustment();
|
||||
this.views.viewfinder.setZoomPreviewAdjustment(zoomPreviewAdjustment);
|
||||
this.views.viewfinder.setZoom(zoom);
|
||||
};
|
||||
|
||||
ViewfinderController.prototype.onViewfinderClicked = function(e) {
|
||||
if (!this.touchFocusEnabled) { return; }
|
||||
this.views.faces.hide();
|
||||
this.changeFocusPoint(e.pageX, e.pageY);
|
||||
};
|
||||
|
||||
ViewfinderController.prototype.changeFocusPoint = function(x, y) {
|
||||
var viewfinderSize = this.views.viewfinder.getSize();
|
||||
var viewportHeight = viewfinderSize.height;
|
||||
var viewportWidth = viewfinderSize.width;
|
||||
var sensorAngle = this.camera.getSensorAngle();
|
||||
var focusAreaSize = 10;
|
||||
var focusAreaHalfSide = Math.round(focusAreaSize / 2);
|
||||
// Top Left corner of the area and its size
|
||||
var focusAreaPixels = {
|
||||
left: x - focusAreaHalfSide,
|
||||
top: y - focusAreaHalfSide,
|
||||
right: x + focusAreaHalfSide,
|
||||
bottom: y + focusAreaHalfSide,
|
||||
width: focusAreaSize,
|
||||
height: focusAreaSize
|
||||
};
|
||||
var camera = this.app.settings.cameras.selected('key');
|
||||
var isFrontCamera = camera === 'front';
|
||||
var focusArea = cameraCoordinates.faceToCamera(
|
||||
focusAreaPixels, viewportWidth, viewportHeight, sensorAngle, isFrontCamera);
|
||||
var focusPoint = {
|
||||
x: x,
|
||||
y: y,
|
||||
area: focusArea
|
||||
};
|
||||
this.views.focus.setPosition(x, y);
|
||||
this.app.emit('viewfinder:focuspointchanged', focusPoint);
|
||||
};
|
||||
|
||||
ViewfinderController.prototype.onSettingsOpened = function() {
|
||||
this.hideGrid();
|
||||
// Make viewfinder invisible to the screen reader since it is behind settings
|
||||
// overlay.
|
||||
this.views.viewfinder.set('ariaHidden', true);
|
||||
};
|
||||
|
||||
ViewfinderController.prototype.onSettingsClosed = function() {
|
||||
this.configureGrid();
|
||||
// Make viewfinder visible to the screen reader again when settings are
|
||||
// closed.
|
||||
this.views.viewfinder.set('ariaHidden', false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Disables the viewfinder stream
|
||||
* and pinch events.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.onGalleryOpened = function() {
|
||||
this.views.viewfinder.disable();
|
||||
// Make viewfinder invisible to the screen reader since it is behind gallery
|
||||
// overlay.
|
||||
this.views.viewfinder.set('ariaHidden', true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enables the viewfinder stream
|
||||
* and pinch events.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.onGalleryClosed = function() {
|
||||
this.views.viewfinder.enable();
|
||||
// Make viewfinder visible to the screen reader again when gallery is closed.
|
||||
this.views.viewfinder.set('ariaHidden', false);
|
||||
};
|
||||
|
||||
ViewfinderController.prototype.onPreviewActive = function(active) {
|
||||
if (!active) {
|
||||
this.stopStream();
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
84
gaia/apps/camera/js/controllers/zoom-bar.js
Executable file
84
gaia/apps/camera/js/controllers/zoom-bar.js
Executable file
|
|
@ -0,0 +1,84 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('controller:zoom-bar');
|
||||
var ZoomBar = require('views/zoom-bar');
|
||||
var bindAll = require('lib/bind-all');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(app) { return new ZoomBarController(app); };
|
||||
module.exports.ZoomBarController = ZoomBarController;
|
||||
|
||||
/**
|
||||
* Initialize a new `ZoomBarController`
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
function ZoomBarController(app) {
|
||||
bindAll(this);
|
||||
this.app = app;
|
||||
this.camera = app.camera;
|
||||
this.createView();
|
||||
this.bindEvents();
|
||||
this.configureZoomBar();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
ZoomBarController.prototype.createView = function() {
|
||||
this.view = this.app.views.zoombar || new ZoomBar();
|
||||
this.view.hide();
|
||||
this.view.appendTo(this.app.el);
|
||||
};
|
||||
|
||||
ZoomBarController.prototype.bindEvents = function() {
|
||||
this.view.on('change', this.onChange);
|
||||
|
||||
// TODO: Camera events should be relayed through
|
||||
// the app, so that controllers dont' have a
|
||||
// hard dependency on each other.
|
||||
this.camera.on('zoomconfigured', this.onZoomConfigured);
|
||||
this.camera.on('zoomchanged', this.setZoom);
|
||||
};
|
||||
|
||||
ZoomBarController.prototype.configureZoomBar = function() {
|
||||
var zoomSupported = this.camera.isZoomSupported();
|
||||
var zoomEnabled = this.app.settings.zoom.enabled();
|
||||
this.enableZoom = zoomSupported && zoomEnabled;
|
||||
};
|
||||
|
||||
ZoomBarController.prototype.onChange = function(value) {
|
||||
var minimumZoom = this.camera.getMinimumZoom();
|
||||
var maximumZoom = this.camera.getMaximumZoom();
|
||||
var range = maximumZoom - minimumZoom;
|
||||
var zoom = (range * value / 100) + minimumZoom;
|
||||
this.camera.setZoom(zoom);
|
||||
};
|
||||
|
||||
ZoomBarController.prototype.onZoomConfigured = function(zoom) {
|
||||
this.configureZoomBar();
|
||||
this.setZoom(zoom);
|
||||
this.view.hide();
|
||||
};
|
||||
|
||||
ZoomBarController.prototype.setZoom = function(zoom) {
|
||||
if (!this.enableZoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
debug('set zoom');
|
||||
var minimumZoom = this.camera.getMinimumZoom();
|
||||
var maximumZoom = this.camera.getMaximumZoom();
|
||||
var range = maximumZoom - minimumZoom;
|
||||
var percent = (zoom - minimumZoom) / range * 100;
|
||||
this.view.setValue(percent);
|
||||
debug('zoom set');
|
||||
};
|
||||
|
||||
});
|
||||
15
gaia/apps/camera/js/lib/bind-all.js
Executable file
15
gaia/apps/camera/js/lib/bind-all.js
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
module.exports = function(object) {
|
||||
var key;
|
||||
var fn;
|
||||
for (key in object) {
|
||||
fn = object[key];
|
||||
if (typeof fn === 'function') {
|
||||
object[key] = fn.bind(object);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
30
gaia/apps/camera/js/lib/bind.js
Executable file
30
gaia/apps/camera/js/lib/bind.js
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
exports = module.exports = bind;
|
||||
|
||||
/**
|
||||
* addEventListener shorthand.
|
||||
* @param {Element} el
|
||||
* @param {String} name
|
||||
* @param {Function} fn
|
||||
*/
|
||||
function bind(el, name, fn, capture) {
|
||||
el.addEventListener(name, fn, capture || false);
|
||||
}
|
||||
|
||||
/**
|
||||
* removeEventListener shorthand.
|
||||
* @param {Element} el
|
||||
* @param {String} name
|
||||
* @param {Function} fn
|
||||
*/
|
||||
exports.unbind = function(el, name, fn, capture) {
|
||||
el.removeEventListener(name, fn, capture || false);
|
||||
};
|
||||
|
||||
});
|
||||
11
gaia/apps/camera/js/lib/bytes-to-pixels.js
Executable file
11
gaia/apps/camera/js/lib/bytes-to-pixels.js
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
module.exports = function(bytes) {
|
||||
var bytesPerPixel = 3;
|
||||
var avgJpegCompression = window.CONFIG_AVG_JPEG_COMPRESSION_RATIO || 8;
|
||||
var uncompressedBytes = bytes * avgJpegCompression;
|
||||
return Math.round(uncompressedBytes / bytesPerPixel);
|
||||
};
|
||||
|
||||
});
|
||||
527
gaia/apps/camera/js/lib/camera-coordinates.js
Executable file
527
gaia/apps/camera/js/lib/camera-coordinates.js
Executable file
|
|
@ -0,0 +1,527 @@
|
|||
/*
|
||||
|
||||
This module contains functions to deal with android camera coordinates.
|
||||
|
||||
Example use cases:
|
||||
|
||||
1. In the camera application we want to convert to screen coordinates
|
||||
the top left corner of the faces detected by the camera so we can position a
|
||||
DOM element to highlight the corresponding area.
|
||||
|
||||
2. We have to transform screen to android camera coordinates when
|
||||
the user touches the screen to focus on a specific area.
|
||||
|
||||
In both cases we have to take into account the possible mismatch between the
|
||||
screen pixels order and the order of the pixels sent by the driver.
|
||||
|
||||
ANDROID CAMERA COORDINATES
|
||||
|
||||
This documentation contain how android coordinates work and how the screen,
|
||||
sensor and scene coordinates relate to one another.
|
||||
|
||||
The following is *always* true: the active rectangles of
|
||||
the phone/screen and the sensor always -physically- line up.
|
||||
It has to be thus, otherwise it would be impossible to
|
||||
fill a portrait viewfinder with a portrait view, or a
|
||||
landscape viewfinder with a landscape view.
|
||||
|
||||
Visually, this looks like:
|
||||
|
||||
phone/screen
|
||||
+------+
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+------+
|
||||
|
||||
sensor
|
||||
+----+
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+----+
|
||||
|
||||
detected face reported by the camera driver
|
||||
-------------------------------------------
|
||||
|
||||
top
|
||||
+-----+
|
||||
| |
|
||||
left | | right
|
||||
| |
|
||||
+-----+
|
||||
bottom
|
||||
|
||||
The driver also reports width and height
|
||||
|
||||
If sensor orientation is 0, the sensor and the screen are scanned like this:
|
||||
|
||||
phone/screen
|
||||
+------+
|
||||
|aabbcc|
|
||||
|aabbcc|
|
||||
|ddeeff|
|
||||
|ddeeff|
|
||||
|gghhii|
|
||||
|gghhii|
|
||||
|jjkkll|
|
||||
|jjkkll|
|
||||
+------+
|
||||
|
||||
sensor
|
||||
O---+ O = Origin of sensor coordinates (-1000, -1000)
|
||||
|abc| X = (1000, 1000)
|
||||
|def| sensor-to-screen conversion => none (x = x, y = y)
|
||||
|ghi| face coordinate to convert => top, left
|
||||
|jkl|
|
||||
+---X
|
||||
|
||||
If there is an arrow pointing right:
|
||||
o-->
|
||||
...the sensor sees this arrow as such:
|
||||
|
||||
+----+
|
||||
| |
|
||||
|o-->|
|
||||
| |
|
||||
| |
|
||||
+----+
|
||||
|
||||
...and if the sensor orientation is 0, the
|
||||
arrow appears on the screen (magnified) like this:
|
||||
|
||||
+--------+
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
|()====>>|
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+--------+
|
||||
|
||||
If the sensor orientation is, e.g., 90-degrees--that is,
|
||||
the "top" of the sensor corresponds to the "right" of the
|
||||
screen--then without applying any rotation to the UI,
|
||||
the arrow appears on the screen like this:
|
||||
|
||||
+--------+
|
||||
| |
|
||||
| |
|
||||
| ^ |
|
||||
| | |
|
||||
| O |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+--------+
|
||||
|
||||
The arrow is smaller, because the frames need to be
|
||||
shrunk so that the entire height of the sensor image
|
||||
fits into the width of the screen.
|
||||
In this case, rotating the UI by 90-degrees clockwise,
|
||||
and stretching it to fill the screen, gives:
|
||||
|
||||
+--------+
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
|()====>>|
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+--------+
|
||||
|
||||
This is because, in the 90-degree sensor
|
||||
orientation case, the scanning is like this:
|
||||
|
||||
sensor
|
||||
+---O O = Origin of sensor coordinates (-1000, -1000)
|
||||
|iea| X = (1000, 1000)
|
||||
|jfb| sensor-to-screen conversion => x = -y, y = x
|
||||
|kgc| face coordinate to convert => bottom, left
|
||||
|lhd|
|
||||
X---+
|
||||
|
||||
And without compensating for this rotation,
|
||||
the image on the screen would appear like this:
|
||||
+------+
|
||||
| |
|
||||
| |
|
||||
| abcd |
|
||||
| efgh |
|
||||
| ijkl |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+------+
|
||||
|
||||
Thus sensor orientation has nothing to do with the physical
|
||||
orientation of the sensor. Rather, it has to do with which
|
||||
scan-line in the sensor is considered the "top".
|
||||
Given all this, the coordinates of the "top-left" of
|
||||
the sensor view, (-1000, -1000), are the coordinates of the
|
||||
first pixel of the first scan-line. Similarly, the "bottom-right"
|
||||
of the sensor is the last pixel of the last scan-line,
|
||||
and has coordinates (1000, 1000).
|
||||
|
||||
----- 8< -----
|
||||
As requested, the 180-degree case is:
|
||||
|
||||
phone/screen
|
||||
+------+
|
||||
|aabbcc|
|
||||
|aabbcc|
|
||||
|ddeeff|
|
||||
|ddeeff|
|
||||
|gghhii|
|
||||
|gghhii|
|
||||
|jjkkll|
|
||||
|jjkkll|
|
||||
+------+
|
||||
|
||||
sensor
|
||||
X---+ O = Origin of sensor coordinates (-1000, -1000)
|
||||
|jkl| X = (1000, 1000)
|
||||
|ihg| sensor-to-screen conversion => x = -x, y = -y
|
||||
|fed| face coordinate to convert => bottom, right
|
||||
|cba|
|
||||
+---O
|
||||
|
||||
And the 270-degree case is:
|
||||
|
||||
phone/screen
|
||||
+------+
|
||||
| |
|
||||
| |
|
||||
| abcd |
|
||||
| efgh |
|
||||
| ijkl |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+------+
|
||||
|
||||
sensor
|
||||
+---X O = Origin of sensor coordinates (-1000, -1000)
|
||||
|dhl| X = (1000, 1000)
|
||||
|cgk| sensor-to-screen conversion => x = y, y = -x
|
||||
|bfj| face coordinate to convert => bottom, right
|
||||
|aei|
|
||||
O---+
|
||||
|
||||
*/
|
||||
define(function(require, exports, module) {
|
||||
/*jshint maxlen:false*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* It transforms camera coordinates to pixels
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function toPixels(x, y, viewportWidth, viewportHeight) {
|
||||
// In camera coordinate system, (-1000, -1000) represents the
|
||||
// top-left of the camera field of view, and (1000, 1000) represents
|
||||
// the bottom-right.
|
||||
// For convenience we assume that coordinates go from 0-2000
|
||||
var cameraCoordinatesRange = 2000;
|
||||
|
||||
// How many pixels per camera unit?
|
||||
var pixelsPerCameraUnitWidth = viewportWidth / cameraCoordinatesRange;
|
||||
var pixelsPerCameraUnitHeight = viewportHeight / cameraCoordinatesRange;
|
||||
// It makes the faces coordinates to go from 0-2000 for convenience
|
||||
var xCameraCoordinates = x + 1000;
|
||||
var yCameraCoordinates = y + 1000;
|
||||
var xPixelCoordinates = xCameraCoordinates * pixelsPerCameraUnitWidth;
|
||||
var yPixelCoordinates = yCameraCoordinates * pixelsPerCameraUnitHeight;
|
||||
return {
|
||||
x: Math.round(xPixelCoordinates),
|
||||
y: Math.round(yPixelCoordinates)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* It transforms pixels to camera coordinates
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function toCamera(x, y, viewportWidth, viewportHeight) {
|
||||
// In camera coordinate system, (-1000, -1000) represents the
|
||||
// top-left of the camera field of view, and (1000, 1000) represents
|
||||
// the bottom-right.
|
||||
var cameraCoordinatesRange = 2000;
|
||||
// How many camera units per pixel?
|
||||
var cameraUnitsPerPixelWidth = cameraCoordinatesRange / viewportWidth;
|
||||
var cameraUnitsPerPixelHeight = cameraCoordinatesRange / viewportHeight;
|
||||
return {
|
||||
x: Math.round(x * cameraUnitsPerPixelWidth) - 1000,
|
||||
y: Math.round(y * cameraUnitsPerPixelHeight) - 1000
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* It rotates camera coordinates given an angle 0, 90, 180, 270
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function rotatePoint(x, y, angle) {
|
||||
// It clamps the angle to +/- 0-270
|
||||
angle = (Math.round((angle % 360) / 90) % 4) * 90;
|
||||
switch (angle) {
|
||||
case 0:
|
||||
return {
|
||||
x: x,
|
||||
y: y
|
||||
};
|
||||
case 90:
|
||||
case -270:
|
||||
return {
|
||||
x: -y,
|
||||
y: x
|
||||
};
|
||||
case 180:
|
||||
case -180:
|
||||
return {
|
||||
x: -x,
|
||||
y: -y
|
||||
};
|
||||
case 270:
|
||||
case -90:
|
||||
return {
|
||||
x: y,
|
||||
y: -x
|
||||
};
|
||||
default:
|
||||
console.error('wrong angle value');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The sensor orientation is the orientation of the camera output
|
||||
* (the order of the pixels sent from the sensor to the preview stream) with
|
||||
* respect to the device screen in portrait orientation.
|
||||
* This function matches the orientation of the sensor and screen coordinates
|
||||
* We're interested on the top left and bottom-rignt corners of the
|
||||
* area as observed through the lenses (scene coordinates).
|
||||
* We take into account the orientation of the sensor to pick the correct
|
||||
* corner that corresponds to the top most, left most, right most and
|
||||
* bottom most coordinates in the physical/scene space as observed through the camera preview.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function rotateArea(area, sensorOrientation) {
|
||||
var topLeft = rotatePoint(area.left, area.top, sensorOrientation);
|
||||
var bottomRight = rotatePoint(area.right, area.bottom, sensorOrientation);
|
||||
// It picks the top left and bottom right corners on the scene / physical space
|
||||
return {
|
||||
top: Math.min(topLeft.y, bottomRight.y),
|
||||
left: Math.min(topLeft.x, bottomRight.x),
|
||||
bottom: Math.max(topLeft.y, bottomRight.y),
|
||||
right: Math.max(topLeft.x, bottomRight.x),
|
||||
width: Math.abs(topLeft.x - bottomRight.x),
|
||||
height: Math.abs(topLeft.y - bottomRight.y)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* It mirrors a given area in camera coordinates
|
||||
* Use case: Display detected faces by the front camera.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function mirrorAreaCamera(area) {
|
||||
return {
|
||||
top: area.top,
|
||||
left: -area.right,
|
||||
bottom: area.bottom,
|
||||
right: -area.left,
|
||||
width: area.width,
|
||||
height: area.height
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* It mirrors a given area in camera coordinates
|
||||
* Use case: Display detected faces by the front camera.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function mirrorAreaPixels(area, viewportWidth) {
|
||||
return {
|
||||
top: area.top,
|
||||
left: viewportWidth - area.right,
|
||||
bottom: area.bottom,
|
||||
right: viewportWidth - area.left,
|
||||
width: area.width,
|
||||
height: area.height
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a distance in pixels and a screen size it returns
|
||||
* the distance in camera units.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function sizeToCamera(width, height, viewportWidth, viewportHeight) {
|
||||
// In camera coordinate system, (-1000, -1000) represents the
|
||||
// top-left of the camera field of view, and (1000, 1000) represents
|
||||
// the bottom-right of the field of view.
|
||||
// There are 2000 units in each axis of the camera coordinates
|
||||
var cameraCoordinatesRange = 2000;
|
||||
// How many camera units per pixel?
|
||||
var cameraUnitsPerPixelWidth = cameraCoordinatesRange/ viewportWidth;
|
||||
var cameraUnitsPerPixelHeight = cameraCoordinatesRange / viewportHeight;
|
||||
return {
|
||||
width: Math.round(width * cameraUnitsPerPixelWidth),
|
||||
height: Math.round(height * cameraUnitsPerPixelHeight)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a distance in camera units and a screen size it returns
|
||||
* the distance in pixels.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function sizeToPixels(width, height, viewportWidth, viewportHeight) {
|
||||
// In camera coordinate system, (-1000, -1000) represents the
|
||||
// top-left of the camera field of view, and (1000, 1000) represents
|
||||
// the bottom-right of the field of view.
|
||||
// There are 2000 units in each axis of the camera coordinates
|
||||
var cameraCoordinatesRange = 2000;
|
||||
// How many pixels per camera coordinates unit?
|
||||
var pixelsPerCameraUnitWidth = viewportWidth / cameraCoordinatesRange;
|
||||
var pixelsPerCameraUnitHeight = viewportHeight / cameraCoordinatesRange;
|
||||
return {
|
||||
width: Math.round(width * pixelsPerCameraUnitWidth),
|
||||
height: Math.round(height * pixelsPerCameraUnitHeight)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an area in screen camera units (-1000, 1000)
|
||||
* and viewport dimensions it returns the same area
|
||||
* in pixels
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function areaToPixels(area, viewportWidth, viewportHeight) {
|
||||
// It converts the face from Android camera coordinates to pixels
|
||||
var areaPixels = toPixels(
|
||||
area.left, area.top, viewportWidth, viewportHeight);
|
||||
// It converts the face size from Android camera units to pixels
|
||||
var areaPixelSize = sizeToPixels(
|
||||
area.width, area.height,
|
||||
viewportWidth, viewportHeight);
|
||||
var width = areaPixelSize.width;
|
||||
var height = areaPixelSize.height;
|
||||
return {
|
||||
top: areaPixels.y,
|
||||
left: areaPixels.x,
|
||||
bottom: areaPixels.y + height,
|
||||
right: areaPixels.x + width,
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an area in screen pixel coordinates
|
||||
* and viewport dimensions it returns the same area
|
||||
* in android camera coordinates (-1000, 1000)
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function areaToCamera(area, viewportWidth, viewportHeight) {
|
||||
var topLeft = toCamera(
|
||||
area.left, area.top, viewportWidth, viewportHeight);
|
||||
// It converts the size from pixels to camera units
|
||||
var areaCameraUnits = sizeToCamera(
|
||||
area.width, area.height,
|
||||
viewportWidth, viewportHeight);
|
||||
var width = areaCameraUnits.width;
|
||||
var height = areaCameraUnits.height;
|
||||
var areaCamera = {
|
||||
top: topLeft.y,
|
||||
left: topLeft.x,
|
||||
bottom: topLeft.y + height,
|
||||
right: topLeft.x + width,
|
||||
height: height,
|
||||
width: width
|
||||
};
|
||||
return areaCamera;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given face coordinates in pixels
|
||||
* a viewport width and height it returns the face coordinates
|
||||
* and area in camera units
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function faceToCamera(face, viewportWidth, viewportHeight, sensorOrientation, mirrored) {
|
||||
if (mirrored) {
|
||||
face = mirrorAreaPixels(face, viewportWidth);
|
||||
}
|
||||
face = areaToCamera(face, viewportWidth, viewportHeight);
|
||||
// The orientation of the screen and the sensor might not match.
|
||||
// We rotate the face so it matches screen coordinates orientation
|
||||
if (sensorOrientation) {
|
||||
face = rotateArea(face, -sensorOrientation);
|
||||
}
|
||||
return face;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given face coordinates as provided by camera controller and
|
||||
* a viewport width and height it returns the face coordinates, diameter
|
||||
* and area in pixel units
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function faceToPixels(face, viewportWidth, viewportHeight, sensorOrientation, mirrored) {
|
||||
// The orientation of the screen and the sensor might not match.
|
||||
// We rotate the face so it matches screen coordinates orientation
|
||||
if (sensorOrientation) {
|
||||
face = rotateArea(face, sensorOrientation);
|
||||
}
|
||||
if (mirrored) {
|
||||
face = mirrorAreaCamera(face);
|
||||
}
|
||||
return areaToPixels(face, viewportWidth, viewportHeight);
|
||||
}
|
||||
|
||||
return {
|
||||
// Face transformations
|
||||
faceToCamera: faceToCamera,
|
||||
faceToPixels: faceToPixels,
|
||||
// Area transformations
|
||||
private: {
|
||||
// Area transformations
|
||||
areaToCamera: areaToCamera,
|
||||
areaToPixels: areaToPixels,
|
||||
mirrorAreaCamera: mirrorAreaCamera,
|
||||
mirrorAreaPixels: mirrorAreaPixels,
|
||||
rotateArea: rotateArea,
|
||||
// Size transformations
|
||||
sizeToCamera: sizeToCamera,
|
||||
sizeToPixels: sizeToPixels,
|
||||
// Point transformations
|
||||
toCamera: toCamera,
|
||||
toPixels: toPixels,
|
||||
rotatePoint: rotatePoint
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
75
gaia/apps/camera/js/lib/camera-utils.js
Executable file
75
gaia/apps/camera/js/lib/camera-utils.js
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
define(function(require) {
|
||||
/*jshint maxlen:false*/
|
||||
'use strict';
|
||||
|
||||
var CameraUtils = {};
|
||||
|
||||
CameraUtils.scaleSizeToFitViewport = function(viewportSize, imageSize) {
|
||||
var sw = viewportSize.width / imageSize.width,
|
||||
sh = viewportSize.height / imageSize.height,
|
||||
scale;
|
||||
|
||||
// Select the smaller scale to fit image completely within the viewport
|
||||
scale = Math.min(sw, sh);
|
||||
|
||||
return {
|
||||
width: imageSize.width * scale,
|
||||
height: imageSize.height * scale
|
||||
};
|
||||
};
|
||||
|
||||
CameraUtils.scaleSizeToFillViewport = function(viewportSize, imageSize) {
|
||||
var sw = viewportSize.width / imageSize.width,
|
||||
sh = viewportSize.height / imageSize.height,
|
||||
scale;
|
||||
|
||||
// Select the larger scale to fill and overflow viewport with image
|
||||
scale = Math.max(sw, sh);
|
||||
|
||||
return {
|
||||
width: imageSize.width * scale,
|
||||
height: imageSize.height * scale
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the maximum preview size (in terms of area) from a list of
|
||||
* possible preview sizes.
|
||||
*
|
||||
* NOTE: If an `aspectRatio` value is provided, the search will be
|
||||
* constrained to only accept preview sizes matching that aspect
|
||||
* ratio.
|
||||
*
|
||||
* @param {Array} previewSizes
|
||||
* @param {Number} aspectRatio
|
||||
* @return {Object}
|
||||
*/
|
||||
CameraUtils.getMaximumPreviewSize = function(previewSizes, aspectRatio) {
|
||||
|
||||
// Use a very small tolerance because we want an exact match if we are
|
||||
// constraining to only include specific aspect ratios.
|
||||
const ASPECT_TOLERANCE = 0.001;
|
||||
|
||||
var maximumArea = 0;
|
||||
var maximumPreviewSize = null;
|
||||
previewSizes.forEach(function(previewSize) {
|
||||
var area = previewSize.width * previewSize.height;
|
||||
|
||||
if (aspectRatio) {
|
||||
var ratio = previewSize.width / previewSize.height;
|
||||
if (Math.abs(ratio - aspectRatio) > ASPECT_TOLERANCE) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (area > maximumArea) {
|
||||
maximumArea = area;
|
||||
maximumPreviewSize = previewSize;
|
||||
}
|
||||
});
|
||||
|
||||
return maximumPreviewSize;
|
||||
};
|
||||
|
||||
return CameraUtils;
|
||||
});
|
||||
1491
gaia/apps/camera/js/lib/camera/camera.js
Executable file
1491
gaia/apps/camera/js/lib/camera/camera.js
Executable file
File diff suppressed because it is too large
Load diff
325
gaia/apps/camera/js/lib/camera/focus.js
Executable file
325
gaia/apps/camera/js/lib/camera/focus.js
Executable file
|
|
@ -0,0 +1,325 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Module Dependencies
|
||||
*/
|
||||
|
||||
var bindAll = require('lib/bind-all');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = Focus;
|
||||
|
||||
/**
|
||||
* Options:
|
||||
*
|
||||
* - {Boolean} `continousAutoFocus`
|
||||
* - {Boolean} `touchFocus`
|
||||
* - {Boolean} `faceDetection`
|
||||
*/
|
||||
function Focus(options) {
|
||||
bindAll(this);
|
||||
this.userPreferences = options || {};
|
||||
this.detectedFaces = [];
|
||||
}
|
||||
|
||||
Focus.prototype.configure = function(mozCamera, mode) {
|
||||
var focusModes = mozCamera.capabilities.focusModes;
|
||||
var focusMode;
|
||||
this.mozCamera = mozCamera;
|
||||
this.configureFocusModes(mode);
|
||||
// Determines focus mode based on camera mode
|
||||
// In case of C-AF enabled
|
||||
if (this.continuousAutoFocus) {
|
||||
if (mode === 'picture') {
|
||||
focusMode = 'continuous-picture';
|
||||
} else if (mode === 'video'){
|
||||
focusMode = 'continuous-video';
|
||||
}
|
||||
}
|
||||
|
||||
// auto is the default focus mode
|
||||
focusMode = focusMode || 'auto';
|
||||
|
||||
// If auto or passed mode is not supported we pick the
|
||||
// first available in the hardware list
|
||||
if (focusModes.indexOf(focusMode) === -1) {
|
||||
focusMode = focusModes[0];
|
||||
}
|
||||
|
||||
// If touch-to-focus is in progress we need to ensure
|
||||
// the correct mode is restored when it is complete
|
||||
this.suspendedMode = focusMode;
|
||||
mozCamera.focusMode = focusMode;
|
||||
this.reboot();
|
||||
};
|
||||
|
||||
Focus.prototype.getMode = function() {
|
||||
var mozCamera = this.mozCamera;
|
||||
return this.suspendedMode || mozCamera.focusMode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configures focus modes based on user preferences
|
||||
* and hardware availability
|
||||
*/
|
||||
Focus.prototype.configureFocusModes = function(mode) {
|
||||
var userPreferences = this.userPreferences;
|
||||
var continuousAutoFocusUserEnabled =
|
||||
userPreferences.continuousAutoFocus !== false;
|
||||
var touchFocusUserEnabled = userPreferences.touchFocus;
|
||||
var touchFocusSupported = this.isTouchFocusSupported();
|
||||
var faceDetectionUserEnabled = userPreferences.faceDetection;
|
||||
var faceDetectionSupported = this.isFaceDetectionSupported();
|
||||
this.continuousAutoFocus = continuousAutoFocusUserEnabled;
|
||||
// User preferences override defaults
|
||||
this.touchFocus = touchFocusUserEnabled && touchFocusSupported;
|
||||
// Face detection only enabled on picture mode (disabled on video)
|
||||
this.faceDetection =
|
||||
faceDetectionUserEnabled && faceDetectionSupported && mode === 'picture';
|
||||
this.mozCamera.addEventListener('focus', this.onAutoFocusStateChange);
|
||||
};
|
||||
|
||||
Focus.prototype.startFaceDetection = function() {
|
||||
if (!this.faceDetection) { return; }
|
||||
this.mozCamera.addEventListener('facesdetected',
|
||||
this.handleFaceDetectionEvent);
|
||||
this.mozCamera.startFaceDetection();
|
||||
};
|
||||
|
||||
Focus.prototype.stopFaceDetection = function() {
|
||||
// Clear suspenstion timers
|
||||
clearTimeout(this.faceDetectionSuspended);
|
||||
clearTimeout(this.faceDetectionSuspensionTimer);
|
||||
if (this.mozCamera.stopFaceDetection) {
|
||||
this.mozCamera.removeEventListener('facesdetected',
|
||||
this.handleFaceDetectionEvent);
|
||||
this.mozCamera.stopFaceDetection();
|
||||
}
|
||||
this.clearFaceDetection();
|
||||
};
|
||||
|
||||
Focus.prototype.handleFaceDetectionEvent = function(e) {
|
||||
this.focusOnLargestFace(e.faces);
|
||||
};
|
||||
|
||||
Focus.prototype.clearFaceDetection = function() {
|
||||
this.focusOnLargestFace([]);
|
||||
};
|
||||
|
||||
Focus.prototype.suspendFaceDetection = function(ms, delay) {
|
||||
if (!this.faceDetection) {
|
||||
return;
|
||||
}
|
||||
var self = this;
|
||||
delay = delay || 0;
|
||||
clearTimeout(this.faceDetectionSuspended);
|
||||
clearTimeout(this.faceDetectionSuspensionTimer);
|
||||
this.faceDetectionSuspensionTimer = setTimeout(suspendFaceDetection, delay);
|
||||
function suspendFaceDetection() {
|
||||
self.faceDetectionSuspended = setTimeout(clearTimer, ms);
|
||||
}
|
||||
function clearTimer() {
|
||||
self.faceFocused = false;
|
||||
self.faceDetectionSuspended = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
Focus.prototype.stopContinuousFocus = function() {
|
||||
var focusMode = this.mozCamera.focusMode;
|
||||
// Clear suspension timers
|
||||
clearTimeout(this.continuousModeTimer);
|
||||
if (focusMode === 'continuous-picture' || focusMode === 'continuous-video') {
|
||||
this.suspendedMode = this.mozCamera.focusMode;
|
||||
this.mozCamera.focusMode = 'auto';
|
||||
}
|
||||
};
|
||||
|
||||
Focus.prototype.resumeContinuousFocus = function() {
|
||||
this.mozCamera.focusMode = this.suspendedMode;
|
||||
this.suspendedMode = null;
|
||||
this.resetFocusAreas();
|
||||
this.mozCamera.resumeContinuousFocus();
|
||||
};
|
||||
|
||||
Focus.prototype.suspendContinuousFocus = function(ms) {
|
||||
clearTimeout(this.continuousModeTimer);
|
||||
this.stopContinuousFocus();
|
||||
this.continuousModeTimer = setTimeout(this.resumeContinuousFocus, ms);
|
||||
};
|
||||
|
||||
Focus.prototype.updateFocusState = function(state) {
|
||||
// Only update if the state has changed, and only transition to focused
|
||||
// or unfocused if we were previously focusing; this eliminates unfocused
|
||||
// rings just before a focusing state change.
|
||||
if (this.focusState !== state &&
|
||||
(this.focusState === 'focusing' || state === 'focusing')) {
|
||||
this.focusState = state;
|
||||
this.onAutoFocusChanged(state);
|
||||
}
|
||||
};
|
||||
|
||||
Focus.prototype.onAutoFocusStateChange = function(e) {
|
||||
var state = e.newState;
|
||||
if (state === 'unfocused') {
|
||||
state = 'fail';
|
||||
}
|
||||
this.updateFocusState(state);
|
||||
};
|
||||
|
||||
Focus.prototype.onAutoFocusChanged = function(state) {
|
||||
// NO OP by default
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback invoked when faces are detected
|
||||
* It's no op by default and it's meant to be overriden by
|
||||
* the user of the module in order to be informed about detected faces
|
||||
*/
|
||||
Focus.prototype.onFacesDetected = function(faces) {
|
||||
// NO OP by default
|
||||
};
|
||||
|
||||
Focus.prototype.focusOnLargestFace = function(faces) {
|
||||
if (this.faceDetectionSuspended) {
|
||||
this.onFacesDetected([]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.detectedFaces = faces;
|
||||
this.onFacesDetected(this.detectedFaces);
|
||||
};
|
||||
|
||||
/**
|
||||
* Focus the camera, invoke the callback asynchronously when done.
|
||||
*
|
||||
* If we only have fixed focus, then we call the callback right away
|
||||
* (but still asynchronously). Otherwise, we call autoFocus to focus
|
||||
* the camera and call the callback when focus is complete. In C-AF mode
|
||||
* this process should be fast. In manual AF mode, focusing takes about
|
||||
* a second and causes a noticeable delay before the picture is taken.
|
||||
*
|
||||
* @param {Function} done
|
||||
* @private
|
||||
*/
|
||||
Focus.prototype.focus = function(done) {
|
||||
if (!this.mozCamera) { return; }
|
||||
done = done || function() {};
|
||||
var self = this;
|
||||
|
||||
this.suspendContinuousFocus(10000);
|
||||
if (this.mozCamera.focusMode !== 'auto') {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// In either focus mode, we call autoFocus() to ensure that the user gets
|
||||
// a sharp picture. The difference between the two modes is that if
|
||||
// C-AF is on, it is likely that the camera is already focused, so the
|
||||
// call to autoFocus() invokes its callback very quickly and we get much
|
||||
// better response time.
|
||||
//
|
||||
// In either case, the callback is passed a boolean specifying whether
|
||||
// focus was successful or not, and we display a green or red focus ring
|
||||
// then call the done callback, which takes the picture and clears
|
||||
// the focus ring.
|
||||
//
|
||||
this.updateFocusState('focusing');
|
||||
this.mozCamera.autoFocus().then(onSuccess, onError);
|
||||
|
||||
// If focus fails with an error, we still need to signal the
|
||||
// caller. Interruptions are a special case, but other errors
|
||||
// can be treated the same as completing the operation but
|
||||
// remaining unfocused.
|
||||
function onError(err) {
|
||||
self.focused = false;
|
||||
if (err.name === 'NS_ERROR_IN_PROGRESS') {
|
||||
done('interrupted');
|
||||
} else {
|
||||
done('error');
|
||||
}
|
||||
}
|
||||
|
||||
// This is fixed focus: there is nothing we can do here so we
|
||||
// should just call the callback and take the photo. No focus
|
||||
// happens so we don't display a focus ring.
|
||||
function onSuccess(success) {
|
||||
if (success) {
|
||||
self.focused = true;
|
||||
done('focused');
|
||||
} else {
|
||||
self.focused = false;
|
||||
done('failed');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets focus regions
|
||||
*/
|
||||
Focus.prototype.resetFocusAreas = function() {
|
||||
if (!this.touchFocus) {
|
||||
return;
|
||||
}
|
||||
this.mozCamera.setFocusAreas([]);
|
||||
this.mozCamera.setMeteringAreas([]);
|
||||
};
|
||||
|
||||
Focus.prototype.pause = function() {
|
||||
if (this.paused) { return; }
|
||||
this.stopContinuousFocus();
|
||||
this.stopFaceDetection();
|
||||
this.paused = true;
|
||||
delete this.focusState;
|
||||
};
|
||||
|
||||
Focus.prototype.resume = function() {
|
||||
if (!this.paused) { return; }
|
||||
this.resumeContinuousFocus();
|
||||
this.startFaceDetection();
|
||||
this.paused = false;
|
||||
};
|
||||
|
||||
Focus.prototype.reboot = function() {
|
||||
this.pause();
|
||||
this.resume();
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the hardware supports touch to focus
|
||||
*/
|
||||
Focus.prototype.isTouchFocusSupported = function() {
|
||||
var maxFocusAreas = this.mozCamera.capabilities.maxFocusAreas;
|
||||
return maxFocusAreas > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if hardware supports face detection
|
||||
*/
|
||||
Focus.prototype.isFaceDetectionSupported = function() {
|
||||
var cameraDetectsFaces = this.mozCamera.capabilities.maxDetectedFaces > 0;
|
||||
var apiAvailable = !!this.mozCamera.startFaceDetection;
|
||||
this.maxDetectedFaces = this.mozCamera.capabilities.maxDetectedFaces;
|
||||
return cameraDetectsFaces && apiAvailable;
|
||||
};
|
||||
|
||||
Focus.prototype.updateFocusArea = function(rect, done) {
|
||||
done = done || function() {};
|
||||
if (!this.touchFocus) {
|
||||
done('touchToFocusNotAvailable');
|
||||
return;
|
||||
}
|
||||
this.updateFocusState('focusing');
|
||||
this.stopContinuousFocus();
|
||||
this.suspendFaceDetection(10000);
|
||||
this.mozCamera.setFocusAreas([rect]);
|
||||
this.mozCamera.setMeteringAreas([rect]);
|
||||
// Call auto focus to focus on focus area.
|
||||
this.focus(done);
|
||||
};
|
||||
|
||||
});
|
||||
35
gaia/apps/camera/js/lib/create-thumbnail-image.js
Executable file
35
gaia/apps/camera/js/lib/create-thumbnail-image.js
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var cropResizeRotate = require('cropResizeRotate');
|
||||
|
||||
/**
|
||||
* Create a thumbnail size canvas,
|
||||
* copy the <img> or <video> into it
|
||||
* cropping the edges as needed to
|
||||
* make it fit, and then extract the
|
||||
* thumbnail image as a blob and pass
|
||||
* it to the callback.
|
||||
*
|
||||
* @param {Blob} imageBlob [description]
|
||||
* @param {Object} metadata [description]
|
||||
* @param {Object} thumbnailSize [description]
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
module.exports = function(imageBlob, metadata, thumbnailSize, done) {
|
||||
cropResizeRotate(imageBlob, null, thumbnailSize, null, metadata,
|
||||
function(error, resizedBlob) {
|
||||
|
||||
// If we couldn't resize or rotate it, use the original
|
||||
if (error) {
|
||||
console.error('Error while resizing image: ' + error);
|
||||
done(imageBlob);
|
||||
return;
|
||||
}
|
||||
|
||||
done(resizedBlob);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
});
|
||||
101
gaia/apps/camera/js/lib/dcf.js
Executable file
101
gaia/apps/camera/js/lib/dcf.js
Executable file
|
|
@ -0,0 +1,101 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
// This handles the logic pertaining to the naming of files according
|
||||
// to the Design rule for Camera File System
|
||||
// * http://en.wikipedia.org/wiki/Design_rule_for_Camera_File_system
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var asyncStorage = require('asyncStorage');
|
||||
var debug = require('debug')('dcf');
|
||||
var format = require('format');
|
||||
|
||||
/**
|
||||
* Locals
|
||||
*/
|
||||
|
||||
var dcfConfigLoaded = false;
|
||||
var deferredArgs = null;
|
||||
var defaultSeq = {file: 1, dir: 100};
|
||||
var dcfConfig = {
|
||||
key: 'dcf_key',
|
||||
seq: null,
|
||||
postFix: 'MZLLA',
|
||||
prefix: {video: 'VID_', image: 'IMG_'},
|
||||
ext: {video: '3gp', image: 'jpg'}
|
||||
};
|
||||
|
||||
exports.init = function() {
|
||||
debug('initializing');
|
||||
exports.checkFileCounter(function() {
|
||||
// We have a previous call to createDCFFilename that is waiting for
|
||||
// a response, fire it again
|
||||
if (deferredArgs) {
|
||||
var args = deferredArgs;
|
||||
exports.createDCFFilename(args.storage, args.type, args.callback);
|
||||
deferredArgs = null;
|
||||
}
|
||||
|
||||
debug('initialized');
|
||||
});
|
||||
};
|
||||
|
||||
exports.checkFileCounter = function(callback) {
|
||||
asyncStorage.getItem(dcfConfig.key, function(value) {
|
||||
dcfConfigLoaded = true;
|
||||
dcfConfig.seq = value ? value : defaultSeq;
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
exports.createDCFFilename = function(storage, type, callback) {
|
||||
|
||||
// We havent loaded the current counters from indexedDB yet, defer
|
||||
// the call
|
||||
if (!dcfConfigLoaded) {
|
||||
deferredArgs = {storage: storage, type: type, callback: callback};
|
||||
return;
|
||||
}
|
||||
|
||||
var dir = 'DCIM/' + dcfConfig.seq.dir + dcfConfig.postFix + '/';
|
||||
var filename = dcfConfig.prefix[type] +
|
||||
format.padLeft(dcfConfig.seq.file, 4, '0') + '.' +
|
||||
dcfConfig.ext[type];
|
||||
var filepath = dir + filename;
|
||||
|
||||
// A file with this name may have been written by the user or
|
||||
// our indexeddb sequence tracker was cleared, check we wont overwrite
|
||||
// anything
|
||||
var req = storage.get(filepath);
|
||||
|
||||
// A file existed, we bump the directory then try to generate a
|
||||
// new filename
|
||||
req.onsuccess = function() {
|
||||
dcfConfig.seq.file = 1;
|
||||
dcfConfig.seq.dir += 1;
|
||||
asyncStorage.setItem(dcfConfig.key, dcfConfig.seq, function() {
|
||||
exports.createDCFFilename(storage, type, callback);
|
||||
});
|
||||
};
|
||||
|
||||
// No file existed, we are good to go
|
||||
req.onerror = function() {
|
||||
if (dcfConfig.seq.file < 9999) {
|
||||
dcfConfig.seq.file += 1;
|
||||
} else {
|
||||
dcfConfig.seq.file = 1;
|
||||
dcfConfig.seq.dir += 1;
|
||||
}
|
||||
asyncStorage.setItem(dcfConfig.key, dcfConfig.seq, function() {
|
||||
callback(filepath, filename, dir);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
19
gaia/apps/camera/js/lib/debounce.js
Executable file
19
gaia/apps/camera/js/lib/debounce.js
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
module.exports = function(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) { func.apply(context, args); }
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) { func.apply(context, args); }
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
81
gaia/apps/camera/js/lib/format-picture-sizes.js
Executable file
81
gaia/apps/camera/js/lib/format-picture-sizes.js
Executable file
|
|
@ -0,0 +1,81 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var getAspect = require('./get-aspect');
|
||||
|
||||
/**
|
||||
* Returns a formatted list of picture
|
||||
* sizes ready to be set as setting options.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - `maxPixelSize {Number}`
|
||||
* - `exclude {Array}`
|
||||
*
|
||||
* @param {Array} sizes
|
||||
* @param {Object} options
|
||||
* @return {Array}
|
||||
*/
|
||||
module.exports = function(sizes, options) {
|
||||
var maxPixelSize = options && options.maxPixelSize;
|
||||
var exclude = options && options.exclude || {};
|
||||
var include = options && options.include;
|
||||
var formatted = [];
|
||||
var hash = {};
|
||||
|
||||
exclude.aspects = exclude.aspects || [];
|
||||
exclude.keys = exclude.keys || [];
|
||||
|
||||
sizes.forEach(function(size) {
|
||||
var w = size.width;
|
||||
var h = size.height;
|
||||
var key = w + 'x' + h;
|
||||
var pixelSize = w * h;
|
||||
|
||||
if (hash[key]) { return; }
|
||||
|
||||
size.aspect = getAspect(w, h);
|
||||
|
||||
// Don't include pictureSizes above the maxPixelSize limit
|
||||
if (maxPixelSize && pixelSize > maxPixelSize) { return; }
|
||||
|
||||
if (include) {
|
||||
if (include.keys && !~include.keys.indexOf(key)) { return; }
|
||||
if (include.aspects && !~include.aspects.indexOf(size.aspect)) { return; }
|
||||
}
|
||||
|
||||
|
||||
if (exclude.keys.indexOf(key) > -1) { return; }
|
||||
if (exclude.aspects.indexOf(size.aspect) > -1) { return; }
|
||||
|
||||
size.mp = getMP(w, h);
|
||||
hash[key] = true;
|
||||
|
||||
formatted.push({
|
||||
key: key,
|
||||
pixelSize: pixelSize,
|
||||
data: size
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by pixel size
|
||||
formatted.sort(function(a, b) { return b.pixelSize - a.pixelSize; });
|
||||
return formatted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns rounded mega-pixel value.
|
||||
*
|
||||
* @param {Number} w
|
||||
* @param {Number} h
|
||||
* @return {Number}
|
||||
*/
|
||||
function getMP(w, h) {
|
||||
return Math.round((w * h) / 1000000);
|
||||
}
|
||||
|
||||
});
|
||||
59
gaia/apps/camera/js/lib/format-recorder-profiles.js
Executable file
59
gaia/apps/camera/js/lib/format-recorder-profiles.js
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var getAspect = require('./get-aspect');
|
||||
|
||||
/**
|
||||
* Returns a formatted list of recorder
|
||||
* profiles ready to be set as setting options.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - `exclude {Array}`
|
||||
*
|
||||
* @param {Object} profiles
|
||||
* @param {Object} options
|
||||
* @return {Array}
|
||||
*/
|
||||
module.exports = function(profiles, options) {
|
||||
var exclude = options && options.exclude || [];
|
||||
var items = [];
|
||||
var hash = {};
|
||||
|
||||
for (var key in profiles) {
|
||||
if (!profiles.hasOwnProperty(key)) { continue; } // Bug 1091820
|
||||
|
||||
var profile = profiles[key];
|
||||
var video = profile.video;
|
||||
var sizeKey = video.width + 'x' + video.height;
|
||||
|
||||
// guard against duplicate profiles
|
||||
if (hash[sizeKey]) { continue; }
|
||||
|
||||
// Don't include profile if marked as excluded
|
||||
if (exclude.indexOf(key) > -1) { continue; }
|
||||
|
||||
var pixelSize = video.width * video.height;
|
||||
var aspect = getAspect(video.width, video.height);
|
||||
|
||||
hash[sizeKey] = key;
|
||||
|
||||
items.push({
|
||||
key: key,
|
||||
title: key + ' ' + sizeKey + ' ' + aspect,
|
||||
pixelSize: pixelSize,
|
||||
raw: profile
|
||||
});
|
||||
}
|
||||
|
||||
// Sort from largest to small but put the default/preferred profile first
|
||||
items.sort(function(a, b) { return b.pixelSize - a.pixelSize; });
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
});
|
||||
35
gaia/apps/camera/js/lib/format-timer.js
Executable file
35
gaia/apps/camera/js/lib/format-timer.js
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var format = require('format');
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
function digits(value) {
|
||||
return format.padLeft(value, 2, '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(ms) {
|
||||
var totalSeconds = ms / 1000;
|
||||
var seconds = Math.round(totalSeconds % 60);
|
||||
var minutes = Math.floor(totalSeconds / 60);
|
||||
var hours;
|
||||
|
||||
if (minutes < 60) {
|
||||
return digits(minutes) + ':' + digits(seconds);
|
||||
} else {
|
||||
hours = Math.floor(minutes / 60);
|
||||
minutes = Math.round(minutes % 60);
|
||||
return hours + ':' + digits(minutes) + ':' + digits(seconds);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
});
|
||||
68
gaia/apps/camera/js/lib/geo-location.js
Executable file
68
gaia/apps/camera/js/lib/geo-location.js
Executable file
|
|
@ -0,0 +1,68 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('geolocation');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = GeoLocation;
|
||||
|
||||
/**
|
||||
* Interface to the
|
||||
* geolocation API.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function GeoLocation() {
|
||||
this.watcher = null;
|
||||
this.position = null;
|
||||
this.setPosition = this.setPosition.bind(this);
|
||||
this.watch = this.watch.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches device location.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
GeoLocation.prototype.watch = function() {
|
||||
if (!this.watcher) {
|
||||
this.watcher = navigator.geolocation.watchPosition(this.setPosition);
|
||||
debug('started watching');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops watching
|
||||
* device location.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
GeoLocation.prototype.stopWatching = function() {
|
||||
navigator.geolocation.clearWatch(this.watcher);
|
||||
this.watcher = null;
|
||||
debug('stopped watching');
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the stored
|
||||
* position object.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
GeoLocation.prototype.setPosition = function(position) {
|
||||
this.position = {
|
||||
timestamp: position.timestamp,
|
||||
altitude: position.coords.altitude,
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
20
gaia/apps/camera/js/lib/get-aspect.js
Executable file
20
gaia/apps/camera/js/lib/get-aspect.js
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Returns aspect ratio string.
|
||||
*
|
||||
* Makes use of Euclid's GCD algorithm,
|
||||
* http://en.wikipedia.org/wiki/Euclidean_algorithm
|
||||
*
|
||||
* @param {Number} w
|
||||
* @param {Number} h
|
||||
* @return {String}
|
||||
*/
|
||||
module.exports = function(w, h) {
|
||||
var gcd = function(a, b) { return (b === 0) ? a : gcd(b, a % b); };
|
||||
var divisor = gcd(w, h);
|
||||
return (w / divisor) + ':' + (h / divisor);
|
||||
};
|
||||
|
||||
});
|
||||
8
gaia/apps/camera/js/lib/mixin.js
Executable file
8
gaia/apps/camera/js/lib/mixin.js
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
module.exports = function(a, b) {
|
||||
for (var key in b) { a[key] = b[key]; }
|
||||
return a;
|
||||
};
|
||||
});
|
||||
81
gaia/apps/camera/js/lib/orientation.js
Executable file
81
gaia/apps/camera/js/lib/orientation.js
Executable file
|
|
@ -0,0 +1,81 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var listener = require('device-orientation');
|
||||
var classes = document.body.classList;
|
||||
var current = 0;
|
||||
|
||||
listener.on('orientation', onOrientationChange);
|
||||
listener.start();
|
||||
|
||||
function onOrientationChange(degrees) {
|
||||
classes.remove('deg' + current);
|
||||
classes.add('deg' + degrees);
|
||||
current = degrees;
|
||||
}
|
||||
|
||||
// Camera normally has its orientation locked to portrait mode.
|
||||
// But we unlock orientation when displaying image and video previews.
|
||||
// When orientation is unlocked, we call listener.stop().
|
||||
// We calls call stop() when recording a video, and then restart
|
||||
// when recording is done. If our app ever changes so that we can call
|
||||
// unlock while the orientation listener is in the stopped state, then
|
||||
// we would need to modify the lock() function so that it did not
|
||||
// restart the listener. That is not needed now, however and is omitted.
|
||||
|
||||
function unlock() {
|
||||
screen.mozUnlockOrientation();
|
||||
listener.stop();
|
||||
}
|
||||
|
||||
function lock() {
|
||||
screen.mozLockOrientation('default');
|
||||
listener.start();
|
||||
}
|
||||
|
||||
function rationalize(sensorAngle, angle) {
|
||||
if (typeof(angle) === 'undefined') {
|
||||
angle = current;
|
||||
}
|
||||
|
||||
// The result of this operation is an angle from 0..270 degrees,
|
||||
// in steps of 90 degrees. Angles are rounded to the nearest
|
||||
// magnitude, so 45 will be rounded to 90, and -45 will be rounded
|
||||
// to -90 (not 0).
|
||||
var r = angle + sensorAngle;
|
||||
if (r >= 0) {
|
||||
r += 45;
|
||||
} else {
|
||||
r -= 45;
|
||||
}
|
||||
r /= 90;
|
||||
if (r >= 0) {
|
||||
r = Math.floor(r);
|
||||
} else {
|
||||
r = Math.ceil(r);
|
||||
}
|
||||
r %= 4;
|
||||
r *= 90;
|
||||
if (r < 0) {
|
||||
r += 360;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
on: listener.on,
|
||||
off: listener.off,
|
||||
start: listener.start,
|
||||
stop: listener.stop,
|
||||
unlock: unlock,
|
||||
lock: lock,
|
||||
rationalize: rationalize,
|
||||
get: function() {
|
||||
return current;
|
||||
}
|
||||
};
|
||||
});
|
||||
84
gaia/apps/camera/js/lib/panzoom.js
Executable file
84
gaia/apps/camera/js/lib/panzoom.js
Executable file
|
|
@ -0,0 +1,84 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var GestureDetector = require('GestureDetector');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = addPanAndZoomHandlers;
|
||||
|
||||
/*
|
||||
* This module adds pan-and-zoom capability to images displayed by
|
||||
* shared/js/media/media_frame.js.
|
||||
* It is used by preview-gallery.js and confirm.js
|
||||
*/
|
||||
function addPanAndZoomHandlers(frame, swipeCallback) {
|
||||
// frame is the MediaFrame object. container is its DOM element.
|
||||
var container = frame.container;
|
||||
|
||||
// Generate gesture events for the container
|
||||
var gestureDetector = new GestureDetector(container);
|
||||
gestureDetector.startDetecting();
|
||||
|
||||
// When the user touches the screen and moves their finger left or
|
||||
// right, they might want to pan within a zoomed-in image, or they
|
||||
// might want to swipe between multiple items in the camera preview
|
||||
// gallery. We pass the amount of motion to the MediaFrame pan() method,
|
||||
// and it returns the amount that cannot be used to pan the displayed
|
||||
// item. We track this returned amount as how far left or right the
|
||||
// image has been swiped, and pass the number to the swipeCallback.
|
||||
var swipeAmount = 0;
|
||||
|
||||
// And handle them with these listeners
|
||||
container.addEventListener('dbltap', handleDoubleTap);
|
||||
container.addEventListener('transform', handleTransform);
|
||||
container.addEventListener('pan', handlePan);
|
||||
if (swipeCallback) {
|
||||
container.addEventListener('swipe', handleSwipe);
|
||||
}
|
||||
|
||||
function handleDoubleTap(e) {
|
||||
var scale;
|
||||
if (frame.fit.scale > frame.fit.baseScale) {
|
||||
scale = frame.fit.baseScale / frame.fit.scale;
|
||||
}
|
||||
else {
|
||||
scale = 2;
|
||||
}
|
||||
|
||||
frame.zoom(scale, e.detail.clientX, e.detail.clientY, 200);
|
||||
}
|
||||
|
||||
function handleTransform(e) {
|
||||
frame.zoom(e.detail.relative.scale,
|
||||
e.detail.midpoint.clientX, e.detail.midpoint.clientY);
|
||||
}
|
||||
|
||||
function handlePan(e) {
|
||||
var dx = e.detail.relative.dx;
|
||||
var dy = e.detail.relative.dy;
|
||||
|
||||
if (swipeCallback) {
|
||||
dx += swipeAmount;
|
||||
swipeAmount = frame.pan(dx, dy);
|
||||
swipeCallback(swipeAmount);
|
||||
} else {
|
||||
frame.pan(dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSwipe(e) {
|
||||
if (swipeAmount !== 0) {
|
||||
swipeCallback(swipeAmount, e.detail.vx);
|
||||
swipeAmount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
140
gaia/apps/camera/js/lib/pinch.js
Executable file
140
gaia/apps/camera/js/lib/pinch.js
Executable file
|
|
@ -0,0 +1,140 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var unbind = require('lib/bind').unbind;
|
||||
var bindAll = require('lib/bind-all');
|
||||
var bind = require('lib/bind');
|
||||
var events = require('evt');
|
||||
|
||||
/**
|
||||
* Mixin event emitter
|
||||
*/
|
||||
|
||||
events(Pinch.prototype);
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = Pinch;
|
||||
|
||||
/**
|
||||
* Initialize a new `Pinch` interface.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function Pinch(el) {
|
||||
bindAll(this);
|
||||
this.attach(el);
|
||||
}
|
||||
|
||||
Pinch.prototype.attach = function(el) {
|
||||
this.el = el;
|
||||
|
||||
bind(this.el, 'touchstart', this.onTouchStart);
|
||||
bind(window, 'touchmove', this.onTouchMove);
|
||||
bind(window, 'touchend', this.onTouchEnd);
|
||||
};
|
||||
|
||||
Pinch.prototype.detach = function() {
|
||||
unbind(this.el, 'touchstart', this.onTouchStart);
|
||||
unbind(window, 'touchmove', this.onTouchMove);
|
||||
unbind(window, 'touchend', this.onTouchEnd);
|
||||
|
||||
this.el = null;
|
||||
};
|
||||
|
||||
Pinch.prototype.enable = function() {
|
||||
this.disabled = false;
|
||||
};
|
||||
|
||||
Pinch.prototype.disable = function() {
|
||||
this.disabled = true;
|
||||
};
|
||||
|
||||
Pinch.prototype.onTouchStart = function(evt) {
|
||||
if (evt.touches.length !== 2 || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastTouchA = evt.touches[0];
|
||||
this.lastTouchB = evt.touches[1];
|
||||
this.isPinching = true;
|
||||
this.emit('started');
|
||||
};
|
||||
|
||||
Pinch.prototype.onTouchMove = function(evt) {
|
||||
if (!this.isPinching || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
var touchA = getNewTouchA(this, evt.touches);
|
||||
var touchB = getNewTouchB(this, evt.touches);
|
||||
var deltaPinch = getDeltaPinch(this, touchA, touchB);
|
||||
|
||||
this.emit('changed', deltaPinch);
|
||||
|
||||
this.lastTouchA = touchA;
|
||||
this.lastTouchB = touchB;
|
||||
};
|
||||
|
||||
Pinch.prototype.onTouchEnd = function(evt) {
|
||||
if (!this.isPinching || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.touches.length < 2) {
|
||||
this.isPinching = false;
|
||||
this.emit('ended');
|
||||
}
|
||||
};
|
||||
|
||||
function getNewTouchA(pinch, touches) {
|
||||
if (!pinch.lastTouchA) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var i = 0, length = touches.length, touch; i < length; i++) {
|
||||
touch = touches[i];
|
||||
if (touch.identifier === pinch.lastTouchA.identifier) {
|
||||
return touch;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getNewTouchB(pinch, touches) {
|
||||
if (!pinch.lastTouchB) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var i = 0, length = touches.length, touch; i < length; i++) {
|
||||
touch = touches[i];
|
||||
if (touch.identifier === pinch.lastTouchB.identifier) {
|
||||
return touch;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDeltaPinch(pinch, touchA, touchB) {
|
||||
var lastTouchA = pinch.lastTouchA;
|
||||
var lastTouchB = pinch.lastTouchB;
|
||||
if (!touchA || !lastTouchA || !touchB || !lastTouchB) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var oldDistance = Math.sqrt(
|
||||
Math.pow(lastTouchB.pageX - lastTouchA.pageX, 2) +
|
||||
Math.pow(lastTouchB.pageY - lastTouchA.pageY, 2));
|
||||
var newDistance = Math.sqrt(
|
||||
Math.pow(touchB.pageX - touchA.pageX, 2) +
|
||||
Math.pow(touchB.pageY - touchA.pageY, 2));
|
||||
return newDistance - oldDistance;
|
||||
}
|
||||
|
||||
});
|
||||
53
gaia/apps/camera/js/lib/prepare-preview-blob.js
Executable file
53
gaia/apps/camera/js/lib/prepare-preview-blob.js
Executable file
|
|
@ -0,0 +1,53 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Module Dependencies
|
||||
*/
|
||||
|
||||
var parseJpegMetadata = require('jpegMetaDataParser');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(blob, done) {
|
||||
parseJpegMetadata(blob, onJpegParsed);
|
||||
|
||||
function onJpegParsed(metadata) {
|
||||
metadata.blob = blob;
|
||||
|
||||
if (!metadata.preview) {
|
||||
done(metadata);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we found an EXIF preview,
|
||||
// and can determine its size, then
|
||||
// we can display it instead of the
|
||||
// big image and save memory and time.
|
||||
var start = metadata.preview.start;
|
||||
var end = metadata.preview.end;
|
||||
var previewBlob = blob.slice(start, end, 'image/jpeg');
|
||||
parseJpegMetadata(previewBlob, onSuccess, onError);
|
||||
|
||||
// If we parsed the preview image, add its
|
||||
// dimensions to the metadata.preview
|
||||
// object, and then let the MediaFrame
|
||||
// object display the preview instead of
|
||||
// the full-size image.
|
||||
function onSuccess(previewMetadata) {
|
||||
metadata.preview.width = previewMetadata.width;
|
||||
metadata.preview.height = previewMetadata.height;
|
||||
done(metadata);
|
||||
}
|
||||
|
||||
// If we couldn't parse the preview image,
|
||||
// just display full-size.
|
||||
function onError() {
|
||||
done(metadata);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
61
gaia/apps/camera/js/lib/resize-image-and-save.js
Executable file
61
gaia/apps/camera/js/lib/resize-image-and-save.js
Executable file
|
|
@ -0,0 +1,61 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Storage = require('lib/storage');
|
||||
var cropResizeRotate = require('cropResizeRotate');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(options, done) {
|
||||
var blob = options.blob;
|
||||
var outputSize = options.width && options.height ?
|
||||
{
|
||||
width: options.width,
|
||||
height: options.height
|
||||
} : options.size || null;
|
||||
|
||||
cropResizeRotate(blob, null, outputSize, 'image/jpeg+exif',
|
||||
function(error, resizedBlob) {
|
||||
|
||||
// If we couldn't resize or rotate it, use the original
|
||||
if (error) {
|
||||
console.error('Error while resizing image: ' + error);
|
||||
done(blob);
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to send a file-backed blob as the result of a pick activity
|
||||
// (see bug 975599) so we'll overwrite the old blob with the new one.
|
||||
// This means that the image stored will actually match the one passed
|
||||
// to the app that initiated the pick activity. We delete the old file,
|
||||
// then save the new blob with the same name. Then we read the file and
|
||||
// pass that to the callback.
|
||||
if (resizedBlob === blob) {
|
||||
done(blob);
|
||||
return;
|
||||
}
|
||||
|
||||
var storage = new Storage();
|
||||
// We first delete the full resolution picture
|
||||
storage.deletePicture(blob.name, addPicture);
|
||||
// We save the scaled down image
|
||||
function addPicture(error) {
|
||||
if (error) {
|
||||
done(blob);
|
||||
return;
|
||||
}
|
||||
storage.addPicture(resizedBlob, {
|
||||
filepath: blob.name
|
||||
}, onSavedPicture);
|
||||
}
|
||||
|
||||
function onSavedPicture(error, filepath, absolutePath, fileBlob) {
|
||||
done(fileBlob);
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
157
gaia/apps/camera/js/lib/setting-alias.js
Executable file
157
gaia/apps/camera/js/lib/setting-alias.js
Executable file
|
|
@ -0,0 +1,157 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('setting-alias');
|
||||
|
||||
/**
|
||||
* Locals
|
||||
*/
|
||||
|
||||
var forwardMethods = [
|
||||
'filterOptions',
|
||||
'resetOptions',
|
||||
'supported',
|
||||
'selected',
|
||||
'select',
|
||||
'next',
|
||||
'get',
|
||||
'set'
|
||||
];
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = SettingAlias;
|
||||
|
||||
/**
|
||||
* Initialize a new `SettingsAlias`
|
||||
*
|
||||
* SettingsAlias's are kinda confusing,
|
||||
* but they simplify our app code massively
|
||||
* in the long run.
|
||||
*
|
||||
* A `SettingAlias` has the same API as a `Setting`
|
||||
* but behind the scenes provides a single interface
|
||||
* to several `Setting`s. Depending on the state
|
||||
* of the app, the `SettingAlias` will point to
|
||||
* a different settings instance.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* var flashModesPicture = new Setting({ key: 'flashModesPicture' });
|
||||
* var flashModesVideo = new Setting({ key: 'flashModesVideo' });
|
||||
* var mode = 'picture';
|
||||
*
|
||||
* var flashModes = new SettingAlias({
|
||||
* key: 'flashModes',
|
||||
* settings: {
|
||||
* 'picture': flashModesPicture,
|
||||
* 'video': flashModesVideo
|
||||
* },
|
||||
* get: function() {
|
||||
* return this.settings[mode];
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* flashModes.get('key'); //=> 'flashModesPicture'
|
||||
* mode = 'video';
|
||||
* flashModes.get('key'); //=> 'flashModesVideo'
|
||||
*
|
||||
* This means our app can simply call API on
|
||||
* `flashModes` setting-alias and not worry
|
||||
* about which 'mode' the app is in.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - `key` The name of the setting-alias
|
||||
* - `settings` Hash of possible `Setting`s
|
||||
* - `get` Method that returns the current `Setting`
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
function SettingAlias(options) {
|
||||
debug('initialize');
|
||||
this.key = options.key;
|
||||
this.settings = options.settings;
|
||||
this.current = options.get.bind(this);
|
||||
forwardMethods.forEach(this.forward, this);
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over each setting.
|
||||
*
|
||||
* @param {Function} fn
|
||||
* @private
|
||||
*/
|
||||
SettingAlias.prototype.each = function(fn) {
|
||||
for (var key in this.settings) {
|
||||
fn(this.settings[key]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* States whether the passed key
|
||||
* is the currently active setting.
|
||||
*
|
||||
* @param {String} key
|
||||
* @return {Boolean}
|
||||
*/
|
||||
SettingAlias.prototype.is = function(key) {
|
||||
return this.get('key') === key;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches a method that forwards
|
||||
* the call onto the current
|
||||
* matching setting.
|
||||
*
|
||||
* @param {String} method
|
||||
*/
|
||||
SettingAlias.prototype.forward = function(method) {
|
||||
this[method] = function() {
|
||||
var setting = this.current();
|
||||
return setting[method].apply(setting, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an event callback.
|
||||
*
|
||||
* The callback is wrapped in
|
||||
* a function that first checks that
|
||||
* the setting is the active setting
|
||||
* before calling the original callback.
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {Function} fn
|
||||
*/
|
||||
SettingAlias.prototype.on = function(name, fn) {
|
||||
var alias = this;
|
||||
var wrapped = function() {
|
||||
if (!alias.is(this.key)) { return; }
|
||||
fn.apply(this, arguments);
|
||||
};
|
||||
|
||||
this.each(function(setting) { setting.on(name, wrapped); });
|
||||
fn._settingAliasCallback = wrapped;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an event callback.
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {Function} fn
|
||||
*/
|
||||
SettingAlias.prototype.off = function(name, fn) {
|
||||
var wrapped = fn._settingAliasCallback;
|
||||
this.each(function(setting) { setting.off(name, wrapped); });
|
||||
delete fn._settingAliasCallback;
|
||||
};
|
||||
|
||||
});
|
||||
251
gaia/apps/camera/js/lib/setting.js
Executable file
251
gaia/apps/camera/js/lib/setting.js
Executable file
|
|
@ -0,0 +1,251 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('setting');
|
||||
var model = require('model');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = Setting;
|
||||
|
||||
/**
|
||||
* Mixin `Model` methods
|
||||
*/
|
||||
|
||||
model(Setting.prototype);
|
||||
|
||||
/**
|
||||
* Initialize a new `Setting`.
|
||||
*
|
||||
* @param {Object} data
|
||||
*/
|
||||
function Setting(data) {
|
||||
this.key = data.key;
|
||||
this.storage = data.storage || localStorage;
|
||||
this.reset(data, { silent: true });
|
||||
this.select = this.select.bind(this);
|
||||
this.next = this.next.bind(this);
|
||||
this.configure(data);
|
||||
if (data.persistent) { this.on('change:selected', this.save); }
|
||||
}
|
||||
|
||||
Setting.prototype.configure = function(data) {
|
||||
var optionsDefined = !!(data.options && data.options.length);
|
||||
this.options = { defined: optionsDefined };
|
||||
this.resetOptions(data.options);
|
||||
};
|
||||
|
||||
Setting.prototype.resetOptions = function(list) {
|
||||
list = list || [];
|
||||
|
||||
var hash = this.optionsToHash(list);
|
||||
this.options.all = hash;
|
||||
this.options.available = hash;
|
||||
|
||||
this.set('options', list, { silent: true });
|
||||
this.updateSelected({ silent: true });
|
||||
};
|
||||
|
||||
Setting.prototype.filterOptions = function(keys, options) {
|
||||
var available = this.options.available = {};
|
||||
var hash = this.options.all;
|
||||
var filtered = [];
|
||||
|
||||
(keys || []).forEach(function(key) {
|
||||
var option = hash[key];
|
||||
if (option !== undefined) {
|
||||
filtered.push(option);
|
||||
available[key] = option;
|
||||
}
|
||||
});
|
||||
|
||||
this.sortByIndex(filtered);
|
||||
this.set('options', filtered, options);
|
||||
this.updateSelected({ silent: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert this Setting's `options` array as defined
|
||||
* in config/app.js into an object as key/value pairs.
|
||||
* This allows us to look up an option by its `key`.
|
||||
*
|
||||
* e.g.: [ { key: 'a', title: 'A' }, ... ] ->
|
||||
* { a: { key: 'a', title: 'A' }, ... }
|
||||
*
|
||||
* We use this hash to validate
|
||||
* incoming options and ot mixin
|
||||
* config data with dynamic options.
|
||||
*
|
||||
* @param {Array} options
|
||||
* @return {undefined|Object}
|
||||
*/
|
||||
Setting.prototype.optionsToHash = function(options) {
|
||||
var hash = {};
|
||||
options.forEach(function(option, index) {
|
||||
option.index = index;
|
||||
hash[option.key] = option;
|
||||
});
|
||||
return hash;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the selected option,
|
||||
* or just a particular key
|
||||
* if given.
|
||||
*
|
||||
* @param {String} key
|
||||
* @return {Object|*}
|
||||
*/
|
||||
Setting.prototype.selected = function(key) {
|
||||
var hash = this.options.available;
|
||||
var option = hash[this.get('selected')];
|
||||
return key ? option && option[key] : option;
|
||||
};
|
||||
|
||||
/**
|
||||
* Select an option.
|
||||
*
|
||||
* Accepts an string key to select,
|
||||
* or an integer index relating to
|
||||
* the current options list.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - {Boolean} silent
|
||||
*
|
||||
* @param {String|Number} key
|
||||
* @param {Object} opts Model#set() options
|
||||
*/
|
||||
Setting.prototype.select = function(key, options) {
|
||||
var isIndex = typeof key === 'number';
|
||||
var list = this.get('options');
|
||||
var available = this.options.available;
|
||||
var selected = isIndex ? list[key] : available[key];
|
||||
|
||||
// If there are no options, exit
|
||||
if (!list.length) { return; }
|
||||
|
||||
// If an option was not found,
|
||||
// default to selecting the first.
|
||||
if (!selected) { return this.select(0, options); }
|
||||
|
||||
// Store the new choice
|
||||
this.set('selected', selected.key, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts the current options list
|
||||
* by their originally defined
|
||||
* index in the config JSON.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Setting.prototype.sortByIndex = function(list) {
|
||||
return list.sort(function(a, b) { return a.index - b.index; });
|
||||
};
|
||||
|
||||
/**
|
||||
* Silently updates the `selected`
|
||||
* property of the Setting.
|
||||
*
|
||||
* If no selected key is present
|
||||
* we attempt to select the last
|
||||
* fetched persited selection.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Setting.prototype.updateSelected = function(options) {
|
||||
this.select(this.get('selected') || this.fetched, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the `selected` option to
|
||||
* the next option in the list.
|
||||
*
|
||||
* First option is chosen if
|
||||
* there is no next option.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
Setting.prototype.next = function() {
|
||||
var options = this.get('options');
|
||||
var selected = this.selected();
|
||||
var index = options.indexOf(selected);
|
||||
var newIndex = (index + 1) % options.length;
|
||||
this.select(newIndex);
|
||||
debug('set \'%s\' to index: %s', this.key, newIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Persists the current selection
|
||||
* to storage for retreval in the
|
||||
* next session.
|
||||
*
|
||||
* We're using localStorage as performance
|
||||
* vastly outweighed indexedBD (asyncStorage)
|
||||
* by 1ms/500ms on Hamachi device.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
Setting.prototype.save = function() {
|
||||
var selected = this.get('selected');
|
||||
debug('saving key: %s, selected: %s', this.key, selected);
|
||||
this.storage.setItem('setting:' + this.key, selected);
|
||||
debug('saved key: %s', selected);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the persisted selection
|
||||
* from storage, updating the
|
||||
* `selected` key.
|
||||
*
|
||||
* We're using localStorage, as performance
|
||||
* vastly outweighed indexedBD (asyncStorage)
|
||||
* by 1ms/500ms on Hamachi device.
|
||||
*
|
||||
* Leaving in the `done` callback in-case
|
||||
* storage goes async again in future.
|
||||
*
|
||||
* @param {Function} done
|
||||
* @public
|
||||
*/
|
||||
Setting.prototype.fetch = function() {
|
||||
if (!this.get('persistent')) { return; }
|
||||
debug('fetch value key: %s', this.key);
|
||||
this.fetched = this.storage.getItem('setting:' + this.key);
|
||||
debug('fetched %s value: %s', this.key, this.fetched);
|
||||
if (this.fetched) { this.select(this.fetched, { silent: true }); }
|
||||
};
|
||||
|
||||
/**
|
||||
* States whether this setting
|
||||
* is currently supported.
|
||||
*
|
||||
* 'Supported' means, it's not been
|
||||
* disabled, and there are options
|
||||
* to be chosen from.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
Setting.prototype.supported = function() {
|
||||
return this.enabled() && !!this.get('options').length;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Check if this setting is not
|
||||
* marked as disabled.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
Setting.prototype.enabled = function() {
|
||||
return !this.get('disabled');
|
||||
};
|
||||
|
||||
});
|
||||
105
gaia/apps/camera/js/lib/settings.js
Executable file
105
gaia/apps/camera/js/lib/settings.js
Executable file
|
|
@ -0,0 +1,105 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var SettingAlias = require('./setting-alias');
|
||||
var debug = require('debug')('settings');
|
||||
var Setting = require('./setting');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = Settings;
|
||||
|
||||
/**
|
||||
* Initialize a new 'Setting'
|
||||
*
|
||||
* @param {Object} items
|
||||
*/
|
||||
function Settings(items) {
|
||||
this.ids = {};
|
||||
this.items = [];
|
||||
this.aliases = {};
|
||||
this.SettingAlias = SettingAlias; // Test hook
|
||||
this.dontSave = this.dontSave.bind(this);
|
||||
this.addEach(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add several Settings in one go.
|
||||
*
|
||||
* @param {Object} items
|
||||
*/
|
||||
Settings.prototype.addEach = function(items) {
|
||||
if (!items) { return; }
|
||||
var item;
|
||||
var key;
|
||||
|
||||
for (key in items) {
|
||||
item = items[key];
|
||||
item.key = item.key || key;
|
||||
this.add(items[key]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new Setting to the settings collection.
|
||||
*
|
||||
* @param {Object} data
|
||||
*/
|
||||
Settings.prototype.add = function(data) {
|
||||
var setting = new Setting(data);
|
||||
this.items.push(setting);
|
||||
this.ids[setting.key] = this[setting.key] = setting;
|
||||
debug('added setting: %s', setting.key);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch all the Settings previous
|
||||
* state from storage.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
Settings.prototype.fetch = function() {
|
||||
this.items.forEach(function(setting) { setting.fetch(); });
|
||||
};
|
||||
|
||||
/**
|
||||
* Prevent all settings from saving
|
||||
* when they are changed.
|
||||
*
|
||||
* This is used in activity sessions
|
||||
* whereby we don't want any settings
|
||||
* changes to persist to future camera
|
||||
* sessions.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
Settings.prototype.dontSave = function() {
|
||||
this.items.forEach(function(setting) {
|
||||
setting.off('change:selected', setting.save);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a SettingAlias to the settings.
|
||||
* (see lib/setting-alias.js for more info)
|
||||
*
|
||||
* @param {Object} options
|
||||
* @public
|
||||
*/
|
||||
Settings.prototype.alias = function(options) {
|
||||
var alias = new this.SettingAlias(options);
|
||||
this.aliases[options.key] = alias;
|
||||
this[options.key] = alias;
|
||||
};
|
||||
|
||||
Settings.prototype.removeAlias = function(key) {
|
||||
// TODO: Implement
|
||||
};
|
||||
|
||||
});
|
||||
162
gaia/apps/camera/js/lib/sounds.js
Executable file
162
gaia/apps/camera/js/lib/sounds.js
Executable file
|
|
@ -0,0 +1,162 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = Sounds;
|
||||
|
||||
/**
|
||||
* Initialize a new `Sounds` interface.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function Sounds(list) {
|
||||
this.items = {};
|
||||
(list || []).forEach(this.add, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new sound.
|
||||
*
|
||||
* Checks if this sound
|
||||
* is enabled, and sets
|
||||
* up an observer.
|
||||
*
|
||||
* @param {Object} data
|
||||
*
|
||||
*/
|
||||
Sounds.prototype.add = function(data) {
|
||||
var self = this;
|
||||
var sound = {
|
||||
name: data.name,
|
||||
url: data.url,
|
||||
setting: data.setting,
|
||||
enabled: false
|
||||
};
|
||||
|
||||
this.items[data.name] = sound;
|
||||
this.isEnabled(sound, function(value) {
|
||||
self.setEnabled(sound, value);
|
||||
self.observeSetting(sound);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a sound is
|
||||
* enabled inside mozSettings.
|
||||
*
|
||||
* This is wrapped in a setTimeout
|
||||
* to prevent expensive mozSettings
|
||||
* API from blocking other operations.
|
||||
*
|
||||
* @param {Object} sound
|
||||
* @param {Function} done
|
||||
*/
|
||||
Sounds.prototype.isEnabled = function(sound, done) {
|
||||
setTimeout(function() {
|
||||
var mozSettings = navigator.mozSettings;
|
||||
var key = sound.setting;
|
||||
|
||||
// Requires navigator.mozSettings
|
||||
if (!mozSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
mozSettings
|
||||
.createLock()
|
||||
.get(key)
|
||||
.onsuccess = onSuccess;
|
||||
|
||||
function onSuccess(e) {
|
||||
var result = e.target.result[key];
|
||||
done(result);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Observe mozSettings for changes
|
||||
* on the given settings key.
|
||||
*
|
||||
* @param {Object} sound
|
||||
*
|
||||
*/
|
||||
Sounds.prototype.observeSetting = function(sound) {
|
||||
var mozSettings = navigator.mozSettings;
|
||||
var key = sound.setting;
|
||||
var self = this;
|
||||
if (mozSettings) {
|
||||
mozSettings.addObserver(key, function(e) {
|
||||
self.setEnabled(sound, e.settingValue);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a sounds `enabled` key.
|
||||
*
|
||||
* @param {Object} sound
|
||||
* @param {Boolean} value
|
||||
*
|
||||
*/
|
||||
Sounds.prototype.setEnabled = function(sound, value) {
|
||||
sound.enabled = value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Play a sound by name.
|
||||
*
|
||||
* @param {String} name
|
||||
* @public
|
||||
*/
|
||||
Sounds.prototype.play = function(name) {
|
||||
var sound = this.items[name];
|
||||
if (!sound.audio) {
|
||||
sound.audio = this.createAudio(sound.url);
|
||||
}
|
||||
|
||||
this.playSound(sound);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a function that plays
|
||||
* the given sound when called.
|
||||
*
|
||||
* @param {String} name
|
||||
* @return {Funciton}
|
||||
* @public
|
||||
*/
|
||||
Sounds.prototype.player = function(name) {
|
||||
return (function() { this.play(name); }).bind(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Play a sound.
|
||||
*
|
||||
* @param {Object} sound
|
||||
*
|
||||
*/
|
||||
Sounds.prototype.playSound = function(sound) {
|
||||
if (sound.enabled) {
|
||||
// cloneNode() lets us play overlapping sounds and is needed when the
|
||||
// user takes pictures very rapidly.
|
||||
// See http://robert.ocallahan.org/2011/11/latency-of-html5-sounds.html
|
||||
sound.audio.cloneNode(true).play();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an audio element.
|
||||
*
|
||||
* @param {String} url
|
||||
* @return {HTMLAudioElement}
|
||||
*/
|
||||
Sounds.prototype.createAudio = function(url) {
|
||||
var audio = new Audio(url);
|
||||
audio.mozAudioChannelType = 'notification';
|
||||
return audio;
|
||||
};
|
||||
|
||||
});
|
||||
410
gaia/apps/camera/js/lib/storage.js
Executable file
410
gaia/apps/camera/js/lib/storage.js
Executable file
|
|
@ -0,0 +1,410 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('storage');
|
||||
var bindAll = require('lib/bind-all');
|
||||
var events = require('evt');
|
||||
var storageSingleton;
|
||||
|
||||
/**
|
||||
* Expose `Storage`
|
||||
*/
|
||||
|
||||
module.exports = Storage;
|
||||
|
||||
/**
|
||||
* Mixin event emitter
|
||||
*/
|
||||
|
||||
events(Storage.prototype);
|
||||
|
||||
/**
|
||||
* Initialize a new `Storage`.
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
function Storage(options) {
|
||||
if (storageSingleton) {
|
||||
return storageSingleton;
|
||||
}
|
||||
storageSingleton = this;
|
||||
bindAll(this);
|
||||
this.maxFileSize = 0;
|
||||
options = options || {};
|
||||
this.dcf = options.dcf;
|
||||
if (this.dcf) {
|
||||
this.dcf.init();
|
||||
}
|
||||
this.require = options.require || require;
|
||||
navigator.mozSettings.addObserver(
|
||||
'device.storage.writable.name',
|
||||
this.onStorageVolumeChanged);
|
||||
this.configure();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
Storage.prototype.createFilename = function(storage, type, done) {
|
||||
var self = this;
|
||||
this.require(['asyncStorage', 'lib/dcf'], function(as, dcf) {
|
||||
if (!self.dcf) {
|
||||
self.dcf = dcf;
|
||||
self.dcf.init();
|
||||
}
|
||||
|
||||
self.dcf.createDCFFilename(storage, type, done);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the image Blob to DeviceStorage
|
||||
* then lookup the File reference and
|
||||
* return that in the callback as well
|
||||
* as the resulting paths.
|
||||
*
|
||||
* You always want to forget about the
|
||||
* Blob you told us about and use the
|
||||
* File instead since otherwise you
|
||||
* are wasting precious memory.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @param {String} options.filepath
|
||||
* The path to save the image to.
|
||||
*/
|
||||
Storage.prototype.addPicture = function(blob, options, done) {
|
||||
if (typeof options === 'function') {
|
||||
done = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
done = done || function() {};
|
||||
var filepath = options && options.filepath;
|
||||
var self = this;
|
||||
debug('add picture', filepath);
|
||||
|
||||
// Create a filepath if
|
||||
// one hasn't been given.
|
||||
if (!filepath) {
|
||||
debug('creating filename');
|
||||
this.createFilename(this.picture, 'image', onCreated);
|
||||
} else {
|
||||
onCreated(filepath);
|
||||
}
|
||||
|
||||
function onCreated(filepath) {
|
||||
var req = self.picture.addNamed(blob, filepath);
|
||||
req.onerror = function() { self.emit('error'); };
|
||||
req.onsuccess = function(e) {
|
||||
debug('image stored', filepath);
|
||||
var absolutePath = e.target.result;
|
||||
|
||||
// `addNamed` does not give us a File
|
||||
// handle so we need to get() it again.
|
||||
refetchFile(filepath, absolutePath);
|
||||
};
|
||||
}
|
||||
|
||||
function refetchFile(filepath, absolutePath) {
|
||||
var req = self.picture.get(filepath);
|
||||
req.onerror = function() {
|
||||
self.emit('error');
|
||||
done('Error adding picture to storage');
|
||||
};
|
||||
req.onsuccess = function(e) {
|
||||
debug('image file blob handle retrieved');
|
||||
var fileBlob = e.target.result;
|
||||
done(null, filepath, absolutePath, fileBlob);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new video filepath.
|
||||
*
|
||||
* The CameraControl API will not
|
||||
* automatically create directories
|
||||
* for the new file if they do not
|
||||
* exist.
|
||||
*
|
||||
* So we write a dummy file to the
|
||||
* same directory via DeviceStorage
|
||||
* to ensure that the directory exists
|
||||
* before attempting to record to this
|
||||
* filepath.
|
||||
*
|
||||
* If the request errors it can mean that
|
||||
* there is a file that already exists
|
||||
* in that name. If so, we attempt to delete
|
||||
* it and try to create the filepath again.
|
||||
*
|
||||
* @param {Function} done
|
||||
* @public
|
||||
*/
|
||||
Storage.prototype.createVideoFilepath = function(done) {
|
||||
var videoStorage = this.video;
|
||||
var self = this;
|
||||
|
||||
this.createFilename(this.video, 'video', function(filepath) {
|
||||
var dummyFilepath = getDir(filepath) + '.tmp.3gp';
|
||||
var blob = new Blob([''], { type: 'video/3gpp' });
|
||||
var req = videoStorage.addNamed(blob, dummyFilepath);
|
||||
|
||||
req.onerror = function(e) {
|
||||
debug('Failed to add ' + filepath + ' to DeviceStorage', e);
|
||||
var req = videoStorage.delete(dummyFilepath);
|
||||
req.onerror = function() { done('Error creating video file path'); };
|
||||
req.onsuccess = function() { self.createVideoFilepath(done); };
|
||||
};
|
||||
|
||||
req.onsuccess = function(e) {
|
||||
videoStorage.delete(e.target.result);
|
||||
done(null, filepath);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
Storage.prototype.onStorageChange = function(e) {
|
||||
debug('state change: %s', e.reason);
|
||||
var value = e.reason;
|
||||
|
||||
switch (value) {
|
||||
case 'deleted':
|
||||
// Emit an `itemdeleted` event to
|
||||
// allow parts of the UI to update.
|
||||
var filepath = this.checkFilepath(e.path);
|
||||
this.emit('itemdeleted', { path: filepath });
|
||||
break;
|
||||
case 'available':
|
||||
case 'shared':
|
||||
case 'unavailable':
|
||||
this.setState(value);
|
||||
break;
|
||||
}
|
||||
|
||||
// Check storage
|
||||
// has spare capacity
|
||||
this.check();
|
||||
};
|
||||
|
||||
Storage.prototype.configure = function(storageVolumeName) {
|
||||
var i;
|
||||
var videosStorages;
|
||||
var picturesStorages;
|
||||
// If we had a previous ds for pictures, let's remove the observer
|
||||
// we had set as well before fetching new ds.
|
||||
if (this.picture) {
|
||||
this.picture.removeEventListener('change', this.onStorageChange);
|
||||
}
|
||||
if (!storageVolumeName) {
|
||||
this.video = navigator.getDeviceStorage('videos');
|
||||
this.picture = navigator.getDeviceStorage('pictures');
|
||||
} else { // We select the volumes with the passed name
|
||||
videosStorages = navigator.getDeviceStorages('videos');
|
||||
this.video = videosStorages[0];
|
||||
for (i = 0; i < videosStorages.length; ++i) {
|
||||
if (videosStorages[i].storageName === storageVolumeName) {
|
||||
this.video = videosStorages[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
picturesStorages = navigator.getDeviceStorages('pictures');
|
||||
this.picture = picturesStorages[0];
|
||||
for (i = 0; i < picturesStorages.length; ++i) {
|
||||
if (picturesStorages[i].storageName === storageVolumeName) {
|
||||
this.picture = picturesStorages[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shouldn't happen?
|
||||
if (!this.picture) {
|
||||
this.setState('unavailable');
|
||||
return;
|
||||
}
|
||||
|
||||
this.picture.addEventListener('change', this.onStorageChange);
|
||||
this.emit('volumechanged',{
|
||||
video: this.video,
|
||||
picture: this.picture
|
||||
});
|
||||
};
|
||||
|
||||
Storage.prototype.onStorageVolumeChanged = function(setting) {
|
||||
debug('default storage volume change: %s', setting.settingValue);
|
||||
this.configure(setting.settingValue);
|
||||
};
|
||||
|
||||
Storage.prototype.checkFilepath = function(filepath) {
|
||||
var startString = filepath.indexOf('DCIM/');
|
||||
|
||||
if (startString < -1) { return; }
|
||||
else if (startString > 0) {
|
||||
filepath = filepath.substr(startString);
|
||||
}
|
||||
|
||||
// Check whether filepath is a video poster image or not. If filepath
|
||||
// contains 'VID' and ends with '.jpg', consider it a video poster
|
||||
// image and get the video filepath by changing '.jpg' to '.3gp'
|
||||
if (filepath.indexOf('VID') != -1 &&
|
||||
filepath.lastIndexOf('.jpg') === filepath.length - 4) {
|
||||
filepath = filepath.replace('.jpg', '.3gp');
|
||||
}
|
||||
|
||||
return filepath;
|
||||
};
|
||||
|
||||
Storage.prototype.setState = function(value) {
|
||||
this.state = value;
|
||||
debug('set state: %s', value);
|
||||
this.emit('changed', value);
|
||||
};
|
||||
|
||||
Storage.prototype.setMaxFileSize = function(maxFileSize) {
|
||||
this.maxFileSize = maxFileSize;
|
||||
this.check();
|
||||
debug('max file size set: %d', maxFileSize);
|
||||
};
|
||||
|
||||
/**
|
||||
* Run a full storage check.
|
||||
*
|
||||
* @param {Function} done
|
||||
* @public
|
||||
*/
|
||||
Storage.prototype.check = function(done) {
|
||||
debug('check');
|
||||
|
||||
var self = this;
|
||||
done = done || function() {};
|
||||
|
||||
this.getState(function(result) {
|
||||
self.setState(result);
|
||||
|
||||
if (!self.available()) {
|
||||
onComplete('unhealthy');
|
||||
return;
|
||||
}
|
||||
|
||||
// check file counter in dcf if it already exist.
|
||||
if (self.dcf) {
|
||||
self.dcf.checkFileCounter();
|
||||
}
|
||||
self.isSpace(function(result) {
|
||||
if (!result) { self.setState('nospace'); }
|
||||
onComplete('healthy');
|
||||
});
|
||||
});
|
||||
|
||||
function onComplete(state) {
|
||||
self.emit('checked', state);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if there is enough space to
|
||||
* accomdate the current `maxFileSize`.
|
||||
*
|
||||
* @param {Function} done
|
||||
*/
|
||||
Storage.prototype.isSpace = function(done) {
|
||||
var maxFileSize = this.maxFileSize;
|
||||
this.picture
|
||||
.freeSpace()
|
||||
.onsuccess = function(e) {
|
||||
var freeSpace = e.target.result;
|
||||
var result = freeSpace > maxFileSize;
|
||||
debug('is space: %s', result, freeSpace, maxFileSize);
|
||||
done(result);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current storage state.
|
||||
*
|
||||
* @param {Function} done
|
||||
*/
|
||||
Storage.prototype.getState = function(done) {
|
||||
if (!this.picture) {
|
||||
setTimeout(function() {
|
||||
done('unavailable');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.picture
|
||||
.available()
|
||||
.onsuccess = function(e) {
|
||||
done(e.target.result);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* States if sotrage is available.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
Storage.prototype.available = function() {
|
||||
return this.state === 'available';
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a picture.
|
||||
*
|
||||
* @param {String} filepath
|
||||
*/
|
||||
Storage.prototype.deletePicture = function(filepath, done) {
|
||||
var req = this.picture.delete(filepath);
|
||||
req.onerror = function(e) {
|
||||
var message = 'Failed to delete ' + filepath +
|
||||
' from DeviceStorage:' + e.target.error;
|
||||
console.warn(message);
|
||||
done(message);
|
||||
};
|
||||
|
||||
if (done) {
|
||||
req.onsuccess = function() {
|
||||
done(null);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a video and accompanying
|
||||
* poster image.
|
||||
*
|
||||
* @param {String} filepath
|
||||
*/
|
||||
Storage.prototype.deleteVideo = function(filepath) {
|
||||
var poster = filepath.replace('.3gp', '.jpg');
|
||||
|
||||
this.video.delete(filepath).onerror = function(e) {
|
||||
console.warn(
|
||||
'Failed to delete', filepath,
|
||||
'from DeviceStorage:', e.target.error);
|
||||
};
|
||||
|
||||
this.picture.delete(poster).onerror = function(e) {
|
||||
console.warn(
|
||||
'Failed to delete poster image', poster,
|
||||
'for video', filepath,
|
||||
'from DeviceStorage:', e.target.error);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the directory from a filepath.
|
||||
*
|
||||
* @param {String} filepath
|
||||
* @return {String}
|
||||
*/
|
||||
function getDir(filepath) {
|
||||
var index = filepath.lastIndexOf('/') + 1;
|
||||
return index ? filepath.substring(0, index) : '';
|
||||
}
|
||||
|
||||
});
|
||||
27
gaia/apps/camera/js/lib/string-utils.js
Executable file
27
gaia/apps/camera/js/lib/string-utils.js
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var StringUtils = {};
|
||||
|
||||
StringUtils.toCamelCase = function(str) {
|
||||
|
||||
// Convert SNAKE_CASE strings to hyphenated
|
||||
str = str.toLowerCase().replace(/_/g, '-');
|
||||
return str.replace(/-([a-z])/g, function(g) {
|
||||
return g[1].toUpperCase();
|
||||
});
|
||||
};
|
||||
|
||||
StringUtils.toHyphenate = function(str) {
|
||||
return str.replace(/([a-z][A-Z])/g, function(g) {
|
||||
return g[0] + '-' + g[1].toLowerCase();
|
||||
});
|
||||
};
|
||||
|
||||
StringUtils.lastPathComponent = function(path) {
|
||||
var pathComponents = path.split('/');
|
||||
return pathComponents[pathComponents.length - 1];
|
||||
};
|
||||
|
||||
return StringUtils;
|
||||
});
|
||||
79
gaia/apps/camera/js/main.js
Executable file
79
gaia/apps/camera/js/main.js
Executable file
|
|
@ -0,0 +1,79 @@
|
|||
'use strict';
|
||||
|
||||
// Store timestamp when JS started running
|
||||
window.jsStarted = Date.now();
|
||||
|
||||
define(function(require) {
|
||||
|
||||
// Store performance timestamps
|
||||
var perf = {
|
||||
jsStarted: window.jsStarted,
|
||||
firstModule: Date.now()
|
||||
};
|
||||
|
||||
/**
|
||||
* Module Dependencies
|
||||
*/
|
||||
|
||||
var Settings = require('lib/settings');
|
||||
var GeoLocation = require('lib/geo-location');
|
||||
var settingsData = require('config/config');
|
||||
var settings = new Settings(settingsData);
|
||||
var Camera = require('lib/camera/camera');
|
||||
var App = require('app');
|
||||
|
||||
// Create globals specified in the config file
|
||||
var key;
|
||||
if (settingsData.globals) {
|
||||
for (key in settingsData.globals) {
|
||||
window[key] = settingsData.globals[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Create new `App`
|
||||
var app = window.app = new App({
|
||||
settings: settings,
|
||||
geolocation: new GeoLocation(),
|
||||
el: document.body,
|
||||
doc: document,
|
||||
win: window,
|
||||
perf: perf,
|
||||
|
||||
camera: new Camera({
|
||||
focus: settingsData.focus
|
||||
}),
|
||||
|
||||
controllers: {
|
||||
overlay: require('controllers/overlay'),
|
||||
battery: require('controllers/battery'),
|
||||
hud: require('controllers/hud'),
|
||||
controls: require('controllers/controls'),
|
||||
viewfinder: require('controllers/viewfinder'),
|
||||
settings: require('controllers/settings'),
|
||||
activity: require('controllers/activity'),
|
||||
camera: require('controllers/camera'),
|
||||
storage: require('controllers/storage'),
|
||||
|
||||
// Lazy loaded controllers
|
||||
lazy: [
|
||||
'controllers/zoom-bar',
|
||||
'controllers/indicators',
|
||||
'controllers/media',
|
||||
'controllers/sounds'
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// We start the camera loading straight
|
||||
// away (async), as this is the slowest
|
||||
// part of the boot process.
|
||||
app.camera.load();
|
||||
app.settings.fetch();
|
||||
app.boot();
|
||||
|
||||
// Clean up
|
||||
for (key in settingsData) {
|
||||
delete settingsData[key];
|
||||
}
|
||||
|
||||
});
|
||||
148
gaia/apps/camera/js/views/confirm.js
Executable file
148
gaia/apps/camera/js/views/confirm.js
Executable file
|
|
@ -0,0 +1,148 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:confirm');
|
||||
var addPanAndZoomHandlers = require('lib/panzoom');
|
||||
var orientation = require('lib/orientation');
|
||||
var MediaFrame = require('MediaFrame');
|
||||
var bind = require('lib/bind');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'confirm',
|
||||
|
||||
initialize: function() {
|
||||
this.on('destroy', this.onDestroy);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
|
||||
// Get elements
|
||||
this.els.mediaFrame = this.find('.js-media-frame');
|
||||
this.els.retake = this.find('.js-retake');
|
||||
this.els.select = this.find('.js-select');
|
||||
|
||||
// Disable buttons on this view by default
|
||||
// until an image/video is displayed
|
||||
this.disableButtons();
|
||||
|
||||
// Initialize the MediaFrame component
|
||||
this.setupMediaFrame();
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.els.retake, 'click', this.onButtonClick);
|
||||
bind(this.els.select, 'click', this.onButtonClick);
|
||||
return this;
|
||||
},
|
||||
|
||||
setupMediaFrame: function() {
|
||||
this.mediaFrame = new MediaFrame(this.els.mediaFrame, true,
|
||||
this.maxPreviewSize);
|
||||
this.mediaFrame.video.onloading = this.onVideoLoading;
|
||||
this.mediaFrame.video.onplaying = this.onVideoPlaying;
|
||||
addPanAndZoomHandlers(this.mediaFrame);
|
||||
window.addEventListener('resize', this.onResize);
|
||||
return this;
|
||||
},
|
||||
|
||||
onVideoLoading: function() {
|
||||
this.emit('loadingvideo', 'loadingVideo');
|
||||
},
|
||||
|
||||
onVideoPlaying: function() {
|
||||
this.emit('playingvideo');
|
||||
},
|
||||
|
||||
clearMediaFrame: function() {
|
||||
this.mediaFrame.clear();
|
||||
this.disableButtons();
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this.el.classList.add('hidden');
|
||||
orientation.lock();
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this.el.classList.remove('hidden');
|
||||
orientation.unlock();
|
||||
},
|
||||
|
||||
disableButtons: function() {
|
||||
this.els.retake.setAttribute('disabled', true);
|
||||
this.els.select.setAttribute('disabled', true);
|
||||
},
|
||||
|
||||
enableButtons: function() {
|
||||
this.els.retake.removeAttribute('disabled');
|
||||
this.els.select.removeAttribute('disabled');
|
||||
},
|
||||
|
||||
showImage: function(image) {
|
||||
this.enableButtons();
|
||||
this.mediaFrame.displayImage(
|
||||
image.blob,
|
||||
image.width,
|
||||
image.height,
|
||||
image.preview,
|
||||
image.rotation,
|
||||
image.mirrored);
|
||||
return this;
|
||||
},
|
||||
|
||||
showVideo: function(video) {
|
||||
this.enableButtons();
|
||||
this.mediaFrame.displayVideo(
|
||||
video.blob,
|
||||
video.poster.blob,
|
||||
video.poster.width / video.poster.height,
|
||||
video.rotation);
|
||||
return this;
|
||||
},
|
||||
|
||||
template: function() {
|
||||
/*jshint maxlen:false*/
|
||||
return '<div class="confirm-media-frame js-media-frame"></div>' +
|
||||
'<footer id="confirm-controls" class="confirm-controls">' +
|
||||
'<button class="retake-button js-retake" data-l10n-id="retake-button" ' +
|
||||
'name="retake"></button>' +
|
||||
'<button class="select-button test-confirm-select js-select" ' +
|
||||
'data-l10n-id="select-button" name="select"></button></footer>';
|
||||
},
|
||||
|
||||
onButtonClick: function(event) {
|
||||
var el = event.currentTarget;
|
||||
var name = el.getAttribute('name');
|
||||
this.emit('click:' + name);
|
||||
},
|
||||
|
||||
onResize: function() {
|
||||
this.mediaFrame.resize();
|
||||
if (this.mediaFrame.displayingVideo) {
|
||||
this.mediaFrame.video.setPlayerSize();
|
||||
}
|
||||
},
|
||||
|
||||
onDestroy: function() {
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
this.mediaFrame.clear();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
344
gaia/apps/camera/js/views/controls.js
vendored
Executable file
344
gaia/apps/camera/js/views/controls.js
vendored
Executable file
|
|
@ -0,0 +1,344 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:controls');
|
||||
var debounce = require('lib/debounce');
|
||||
var bind = require('lib/bind');
|
||||
var View = require('view');
|
||||
var Drag = require('drag');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'controls',
|
||||
className: 'test-controls',
|
||||
|
||||
initialize: function(options) {
|
||||
this.drag = options && options.drag; // test hook
|
||||
this.once('inserted', this.setupSwitch);
|
||||
this.render();
|
||||
},
|
||||
|
||||
switchPositions: {
|
||||
left: 'picture',
|
||||
right: 'video',
|
||||
picture: 'left',
|
||||
video: 'right'
|
||||
},
|
||||
|
||||
// {node}: {data-l10n-id} pairs used for localization.
|
||||
elsL10n: {
|
||||
cancel: 'controls-button-close',
|
||||
thumbnail: 'preview-button',
|
||||
capture: 'capture-button'
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
|
||||
// Get nodes
|
||||
this.els.switchHandle = this.find('.js-switch-handle');
|
||||
this.els.thumbnail = this.find('.js-thumbnail');
|
||||
this.els.capture = this.find('.js-capture');
|
||||
this.els.cancel = this.find('.js-cancel');
|
||||
this.els.switch = this.find('.js-switch');
|
||||
this.els.icons = {
|
||||
camera: this.find('.js-icon-camera'),
|
||||
video: this.find('.js-icon-video')
|
||||
};
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
/**
|
||||
* Respond to click events on the buttons
|
||||
* other than the switch, which is a special
|
||||
* case.
|
||||
*
|
||||
* We 'debouce' the callback to defend
|
||||
* against button-bashing.
|
||||
*
|
||||
* @return {ControlsView} for chaining
|
||||
* @private
|
||||
*/
|
||||
bindEvents: function() {
|
||||
this.onButtonClick = debounce(this.onButtonClick, 300, true);
|
||||
bind(this.els.thumbnail, 'click', this.onButtonClick);
|
||||
bind(this.els.capture, 'click', this.onButtonClick);
|
||||
bind(this.els.cancel, 'click', this.onButtonClick);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create the draggable switch.
|
||||
*
|
||||
* We debouce the tapped callback to
|
||||
* defend against button-bashing.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
setupSwitch: function() {
|
||||
debug('setup dragger');
|
||||
|
||||
// Wait until the document is complete
|
||||
// to avoid any forced sync reflows.
|
||||
if (document.readyState !== 'complete') {
|
||||
window.addEventListener('load', this.setupSwitch);
|
||||
debug('deferred switch setup till after load');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer existing drag (test hook)
|
||||
this.drag = this.drag || new Drag({
|
||||
handle: this.els.switchHandle,
|
||||
container: this.els.switch
|
||||
});
|
||||
|
||||
this.drag.on('tapped', debounce(this.onSwitchTapped, 300, true));
|
||||
this.drag.on('ended', this.drag.snapToClosestEdge);
|
||||
this.drag.on('translate', this.onSwitchTranslate);
|
||||
this.drag.on('snapped', this.onSwitchSnapped);
|
||||
|
||||
this.drag.updateDimensions();
|
||||
this.updateSwitchPosition();
|
||||
|
||||
// Tidy up
|
||||
window.removeEventListener('load', this.setupSwitch);
|
||||
},
|
||||
|
||||
setCaptureLabel: function(recording) {
|
||||
this.els.capture.setAttribute('data-l10n-id',
|
||||
recording ? 'stop-capture-button' : 'capture-button');
|
||||
},
|
||||
|
||||
onSwitchSnapped: function(edges) {
|
||||
var mode = this.switchPositions[edges.x];
|
||||
var changed = mode !== this.get('mode');
|
||||
if (changed) { this.onSwitchChanged(); }
|
||||
},
|
||||
|
||||
onSwitchChanged: function() {
|
||||
this.emit('modechanged');
|
||||
},
|
||||
|
||||
onSwitchTapped: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
debug('switch tapped');
|
||||
this.onSwitchChanged();
|
||||
},
|
||||
|
||||
onSwitchTranslate: function(e) {
|
||||
this.setSwitchIcon(e.position.ratio.x);
|
||||
},
|
||||
|
||||
setSwitchIcon: function(ratio) {
|
||||
var skew = 2;
|
||||
var ratioSkewed = ratio * skew;
|
||||
var camera = Math.max(0, 1 - ratioSkewed);
|
||||
var video = Math.max(0, -1 + ratioSkewed);
|
||||
this.els.icons.camera.style.opacity = camera;
|
||||
this.els.icons.video.style.opacity = video;
|
||||
debug('set switch icon camera: %s, video: %s', camera, video);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set view screen reader visibility. In some cases, though the view is behind
|
||||
* an overlay and not hidden off screen, it still needs to be
|
||||
* hidden/inaccessible from the screen reader.
|
||||
*/
|
||||
setScreenReaderVisible: function(visible) {
|
||||
this.el.setAttribute('aria-hidden', !visible);
|
||||
},
|
||||
|
||||
onButtonClick: function(e) {
|
||||
e.stopPropagation();
|
||||
debug('button click');
|
||||
var name = e.currentTarget.getAttribute('name');
|
||||
this.emit('click:' + name, e);
|
||||
},
|
||||
|
||||
suspendModeSwitch: function(suspended) {
|
||||
if (suspended) {
|
||||
this.set('switch-toggle-disabled');
|
||||
} else {
|
||||
this.unset('switch-toggle-disabled');
|
||||
}
|
||||
},
|
||||
|
||||
setMode: function(mode) {
|
||||
debug('set mode: %s', mode);
|
||||
this.set('mode', mode);
|
||||
this.switchPosition = this.switchPositions[mode];
|
||||
var ratio = { left: 0, right: 1 }[this.switchPosition];
|
||||
this.updateSwitchPosition();
|
||||
this.setSwitchIcon(ratio);
|
||||
// Set appropriate mode switch label for screen reader.
|
||||
this.els.switch.setAttribute('data-l10n-id', mode + '-mode-button');
|
||||
debug('mode set pos: %s', this.switchPosition);
|
||||
},
|
||||
|
||||
updateSwitchPosition: function() {
|
||||
debug('updateSwitchPosition');
|
||||
if (!this.drag) { return; }
|
||||
this.drag.set({ x: this.switchPosition });
|
||||
debug('updated switch position: %s', this.switchPosition);
|
||||
},
|
||||
|
||||
setThumbnail: function(blob) {
|
||||
if (!this.els.image) {
|
||||
this.els.image = new Image();
|
||||
this.els.image.classList.add('test-thumbnail');
|
||||
this.els.thumbnail.appendChild(this.els.image);
|
||||
this.set('thumbnail', true);
|
||||
} else {
|
||||
window.URL.revokeObjectURL(this.els.image.src);
|
||||
}
|
||||
|
||||
this.els.image.src = window.URL.createObjectURL(blob);
|
||||
debug('thumbnail set');
|
||||
},
|
||||
|
||||
removeThumbnail: function() {
|
||||
if (this.els.image) {
|
||||
this.els.thumbnail.removeChild(this.els.image);
|
||||
window.URL.revokeObjectURL(this.els.image.src);
|
||||
this.els.image = null;
|
||||
}
|
||||
|
||||
this.set('thumbnail', false);
|
||||
},
|
||||
|
||||
/**
|
||||
* NOTE: The below functions are a first
|
||||
* attempt at replacing the default View
|
||||
* `.set()`, `.enable()` and `.disable()` APIs
|
||||
* to avoid having to use attributes to style
|
||||
* state in our CSS.
|
||||
*/
|
||||
|
||||
set: function(key, value) {
|
||||
if (typeof key !== 'string') { return; }
|
||||
if (arguments.length === 1) { value = true; }
|
||||
if (!value) { return this.unset(key); }
|
||||
|
||||
var attr = 'data-' + key;
|
||||
var oldValue = this.el.getAttribute(attr);
|
||||
var oldClass = oldValue && classFrom(key, oldValue);
|
||||
var newClass = classFrom(key, value);
|
||||
|
||||
if (oldClass) { this.el.classList.remove(oldClass); }
|
||||
if (newClass) { this.el.classList.add(newClass); }
|
||||
|
||||
this.el.setAttribute(attr, value);
|
||||
debug('remove: %s, add: %s', oldClass, newClass);
|
||||
debug('attr key: %s, value: %s', attr, value);
|
||||
},
|
||||
|
||||
get: function(key) {
|
||||
var attr = 'data-' + key;
|
||||
return this.el.getAttribute(attr);
|
||||
},
|
||||
|
||||
unset: function(key) {
|
||||
var attr = 'data-' + key;
|
||||
var value = this.el.getAttribute(attr);
|
||||
this.el.classList.remove(classFrom(key, value));
|
||||
this.el.removeAttribute(attr);
|
||||
},
|
||||
|
||||
enable: function(key) {
|
||||
this.set(key ? key + '-enabled' : 'enabled');
|
||||
this.unset(key ? key + '-disabled' : 'disabled');
|
||||
},
|
||||
|
||||
disable: function(key) {
|
||||
this.set(key ? key + '-disabled' : 'disabled');
|
||||
this.unset(key ? key + '-enabled' : 'enabled');
|
||||
},
|
||||
|
||||
/**
|
||||
* Localize the template based on a list of localizable elements - elsL10n. In
|
||||
* case the template is loaded before l10n is ready, localize will peform the
|
||||
* initial localization.
|
||||
*/
|
||||
localize: function() {
|
||||
// Switch mode label depends on the mode that is currently set.
|
||||
var mode = this.get('mode') || 'picture';
|
||||
this.els.switch.setAttribute('data-l10n-id', mode + '-mode-button');
|
||||
},
|
||||
|
||||
template: function() {
|
||||
/*jshint maxlen:false*/
|
||||
return '<div class="controls-left">' +
|
||||
'<div class="controls-button controls-thumbnail-button test-thumbnail js-thumbnail rotates" ' +
|
||||
'name="thumbnail" role="button" data-l10n-id="preview-button"></div>' +
|
||||
'<div class="controls-button controls-cancel-pick-button test-cancel-pick rotates js-cancel" ' +
|
||||
'name="cancel" data-icon="close" role="button" data-l10n-id="controls-button-close"></div>' +
|
||||
'</div>' +
|
||||
'<div class="controls-middle">' +
|
||||
'<div class="capture-button test-capture rotates js-capture" name="capture" ' +
|
||||
'data-l10n-id="capture-button" role="button">' +
|
||||
'<div class="circle outer-circle"></div>' +
|
||||
'<div class="circle inner-circle"></div>' +
|
||||
'<div class="center" data-icon="camera" aria-hidden="true"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="controls-right">' +
|
||||
'<div class="mode-switch test-switch" name="switch">' +
|
||||
'<div class="inner js-switch" role="button">' +
|
||||
'<div class="mode-switch_bg-icon rotates" data-icon="camera" aria-hidden="true"></div>' +
|
||||
'<div class="mode-switch_bg-icon rotates" data-icon="video" aria-hidden="true"></div>' +
|
||||
'<div class="mode-switch_handle js-switch-handle" aria-hidden="true">' +
|
||||
'<div class="mode-switch_current-icon camera rotates js-icon-camera" data-icon="camera" aria-hidden="true"></div>' +
|
||||
'<div class="mode-switch_current-icon video rotates js-icon-video" data-icon="video" aria-hidden="true"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Examples:
|
||||
*
|
||||
* classFrom('recording', true); //=> 'recording'
|
||||
* classFrom('flash', 'on'); //=> 'flash-on'
|
||||
* classFrom('recording', false); //=> ''
|
||||
* classFrom('recording'); //=> 'recording'
|
||||
* classFrom('recording', 'true'); //=> 'recording'
|
||||
* classFrom('recording', 'false'); //=> ''
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {*} value
|
||||
* @return {String}
|
||||
*/
|
||||
function classFrom(key, value) {
|
||||
value = detectBooleans(value);
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? key : '';
|
||||
} else if (value) {
|
||||
return key + '-' + value ;
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
function detectBooleans(value) {
|
||||
if (typeof value === 'boolean') { return value; }
|
||||
else if (value === 'true') { return true; }
|
||||
else if (value === 'false') { return false; }
|
||||
else { return value; }
|
||||
}
|
||||
|
||||
});
|
||||
83
gaia/apps/camera/js/views/countdown.js
Executable file
83
gaia/apps/camera/js/views/countdown.js
Executable file
|
|
@ -0,0 +1,83 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:countdown');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
/**
|
||||
* Countdown
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
module.exports = View.extend({
|
||||
name: 'countdown',
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
this.els.count = this.find('.js-count');
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this;
|
||||
},
|
||||
|
||||
set: function(time) {
|
||||
|
||||
// Update the number
|
||||
this.els.count.textContent = time;
|
||||
|
||||
// Trigger the shrink animation
|
||||
this.el.classList.remove('shrink');
|
||||
this.reflow = this.el.offsetTop;
|
||||
this.el.classList.add('shrink');
|
||||
|
||||
debug('set time: %s, near: %s', time);
|
||||
return this;
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this.el.classList.remove('hidden');
|
||||
this.el.classList.add('visible');
|
||||
},
|
||||
|
||||
hide: function(callback) {
|
||||
this.el.classList.remove('visible');
|
||||
this.el.classList.add('hidden');
|
||||
setTimeout(callback, 200);
|
||||
return this;
|
||||
},
|
||||
|
||||
setImmanent: function(value) {
|
||||
this.el.classList.toggle('immanent', value);
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.els.count.textContent = '';
|
||||
this.el.classList.remove('immanent', 'shrink');
|
||||
return this;
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div class="countdown_circle-1"></div>' +
|
||||
'<div class="countdown_circle-2"></div>' +
|
||||
'<div class="countdown_count">' +
|
||||
'<div class="rotates js-count"></div>' +
|
||||
'</div>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
38
gaia/apps/camera/js/views/face.js
Executable file
38
gaia/apps/camera/js/views/face.js
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'face',
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
this.el.classList.add('js-face');
|
||||
},
|
||||
|
||||
setPosition: function(x, y) {
|
||||
this.el.style.left = x + 'px';
|
||||
this.el.style.top = y + 'px';
|
||||
},
|
||||
|
||||
setDiameter: function(diameter) {
|
||||
this.el.style.width = diameter + 'px';
|
||||
this.el.style.height = diameter + 'px';
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
74
gaia/apps/camera/js/views/faces.js
Executable file
74
gaia/apps/camera/js/views/faces.js
Executable file
|
|
@ -0,0 +1,74 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var FaceView = require('views/face');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'faces',
|
||||
faces: [],
|
||||
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
this.el.innerHTML = this.template();
|
||||
this.FaceView = options.FaceView || FaceView;
|
||||
},
|
||||
|
||||
// It creates the DOM elements that will display circles
|
||||
// around the detected faces.
|
||||
createFaces: function(maxNumberFaces) {
|
||||
var faceView;
|
||||
var i;
|
||||
for(i = 0; i < maxNumberFaces; ++i) {
|
||||
faceView = new this.FaceView();
|
||||
faceView.hide();
|
||||
this.faces.push(faceView);
|
||||
faceView.appendTo(this.el);
|
||||
}
|
||||
},
|
||||
|
||||
render: function(faces) {
|
||||
var self = this;
|
||||
this.hideFaces();
|
||||
if (faces.length > this.faces.length) {
|
||||
this.createFaces(faces.length - this.faces.length);
|
||||
}
|
||||
faces.forEach(function(face, index) {
|
||||
var faceView = self.faces[index];
|
||||
self.renderFace(face, faceView);
|
||||
});
|
||||
},
|
||||
|
||||
renderFace: function(face, faceView) {
|
||||
// Maximum diameter is 300px as in the visual spec
|
||||
var diameter = Math.min(300, face.diameter);
|
||||
faceView.setPosition(face.x, face.y);
|
||||
faceView.setDiameter(diameter);
|
||||
faceView.show();
|
||||
},
|
||||
|
||||
hideFaces: function() {
|
||||
this.faces.forEach(function(faceView) {
|
||||
faceView.hide();
|
||||
});
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
var self = this;
|
||||
this.faces.forEach(function(faceView) {
|
||||
self.el.removeChild(faceView.el);
|
||||
});
|
||||
this.faces = [];
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
74
gaia/apps/camera/js/views/focus.js
Executable file
74
gaia/apps/camera/js/views/focus.js
Executable file
|
|
@ -0,0 +1,74 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:focus');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'focus',
|
||||
fadeTime: 500,
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
this.setFocusState('none');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
delete this.template; // Clean up
|
||||
debug('rendered');
|
||||
return this;
|
||||
},
|
||||
|
||||
setFocusState: function(state) {
|
||||
this.set('state', state);
|
||||
if (state !== 'focusing') {
|
||||
this.fadeOut();
|
||||
}
|
||||
},
|
||||
|
||||
setFocusMode: function(mode) {
|
||||
this.reset();
|
||||
this.set('mode', mode);
|
||||
},
|
||||
|
||||
setPosition: function(x, y) {
|
||||
if (this.fadeOutTimer) {
|
||||
clearTimeout(this.fadeOutTimer);
|
||||
}
|
||||
this.el.style.left = x + 'px';
|
||||
this.el.style.top = y + 'px';
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.el.style.left = '50%';
|
||||
this.el.style.top = '50%';
|
||||
this.set('state', 'none');
|
||||
},
|
||||
|
||||
fadeOut: function() {
|
||||
var self = this;
|
||||
this.fadeOutTimer = setTimeout(hide, this.fadeTime);
|
||||
function hide() {
|
||||
self.reset();
|
||||
}
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div class="focus_locking" data-icon="focus-locking" ' +
|
||||
'aria-hidden="true"></div>' +
|
||||
'<div class="focus_locked" data-icon="focus-locked" aria-hidden="true">' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
102
gaia/apps/camera/js/views/hud.js
Executable file
102
gaia/apps/camera/js/views/hud.js
Executable file
|
|
@ -0,0 +1,102 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:hud');
|
||||
var bind = require('lib/bind');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
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');
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.els.flash, 'click', this.onFlashClick);
|
||||
bind(this.els.camera, 'click', this.onCameraClick);
|
||||
bind(this.els.settings, 'click', this.onSettingsClick, true);
|
||||
return this;
|
||||
},
|
||||
|
||||
_setLabel: function(element, mode) {
|
||||
if (mode) {
|
||||
this.els[element].setAttribute('data-l10n-id', mode.title + '-button');
|
||||
} else {
|
||||
this.els[element].removeAttribute('data-l10n-id');
|
||||
this.els[element].removeAttribute('aria-label');
|
||||
}
|
||||
},
|
||||
|
||||
setFlashModeLabel: function(mode) {
|
||||
this._setLabel('flash', mode);
|
||||
},
|
||||
|
||||
setFlashMode: function(mode) {
|
||||
if (!mode) { return; }
|
||||
this.els.flash.dataset.icon = mode.icon;
|
||||
this.setFlashModeLabel(mode);
|
||||
},
|
||||
|
||||
setCameraLabel: function(camera) {
|
||||
this._setLabel('camera', camera);
|
||||
},
|
||||
|
||||
setCamera: function(camera) {
|
||||
if (!camera) { return; }
|
||||
this.els.camera.dataset.icon = camera.icon;
|
||||
this.setCameraLabel(camera);
|
||||
},
|
||||
|
||||
setMenuLabel: function() {
|
||||
this._setLabel('settings', { title: 'menu' });
|
||||
},
|
||||
|
||||
onFlashClick: function(event) {
|
||||
event.stopPropagation();
|
||||
this.emit('click:flash');
|
||||
},
|
||||
|
||||
onCameraClick: function(event) {
|
||||
event.stopPropagation();
|
||||
this.emit('click:camera');
|
||||
},
|
||||
|
||||
onSettingsClick: function(event) {
|
||||
event.stopPropagation();
|
||||
this.emit('click:settings');
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div role="button" class="hud_btn hud_camera rotates ' +
|
||||
'test-camera-toggle js-camera"></div>' +
|
||||
'<div role="button" class="hud_btn hud_flash rotates test-flash-button ' +
|
||||
'js-flash"></div>' +
|
||||
'<div role="button" class="hud_btn hud_settings rotates ' +
|
||||
'test-settings-toggle js-settings" data-icon="menu" ' +
|
||||
'data-l10n-id="menu-button"></div>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
45
gaia/apps/camera/js/views/indicators.js
Executable file
45
gaia/apps/camera/js/views/indicators.js
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:indicators');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'indicators',
|
||||
tag: 'ul',
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this;
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<li class="indicator_countdown rotates" data-icon="self-timer" ' +
|
||||
'data-l10n-id="self-timer-indicator"></li>' +
|
||||
'<li class="indicator_hdr rotates" data-icon="hdr" ' +
|
||||
'data-l10n-id="hdr-indicator"></li>' +
|
||||
'<li class="indicator_geolocation rotates" data-icon="location" ' +
|
||||
'data-l10n-id="location-indicator"></li>' +
|
||||
'<li class="indicator_battery rotates" data-icon="battery-3" ' +
|
||||
'data-l10n-id="battery-low-indicator"></li>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
38
gaia/apps/camera/js/views/loading-screen.js
Executable file
38
gaia/apps/camera/js/views/loading-screen.js
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
define(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;
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this;
|
||||
},
|
||||
|
||||
show: function(done) {
|
||||
this.reflow = this.el.offsetTop;
|
||||
View.prototype.show.call(this);
|
||||
},
|
||||
|
||||
hide: function(done) {
|
||||
View.prototype.hide.call(this);
|
||||
if (done) { setTimeout(done, this.fadeTime); }
|
||||
},
|
||||
|
||||
template: '<progress></progress>'
|
||||
});
|
||||
|
||||
});
|
||||
152
gaia/apps/camera/js/views/notification.js
Executable file
152
gaia/apps/camera/js/views/notification.js
Executable file
|
|
@ -0,0 +1,152 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var mix = require('lib/mixin');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name:'notification',
|
||||
tag: 'ul',
|
||||
time: 3000,
|
||||
|
||||
initialize: function() {
|
||||
this.counter = 0;
|
||||
this.hash = {};
|
||||
// For accessibility purposes list/listitem semantics is not necessary.
|
||||
// Instead notifications are presented as status ARIA live regions.
|
||||
this.el.setAttribute('role', 'presentation');
|
||||
},
|
||||
|
||||
/**
|
||||
* Display a new notification.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - `text {L10nId}`
|
||||
* L10nId may be:
|
||||
* a string -> l10nId
|
||||
* an object -> {id: l10nId, args: l10nArgs}
|
||||
* an object -> {html: string}
|
||||
* read more at: http://mzl.la/1A76Fby
|
||||
* - `className {String}`
|
||||
* - `persistent {Boolean}`
|
||||
*
|
||||
* @param {Object} options
|
||||
* @return {Number} id for clearing
|
||||
* @public
|
||||
*/
|
||||
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); }
|
||||
// Notification should have the semantics of the status ARIA live region.
|
||||
item.el.setAttribute('role', 'status');
|
||||
// Making notfication assertive, so it is spoken right away and not queued.
|
||||
item.el.setAttribute('aria-live', 'assertive');
|
||||
this.el.appendChild(item.el);
|
||||
|
||||
// Remove last temporary
|
||||
// notification in the way
|
||||
this.clear(this.temporary);
|
||||
|
||||
// Remove non-persistent
|
||||
// messages after 3s
|
||||
if (!item.persistent) {
|
||||
this.temporary = id;
|
||||
this.hide(this.persistent);
|
||||
item.clearTimeout = setTimeout(function() {
|
||||
self.clear(id);
|
||||
}, this.time);
|
||||
}
|
||||
|
||||
// Remove previous persistent
|
||||
if (item.persistent) {
|
||||
this.clear(this.persistent);
|
||||
this.persistent = id;
|
||||
}
|
||||
|
||||
// Store and return
|
||||
this.hash[id] = item;
|
||||
return id;
|
||||
},
|
||||
|
||||
setAttributes: function(el, attrs) {
|
||||
for (var key in attrs) { el.setAttribute(key, attrs[key]); }
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear notfication by id.
|
||||
*
|
||||
* Remove the notification from the DOM,
|
||||
* clear any existing `clearTimeout` that
|
||||
* may have been installed on creation.
|
||||
*
|
||||
* @param {Number} item
|
||||
* @public
|
||||
*/
|
||||
clear: function(id) {
|
||||
var item = this.hash[id];
|
||||
if (!item || item.cleared) { return; }
|
||||
|
||||
this.el.removeChild(item.el);
|
||||
clearTimeout(item.clearTimeout);
|
||||
item.cleared = true;
|
||||
|
||||
// Clear references
|
||||
if (item === this.temporary) { this.temporary = null; }
|
||||
if (item === this.persistent) { this.persistent = null; }
|
||||
delete this.hash[id];
|
||||
|
||||
// Show persistent notification
|
||||
// (if there still is one)
|
||||
this.show(this.persistent);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide a notification.
|
||||
*
|
||||
* @param {Number} id
|
||||
* @private
|
||||
*/
|
||||
hide: function(id) {
|
||||
var item = this.hash[id];
|
||||
if (!item) { return; }
|
||||
item.el.classList.add('hidden');
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a hidden notification.
|
||||
*
|
||||
* @param {Number} id
|
||||
* @private
|
||||
*/
|
||||
show: function(id) {
|
||||
var item = this.hash[id];
|
||||
if (!item) { return; }
|
||||
item.el.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
115
gaia/apps/camera/js/views/overlay.js
Executable file
115
gaia/apps/camera/js/views/overlay.js
Executable file
|
|
@ -0,0 +1,115 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:overlay');
|
||||
var View = require('view');
|
||||
var bind = require('lib/bind');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
className: 'overlay',
|
||||
|
||||
initialize: function(options) {
|
||||
var data = this.getData(options.type);
|
||||
if (!data) { return; }
|
||||
|
||||
this.data('type', options.type);
|
||||
this.data('closable', options.closable);
|
||||
this.render(data);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
|
||||
// Inject HTML
|
||||
this.el.innerHTML = this.template(data);
|
||||
|
||||
// Pick out elements
|
||||
this.els.buttons = {
|
||||
close: this.find('.js-close-btn')
|
||||
};
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
rendered: function() {
|
||||
return this.el.innerHTML.length > 0;
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.els.buttons.close, 'click', this.onButtonClick);
|
||||
return this;
|
||||
},
|
||||
|
||||
template: function(data) {
|
||||
/*jshint maxlen:false*/
|
||||
return '<form role="dialog" data-type="confirm">' +
|
||||
'<section>' +
|
||||
'<h1 class="overlay-title" data-l10n-id="' + data.title + '"></h1>' +
|
||||
'<p id="overlay-text" data-l10n-id="' + data.body + '"></p>' +
|
||||
'</section>' +
|
||||
'<menu class="overlay-menu-close">' +
|
||||
'<button class="full js-close-btn" type="button" ' +
|
||||
'data-l10n-id="' + data.closeButtonText + '" name="close-btn">' +
|
||||
'</button>' +
|
||||
'</menu>' +
|
||||
'</form>';
|
||||
},
|
||||
|
||||
getData: function(type) {
|
||||
var data = {};
|
||||
|
||||
switch (type) {
|
||||
case 'unavailable':
|
||||
data.title = 'nocard2-title';
|
||||
data.body = 'nocard3-text';
|
||||
break;
|
||||
case 'nospace':
|
||||
data.title = 'nospace2-title';
|
||||
data.body = 'nospace2-text';
|
||||
break;
|
||||
case 'shared':
|
||||
data.title = 'pluggedin2-title';
|
||||
data.body = 'pluggedin2-text';
|
||||
break;
|
||||
case 'shutdown':
|
||||
data.title = 'battery-shutdown-title';
|
||||
data.body = 'battery-shutdown-text';
|
||||
break;
|
||||
case 'request-fail':
|
||||
data.title = 'camera-unavailable-title';
|
||||
data.body = 'camera-unavailable-text';
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
data.closeButtonText = 'close-button';
|
||||
return data;
|
||||
},
|
||||
|
||||
data: function(key, value) {
|
||||
switch (arguments.length) {
|
||||
case 1: return this.el.getAttribute('data-' + key);
|
||||
case 2: this.el.setAttribute('data-' + key, value);
|
||||
}
|
||||
},
|
||||
|
||||
onButtonClick: function(event) {
|
||||
var el = event.currentTarget;
|
||||
var name = el.getAttribute('name');
|
||||
this.emit('click:' + name);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
377
gaia/apps/camera/js/views/preview-gallery.js
Executable file
377
gaia/apps/camera/js/views/preview-gallery.js
Executable file
|
|
@ -0,0 +1,377 @@
|
|||
/* jshint -W098 */
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:preview-gallery');
|
||||
var addPanAndZoomHandlers = require('lib/panzoom');
|
||||
var orientation = require('lib/orientation');
|
||||
var MediaFrame = require('MediaFrame');
|
||||
var bind = require('lib/bind');
|
||||
var attach = require('attach');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* `<gaia-header>` used in template
|
||||
*/
|
||||
|
||||
require('gaia-header');
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
var SWIPE_DISTANCE_THRESHOLD = window.innerWidth / 3; // pixels
|
||||
var SWIPE_VELOCITY_THRESHOLD = 1.0; // pixels/ms
|
||||
|
||||
var SWIPE_DURATION = 150; // How long to animate the swipe
|
||||
var FADE_IN_DURATION = 450; // How long to animate the fade in after swipe
|
||||
|
||||
/**
|
||||
* Locals
|
||||
*/
|
||||
|
||||
return View.extend({
|
||||
name: 'preview-gallery',
|
||||
className: 'offscreen',
|
||||
|
||||
initialize: function() {
|
||||
debug('initialized');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
this.els.frameContainer = this.find('.js-frame-container');
|
||||
this.els.previewMenu = this.find('.js-preview-menu');
|
||||
this.els.mediaFrame = this.find('.js-media-frame');
|
||||
this.els.countText = this.find('.js-count-text');
|
||||
this.els.options = this.find('.js-options');
|
||||
this.els.header = this.find('.js-header');
|
||||
this.els.share = this.find('.js-share');
|
||||
|
||||
// We're appending new elements to DOM so to make sure headers are
|
||||
// properly resized and centered, we emmit a lazyload event.
|
||||
// This will be removed when the gaia-header web component lands.
|
||||
window.dispatchEvent(new CustomEvent('lazyload', {
|
||||
detail: this.el
|
||||
}));
|
||||
|
||||
// Configure the MediaFrame component
|
||||
this.configure();
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.el, 'tap', this.onTap);
|
||||
bind(this.els.header, 'action', this.firer('click:back'));
|
||||
bind(this.els.options, 'click', this.onButtonClick);
|
||||
bind(this.els.share, 'click', this.onButtonClick);
|
||||
// Our screen reader synthesizes wheel events when the user swipes with two
|
||||
// fingers in either direction. Those events always have their deltaMode set
|
||||
// to DOM_DELTA_PAGE, and the delta value can either be -1, 0 or 1.
|
||||
bind(this.els.mediaFrame, 'wheel', this.onFrameWheel);
|
||||
return this;
|
||||
},
|
||||
|
||||
configure: function() {
|
||||
this.currentIndex = this.lastIndex = 0;
|
||||
|
||||
this.frame = new MediaFrame(this.els.mediaFrame, true, this.maxPreviewSize);
|
||||
this.frame.video.onloading = this.onVideoLoading;
|
||||
this.frame.video.onplaying = this.onVideoPlaying;
|
||||
this.frame.video.onpaused = this.onVideoPaused;
|
||||
|
||||
addPanAndZoomHandlers(this.frame, this.swipeCallback);
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div class="preview-menu js-preview-menu">' +
|
||||
'<gaia-header class="js-header" action="back" ignore-dir>' +
|
||||
'<h1 data-l10n-id="preview">Preview</h1>' +
|
||||
'<button class="preview-share-icon js-share"' +
|
||||
'name="share" data-icon="share" ' +
|
||||
'data-l10n-id="share-button"></button>' +
|
||||
'<button class="preview-option-icon ' +
|
||||
'js-options" name="options" data-icon="more" ' +
|
||||
'data-l10n-id="more-button"></button>' +
|
||||
'</gaia-header>' +
|
||||
'</div>' +
|
||||
'<div class="frame-container js-frame-container">' +
|
||||
'<div class="media-frame js-media-frame"></div>' +
|
||||
'</div>' +
|
||||
'<div class="count-text js-count-text"></div>';
|
||||
},
|
||||
|
||||
onTap: function() {
|
||||
if (this.videoPlaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
var isShown = this.els.previewMenu.classList.contains('visible');
|
||||
if (isShown) {
|
||||
this.previewMenuFadeOut();
|
||||
} else {
|
||||
this.previewMenuFadeIn();
|
||||
}
|
||||
},
|
||||
|
||||
previewMenuFadeIn: function() {
|
||||
this.els.previewMenu.classList.add('visible');
|
||||
},
|
||||
|
||||
previewMenuFadeOut: function() {
|
||||
this.els.previewMenu.classList.remove('visible');
|
||||
},
|
||||
|
||||
onFrameWheel: function(event) {
|
||||
if (event.deltaMode !== event.DOM_DELTA_PAGE || event.deltaY) {
|
||||
return;
|
||||
}
|
||||
// If the video is playing, switch to preview.
|
||||
if (this.videoPlaying) {
|
||||
this.onVideoPaused();
|
||||
}
|
||||
if (event.deltaX > 0) {
|
||||
this.emit('swipe', 'left');
|
||||
} else if (event.deltaX < 0) {
|
||||
this.emit('swipe', 'right');
|
||||
}
|
||||
},
|
||||
|
||||
swipeCallback: function(swipeAmount, swipeVelocity) {
|
||||
var self = this;
|
||||
|
||||
if (swipeVelocity === undefined) {
|
||||
// If this is not the end of the gesture, then just move the media
|
||||
// frame left or right by the specified amount.
|
||||
this.els.frameContainer.style.transform =
|
||||
'translate(' + swipeAmount + 'px, 0)';
|
||||
return;
|
||||
}
|
||||
|
||||
// If we were passed a velocity, then this is the end of the gesture
|
||||
// and we have to figure out whether we are going to go to the
|
||||
// next item, the previous item, or remain on the current item.
|
||||
var direction, translation;
|
||||
|
||||
// Should we move to the previous item?
|
||||
if (swipeAmount > 0 && swipeVelocity > 0 && // Same sign and
|
||||
this.currentIndex > 1 && // has previous item and
|
||||
(swipeAmount > SWIPE_DISTANCE_THRESHOLD || // distance big enough or
|
||||
swipeVelocity > SWIPE_VELOCITY_THRESHOLD)) // speed fast enough
|
||||
{
|
||||
direction = 'right';
|
||||
translation = '100%';
|
||||
}
|
||||
// Should we move to the next item?
|
||||
else if (swipeAmount <= 0 && swipeVelocity <= 0 &&
|
||||
this.currentIndex < this.lastIndex &&
|
||||
(swipeAmount < -SWIPE_DISTANCE_THRESHOLD ||
|
||||
swipeVelocity < -SWIPE_VELOCITY_THRESHOLD)) {
|
||||
direction = 'left';
|
||||
translation = '-100%';
|
||||
}
|
||||
|
||||
// If we're not moving either left or right, just animate the
|
||||
// item to its starting point and we're done.
|
||||
if (!direction) {
|
||||
animate('transform', 'translate(0,0)', SWIPE_DURATION);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we get here, we are going to slide the current item off the screen
|
||||
// and display the next or previous item.
|
||||
|
||||
// First, stop the video if there is one and it is playing.
|
||||
if (this.videoPlaying) {
|
||||
this.onVideoPaused();
|
||||
}
|
||||
|
||||
// Now animate the item off the screen
|
||||
animate('transform', 'translate(' + translation + ', 0)',
|
||||
SWIPE_DURATION,
|
||||
function() {
|
||||
// Once we're off screen ask the controller to
|
||||
// switch to the new image.
|
||||
self.emit('swipe', direction);
|
||||
|
||||
// On the next redraw, put the frame back on the screen
|
||||
// but make it opaque
|
||||
window.requestAnimationFrame(function() {
|
||||
// Next, make the frame invisible
|
||||
// and put it back on the screen
|
||||
self.els.frameContainer.style.opacity = 0;
|
||||
self.els.frameContainer.style.transform = 'translate(0,0)';
|
||||
|
||||
// Then on the frame after that, animate the opacity to
|
||||
// make it visible again.
|
||||
window.requestAnimationFrame(function() {
|
||||
animate('opacity', 1, FADE_IN_DURATION);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// A helper function to animate the specified CSS property to the
|
||||
// specified value for the specified duration. When the animation
|
||||
// is complete, reset everything and call the done callback
|
||||
function animate(property, value, duration, done) {
|
||||
var e = self.els.frameContainer;
|
||||
e.addEventListener('transitionend', onTransitionEnd);
|
||||
e.style.transitionProperty = property;
|
||||
e.style.transitionDuration = duration + 'ms';
|
||||
e.style[property] = value;
|
||||
|
||||
function onTransitionEnd() {
|
||||
e.removeEventListener('transitionend', onTransitionEnd);
|
||||
delete e.style.transitionProperty;
|
||||
delete e.style.transitionDuration;
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onButtonClick: function(e, el) {
|
||||
if (this.videoPlaying) { return; }
|
||||
|
||||
el = el || e.currentTarget;
|
||||
var name = el.getAttribute('name');
|
||||
if (this.optionsMenuContainer) {
|
||||
this.hideOptionsMenu();
|
||||
}
|
||||
this.emit('click:' + name, e);
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
open: function() {
|
||||
window.addEventListener('resize', this.onResize);
|
||||
orientation.unlock();
|
||||
this.previewMenuFadeIn();
|
||||
this.el.classList.remove('offscreen');
|
||||
},
|
||||
|
||||
close: function() {
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
orientation.lock();
|
||||
this.previewMenuFadeOut();
|
||||
this.el.classList.add('offscreen');
|
||||
this.frame.clear();
|
||||
},
|
||||
|
||||
updateCountText: function(current, total) {
|
||||
this.currentIndex = current;
|
||||
this.lastIndex = total;
|
||||
this.els.countText.textContent = current + '/' + total;
|
||||
|
||||
// Ensure that the information about the number of thumbnails is provided to
|
||||
// the screen reader.
|
||||
this.els.mediaFrame.setAttribute('data-l10n-id', 'media-frame');
|
||||
this.els.mediaFrame.setAttribute('data-l10n-args', JSON.stringify(
|
||||
{ total: total, current: current }));
|
||||
},
|
||||
|
||||
onResize: function() {
|
||||
// And we have to resize the frame (and its video player)
|
||||
this.frame.resize();
|
||||
if (this.frame.displayingVideo) {
|
||||
this.frame.video.setPlayerSize();
|
||||
}
|
||||
},
|
||||
|
||||
showImage: function(image) {
|
||||
this.frame.displayImage(
|
||||
image.blob,
|
||||
image.width,
|
||||
image.height,
|
||||
image.preview,
|
||||
image.rotation,
|
||||
image.mirrored);
|
||||
},
|
||||
|
||||
showVideo: function(video) {
|
||||
this.frame.displayVideo(
|
||||
video.blob,
|
||||
video.poster.blob,
|
||||
video.poster.width / video.poster.height,
|
||||
video.rotation);
|
||||
},
|
||||
|
||||
onVideoLoading: function() {
|
||||
this.emit('loadingvideo', 'loadingVideo');
|
||||
},
|
||||
|
||||
onVideoPlaying: function() {
|
||||
if (this.videoPlaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoPlaying = true;
|
||||
this.previewMenuFadeOut();
|
||||
|
||||
this.emit('playingvideo');
|
||||
},
|
||||
|
||||
onVideoPaused: function() {
|
||||
if (!this.videoPlaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoPlaying = false;
|
||||
this.previewMenuFadeIn();
|
||||
},
|
||||
|
||||
showOptionsMenu: function() {
|
||||
this.optionsMenuContainer = document.createElement('div');
|
||||
this.optionsMenuContainer.innerHTML = this.optionTemplate();
|
||||
this.el.appendChild(this.optionsMenuContainer);
|
||||
|
||||
this.el.classList.add('action-menu');
|
||||
|
||||
this.menu = this.find('.js-menu');
|
||||
|
||||
// We add the event listner for menu items and cancel buttons
|
||||
var cancelButton = this.find('.js-cancel');
|
||||
bind(cancelButton, 'click', this.hideOptionsMenu);
|
||||
if (this.menu) {
|
||||
attach.on(this.menu, 'click', '.js-btn', this.onButtonClick);
|
||||
}
|
||||
},
|
||||
|
||||
hideOptionsMenu: function() {
|
||||
if (this.optionsMenuContainer) {
|
||||
this.el.classList.remove('action-menu');
|
||||
this.optionsMenuContainer.parentElement.removeChild(
|
||||
this.optionsMenuContainer);
|
||||
this.optionsMenuContainer = null;
|
||||
}
|
||||
},
|
||||
|
||||
optionTemplate: function() {
|
||||
return '<form class="visible" data-type="action"' +
|
||||
'role="dialog" data-z-index-level="action-menu">' +
|
||||
'<header data-l10n-id="options">Options</header>' +
|
||||
'<menu class="js-menu">' +
|
||||
'<button class="js-btn" name="gallery" data-l10n-id="open-gallery">' +
|
||||
'Open Gallery' +
|
||||
'</button>' +
|
||||
'<button class="js-btn" name="delete" data-l10n-id="delete">' +
|
||||
'Delete' +
|
||||
'</button>' +
|
||||
'<button class="js-cancel" data-action="cancel" data-l10n-id="cancel">' +
|
||||
'Cancel' +
|
||||
'</button>' +
|
||||
'</menu>' +
|
||||
'</form>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
30
gaia/apps/camera/js/views/recording-timer.js
Executable file
30
gaia/apps/camera/js/views/recording-timer.js
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:recording-timer');
|
||||
var formatTimer = require('lib/format-timer');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'recording-timer',
|
||||
|
||||
initialize: function() {
|
||||
this.value(0);
|
||||
this.el.setAttribute('role', 'timer');
|
||||
},
|
||||
|
||||
value: function(value) {
|
||||
this.el.textContent = formatTimer(value);
|
||||
debug('set value: %s', value);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
108
gaia/apps/camera/js/views/setting-options.js
Executable file
108
gaia/apps/camera/js/views/setting-options.js
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:setting-options');
|
||||
var attach = require('attach');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'setting-options',
|
||||
|
||||
initialize: function(options) {
|
||||
this.model = options.model;
|
||||
this.l10n = options.l10n || document.l10n;
|
||||
this.on('destroy', this.onDestroy);
|
||||
attach(this.el, 'click', 'li', this.onOptionClick);
|
||||
attach(this.el, 'click', '.js-back', this.firer('click:back'));
|
||||
this.model.on('change:selected', this.onSelectedChange);
|
||||
debug('initialized');
|
||||
},
|
||||
|
||||
onDestroy: function() {
|
||||
this.model.off('change:selected', this.onSelectedChange);
|
||||
},
|
||||
|
||||
onOptionClick: function(event, el) {
|
||||
var key = el.getAttribute('data-key');
|
||||
this.emit('click:option', key, this.model);
|
||||
},
|
||||
|
||||
onSelectedChange: function(key) {
|
||||
var next = this.els[key];
|
||||
this.els.selected.classList.remove('selected');
|
||||
next.classList.add('selected');
|
||||
this.els.selected = next;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var data = this.model.get();
|
||||
this.selectedKey = data.selected;
|
||||
this.el.innerHTML = this.template(data);
|
||||
this.els.ul = this.find('.js-list');
|
||||
|
||||
// we need to pass a boolean flag indicating if the
|
||||
// options should be localized
|
||||
var localizable = data.optionsLocalizable === false ? false : true;
|
||||
data.options.forEach(this.renderOption.bind(this, localizable));
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this;
|
||||
},
|
||||
|
||||
renderOption: function(localizable, option) {
|
||||
var li = document.createElement('li');
|
||||
var isSelected = option.key === this.selectedKey;
|
||||
|
||||
if (localizable) {
|
||||
li.setAttribute('data-l10n-id', option.title);
|
||||
this.l10n.formatValue(option.title).then((title) => {
|
||||
li.setAttribute('aria-label', title);
|
||||
});
|
||||
} else {
|
||||
li.textContent = option.title;
|
||||
li.setAttribute('aria-label', option.title);
|
||||
}
|
||||
li.setAttribute('data-key', option.key);
|
||||
// The settings options list is a listbox (list of actionable items) thus
|
||||
// each iteam must have an 'option' role.
|
||||
li.setAttribute('role', 'option');
|
||||
li.className = 'setting-option';
|
||||
li.dataset.icon = 'tick';
|
||||
this.els.ul.appendChild(li);
|
||||
this.els[option.key] = li;
|
||||
|
||||
if (isSelected) {
|
||||
li.classList.add('selected');
|
||||
// Make sure selected semantics is conveyed to the screen reader.
|
||||
li.setAttribute('aria-selected', true);
|
||||
this.els.selected = li;
|
||||
}
|
||||
},
|
||||
|
||||
template: function(data) {
|
||||
return '<div class="inner">' +
|
||||
'<div class="settings_header">' +
|
||||
'<div class="settings-back-btn js-back" ' +
|
||||
'data-icon="left" role="button" data-l10n-id="back-button"></div>' +
|
||||
'<h2 aria-level="1" class="settings_title" data-l10n-id="' +
|
||||
data.header + '"></h2>' +
|
||||
'</div>' +
|
||||
'<div class="settings_items">' +
|
||||
'<ul role="listbox" class="inner js-list"></ul>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
84
gaia/apps/camera/js/views/setting.js
Executable file
84
gaia/apps/camera/js/views/setting.js
Executable file
|
|
@ -0,0 +1,84 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:setting');
|
||||
var bind = require('lib/bind');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
tag: 'li',
|
||||
name: 'setting',
|
||||
|
||||
initialize: function(options) {
|
||||
this.l10n = options.l10n || document.l10n;
|
||||
this.model = options.model;
|
||||
this.model.on('change', this.render);
|
||||
this.on('destroy', this.onDestroy);
|
||||
this.el.dataset.icon = this.model.get('icon');
|
||||
this.el.classList.add('test-' + this.model.get('title') + '-setting');
|
||||
bind(this.el, 'click', this.onClick);
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
this.emit('click', this);
|
||||
},
|
||||
|
||||
onDestroy: function() {
|
||||
this.model.off('change', this.render);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var data = this.model.get();
|
||||
data.selected = this.model.selected();
|
||||
data.value = data.selected && data.selected.title;
|
||||
this.el.setAttribute('role', 'option');
|
||||
|
||||
|
||||
var localizedValue;
|
||||
if (data.optionsLocalizable === false) {
|
||||
localizedValue = Promise.resolve(data.value);
|
||||
} else {
|
||||
localizedValue = this.l10n.formatValue(data.value);
|
||||
}
|
||||
|
||||
localizedValue.then(valueString => {
|
||||
this.l10n.setAttributes(
|
||||
this.el,
|
||||
'setting-option-' + data.title,
|
||||
{ value: valueString }
|
||||
);
|
||||
this.el.innerHTML = this.template({
|
||||
titleL10n: 'data-l10n-id="' + data.title + '"',
|
||||
valueL10n: data.optionsLocalizable !== false ? 'data-l10n-id="' +
|
||||
data.value + '"' : '',
|
||||
valueText: data.optionsLocalizable === false ? data.value : '',
|
||||
});
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered (item %s)', data.key);
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
template: function(data) {
|
||||
return '<div class="setting_text">' +
|
||||
'<h4 class="setting_title" ' + data.titleL10n + '></h4>' +
|
||||
'<h5 class="setting_value" ' + data.valueL10n + '>' +
|
||||
data.valueText +
|
||||
'</h5>' +
|
||||
'</div>';
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
139
gaia/apps/camera/js/views/settings.js
Executable file
139
gaia/apps/camera/js/views/settings.js
Executable file
|
|
@ -0,0 +1,139 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:settings');
|
||||
var OptionsView = require('views/setting-options');
|
||||
var SettingView = require('views/setting');
|
||||
var View = require('view');
|
||||
var bind = require('lib/bind');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'settings',
|
||||
fadeTime: 150,
|
||||
|
||||
initialize: function(options) {
|
||||
this.OptionsView = options.OptionsView || OptionsView;
|
||||
this.items = options.items;
|
||||
this.children = [];
|
||||
this.on('destroy', this.onDestroy);
|
||||
bind(this.el, 'click', this.onClick);
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
onItemClick: function(view) {
|
||||
this.showSetting(view.model);
|
||||
},
|
||||
|
||||
showSetting: function(model) {
|
||||
this.optionsView = new this.OptionsView({ model: model })
|
||||
.render()
|
||||
.appendTo(this.els.pane2)
|
||||
.on('click:option', this.firer('click:option'))
|
||||
.on('click:back', this.goBack);
|
||||
|
||||
this.showPane(2);
|
||||
},
|
||||
|
||||
onDestroy: function() {
|
||||
this.children.forEach(this.destroyChild);
|
||||
this.destroyOptionsView();
|
||||
debug('destroyed');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
this.els.items = this.find('.js-items');
|
||||
this.els.pane2 = this.find('.js-pane-2');
|
||||
this.els.close = this.find('.js-close');
|
||||
this.items.forEach(this.addItem);
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.els.close, 'click', this.firer('click:close'));
|
||||
return this;
|
||||
},
|
||||
|
||||
goBack: function() {
|
||||
this.showPane(1);
|
||||
this.destroyOptionsView();
|
||||
},
|
||||
|
||||
destroyChild: function(view) {
|
||||
view.destroy();
|
||||
debug('destroyed child');
|
||||
},
|
||||
|
||||
destroyOptionsView: function() {
|
||||
if (this.optionsView) {
|
||||
this.optionsView.destroy();
|
||||
this.optionsView = null;
|
||||
debug('options view destroyed');
|
||||
}
|
||||
},
|
||||
|
||||
addItem: function(model) {
|
||||
var setting = new SettingView({ model: model })
|
||||
.render()
|
||||
.appendTo(this.els.items)
|
||||
.on('click', this.onItemClick);
|
||||
|
||||
this.children.push(setting);
|
||||
debug('add item key: %s', model.key);
|
||||
},
|
||||
|
||||
showPane: function(name) {
|
||||
this.el.setAttribute('show-pane', 'pane-' + name);
|
||||
},
|
||||
|
||||
fadeIn: function(done) {
|
||||
setTimeout(this.show);
|
||||
if (!done) { return; }
|
||||
setTimeout(done, this.fadeTime);
|
||||
},
|
||||
|
||||
fadeOut: function(done) {
|
||||
this.hide();
|
||||
if (!done) { return; }
|
||||
setTimeout(done, this.fadeTime);
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div class="pane pane-1">' +
|
||||
'<div class="inner">' +
|
||||
'<div class="settings_inner">' +
|
||||
'<div class="settings_header">' +
|
||||
'<h2 class="settings_title" data-l10n-id="options" ' +
|
||||
'aria-level="1"></h2>' +
|
||||
'</div>' +
|
||||
'<div class="settings_items">' +
|
||||
'<ul class="inner js-items" role="listbox"></ul>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="pane pane-2">' +
|
||||
'<div class="inner js-pane-2"></div>' +
|
||||
'</div>' +
|
||||
'<div role="button" class="settings_close js-close" data-icon="menu" ' +
|
||||
'data-l10n-id="close-menu-button"></div>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
312
gaia/apps/camera/js/views/viewfinder.js
Executable file
312
gaia/apps/camera/js/views/viewfinder.js
Executable file
|
|
@ -0,0 +1,312 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:viewfinder');
|
||||
var bind = require('lib/bind');
|
||||
var CameraUtils = require('lib/camera-utils');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Locals
|
||||
*/
|
||||
|
||||
var isZoomEnabled = false;
|
||||
var scaleSizeTo = {
|
||||
fill: CameraUtils.scaleSizeToFillViewport,
|
||||
fit: CameraUtils.scaleSizeToFitViewport
|
||||
};
|
||||
|
||||
var clamp = function(value, minimum, maximum) {
|
||||
return Math.min(Math.max(value, minimum), maximum);
|
||||
};
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'viewfinder',
|
||||
className: 'js-viewfinder',
|
||||
fadeTime: 360,
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
this.getSize();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
this.els.frame = this.find('.js-frame');
|
||||
this.els.video = this.find('.js-video');
|
||||
this.els.videoContainer = this.find('.js-video-container');
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.el, 'click', this.onClick);
|
||||
bind(this.el, 'animationend', this.onShutterEnd);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the container size.
|
||||
*
|
||||
* We're using window dimensions
|
||||
* here to avoid causing costly
|
||||
* reflows.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getSize: function() {
|
||||
this.width = window.innerWidth;
|
||||
this.height = window.innerHeight;
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height
|
||||
};
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
this.emit('click', e);
|
||||
},
|
||||
|
||||
enableZoom: function(minimumZoom, maximumZoom) {
|
||||
if (minimumZoom) {
|
||||
this._minimumZoom = minimumZoom;
|
||||
}
|
||||
|
||||
if (maximumZoom) {
|
||||
this._maximumZoom = maximumZoom;
|
||||
}
|
||||
|
||||
isZoomEnabled = true;
|
||||
},
|
||||
|
||||
disableZoom: function() {
|
||||
this._minimumZoom = 1.0;
|
||||
this._maximumZoom = 1.0;
|
||||
|
||||
this.setZoom(1.0);
|
||||
|
||||
isZoomEnabled = false;
|
||||
},
|
||||
|
||||
_minimumZoom: 1.0,
|
||||
|
||||
setMinimumZoom: function(minimumZoom) {
|
||||
this._minimumZoom = minimumZoom;
|
||||
},
|
||||
|
||||
_maximumZoom: 1.0,
|
||||
|
||||
setMaximumZoom: function(maximumZoom) {
|
||||
this._maximumZoom = maximumZoom;
|
||||
},
|
||||
|
||||
_zoom: 1.0,
|
||||
|
||||
/**
|
||||
* Update the internal state of the view so that any future
|
||||
* pinch-to-zoom gestures can correctly adjust the current zoom
|
||||
* level in the event that the zoom level is changed outside of
|
||||
* the pinch-to-zoom gesture (e.g.: ZoomBar). This gets called
|
||||
* when the `Camera` emits a `zoomchanged` event.
|
||||
*/
|
||||
setZoom: function(zoom) {
|
||||
if (!isZoomEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._zoom = clamp(zoom, this._minimumZoom, this._maximumZoom);
|
||||
},
|
||||
|
||||
_useZoomPreviewAdjustment: false,
|
||||
|
||||
enableZoomPreviewAdjustment: function() {
|
||||
this._useZoomPreviewAdjustment = true;
|
||||
},
|
||||
|
||||
disableZoomPreviewAdjustment: function() {
|
||||
this._useZoomPreviewAdjustment = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adjust the scale of the <video/> tag to compensate for the inability
|
||||
* of the Camera API to zoom the preview stream beyond a certain point.
|
||||
* This gets called when the `Camera` emits a `zoomchanged` event and is
|
||||
* calculated by `Camera.prototype.getZoomPreviewAdjustment()`.
|
||||
*/
|
||||
setZoomPreviewAdjustment: function(zoomPreviewAdjustment) {
|
||||
if (this._useZoomPreviewAdjustment) {
|
||||
this.els.video.style.transform = 'scale(' + zoomPreviewAdjustment + ')';
|
||||
}
|
||||
},
|
||||
|
||||
stopStream: function() {
|
||||
this.els.video.mozSrcObject = null;
|
||||
},
|
||||
|
||||
fadeOut: function() {
|
||||
debug('fade-out');
|
||||
var self = this;
|
||||
this.hide();
|
||||
clearTimeout(this.fadeTimeout);
|
||||
this.fadeTimeout = setTimeout(function() {
|
||||
self.emit('fadedout');
|
||||
}, this.fadeTime);
|
||||
},
|
||||
|
||||
fadeIn: function(firstRun) {
|
||||
debug('fade-in');
|
||||
this.show();
|
||||
if (firstRun) {
|
||||
this.emit('fadedin');
|
||||
// Delay to prevent the duration from applying too early
|
||||
requestAnimationFrame(() => {
|
||||
this.el.style.transitionDuration = this.fadeTime + 'ms';
|
||||
});
|
||||
} else {
|
||||
this.fadeTimeout = setTimeout(this.firer('fadedin'), this.fadeTime);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers a quick shutter style animation.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
shutter: function() {
|
||||
this.el.classList.add('shutter');
|
||||
},
|
||||
|
||||
/**
|
||||
* Force a reflow before removing
|
||||
* the shutter class so that it
|
||||
* doesn't impact the animation.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
onShutterEnd: function() {
|
||||
this.reflow = this.el.offsetTop;
|
||||
this.el.classList.remove('shutter');
|
||||
},
|
||||
|
||||
/**
|
||||
* Sizes and positions the preview stream.
|
||||
*
|
||||
* @param {Object} preview
|
||||
* @param {Number} sensorAngle
|
||||
* @param {Boolean} mirrored
|
||||
*/
|
||||
updatePreview: function(preview, sensorAngle, mirrored) {
|
||||
if (!preview) { return; }
|
||||
var aspect;
|
||||
|
||||
// Invert dimensions if the camera's `sensorAngle` is
|
||||
// 0 or 180 degrees.
|
||||
if (sensorAngle % 180 === 0) {
|
||||
this.container = {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
aspect: this.width / this.height
|
||||
};
|
||||
|
||||
aspect = preview.height / preview.width;
|
||||
} else {
|
||||
this.container = {
|
||||
width: this.height,
|
||||
height: this.width,
|
||||
aspect: this.height / this.width
|
||||
};
|
||||
|
||||
aspect = preview.width / preview.height;
|
||||
}
|
||||
|
||||
var shouldFill = aspect > this.container.aspect;
|
||||
var scaleType = this.scaleType || (shouldFill ? 'fill' : 'fit');
|
||||
|
||||
this.updatePreviewMetrics(preview, sensorAngle, mirrored, scaleType);
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates the correct sizing
|
||||
* depending on the chosen 'scaleType'.
|
||||
*
|
||||
* 'scale-type' attribute set as a styling hook.
|
||||
*
|
||||
* @param {Object} preview
|
||||
* @param {Number} sensorAngle
|
||||
* @param {Boolean} mirrored
|
||||
* @param {String} scaleType 'fill'|'fit'
|
||||
*/
|
||||
updatePreviewMetrics: function(preview, sensorAngle, mirrored, scaleType) {
|
||||
debug('update preview scaleType: %s', scaleType, preview);
|
||||
|
||||
// Calculate the correct scale to apply to the
|
||||
// preview to either 'fill' or 'fit' the viewfinder
|
||||
// container (always preserving the aspect ratio).
|
||||
var landscape = scaleSizeTo[scaleType](this.container, preview);
|
||||
var portrait = { width: landscape.height, height: landscape.width };
|
||||
|
||||
// Set the size of the frame to match 'portrait' dimensions
|
||||
this.els.frame.style.width = portrait.width + 'px';
|
||||
this.els.frame.style.height = portrait.height + 'px';
|
||||
|
||||
var transform = '';
|
||||
if (mirrored) {
|
||||
transform += 'scale(-1, 1) ';
|
||||
}
|
||||
|
||||
transform += 'rotate(' + sensorAngle + 'deg)';
|
||||
|
||||
// Set the size of the video container to match the
|
||||
// 'landscape' dimensions (CSS is used to rotate
|
||||
// the 'landscape' video stream to 'portrait')
|
||||
this.els.videoContainer.style.width = landscape.width + 'px';
|
||||
this.els.videoContainer.style.height = landscape.height + 'px';
|
||||
this.els.videoContainer.style.transform = transform;
|
||||
|
||||
// CSS aligns the contents slightly
|
||||
// differently depending on the scaleType
|
||||
this.set('scaleType', scaleType);
|
||||
|
||||
debug('updated preview size/position', landscape);
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div class="viewfinder-frame js-frame">' +
|
||||
'<div class="viewfinder-video-container js-video-container" ' +
|
||||
'aria-hidden="true">' +
|
||||
'<video class="viewfinder-video js-video"></video>' +
|
||||
'</div>' +
|
||||
'<div class="viewfinder-grid">' +
|
||||
'<div class="row"></div>' +
|
||||
'<div class="row middle"></div>' +
|
||||
'<div class="row"></div>' +
|
||||
'<div class="column left">' +
|
||||
'<div class="cell top"></div>' +
|
||||
'<div class="cell middle"></div>' +
|
||||
'<div class="cell bottom"></div>' +
|
||||
'</div>' +
|
||||
'<div class="column middle">' +
|
||||
'<div class="cell top"></div>' +
|
||||
'<div class="cell middle"></div>' +
|
||||
'<div class="cell bottom"></div>' +
|
||||
'</div>' +
|
||||
'<div class="column right">' +
|
||||
'<div class="cell top"></div>' +
|
||||
'<div class="cell middle"></div>' +
|
||||
'<div class="cell bottom"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
224
gaia/apps/camera/js/views/zoom-bar.js
Executable file
224
gaia/apps/camera/js/views/zoom-bar.js
Executable file
|
|
@ -0,0 +1,224 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:zoom-bar');
|
||||
var View = require('view');
|
||||
var bind = require('lib/bind');
|
||||
var orientation = require('lib/orientation');
|
||||
|
||||
/**
|
||||
* Locals
|
||||
*/
|
||||
|
||||
var lastTouch = null;
|
||||
|
||||
var clamp = function(value, minimum, maximum) {
|
||||
return Math.min(Math.max(value, minimum), maximum);
|
||||
};
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'zoom-bar',
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
|
||||
this._orientation = orientation.get();
|
||||
|
||||
// Amount (%) to adjust the Zoom Bar by when tapping the min/max indicators
|
||||
this.zoomBarIndicatorInterval = 10;
|
||||
|
||||
// Amount of inactivity time (in milliseconds) to hide the Zoom Bar
|
||||
this.zoomBarInactivityTimeout = 3000;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
this.els.maxIndicator = this.find('.zoom-bar-max-indicator');
|
||||
this.els.minIndicator = this.find('.zoom-bar-min-indicator');
|
||||
this.els.inner = this.find('.zoom-bar-inner');
|
||||
this.els.track = this.find('.zoom-bar-track');
|
||||
this.els.scrubber = this.find('.zoom-bar-scrubber');
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.els.scrubber, 'touchstart', this.onTouchStart);
|
||||
bind(this.els.scrubber, 'touchmove', this.onTouchMove);
|
||||
bind(this.els.scrubber, 'touchend', this.onTouchEnd);
|
||||
bind(this.els.maxIndicator, 'click', this.onIncrement);
|
||||
bind(this.els.minIndicator, 'click', this.onDecrement);
|
||||
orientation.on('orientation', this.setOrientation);
|
||||
return this;
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div class="zoom-bar-max-indicator"></div>' +
|
||||
'<div class="zoom-bar-inner">' +
|
||||
'<div class="zoom-bar-track"></div>' +
|
||||
'<div class="zoom-bar-scrubber"></div>' +
|
||||
'</div>' +
|
||||
'<div class="zoom-bar-min-indicator"></div>';
|
||||
},
|
||||
|
||||
onTouchStart: function(evt) {
|
||||
lastTouch = evt.touches[0];
|
||||
this.resetInactivityTimeout();
|
||||
this.setScrubberActive(true);
|
||||
this._innerHeight = this.els.inner.offsetHeight;
|
||||
|
||||
evt.stopPropagation();
|
||||
},
|
||||
|
||||
onTouchMove: function(evt) {
|
||||
if (!lastTouch) {
|
||||
return;
|
||||
}
|
||||
|
||||
var touch = evt.touches[0];
|
||||
var deltaX = lastTouch.pageX - touch.pageX;
|
||||
var deltaY = lastTouch.pageY - touch.pageY;
|
||||
|
||||
var scale = 100 / this._innerHeight;
|
||||
|
||||
deltaX *= scale;
|
||||
deltaY *= scale;
|
||||
|
||||
switch (this._orientation) {
|
||||
case 0:
|
||||
this.setValue(this._value + deltaY, true);
|
||||
break;
|
||||
case 90:
|
||||
this.setValue(this._value + deltaX, true);
|
||||
break;
|
||||
case 180:
|
||||
this.setValue(this._value - deltaY, true);
|
||||
break;
|
||||
case 270:
|
||||
this.setValue(this._value - deltaX, true);
|
||||
break;
|
||||
}
|
||||
|
||||
lastTouch = touch;
|
||||
},
|
||||
|
||||
onTouchEnd: function(evt) {
|
||||
if (!lastTouch) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastTouch = null;
|
||||
this.resetInactivityTimeout();
|
||||
this.setScrubberActive(false);
|
||||
},
|
||||
|
||||
onIncrement: function(evt) {
|
||||
this.setValue(this._value + this.zoomBarIndicatorInterval, true);
|
||||
this.flashScrubberActive();
|
||||
evt.stopPropagation();
|
||||
},
|
||||
|
||||
onDecrement: function(evt) {
|
||||
this.setValue(this._value - this.zoomBarIndicatorInterval, true);
|
||||
this.flashScrubberActive();
|
||||
evt.stopPropagation();
|
||||
},
|
||||
|
||||
_orientation: 0,
|
||||
|
||||
setOrientation: function(orientation) {
|
||||
var el = this.el;
|
||||
el.classList.remove('zooming');
|
||||
|
||||
// Force ZoomBar to hide *immediately* on orientation change
|
||||
window.requestAnimationFrame(function() {
|
||||
el.style.transitionDuration = '0ms';
|
||||
window.requestAnimationFrame(function() {
|
||||
el.style.transitionDuration = '';
|
||||
});
|
||||
});
|
||||
|
||||
this._orientation = orientation;
|
||||
},
|
||||
|
||||
_value: 0,
|
||||
|
||||
setValue: function(value, emitChange) {
|
||||
this.resetInactivityTimeout();
|
||||
|
||||
var lastValue = this._value;
|
||||
this._value = clamp(value, 0, 100);
|
||||
if (this._value === lastValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._value === 0) {
|
||||
this.els.minIndicator.classList.add('active');
|
||||
} else {
|
||||
this.els.minIndicator.classList.remove('active');
|
||||
}
|
||||
|
||||
if (this._value === 100) {
|
||||
this.els.maxIndicator.classList.add('active');
|
||||
} else {
|
||||
this.els.maxIndicator.classList.remove('active');
|
||||
}
|
||||
|
||||
var self = this;
|
||||
window.requestAnimationFrame(function() {
|
||||
self.els.track.style.top = (100 - self._value) + '%';
|
||||
self.els.scrubber.style.bottom = self._value + '%';
|
||||
});
|
||||
|
||||
if (emitChange) {
|
||||
this.emit('change', this._value);
|
||||
}
|
||||
},
|
||||
|
||||
setScrubberActive: function(active) {
|
||||
window.clearTimeout(this._scrubberTimeout);
|
||||
|
||||
if (active) {
|
||||
this.els.scrubber.classList.add('active');
|
||||
} else {
|
||||
this.els.scrubber.classList.remove('active');
|
||||
}
|
||||
},
|
||||
|
||||
flashScrubberActive: function() {
|
||||
this.setScrubberActive(true);
|
||||
var self = this;
|
||||
this._scrubberTimeout = window.setTimeout(function() {
|
||||
self.setScrubberActive(false);
|
||||
}, 150);
|
||||
},
|
||||
|
||||
resetInactivityTimeout: function() {
|
||||
window.clearTimeout(this._inactivityTimeout);
|
||||
|
||||
this.show();
|
||||
|
||||
var self = this;
|
||||
this._inactivityTimeout = window.setTimeout(function() {
|
||||
self.hide();
|
||||
}, this.zoomBarInactivityTimeout);
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this.el.classList.add('zooming');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this.el.classList.remove('zooming');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
Loading…
Reference in a new issue