Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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