Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
15
gaia/apps/camera/js/lib/bind-all.js
Executable file
15
gaia/apps/camera/js/lib/bind-all.js
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
module.exports = function(object) {
|
||||
var key;
|
||||
var fn;
|
||||
for (key in object) {
|
||||
fn = object[key];
|
||||
if (typeof fn === 'function') {
|
||||
object[key] = fn.bind(object);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
30
gaia/apps/camera/js/lib/bind.js
Executable file
30
gaia/apps/camera/js/lib/bind.js
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
exports = module.exports = bind;
|
||||
|
||||
/**
|
||||
* addEventListener shorthand.
|
||||
* @param {Element} el
|
||||
* @param {String} name
|
||||
* @param {Function} fn
|
||||
*/
|
||||
function bind(el, name, fn, capture) {
|
||||
el.addEventListener(name, fn, capture || false);
|
||||
}
|
||||
|
||||
/**
|
||||
* removeEventListener shorthand.
|
||||
* @param {Element} el
|
||||
* @param {String} name
|
||||
* @param {Function} fn
|
||||
*/
|
||||
exports.unbind = function(el, name, fn, capture) {
|
||||
el.removeEventListener(name, fn, capture || false);
|
||||
};
|
||||
|
||||
});
|
||||
11
gaia/apps/camera/js/lib/bytes-to-pixels.js
Executable file
11
gaia/apps/camera/js/lib/bytes-to-pixels.js
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
module.exports = function(bytes) {
|
||||
var bytesPerPixel = 3;
|
||||
var avgJpegCompression = window.CONFIG_AVG_JPEG_COMPRESSION_RATIO || 8;
|
||||
var uncompressedBytes = bytes * avgJpegCompression;
|
||||
return Math.round(uncompressedBytes / bytesPerPixel);
|
||||
};
|
||||
|
||||
});
|
||||
527
gaia/apps/camera/js/lib/camera-coordinates.js
Executable file
527
gaia/apps/camera/js/lib/camera-coordinates.js
Executable file
|
|
@ -0,0 +1,527 @@
|
|||
/*
|
||||
|
||||
This module contains functions to deal with android camera coordinates.
|
||||
|
||||
Example use cases:
|
||||
|
||||
1. In the camera application we want to convert to screen coordinates
|
||||
the top left corner of the faces detected by the camera so we can position a
|
||||
DOM element to highlight the corresponding area.
|
||||
|
||||
2. We have to transform screen to android camera coordinates when
|
||||
the user touches the screen to focus on a specific area.
|
||||
|
||||
In both cases we have to take into account the possible mismatch between the
|
||||
screen pixels order and the order of the pixels sent by the driver.
|
||||
|
||||
ANDROID CAMERA COORDINATES
|
||||
|
||||
This documentation contain how android coordinates work and how the screen,
|
||||
sensor and scene coordinates relate to one another.
|
||||
|
||||
The following is *always* true: the active rectangles of
|
||||
the phone/screen and the sensor always -physically- line up.
|
||||
It has to be thus, otherwise it would be impossible to
|
||||
fill a portrait viewfinder with a portrait view, or a
|
||||
landscape viewfinder with a landscape view.
|
||||
|
||||
Visually, this looks like:
|
||||
|
||||
phone/screen
|
||||
+------+
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+------+
|
||||
|
||||
sensor
|
||||
+----+
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+----+
|
||||
|
||||
detected face reported by the camera driver
|
||||
-------------------------------------------
|
||||
|
||||
top
|
||||
+-----+
|
||||
| |
|
||||
left | | right
|
||||
| |
|
||||
+-----+
|
||||
bottom
|
||||
|
||||
The driver also reports width and height
|
||||
|
||||
If sensor orientation is 0, the sensor and the screen are scanned like this:
|
||||
|
||||
phone/screen
|
||||
+------+
|
||||
|aabbcc|
|
||||
|aabbcc|
|
||||
|ddeeff|
|
||||
|ddeeff|
|
||||
|gghhii|
|
||||
|gghhii|
|
||||
|jjkkll|
|
||||
|jjkkll|
|
||||
+------+
|
||||
|
||||
sensor
|
||||
O---+ O = Origin of sensor coordinates (-1000, -1000)
|
||||
|abc| X = (1000, 1000)
|
||||
|def| sensor-to-screen conversion => none (x = x, y = y)
|
||||
|ghi| face coordinate to convert => top, left
|
||||
|jkl|
|
||||
+---X
|
||||
|
||||
If there is an arrow pointing right:
|
||||
o-->
|
||||
...the sensor sees this arrow as such:
|
||||
|
||||
+----+
|
||||
| |
|
||||
|o-->|
|
||||
| |
|
||||
| |
|
||||
+----+
|
||||
|
||||
...and if the sensor orientation is 0, the
|
||||
arrow appears on the screen (magnified) like this:
|
||||
|
||||
+--------+
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
|()====>>|
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+--------+
|
||||
|
||||
If the sensor orientation is, e.g., 90-degrees--that is,
|
||||
the "top" of the sensor corresponds to the "right" of the
|
||||
screen--then without applying any rotation to the UI,
|
||||
the arrow appears on the screen like this:
|
||||
|
||||
+--------+
|
||||
| |
|
||||
| |
|
||||
| ^ |
|
||||
| | |
|
||||
| O |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+--------+
|
||||
|
||||
The arrow is smaller, because the frames need to be
|
||||
shrunk so that the entire height of the sensor image
|
||||
fits into the width of the screen.
|
||||
In this case, rotating the UI by 90-degrees clockwise,
|
||||
and stretching it to fill the screen, gives:
|
||||
|
||||
+--------+
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
|()====>>|
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+--------+
|
||||
|
||||
This is because, in the 90-degree sensor
|
||||
orientation case, the scanning is like this:
|
||||
|
||||
sensor
|
||||
+---O O = Origin of sensor coordinates (-1000, -1000)
|
||||
|iea| X = (1000, 1000)
|
||||
|jfb| sensor-to-screen conversion => x = -y, y = x
|
||||
|kgc| face coordinate to convert => bottom, left
|
||||
|lhd|
|
||||
X---+
|
||||
|
||||
And without compensating for this rotation,
|
||||
the image on the screen would appear like this:
|
||||
+------+
|
||||
| |
|
||||
| |
|
||||
| abcd |
|
||||
| efgh |
|
||||
| ijkl |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+------+
|
||||
|
||||
Thus sensor orientation has nothing to do with the physical
|
||||
orientation of the sensor. Rather, it has to do with which
|
||||
scan-line in the sensor is considered the "top".
|
||||
Given all this, the coordinates of the "top-left" of
|
||||
the sensor view, (-1000, -1000), are the coordinates of the
|
||||
first pixel of the first scan-line. Similarly, the "bottom-right"
|
||||
of the sensor is the last pixel of the last scan-line,
|
||||
and has coordinates (1000, 1000).
|
||||
|
||||
----- 8< -----
|
||||
As requested, the 180-degree case is:
|
||||
|
||||
phone/screen
|
||||
+------+
|
||||
|aabbcc|
|
||||
|aabbcc|
|
||||
|ddeeff|
|
||||
|ddeeff|
|
||||
|gghhii|
|
||||
|gghhii|
|
||||
|jjkkll|
|
||||
|jjkkll|
|
||||
+------+
|
||||
|
||||
sensor
|
||||
X---+ O = Origin of sensor coordinates (-1000, -1000)
|
||||
|jkl| X = (1000, 1000)
|
||||
|ihg| sensor-to-screen conversion => x = -x, y = -y
|
||||
|fed| face coordinate to convert => bottom, right
|
||||
|cba|
|
||||
+---O
|
||||
|
||||
And the 270-degree case is:
|
||||
|
||||
phone/screen
|
||||
+------+
|
||||
| |
|
||||
| |
|
||||
| abcd |
|
||||
| efgh |
|
||||
| ijkl |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
+------+
|
||||
|
||||
sensor
|
||||
+---X O = Origin of sensor coordinates (-1000, -1000)
|
||||
|dhl| X = (1000, 1000)
|
||||
|cgk| sensor-to-screen conversion => x = y, y = -x
|
||||
|bfj| face coordinate to convert => bottom, right
|
||||
|aei|
|
||||
O---+
|
||||
|
||||
*/
|
||||
define(function(require, exports, module) {
|
||||
/*jshint maxlen:false*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* It transforms camera coordinates to pixels
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function toPixels(x, y, viewportWidth, viewportHeight) {
|
||||
// In camera coordinate system, (-1000, -1000) represents the
|
||||
// top-left of the camera field of view, and (1000, 1000) represents
|
||||
// the bottom-right.
|
||||
// For convenience we assume that coordinates go from 0-2000
|
||||
var cameraCoordinatesRange = 2000;
|
||||
|
||||
// How many pixels per camera unit?
|
||||
var pixelsPerCameraUnitWidth = viewportWidth / cameraCoordinatesRange;
|
||||
var pixelsPerCameraUnitHeight = viewportHeight / cameraCoordinatesRange;
|
||||
// It makes the faces coordinates to go from 0-2000 for convenience
|
||||
var xCameraCoordinates = x + 1000;
|
||||
var yCameraCoordinates = y + 1000;
|
||||
var xPixelCoordinates = xCameraCoordinates * pixelsPerCameraUnitWidth;
|
||||
var yPixelCoordinates = yCameraCoordinates * pixelsPerCameraUnitHeight;
|
||||
return {
|
||||
x: Math.round(xPixelCoordinates),
|
||||
y: Math.round(yPixelCoordinates)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* It transforms pixels to camera coordinates
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function toCamera(x, y, viewportWidth, viewportHeight) {
|
||||
// In camera coordinate system, (-1000, -1000) represents the
|
||||
// top-left of the camera field of view, and (1000, 1000) represents
|
||||
// the bottom-right.
|
||||
var cameraCoordinatesRange = 2000;
|
||||
// How many camera units per pixel?
|
||||
var cameraUnitsPerPixelWidth = cameraCoordinatesRange / viewportWidth;
|
||||
var cameraUnitsPerPixelHeight = cameraCoordinatesRange / viewportHeight;
|
||||
return {
|
||||
x: Math.round(x * cameraUnitsPerPixelWidth) - 1000,
|
||||
y: Math.round(y * cameraUnitsPerPixelHeight) - 1000
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* It rotates camera coordinates given an angle 0, 90, 180, 270
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function rotatePoint(x, y, angle) {
|
||||
// It clamps the angle to +/- 0-270
|
||||
angle = (Math.round((angle % 360) / 90) % 4) * 90;
|
||||
switch (angle) {
|
||||
case 0:
|
||||
return {
|
||||
x: x,
|
||||
y: y
|
||||
};
|
||||
case 90:
|
||||
case -270:
|
||||
return {
|
||||
x: -y,
|
||||
y: x
|
||||
};
|
||||
case 180:
|
||||
case -180:
|
||||
return {
|
||||
x: -x,
|
||||
y: -y
|
||||
};
|
||||
case 270:
|
||||
case -90:
|
||||
return {
|
||||
x: y,
|
||||
y: -x
|
||||
};
|
||||
default:
|
||||
console.error('wrong angle value');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The sensor orientation is the orientation of the camera output
|
||||
* (the order of the pixels sent from the sensor to the preview stream) with
|
||||
* respect to the device screen in portrait orientation.
|
||||
* This function matches the orientation of the sensor and screen coordinates
|
||||
* We're interested on the top left and bottom-rignt corners of the
|
||||
* area as observed through the lenses (scene coordinates).
|
||||
* We take into account the orientation of the sensor to pick the correct
|
||||
* corner that corresponds to the top most, left most, right most and
|
||||
* bottom most coordinates in the physical/scene space as observed through the camera preview.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function rotateArea(area, sensorOrientation) {
|
||||
var topLeft = rotatePoint(area.left, area.top, sensorOrientation);
|
||||
var bottomRight = rotatePoint(area.right, area.bottom, sensorOrientation);
|
||||
// It picks the top left and bottom right corners on the scene / physical space
|
||||
return {
|
||||
top: Math.min(topLeft.y, bottomRight.y),
|
||||
left: Math.min(topLeft.x, bottomRight.x),
|
||||
bottom: Math.max(topLeft.y, bottomRight.y),
|
||||
right: Math.max(topLeft.x, bottomRight.x),
|
||||
width: Math.abs(topLeft.x - bottomRight.x),
|
||||
height: Math.abs(topLeft.y - bottomRight.y)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* It mirrors a given area in camera coordinates
|
||||
* Use case: Display detected faces by the front camera.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function mirrorAreaCamera(area) {
|
||||
return {
|
||||
top: area.top,
|
||||
left: -area.right,
|
||||
bottom: area.bottom,
|
||||
right: -area.left,
|
||||
width: area.width,
|
||||
height: area.height
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* It mirrors a given area in camera coordinates
|
||||
* Use case: Display detected faces by the front camera.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function mirrorAreaPixels(area, viewportWidth) {
|
||||
return {
|
||||
top: area.top,
|
||||
left: viewportWidth - area.right,
|
||||
bottom: area.bottom,
|
||||
right: viewportWidth - area.left,
|
||||
width: area.width,
|
||||
height: area.height
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a distance in pixels and a screen size it returns
|
||||
* the distance in camera units.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function sizeToCamera(width, height, viewportWidth, viewportHeight) {
|
||||
// In camera coordinate system, (-1000, -1000) represents the
|
||||
// top-left of the camera field of view, and (1000, 1000) represents
|
||||
// the bottom-right of the field of view.
|
||||
// There are 2000 units in each axis of the camera coordinates
|
||||
var cameraCoordinatesRange = 2000;
|
||||
// How many camera units per pixel?
|
||||
var cameraUnitsPerPixelWidth = cameraCoordinatesRange/ viewportWidth;
|
||||
var cameraUnitsPerPixelHeight = cameraCoordinatesRange / viewportHeight;
|
||||
return {
|
||||
width: Math.round(width * cameraUnitsPerPixelWidth),
|
||||
height: Math.round(height * cameraUnitsPerPixelHeight)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a distance in camera units and a screen size it returns
|
||||
* the distance in pixels.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function sizeToPixels(width, height, viewportWidth, viewportHeight) {
|
||||
// In camera coordinate system, (-1000, -1000) represents the
|
||||
// top-left of the camera field of view, and (1000, 1000) represents
|
||||
// the bottom-right of the field of view.
|
||||
// There are 2000 units in each axis of the camera coordinates
|
||||
var cameraCoordinatesRange = 2000;
|
||||
// How many pixels per camera coordinates unit?
|
||||
var pixelsPerCameraUnitWidth = viewportWidth / cameraCoordinatesRange;
|
||||
var pixelsPerCameraUnitHeight = viewportHeight / cameraCoordinatesRange;
|
||||
return {
|
||||
width: Math.round(width * pixelsPerCameraUnitWidth),
|
||||
height: Math.round(height * pixelsPerCameraUnitHeight)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an area in screen camera units (-1000, 1000)
|
||||
* and viewport dimensions it returns the same area
|
||||
* in pixels
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function areaToPixels(area, viewportWidth, viewportHeight) {
|
||||
// It converts the face from Android camera coordinates to pixels
|
||||
var areaPixels = toPixels(
|
||||
area.left, area.top, viewportWidth, viewportHeight);
|
||||
// It converts the face size from Android camera units to pixels
|
||||
var areaPixelSize = sizeToPixels(
|
||||
area.width, area.height,
|
||||
viewportWidth, viewportHeight);
|
||||
var width = areaPixelSize.width;
|
||||
var height = areaPixelSize.height;
|
||||
return {
|
||||
top: areaPixels.y,
|
||||
left: areaPixels.x,
|
||||
bottom: areaPixels.y + height,
|
||||
right: areaPixels.x + width,
|
||||
width: width,
|
||||
height: height
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an area in screen pixel coordinates
|
||||
* and viewport dimensions it returns the same area
|
||||
* in android camera coordinates (-1000, 1000)
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function areaToCamera(area, viewportWidth, viewportHeight) {
|
||||
var topLeft = toCamera(
|
||||
area.left, area.top, viewportWidth, viewportHeight);
|
||||
// It converts the size from pixels to camera units
|
||||
var areaCameraUnits = sizeToCamera(
|
||||
area.width, area.height,
|
||||
viewportWidth, viewportHeight);
|
||||
var width = areaCameraUnits.width;
|
||||
var height = areaCameraUnits.height;
|
||||
var areaCamera = {
|
||||
top: topLeft.y,
|
||||
left: topLeft.x,
|
||||
bottom: topLeft.y + height,
|
||||
right: topLeft.x + width,
|
||||
height: height,
|
||||
width: width
|
||||
};
|
||||
return areaCamera;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given face coordinates in pixels
|
||||
* a viewport width and height it returns the face coordinates
|
||||
* and area in camera units
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function faceToCamera(face, viewportWidth, viewportHeight, sensorOrientation, mirrored) {
|
||||
if (mirrored) {
|
||||
face = mirrorAreaPixels(face, viewportWidth);
|
||||
}
|
||||
face = areaToCamera(face, viewportWidth, viewportHeight);
|
||||
// The orientation of the screen and the sensor might not match.
|
||||
// We rotate the face so it matches screen coordinates orientation
|
||||
if (sensorOrientation) {
|
||||
face = rotateArea(face, -sensorOrientation);
|
||||
}
|
||||
return face;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given face coordinates as provided by camera controller and
|
||||
* a viewport width and height it returns the face coordinates, diameter
|
||||
* and area in pixel units
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function faceToPixels(face, viewportWidth, viewportHeight, sensorOrientation, mirrored) {
|
||||
// The orientation of the screen and the sensor might not match.
|
||||
// We rotate the face so it matches screen coordinates orientation
|
||||
if (sensorOrientation) {
|
||||
face = rotateArea(face, sensorOrientation);
|
||||
}
|
||||
if (mirrored) {
|
||||
face = mirrorAreaCamera(face);
|
||||
}
|
||||
return areaToPixels(face, viewportWidth, viewportHeight);
|
||||
}
|
||||
|
||||
return {
|
||||
// Face transformations
|
||||
faceToCamera: faceToCamera,
|
||||
faceToPixels: faceToPixels,
|
||||
// Area transformations
|
||||
private: {
|
||||
// Area transformations
|
||||
areaToCamera: areaToCamera,
|
||||
areaToPixels: areaToPixels,
|
||||
mirrorAreaCamera: mirrorAreaCamera,
|
||||
mirrorAreaPixels: mirrorAreaPixels,
|
||||
rotateArea: rotateArea,
|
||||
// Size transformations
|
||||
sizeToCamera: sizeToCamera,
|
||||
sizeToPixels: sizeToPixels,
|
||||
// Point transformations
|
||||
toCamera: toCamera,
|
||||
toPixels: toPixels,
|
||||
rotatePoint: rotatePoint
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
75
gaia/apps/camera/js/lib/camera-utils.js
Executable file
75
gaia/apps/camera/js/lib/camera-utils.js
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
define(function(require) {
|
||||
/*jshint maxlen:false*/
|
||||
'use strict';
|
||||
|
||||
var CameraUtils = {};
|
||||
|
||||
CameraUtils.scaleSizeToFitViewport = function(viewportSize, imageSize) {
|
||||
var sw = viewportSize.width / imageSize.width,
|
||||
sh = viewportSize.height / imageSize.height,
|
||||
scale;
|
||||
|
||||
// Select the smaller scale to fit image completely within the viewport
|
||||
scale = Math.min(sw, sh);
|
||||
|
||||
return {
|
||||
width: imageSize.width * scale,
|
||||
height: imageSize.height * scale
|
||||
};
|
||||
};
|
||||
|
||||
CameraUtils.scaleSizeToFillViewport = function(viewportSize, imageSize) {
|
||||
var sw = viewportSize.width / imageSize.width,
|
||||
sh = viewportSize.height / imageSize.height,
|
||||
scale;
|
||||
|
||||
// Select the larger scale to fill and overflow viewport with image
|
||||
scale = Math.max(sw, sh);
|
||||
|
||||
return {
|
||||
width: imageSize.width * scale,
|
||||
height: imageSize.height * scale
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the maximum preview size (in terms of area) from a list of
|
||||
* possible preview sizes.
|
||||
*
|
||||
* NOTE: If an `aspectRatio` value is provided, the search will be
|
||||
* constrained to only accept preview sizes matching that aspect
|
||||
* ratio.
|
||||
*
|
||||
* @param {Array} previewSizes
|
||||
* @param {Number} aspectRatio
|
||||
* @return {Object}
|
||||
*/
|
||||
CameraUtils.getMaximumPreviewSize = function(previewSizes, aspectRatio) {
|
||||
|
||||
// Use a very small tolerance because we want an exact match if we are
|
||||
// constraining to only include specific aspect ratios.
|
||||
const ASPECT_TOLERANCE = 0.001;
|
||||
|
||||
var maximumArea = 0;
|
||||
var maximumPreviewSize = null;
|
||||
previewSizes.forEach(function(previewSize) {
|
||||
var area = previewSize.width * previewSize.height;
|
||||
|
||||
if (aspectRatio) {
|
||||
var ratio = previewSize.width / previewSize.height;
|
||||
if (Math.abs(ratio - aspectRatio) > ASPECT_TOLERANCE) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (area > maximumArea) {
|
||||
maximumArea = area;
|
||||
maximumPreviewSize = previewSize;
|
||||
}
|
||||
});
|
||||
|
||||
return maximumPreviewSize;
|
||||
};
|
||||
|
||||
return CameraUtils;
|
||||
});
|
||||
1491
gaia/apps/camera/js/lib/camera/camera.js
Executable file
1491
gaia/apps/camera/js/lib/camera/camera.js
Executable file
File diff suppressed because it is too large
Load diff
325
gaia/apps/camera/js/lib/camera/focus.js
Executable file
325
gaia/apps/camera/js/lib/camera/focus.js
Executable file
|
|
@ -0,0 +1,325 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Module Dependencies
|
||||
*/
|
||||
|
||||
var bindAll = require('lib/bind-all');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = Focus;
|
||||
|
||||
/**
|
||||
* Options:
|
||||
*
|
||||
* - {Boolean} `continousAutoFocus`
|
||||
* - {Boolean} `touchFocus`
|
||||
* - {Boolean} `faceDetection`
|
||||
*/
|
||||
function Focus(options) {
|
||||
bindAll(this);
|
||||
this.userPreferences = options || {};
|
||||
this.detectedFaces = [];
|
||||
}
|
||||
|
||||
Focus.prototype.configure = function(mozCamera, mode) {
|
||||
var focusModes = mozCamera.capabilities.focusModes;
|
||||
var focusMode;
|
||||
this.mozCamera = mozCamera;
|
||||
this.configureFocusModes(mode);
|
||||
// Determines focus mode based on camera mode
|
||||
// In case of C-AF enabled
|
||||
if (this.continuousAutoFocus) {
|
||||
if (mode === 'picture') {
|
||||
focusMode = 'continuous-picture';
|
||||
} else if (mode === 'video'){
|
||||
focusMode = 'continuous-video';
|
||||
}
|
||||
}
|
||||
|
||||
// auto is the default focus mode
|
||||
focusMode = focusMode || 'auto';
|
||||
|
||||
// If auto or passed mode is not supported we pick the
|
||||
// first available in the hardware list
|
||||
if (focusModes.indexOf(focusMode) === -1) {
|
||||
focusMode = focusModes[0];
|
||||
}
|
||||
|
||||
// If touch-to-focus is in progress we need to ensure
|
||||
// the correct mode is restored when it is complete
|
||||
this.suspendedMode = focusMode;
|
||||
mozCamera.focusMode = focusMode;
|
||||
this.reboot();
|
||||
};
|
||||
|
||||
Focus.prototype.getMode = function() {
|
||||
var mozCamera = this.mozCamera;
|
||||
return this.suspendedMode || mozCamera.focusMode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Configures focus modes based on user preferences
|
||||
* and hardware availability
|
||||
*/
|
||||
Focus.prototype.configureFocusModes = function(mode) {
|
||||
var userPreferences = this.userPreferences;
|
||||
var continuousAutoFocusUserEnabled =
|
||||
userPreferences.continuousAutoFocus !== false;
|
||||
var touchFocusUserEnabled = userPreferences.touchFocus;
|
||||
var touchFocusSupported = this.isTouchFocusSupported();
|
||||
var faceDetectionUserEnabled = userPreferences.faceDetection;
|
||||
var faceDetectionSupported = this.isFaceDetectionSupported();
|
||||
this.continuousAutoFocus = continuousAutoFocusUserEnabled;
|
||||
// User preferences override defaults
|
||||
this.touchFocus = touchFocusUserEnabled && touchFocusSupported;
|
||||
// Face detection only enabled on picture mode (disabled on video)
|
||||
this.faceDetection =
|
||||
faceDetectionUserEnabled && faceDetectionSupported && mode === 'picture';
|
||||
this.mozCamera.addEventListener('focus', this.onAutoFocusStateChange);
|
||||
};
|
||||
|
||||
Focus.prototype.startFaceDetection = function() {
|
||||
if (!this.faceDetection) { return; }
|
||||
this.mozCamera.addEventListener('facesdetected',
|
||||
this.handleFaceDetectionEvent);
|
||||
this.mozCamera.startFaceDetection();
|
||||
};
|
||||
|
||||
Focus.prototype.stopFaceDetection = function() {
|
||||
// Clear suspenstion timers
|
||||
clearTimeout(this.faceDetectionSuspended);
|
||||
clearTimeout(this.faceDetectionSuspensionTimer);
|
||||
if (this.mozCamera.stopFaceDetection) {
|
||||
this.mozCamera.removeEventListener('facesdetected',
|
||||
this.handleFaceDetectionEvent);
|
||||
this.mozCamera.stopFaceDetection();
|
||||
}
|
||||
this.clearFaceDetection();
|
||||
};
|
||||
|
||||
Focus.prototype.handleFaceDetectionEvent = function(e) {
|
||||
this.focusOnLargestFace(e.faces);
|
||||
};
|
||||
|
||||
Focus.prototype.clearFaceDetection = function() {
|
||||
this.focusOnLargestFace([]);
|
||||
};
|
||||
|
||||
Focus.prototype.suspendFaceDetection = function(ms, delay) {
|
||||
if (!this.faceDetection) {
|
||||
return;
|
||||
}
|
||||
var self = this;
|
||||
delay = delay || 0;
|
||||
clearTimeout(this.faceDetectionSuspended);
|
||||
clearTimeout(this.faceDetectionSuspensionTimer);
|
||||
this.faceDetectionSuspensionTimer = setTimeout(suspendFaceDetection, delay);
|
||||
function suspendFaceDetection() {
|
||||
self.faceDetectionSuspended = setTimeout(clearTimer, ms);
|
||||
}
|
||||
function clearTimer() {
|
||||
self.faceFocused = false;
|
||||
self.faceDetectionSuspended = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
Focus.prototype.stopContinuousFocus = function() {
|
||||
var focusMode = this.mozCamera.focusMode;
|
||||
// Clear suspension timers
|
||||
clearTimeout(this.continuousModeTimer);
|
||||
if (focusMode === 'continuous-picture' || focusMode === 'continuous-video') {
|
||||
this.suspendedMode = this.mozCamera.focusMode;
|
||||
this.mozCamera.focusMode = 'auto';
|
||||
}
|
||||
};
|
||||
|
||||
Focus.prototype.resumeContinuousFocus = function() {
|
||||
this.mozCamera.focusMode = this.suspendedMode;
|
||||
this.suspendedMode = null;
|
||||
this.resetFocusAreas();
|
||||
this.mozCamera.resumeContinuousFocus();
|
||||
};
|
||||
|
||||
Focus.prototype.suspendContinuousFocus = function(ms) {
|
||||
clearTimeout(this.continuousModeTimer);
|
||||
this.stopContinuousFocus();
|
||||
this.continuousModeTimer = setTimeout(this.resumeContinuousFocus, ms);
|
||||
};
|
||||
|
||||
Focus.prototype.updateFocusState = function(state) {
|
||||
// Only update if the state has changed, and only transition to focused
|
||||
// or unfocused if we were previously focusing; this eliminates unfocused
|
||||
// rings just before a focusing state change.
|
||||
if (this.focusState !== state &&
|
||||
(this.focusState === 'focusing' || state === 'focusing')) {
|
||||
this.focusState = state;
|
||||
this.onAutoFocusChanged(state);
|
||||
}
|
||||
};
|
||||
|
||||
Focus.prototype.onAutoFocusStateChange = function(e) {
|
||||
var state = e.newState;
|
||||
if (state === 'unfocused') {
|
||||
state = 'fail';
|
||||
}
|
||||
this.updateFocusState(state);
|
||||
};
|
||||
|
||||
Focus.prototype.onAutoFocusChanged = function(state) {
|
||||
// NO OP by default
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback invoked when faces are detected
|
||||
* It's no op by default and it's meant to be overriden by
|
||||
* the user of the module in order to be informed about detected faces
|
||||
*/
|
||||
Focus.prototype.onFacesDetected = function(faces) {
|
||||
// NO OP by default
|
||||
};
|
||||
|
||||
Focus.prototype.focusOnLargestFace = function(faces) {
|
||||
if (this.faceDetectionSuspended) {
|
||||
this.onFacesDetected([]);
|
||||
return;
|
||||
}
|
||||
|
||||
this.detectedFaces = faces;
|
||||
this.onFacesDetected(this.detectedFaces);
|
||||
};
|
||||
|
||||
/**
|
||||
* Focus the camera, invoke the callback asynchronously when done.
|
||||
*
|
||||
* If we only have fixed focus, then we call the callback right away
|
||||
* (but still asynchronously). Otherwise, we call autoFocus to focus
|
||||
* the camera and call the callback when focus is complete. In C-AF mode
|
||||
* this process should be fast. In manual AF mode, focusing takes about
|
||||
* a second and causes a noticeable delay before the picture is taken.
|
||||
*
|
||||
* @param {Function} done
|
||||
* @private
|
||||
*/
|
||||
Focus.prototype.focus = function(done) {
|
||||
if (!this.mozCamera) { return; }
|
||||
done = done || function() {};
|
||||
var self = this;
|
||||
|
||||
this.suspendContinuousFocus(10000);
|
||||
if (this.mozCamera.focusMode !== 'auto') {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// In either focus mode, we call autoFocus() to ensure that the user gets
|
||||
// a sharp picture. The difference between the two modes is that if
|
||||
// C-AF is on, it is likely that the camera is already focused, so the
|
||||
// call to autoFocus() invokes its callback very quickly and we get much
|
||||
// better response time.
|
||||
//
|
||||
// In either case, the callback is passed a boolean specifying whether
|
||||
// focus was successful or not, and we display a green or red focus ring
|
||||
// then call the done callback, which takes the picture and clears
|
||||
// the focus ring.
|
||||
//
|
||||
this.updateFocusState('focusing');
|
||||
this.mozCamera.autoFocus().then(onSuccess, onError);
|
||||
|
||||
// If focus fails with an error, we still need to signal the
|
||||
// caller. Interruptions are a special case, but other errors
|
||||
// can be treated the same as completing the operation but
|
||||
// remaining unfocused.
|
||||
function onError(err) {
|
||||
self.focused = false;
|
||||
if (err.name === 'NS_ERROR_IN_PROGRESS') {
|
||||
done('interrupted');
|
||||
} else {
|
||||
done('error');
|
||||
}
|
||||
}
|
||||
|
||||
// This is fixed focus: there is nothing we can do here so we
|
||||
// should just call the callback and take the photo. No focus
|
||||
// happens so we don't display a focus ring.
|
||||
function onSuccess(success) {
|
||||
if (success) {
|
||||
self.focused = true;
|
||||
done('focused');
|
||||
} else {
|
||||
self.focused = false;
|
||||
done('failed');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets focus regions
|
||||
*/
|
||||
Focus.prototype.resetFocusAreas = function() {
|
||||
if (!this.touchFocus) {
|
||||
return;
|
||||
}
|
||||
this.mozCamera.setFocusAreas([]);
|
||||
this.mozCamera.setMeteringAreas([]);
|
||||
};
|
||||
|
||||
Focus.prototype.pause = function() {
|
||||
if (this.paused) { return; }
|
||||
this.stopContinuousFocus();
|
||||
this.stopFaceDetection();
|
||||
this.paused = true;
|
||||
delete this.focusState;
|
||||
};
|
||||
|
||||
Focus.prototype.resume = function() {
|
||||
if (!this.paused) { return; }
|
||||
this.resumeContinuousFocus();
|
||||
this.startFaceDetection();
|
||||
this.paused = false;
|
||||
};
|
||||
|
||||
Focus.prototype.reboot = function() {
|
||||
this.pause();
|
||||
this.resume();
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the hardware supports touch to focus
|
||||
*/
|
||||
Focus.prototype.isTouchFocusSupported = function() {
|
||||
var maxFocusAreas = this.mozCamera.capabilities.maxFocusAreas;
|
||||
return maxFocusAreas > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if hardware supports face detection
|
||||
*/
|
||||
Focus.prototype.isFaceDetectionSupported = function() {
|
||||
var cameraDetectsFaces = this.mozCamera.capabilities.maxDetectedFaces > 0;
|
||||
var apiAvailable = !!this.mozCamera.startFaceDetection;
|
||||
this.maxDetectedFaces = this.mozCamera.capabilities.maxDetectedFaces;
|
||||
return cameraDetectsFaces && apiAvailable;
|
||||
};
|
||||
|
||||
Focus.prototype.updateFocusArea = function(rect, done) {
|
||||
done = done || function() {};
|
||||
if (!this.touchFocus) {
|
||||
done('touchToFocusNotAvailable');
|
||||
return;
|
||||
}
|
||||
this.updateFocusState('focusing');
|
||||
this.stopContinuousFocus();
|
||||
this.suspendFaceDetection(10000);
|
||||
this.mozCamera.setFocusAreas([rect]);
|
||||
this.mozCamera.setMeteringAreas([rect]);
|
||||
// Call auto focus to focus on focus area.
|
||||
this.focus(done);
|
||||
};
|
||||
|
||||
});
|
||||
35
gaia/apps/camera/js/lib/create-thumbnail-image.js
Executable file
35
gaia/apps/camera/js/lib/create-thumbnail-image.js
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var cropResizeRotate = require('cropResizeRotate');
|
||||
|
||||
/**
|
||||
* Create a thumbnail size canvas,
|
||||
* copy the <img> or <video> into it
|
||||
* cropping the edges as needed to
|
||||
* make it fit, and then extract the
|
||||
* thumbnail image as a blob and pass
|
||||
* it to the callback.
|
||||
*
|
||||
* @param {Blob} imageBlob [description]
|
||||
* @param {Object} metadata [description]
|
||||
* @param {Object} thumbnailSize [description]
|
||||
* @param {Function} done [description]
|
||||
*/
|
||||
module.exports = function(imageBlob, metadata, thumbnailSize, done) {
|
||||
cropResizeRotate(imageBlob, null, thumbnailSize, null, metadata,
|
||||
function(error, resizedBlob) {
|
||||
|
||||
// If we couldn't resize or rotate it, use the original
|
||||
if (error) {
|
||||
console.error('Error while resizing image: ' + error);
|
||||
done(imageBlob);
|
||||
return;
|
||||
}
|
||||
|
||||
done(resizedBlob);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
});
|
||||
101
gaia/apps/camera/js/lib/dcf.js
Executable file
101
gaia/apps/camera/js/lib/dcf.js
Executable file
|
|
@ -0,0 +1,101 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
// This handles the logic pertaining to the naming of files according
|
||||
// to the Design rule for Camera File System
|
||||
// * http://en.wikipedia.org/wiki/Design_rule_for_Camera_File_system
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var asyncStorage = require('asyncStorage');
|
||||
var debug = require('debug')('dcf');
|
||||
var format = require('format');
|
||||
|
||||
/**
|
||||
* Locals
|
||||
*/
|
||||
|
||||
var dcfConfigLoaded = false;
|
||||
var deferredArgs = null;
|
||||
var defaultSeq = {file: 1, dir: 100};
|
||||
var dcfConfig = {
|
||||
key: 'dcf_key',
|
||||
seq: null,
|
||||
postFix: 'MZLLA',
|
||||
prefix: {video: 'VID_', image: 'IMG_'},
|
||||
ext: {video: '3gp', image: 'jpg'}
|
||||
};
|
||||
|
||||
exports.init = function() {
|
||||
debug('initializing');
|
||||
exports.checkFileCounter(function() {
|
||||
// We have a previous call to createDCFFilename that is waiting for
|
||||
// a response, fire it again
|
||||
if (deferredArgs) {
|
||||
var args = deferredArgs;
|
||||
exports.createDCFFilename(args.storage, args.type, args.callback);
|
||||
deferredArgs = null;
|
||||
}
|
||||
|
||||
debug('initialized');
|
||||
});
|
||||
};
|
||||
|
||||
exports.checkFileCounter = function(callback) {
|
||||
asyncStorage.getItem(dcfConfig.key, function(value) {
|
||||
dcfConfigLoaded = true;
|
||||
dcfConfig.seq = value ? value : defaultSeq;
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
exports.createDCFFilename = function(storage, type, callback) {
|
||||
|
||||
// We havent loaded the current counters from indexedDB yet, defer
|
||||
// the call
|
||||
if (!dcfConfigLoaded) {
|
||||
deferredArgs = {storage: storage, type: type, callback: callback};
|
||||
return;
|
||||
}
|
||||
|
||||
var dir = 'DCIM/' + dcfConfig.seq.dir + dcfConfig.postFix + '/';
|
||||
var filename = dcfConfig.prefix[type] +
|
||||
format.padLeft(dcfConfig.seq.file, 4, '0') + '.' +
|
||||
dcfConfig.ext[type];
|
||||
var filepath = dir + filename;
|
||||
|
||||
// A file with this name may have been written by the user or
|
||||
// our indexeddb sequence tracker was cleared, check we wont overwrite
|
||||
// anything
|
||||
var req = storage.get(filepath);
|
||||
|
||||
// A file existed, we bump the directory then try to generate a
|
||||
// new filename
|
||||
req.onsuccess = function() {
|
||||
dcfConfig.seq.file = 1;
|
||||
dcfConfig.seq.dir += 1;
|
||||
asyncStorage.setItem(dcfConfig.key, dcfConfig.seq, function() {
|
||||
exports.createDCFFilename(storage, type, callback);
|
||||
});
|
||||
};
|
||||
|
||||
// No file existed, we are good to go
|
||||
req.onerror = function() {
|
||||
if (dcfConfig.seq.file < 9999) {
|
||||
dcfConfig.seq.file += 1;
|
||||
} else {
|
||||
dcfConfig.seq.file = 1;
|
||||
dcfConfig.seq.dir += 1;
|
||||
}
|
||||
asyncStorage.setItem(dcfConfig.key, dcfConfig.seq, function() {
|
||||
callback(filepath, filename, dir);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
19
gaia/apps/camera/js/lib/debounce.js
Executable file
19
gaia/apps/camera/js/lib/debounce.js
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
module.exports = function(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) { func.apply(context, args); }
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) { func.apply(context, args); }
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
81
gaia/apps/camera/js/lib/format-picture-sizes.js
Executable file
81
gaia/apps/camera/js/lib/format-picture-sizes.js
Executable file
|
|
@ -0,0 +1,81 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var getAspect = require('./get-aspect');
|
||||
|
||||
/**
|
||||
* Returns a formatted list of picture
|
||||
* sizes ready to be set as setting options.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - `maxPixelSize {Number}`
|
||||
* - `exclude {Array}`
|
||||
*
|
||||
* @param {Array} sizes
|
||||
* @param {Object} options
|
||||
* @return {Array}
|
||||
*/
|
||||
module.exports = function(sizes, options) {
|
||||
var maxPixelSize = options && options.maxPixelSize;
|
||||
var exclude = options && options.exclude || {};
|
||||
var include = options && options.include;
|
||||
var formatted = [];
|
||||
var hash = {};
|
||||
|
||||
exclude.aspects = exclude.aspects || [];
|
||||
exclude.keys = exclude.keys || [];
|
||||
|
||||
sizes.forEach(function(size) {
|
||||
var w = size.width;
|
||||
var h = size.height;
|
||||
var key = w + 'x' + h;
|
||||
var pixelSize = w * h;
|
||||
|
||||
if (hash[key]) { return; }
|
||||
|
||||
size.aspect = getAspect(w, h);
|
||||
|
||||
// Don't include pictureSizes above the maxPixelSize limit
|
||||
if (maxPixelSize && pixelSize > maxPixelSize) { return; }
|
||||
|
||||
if (include) {
|
||||
if (include.keys && !~include.keys.indexOf(key)) { return; }
|
||||
if (include.aspects && !~include.aspects.indexOf(size.aspect)) { return; }
|
||||
}
|
||||
|
||||
|
||||
if (exclude.keys.indexOf(key) > -1) { return; }
|
||||
if (exclude.aspects.indexOf(size.aspect) > -1) { return; }
|
||||
|
||||
size.mp = getMP(w, h);
|
||||
hash[key] = true;
|
||||
|
||||
formatted.push({
|
||||
key: key,
|
||||
pixelSize: pixelSize,
|
||||
data: size
|
||||
});
|
||||
});
|
||||
|
||||
// Sort by pixel size
|
||||
formatted.sort(function(a, b) { return b.pixelSize - a.pixelSize; });
|
||||
return formatted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns rounded mega-pixel value.
|
||||
*
|
||||
* @param {Number} w
|
||||
* @param {Number} h
|
||||
* @return {Number}
|
||||
*/
|
||||
function getMP(w, h) {
|
||||
return Math.round((w * h) / 1000000);
|
||||
}
|
||||
|
||||
});
|
||||
59
gaia/apps/camera/js/lib/format-recorder-profiles.js
Executable file
59
gaia/apps/camera/js/lib/format-recorder-profiles.js
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var getAspect = require('./get-aspect');
|
||||
|
||||
/**
|
||||
* Returns a formatted list of recorder
|
||||
* profiles ready to be set as setting options.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - `exclude {Array}`
|
||||
*
|
||||
* @param {Object} profiles
|
||||
* @param {Object} options
|
||||
* @return {Array}
|
||||
*/
|
||||
module.exports = function(profiles, options) {
|
||||
var exclude = options && options.exclude || [];
|
||||
var items = [];
|
||||
var hash = {};
|
||||
|
||||
for (var key in profiles) {
|
||||
if (!profiles.hasOwnProperty(key)) { continue; } // Bug 1091820
|
||||
|
||||
var profile = profiles[key];
|
||||
var video = profile.video;
|
||||
var sizeKey = video.width + 'x' + video.height;
|
||||
|
||||
// guard against duplicate profiles
|
||||
if (hash[sizeKey]) { continue; }
|
||||
|
||||
// Don't include profile if marked as excluded
|
||||
if (exclude.indexOf(key) > -1) { continue; }
|
||||
|
||||
var pixelSize = video.width * video.height;
|
||||
var aspect = getAspect(video.width, video.height);
|
||||
|
||||
hash[sizeKey] = key;
|
||||
|
||||
items.push({
|
||||
key: key,
|
||||
title: key + ' ' + sizeKey + ' ' + aspect,
|
||||
pixelSize: pixelSize,
|
||||
raw: profile
|
||||
});
|
||||
}
|
||||
|
||||
// Sort from largest to small but put the default/preferred profile first
|
||||
items.sort(function(a, b) { return b.pixelSize - a.pixelSize; });
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
});
|
||||
35
gaia/apps/camera/js/lib/format-timer.js
Executable file
35
gaia/apps/camera/js/lib/format-timer.js
Executable file
|
|
@ -0,0 +1,35 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var format = require('format');
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
function digits(value) {
|
||||
return format.padLeft(value, 2, '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(ms) {
|
||||
var totalSeconds = ms / 1000;
|
||||
var seconds = Math.round(totalSeconds % 60);
|
||||
var minutes = Math.floor(totalSeconds / 60);
|
||||
var hours;
|
||||
|
||||
if (minutes < 60) {
|
||||
return digits(minutes) + ':' + digits(seconds);
|
||||
} else {
|
||||
hours = Math.floor(minutes / 60);
|
||||
minutes = Math.round(minutes % 60);
|
||||
return hours + ':' + digits(minutes) + ':' + digits(seconds);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
});
|
||||
68
gaia/apps/camera/js/lib/geo-location.js
Executable file
68
gaia/apps/camera/js/lib/geo-location.js
Executable file
|
|
@ -0,0 +1,68 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('geolocation');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = GeoLocation;
|
||||
|
||||
/**
|
||||
* Interface to the
|
||||
* geolocation API.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function GeoLocation() {
|
||||
this.watcher = null;
|
||||
this.position = null;
|
||||
this.setPosition = this.setPosition.bind(this);
|
||||
this.watch = this.watch.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches device location.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
GeoLocation.prototype.watch = function() {
|
||||
if (!this.watcher) {
|
||||
this.watcher = navigator.geolocation.watchPosition(this.setPosition);
|
||||
debug('started watching');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops watching
|
||||
* device location.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
GeoLocation.prototype.stopWatching = function() {
|
||||
navigator.geolocation.clearWatch(this.watcher);
|
||||
this.watcher = null;
|
||||
debug('stopped watching');
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the stored
|
||||
* position object.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
GeoLocation.prototype.setPosition = function(position) {
|
||||
this.position = {
|
||||
timestamp: position.timestamp,
|
||||
altitude: position.coords.altitude,
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
20
gaia/apps/camera/js/lib/get-aspect.js
Executable file
20
gaia/apps/camera/js/lib/get-aspect.js
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Returns aspect ratio string.
|
||||
*
|
||||
* Makes use of Euclid's GCD algorithm,
|
||||
* http://en.wikipedia.org/wiki/Euclidean_algorithm
|
||||
*
|
||||
* @param {Number} w
|
||||
* @param {Number} h
|
||||
* @return {String}
|
||||
*/
|
||||
module.exports = function(w, h) {
|
||||
var gcd = function(a, b) { return (b === 0) ? a : gcd(b, a % b); };
|
||||
var divisor = gcd(w, h);
|
||||
return (w / divisor) + ':' + (h / divisor);
|
||||
};
|
||||
|
||||
});
|
||||
8
gaia/apps/camera/js/lib/mixin.js
Executable file
8
gaia/apps/camera/js/lib/mixin.js
Executable file
|
|
@ -0,0 +1,8 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
module.exports = function(a, b) {
|
||||
for (var key in b) { a[key] = b[key]; }
|
||||
return a;
|
||||
};
|
||||
});
|
||||
81
gaia/apps/camera/js/lib/orientation.js
Executable file
81
gaia/apps/camera/js/lib/orientation.js
Executable file
|
|
@ -0,0 +1,81 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var listener = require('device-orientation');
|
||||
var classes = document.body.classList;
|
||||
var current = 0;
|
||||
|
||||
listener.on('orientation', onOrientationChange);
|
||||
listener.start();
|
||||
|
||||
function onOrientationChange(degrees) {
|
||||
classes.remove('deg' + current);
|
||||
classes.add('deg' + degrees);
|
||||
current = degrees;
|
||||
}
|
||||
|
||||
// Camera normally has its orientation locked to portrait mode.
|
||||
// But we unlock orientation when displaying image and video previews.
|
||||
// When orientation is unlocked, we call listener.stop().
|
||||
// We calls call stop() when recording a video, and then restart
|
||||
// when recording is done. If our app ever changes so that we can call
|
||||
// unlock while the orientation listener is in the stopped state, then
|
||||
// we would need to modify the lock() function so that it did not
|
||||
// restart the listener. That is not needed now, however and is omitted.
|
||||
|
||||
function unlock() {
|
||||
screen.mozUnlockOrientation();
|
||||
listener.stop();
|
||||
}
|
||||
|
||||
function lock() {
|
||||
screen.mozLockOrientation('default');
|
||||
listener.start();
|
||||
}
|
||||
|
||||
function rationalize(sensorAngle, angle) {
|
||||
if (typeof(angle) === 'undefined') {
|
||||
angle = current;
|
||||
}
|
||||
|
||||
// The result of this operation is an angle from 0..270 degrees,
|
||||
// in steps of 90 degrees. Angles are rounded to the nearest
|
||||
// magnitude, so 45 will be rounded to 90, and -45 will be rounded
|
||||
// to -90 (not 0).
|
||||
var r = angle + sensorAngle;
|
||||
if (r >= 0) {
|
||||
r += 45;
|
||||
} else {
|
||||
r -= 45;
|
||||
}
|
||||
r /= 90;
|
||||
if (r >= 0) {
|
||||
r = Math.floor(r);
|
||||
} else {
|
||||
r = Math.ceil(r);
|
||||
}
|
||||
r %= 4;
|
||||
r *= 90;
|
||||
if (r < 0) {
|
||||
r += 360;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
on: listener.on,
|
||||
off: listener.off,
|
||||
start: listener.start,
|
||||
stop: listener.stop,
|
||||
unlock: unlock,
|
||||
lock: lock,
|
||||
rationalize: rationalize,
|
||||
get: function() {
|
||||
return current;
|
||||
}
|
||||
};
|
||||
});
|
||||
84
gaia/apps/camera/js/lib/panzoom.js
Executable file
84
gaia/apps/camera/js/lib/panzoom.js
Executable file
|
|
@ -0,0 +1,84 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var GestureDetector = require('GestureDetector');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = addPanAndZoomHandlers;
|
||||
|
||||
/*
|
||||
* This module adds pan-and-zoom capability to images displayed by
|
||||
* shared/js/media/media_frame.js.
|
||||
* It is used by preview-gallery.js and confirm.js
|
||||
*/
|
||||
function addPanAndZoomHandlers(frame, swipeCallback) {
|
||||
// frame is the MediaFrame object. container is its DOM element.
|
||||
var container = frame.container;
|
||||
|
||||
// Generate gesture events for the container
|
||||
var gestureDetector = new GestureDetector(container);
|
||||
gestureDetector.startDetecting();
|
||||
|
||||
// When the user touches the screen and moves their finger left or
|
||||
// right, they might want to pan within a zoomed-in image, or they
|
||||
// might want to swipe between multiple items in the camera preview
|
||||
// gallery. We pass the amount of motion to the MediaFrame pan() method,
|
||||
// and it returns the amount that cannot be used to pan the displayed
|
||||
// item. We track this returned amount as how far left or right the
|
||||
// image has been swiped, and pass the number to the swipeCallback.
|
||||
var swipeAmount = 0;
|
||||
|
||||
// And handle them with these listeners
|
||||
container.addEventListener('dbltap', handleDoubleTap);
|
||||
container.addEventListener('transform', handleTransform);
|
||||
container.addEventListener('pan', handlePan);
|
||||
if (swipeCallback) {
|
||||
container.addEventListener('swipe', handleSwipe);
|
||||
}
|
||||
|
||||
function handleDoubleTap(e) {
|
||||
var scale;
|
||||
if (frame.fit.scale > frame.fit.baseScale) {
|
||||
scale = frame.fit.baseScale / frame.fit.scale;
|
||||
}
|
||||
else {
|
||||
scale = 2;
|
||||
}
|
||||
|
||||
frame.zoom(scale, e.detail.clientX, e.detail.clientY, 200);
|
||||
}
|
||||
|
||||
function handleTransform(e) {
|
||||
frame.zoom(e.detail.relative.scale,
|
||||
e.detail.midpoint.clientX, e.detail.midpoint.clientY);
|
||||
}
|
||||
|
||||
function handlePan(e) {
|
||||
var dx = e.detail.relative.dx;
|
||||
var dy = e.detail.relative.dy;
|
||||
|
||||
if (swipeCallback) {
|
||||
dx += swipeAmount;
|
||||
swipeAmount = frame.pan(dx, dy);
|
||||
swipeCallback(swipeAmount);
|
||||
} else {
|
||||
frame.pan(dx, dy);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSwipe(e) {
|
||||
if (swipeAmount !== 0) {
|
||||
swipeCallback(swipeAmount, e.detail.vx);
|
||||
swipeAmount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
140
gaia/apps/camera/js/lib/pinch.js
Executable file
140
gaia/apps/camera/js/lib/pinch.js
Executable file
|
|
@ -0,0 +1,140 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var unbind = require('lib/bind').unbind;
|
||||
var bindAll = require('lib/bind-all');
|
||||
var bind = require('lib/bind');
|
||||
var events = require('evt');
|
||||
|
||||
/**
|
||||
* Mixin event emitter
|
||||
*/
|
||||
|
||||
events(Pinch.prototype);
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = Pinch;
|
||||
|
||||
/**
|
||||
* Initialize a new `Pinch` interface.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function Pinch(el) {
|
||||
bindAll(this);
|
||||
this.attach(el);
|
||||
}
|
||||
|
||||
Pinch.prototype.attach = function(el) {
|
||||
this.el = el;
|
||||
|
||||
bind(this.el, 'touchstart', this.onTouchStart);
|
||||
bind(window, 'touchmove', this.onTouchMove);
|
||||
bind(window, 'touchend', this.onTouchEnd);
|
||||
};
|
||||
|
||||
Pinch.prototype.detach = function() {
|
||||
unbind(this.el, 'touchstart', this.onTouchStart);
|
||||
unbind(window, 'touchmove', this.onTouchMove);
|
||||
unbind(window, 'touchend', this.onTouchEnd);
|
||||
|
||||
this.el = null;
|
||||
};
|
||||
|
||||
Pinch.prototype.enable = function() {
|
||||
this.disabled = false;
|
||||
};
|
||||
|
||||
Pinch.prototype.disable = function() {
|
||||
this.disabled = true;
|
||||
};
|
||||
|
||||
Pinch.prototype.onTouchStart = function(evt) {
|
||||
if (evt.touches.length !== 2 || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastTouchA = evt.touches[0];
|
||||
this.lastTouchB = evt.touches[1];
|
||||
this.isPinching = true;
|
||||
this.emit('started');
|
||||
};
|
||||
|
||||
Pinch.prototype.onTouchMove = function(evt) {
|
||||
if (!this.isPinching || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
var touchA = getNewTouchA(this, evt.touches);
|
||||
var touchB = getNewTouchB(this, evt.touches);
|
||||
var deltaPinch = getDeltaPinch(this, touchA, touchB);
|
||||
|
||||
this.emit('changed', deltaPinch);
|
||||
|
||||
this.lastTouchA = touchA;
|
||||
this.lastTouchB = touchB;
|
||||
};
|
||||
|
||||
Pinch.prototype.onTouchEnd = function(evt) {
|
||||
if (!this.isPinching || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt.touches.length < 2) {
|
||||
this.isPinching = false;
|
||||
this.emit('ended');
|
||||
}
|
||||
};
|
||||
|
||||
function getNewTouchA(pinch, touches) {
|
||||
if (!pinch.lastTouchA) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var i = 0, length = touches.length, touch; i < length; i++) {
|
||||
touch = touches[i];
|
||||
if (touch.identifier === pinch.lastTouchA.identifier) {
|
||||
return touch;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getNewTouchB(pinch, touches) {
|
||||
if (!pinch.lastTouchB) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (var i = 0, length = touches.length, touch; i < length; i++) {
|
||||
touch = touches[i];
|
||||
if (touch.identifier === pinch.lastTouchB.identifier) {
|
||||
return touch;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDeltaPinch(pinch, touchA, touchB) {
|
||||
var lastTouchA = pinch.lastTouchA;
|
||||
var lastTouchB = pinch.lastTouchB;
|
||||
if (!touchA || !lastTouchA || !touchB || !lastTouchB) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var oldDistance = Math.sqrt(
|
||||
Math.pow(lastTouchB.pageX - lastTouchA.pageX, 2) +
|
||||
Math.pow(lastTouchB.pageY - lastTouchA.pageY, 2));
|
||||
var newDistance = Math.sqrt(
|
||||
Math.pow(touchB.pageX - touchA.pageX, 2) +
|
||||
Math.pow(touchB.pageY - touchA.pageY, 2));
|
||||
return newDistance - oldDistance;
|
||||
}
|
||||
|
||||
});
|
||||
53
gaia/apps/camera/js/lib/prepare-preview-blob.js
Executable file
53
gaia/apps/camera/js/lib/prepare-preview-blob.js
Executable file
|
|
@ -0,0 +1,53 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Module Dependencies
|
||||
*/
|
||||
|
||||
var parseJpegMetadata = require('jpegMetaDataParser');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(blob, done) {
|
||||
parseJpegMetadata(blob, onJpegParsed);
|
||||
|
||||
function onJpegParsed(metadata) {
|
||||
metadata.blob = blob;
|
||||
|
||||
if (!metadata.preview) {
|
||||
done(metadata);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we found an EXIF preview,
|
||||
// and can determine its size, then
|
||||
// we can display it instead of the
|
||||
// big image and save memory and time.
|
||||
var start = metadata.preview.start;
|
||||
var end = metadata.preview.end;
|
||||
var previewBlob = blob.slice(start, end, 'image/jpeg');
|
||||
parseJpegMetadata(previewBlob, onSuccess, onError);
|
||||
|
||||
// If we parsed the preview image, add its
|
||||
// dimensions to the metadata.preview
|
||||
// object, and then let the MediaFrame
|
||||
// object display the preview instead of
|
||||
// the full-size image.
|
||||
function onSuccess(previewMetadata) {
|
||||
metadata.preview.width = previewMetadata.width;
|
||||
metadata.preview.height = previewMetadata.height;
|
||||
done(metadata);
|
||||
}
|
||||
|
||||
// If we couldn't parse the preview image,
|
||||
// just display full-size.
|
||||
function onError() {
|
||||
done(metadata);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
61
gaia/apps/camera/js/lib/resize-image-and-save.js
Executable file
61
gaia/apps/camera/js/lib/resize-image-and-save.js
Executable file
|
|
@ -0,0 +1,61 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var Storage = require('lib/storage');
|
||||
var cropResizeRotate = require('cropResizeRotate');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = function(options, done) {
|
||||
var blob = options.blob;
|
||||
var outputSize = options.width && options.height ?
|
||||
{
|
||||
width: options.width,
|
||||
height: options.height
|
||||
} : options.size || null;
|
||||
|
||||
cropResizeRotate(blob, null, outputSize, 'image/jpeg+exif',
|
||||
function(error, resizedBlob) {
|
||||
|
||||
// If we couldn't resize or rotate it, use the original
|
||||
if (error) {
|
||||
console.error('Error while resizing image: ' + error);
|
||||
done(blob);
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to send a file-backed blob as the result of a pick activity
|
||||
// (see bug 975599) so we'll overwrite the old blob with the new one.
|
||||
// This means that the image stored will actually match the one passed
|
||||
// to the app that initiated the pick activity. We delete the old file,
|
||||
// then save the new blob with the same name. Then we read the file and
|
||||
// pass that to the callback.
|
||||
if (resizedBlob === blob) {
|
||||
done(blob);
|
||||
return;
|
||||
}
|
||||
|
||||
var storage = new Storage();
|
||||
// We first delete the full resolution picture
|
||||
storage.deletePicture(blob.name, addPicture);
|
||||
// We save the scaled down image
|
||||
function addPicture(error) {
|
||||
if (error) {
|
||||
done(blob);
|
||||
return;
|
||||
}
|
||||
storage.addPicture(resizedBlob, {
|
||||
filepath: blob.name
|
||||
}, onSavedPicture);
|
||||
}
|
||||
|
||||
function onSavedPicture(error, filepath, absolutePath, fileBlob) {
|
||||
done(fileBlob);
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
157
gaia/apps/camera/js/lib/setting-alias.js
Executable file
157
gaia/apps/camera/js/lib/setting-alias.js
Executable file
|
|
@ -0,0 +1,157 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('setting-alias');
|
||||
|
||||
/**
|
||||
* Locals
|
||||
*/
|
||||
|
||||
var forwardMethods = [
|
||||
'filterOptions',
|
||||
'resetOptions',
|
||||
'supported',
|
||||
'selected',
|
||||
'select',
|
||||
'next',
|
||||
'get',
|
||||
'set'
|
||||
];
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = SettingAlias;
|
||||
|
||||
/**
|
||||
* Initialize a new `SettingsAlias`
|
||||
*
|
||||
* SettingsAlias's are kinda confusing,
|
||||
* but they simplify our app code massively
|
||||
* in the long run.
|
||||
*
|
||||
* A `SettingAlias` has the same API as a `Setting`
|
||||
* but behind the scenes provides a single interface
|
||||
* to several `Setting`s. Depending on the state
|
||||
* of the app, the `SettingAlias` will point to
|
||||
* a different settings instance.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* var flashModesPicture = new Setting({ key: 'flashModesPicture' });
|
||||
* var flashModesVideo = new Setting({ key: 'flashModesVideo' });
|
||||
* var mode = 'picture';
|
||||
*
|
||||
* var flashModes = new SettingAlias({
|
||||
* key: 'flashModes',
|
||||
* settings: {
|
||||
* 'picture': flashModesPicture,
|
||||
* 'video': flashModesVideo
|
||||
* },
|
||||
* get: function() {
|
||||
* return this.settings[mode];
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* flashModes.get('key'); //=> 'flashModesPicture'
|
||||
* mode = 'video';
|
||||
* flashModes.get('key'); //=> 'flashModesVideo'
|
||||
*
|
||||
* This means our app can simply call API on
|
||||
* `flashModes` setting-alias and not worry
|
||||
* about which 'mode' the app is in.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - `key` The name of the setting-alias
|
||||
* - `settings` Hash of possible `Setting`s
|
||||
* - `get` Method that returns the current `Setting`
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
function SettingAlias(options) {
|
||||
debug('initialize');
|
||||
this.key = options.key;
|
||||
this.settings = options.settings;
|
||||
this.current = options.get.bind(this);
|
||||
forwardMethods.forEach(this.forward, this);
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over each setting.
|
||||
*
|
||||
* @param {Function} fn
|
||||
* @private
|
||||
*/
|
||||
SettingAlias.prototype.each = function(fn) {
|
||||
for (var key in this.settings) {
|
||||
fn(this.settings[key]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* States whether the passed key
|
||||
* is the currently active setting.
|
||||
*
|
||||
* @param {String} key
|
||||
* @return {Boolean}
|
||||
*/
|
||||
SettingAlias.prototype.is = function(key) {
|
||||
return this.get('key') === key;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches a method that forwards
|
||||
* the call onto the current
|
||||
* matching setting.
|
||||
*
|
||||
* @param {String} method
|
||||
*/
|
||||
SettingAlias.prototype.forward = function(method) {
|
||||
this[method] = function() {
|
||||
var setting = this.current();
|
||||
return setting[method].apply(setting, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an event callback.
|
||||
*
|
||||
* The callback is wrapped in
|
||||
* a function that first checks that
|
||||
* the setting is the active setting
|
||||
* before calling the original callback.
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {Function} fn
|
||||
*/
|
||||
SettingAlias.prototype.on = function(name, fn) {
|
||||
var alias = this;
|
||||
var wrapped = function() {
|
||||
if (!alias.is(this.key)) { return; }
|
||||
fn.apply(this, arguments);
|
||||
};
|
||||
|
||||
this.each(function(setting) { setting.on(name, wrapped); });
|
||||
fn._settingAliasCallback = wrapped;
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an event callback.
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {Function} fn
|
||||
*/
|
||||
SettingAlias.prototype.off = function(name, fn) {
|
||||
var wrapped = fn._settingAliasCallback;
|
||||
this.each(function(setting) { setting.off(name, wrapped); });
|
||||
delete fn._settingAliasCallback;
|
||||
};
|
||||
|
||||
});
|
||||
251
gaia/apps/camera/js/lib/setting.js
Executable file
251
gaia/apps/camera/js/lib/setting.js
Executable file
|
|
@ -0,0 +1,251 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('setting');
|
||||
var model = require('model');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = Setting;
|
||||
|
||||
/**
|
||||
* Mixin `Model` methods
|
||||
*/
|
||||
|
||||
model(Setting.prototype);
|
||||
|
||||
/**
|
||||
* Initialize a new `Setting`.
|
||||
*
|
||||
* @param {Object} data
|
||||
*/
|
||||
function Setting(data) {
|
||||
this.key = data.key;
|
||||
this.storage = data.storage || localStorage;
|
||||
this.reset(data, { silent: true });
|
||||
this.select = this.select.bind(this);
|
||||
this.next = this.next.bind(this);
|
||||
this.configure(data);
|
||||
if (data.persistent) { this.on('change:selected', this.save); }
|
||||
}
|
||||
|
||||
Setting.prototype.configure = function(data) {
|
||||
var optionsDefined = !!(data.options && data.options.length);
|
||||
this.options = { defined: optionsDefined };
|
||||
this.resetOptions(data.options);
|
||||
};
|
||||
|
||||
Setting.prototype.resetOptions = function(list) {
|
||||
list = list || [];
|
||||
|
||||
var hash = this.optionsToHash(list);
|
||||
this.options.all = hash;
|
||||
this.options.available = hash;
|
||||
|
||||
this.set('options', list, { silent: true });
|
||||
this.updateSelected({ silent: true });
|
||||
};
|
||||
|
||||
Setting.prototype.filterOptions = function(keys, options) {
|
||||
var available = this.options.available = {};
|
||||
var hash = this.options.all;
|
||||
var filtered = [];
|
||||
|
||||
(keys || []).forEach(function(key) {
|
||||
var option = hash[key];
|
||||
if (option !== undefined) {
|
||||
filtered.push(option);
|
||||
available[key] = option;
|
||||
}
|
||||
});
|
||||
|
||||
this.sortByIndex(filtered);
|
||||
this.set('options', filtered, options);
|
||||
this.updateSelected({ silent: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert this Setting's `options` array as defined
|
||||
* in config/app.js into an object as key/value pairs.
|
||||
* This allows us to look up an option by its `key`.
|
||||
*
|
||||
* e.g.: [ { key: 'a', title: 'A' }, ... ] ->
|
||||
* { a: { key: 'a', title: 'A' }, ... }
|
||||
*
|
||||
* We use this hash to validate
|
||||
* incoming options and ot mixin
|
||||
* config data with dynamic options.
|
||||
*
|
||||
* @param {Array} options
|
||||
* @return {undefined|Object}
|
||||
*/
|
||||
Setting.prototype.optionsToHash = function(options) {
|
||||
var hash = {};
|
||||
options.forEach(function(option, index) {
|
||||
option.index = index;
|
||||
hash[option.key] = option;
|
||||
});
|
||||
return hash;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the selected option,
|
||||
* or just a particular key
|
||||
* if given.
|
||||
*
|
||||
* @param {String} key
|
||||
* @return {Object|*}
|
||||
*/
|
||||
Setting.prototype.selected = function(key) {
|
||||
var hash = this.options.available;
|
||||
var option = hash[this.get('selected')];
|
||||
return key ? option && option[key] : option;
|
||||
};
|
||||
|
||||
/**
|
||||
* Select an option.
|
||||
*
|
||||
* Accepts an string key to select,
|
||||
* or an integer index relating to
|
||||
* the current options list.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - {Boolean} silent
|
||||
*
|
||||
* @param {String|Number} key
|
||||
* @param {Object} opts Model#set() options
|
||||
*/
|
||||
Setting.prototype.select = function(key, options) {
|
||||
var isIndex = typeof key === 'number';
|
||||
var list = this.get('options');
|
||||
var available = this.options.available;
|
||||
var selected = isIndex ? list[key] : available[key];
|
||||
|
||||
// If there are no options, exit
|
||||
if (!list.length) { return; }
|
||||
|
||||
// If an option was not found,
|
||||
// default to selecting the first.
|
||||
if (!selected) { return this.select(0, options); }
|
||||
|
||||
// Store the new choice
|
||||
this.set('selected', selected.key, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts the current options list
|
||||
* by their originally defined
|
||||
* index in the config JSON.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Setting.prototype.sortByIndex = function(list) {
|
||||
return list.sort(function(a, b) { return a.index - b.index; });
|
||||
};
|
||||
|
||||
/**
|
||||
* Silently updates the `selected`
|
||||
* property of the Setting.
|
||||
*
|
||||
* If no selected key is present
|
||||
* we attempt to select the last
|
||||
* fetched persited selection.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
Setting.prototype.updateSelected = function(options) {
|
||||
this.select(this.get('selected') || this.fetched, options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the `selected` option to
|
||||
* the next option in the list.
|
||||
*
|
||||
* First option is chosen if
|
||||
* there is no next option.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
Setting.prototype.next = function() {
|
||||
var options = this.get('options');
|
||||
var selected = this.selected();
|
||||
var index = options.indexOf(selected);
|
||||
var newIndex = (index + 1) % options.length;
|
||||
this.select(newIndex);
|
||||
debug('set \'%s\' to index: %s', this.key, newIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Persists the current selection
|
||||
* to storage for retreval in the
|
||||
* next session.
|
||||
*
|
||||
* We're using localStorage as performance
|
||||
* vastly outweighed indexedBD (asyncStorage)
|
||||
* by 1ms/500ms on Hamachi device.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
Setting.prototype.save = function() {
|
||||
var selected = this.get('selected');
|
||||
debug('saving key: %s, selected: %s', this.key, selected);
|
||||
this.storage.setItem('setting:' + this.key, selected);
|
||||
debug('saved key: %s', selected);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the persisted selection
|
||||
* from storage, updating the
|
||||
* `selected` key.
|
||||
*
|
||||
* We're using localStorage, as performance
|
||||
* vastly outweighed indexedBD (asyncStorage)
|
||||
* by 1ms/500ms on Hamachi device.
|
||||
*
|
||||
* Leaving in the `done` callback in-case
|
||||
* storage goes async again in future.
|
||||
*
|
||||
* @param {Function} done
|
||||
* @public
|
||||
*/
|
||||
Setting.prototype.fetch = function() {
|
||||
if (!this.get('persistent')) { return; }
|
||||
debug('fetch value key: %s', this.key);
|
||||
this.fetched = this.storage.getItem('setting:' + this.key);
|
||||
debug('fetched %s value: %s', this.key, this.fetched);
|
||||
if (this.fetched) { this.select(this.fetched, { silent: true }); }
|
||||
};
|
||||
|
||||
/**
|
||||
* States whether this setting
|
||||
* is currently supported.
|
||||
*
|
||||
* 'Supported' means, it's not been
|
||||
* disabled, and there are options
|
||||
* to be chosen from.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
Setting.prototype.supported = function() {
|
||||
return this.enabled() && !!this.get('options').length;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Check if this setting is not
|
||||
* marked as disabled.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
Setting.prototype.enabled = function() {
|
||||
return !this.get('disabled');
|
||||
};
|
||||
|
||||
});
|
||||
105
gaia/apps/camera/js/lib/settings.js
Executable file
105
gaia/apps/camera/js/lib/settings.js
Executable file
|
|
@ -0,0 +1,105 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var SettingAlias = require('./setting-alias');
|
||||
var debug = require('debug')('settings');
|
||||
var Setting = require('./setting');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = Settings;
|
||||
|
||||
/**
|
||||
* Initialize a new 'Setting'
|
||||
*
|
||||
* @param {Object} items
|
||||
*/
|
||||
function Settings(items) {
|
||||
this.ids = {};
|
||||
this.items = [];
|
||||
this.aliases = {};
|
||||
this.SettingAlias = SettingAlias; // Test hook
|
||||
this.dontSave = this.dontSave.bind(this);
|
||||
this.addEach(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add several Settings in one go.
|
||||
*
|
||||
* @param {Object} items
|
||||
*/
|
||||
Settings.prototype.addEach = function(items) {
|
||||
if (!items) { return; }
|
||||
var item;
|
||||
var key;
|
||||
|
||||
for (key in items) {
|
||||
item = items[key];
|
||||
item.key = item.key || key;
|
||||
this.add(items[key]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new Setting to the settings collection.
|
||||
*
|
||||
* @param {Object} data
|
||||
*/
|
||||
Settings.prototype.add = function(data) {
|
||||
var setting = new Setting(data);
|
||||
this.items.push(setting);
|
||||
this.ids[setting.key] = this[setting.key] = setting;
|
||||
debug('added setting: %s', setting.key);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch all the Settings previous
|
||||
* state from storage.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
Settings.prototype.fetch = function() {
|
||||
this.items.forEach(function(setting) { setting.fetch(); });
|
||||
};
|
||||
|
||||
/**
|
||||
* Prevent all settings from saving
|
||||
* when they are changed.
|
||||
*
|
||||
* This is used in activity sessions
|
||||
* whereby we don't want any settings
|
||||
* changes to persist to future camera
|
||||
* sessions.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
Settings.prototype.dontSave = function() {
|
||||
this.items.forEach(function(setting) {
|
||||
setting.off('change:selected', setting.save);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a SettingAlias to the settings.
|
||||
* (see lib/setting-alias.js for more info)
|
||||
*
|
||||
* @param {Object} options
|
||||
* @public
|
||||
*/
|
||||
Settings.prototype.alias = function(options) {
|
||||
var alias = new this.SettingAlias(options);
|
||||
this.aliases[options.key] = alias;
|
||||
this[options.key] = alias;
|
||||
};
|
||||
|
||||
Settings.prototype.removeAlias = function(key) {
|
||||
// TODO: Implement
|
||||
};
|
||||
|
||||
});
|
||||
162
gaia/apps/camera/js/lib/sounds.js
Executable file
162
gaia/apps/camera/js/lib/sounds.js
Executable file
|
|
@ -0,0 +1,162 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = Sounds;
|
||||
|
||||
/**
|
||||
* Initialize a new `Sounds` interface.
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function Sounds(list) {
|
||||
this.items = {};
|
||||
(list || []).forEach(this.add, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new sound.
|
||||
*
|
||||
* Checks if this sound
|
||||
* is enabled, and sets
|
||||
* up an observer.
|
||||
*
|
||||
* @param {Object} data
|
||||
*
|
||||
*/
|
||||
Sounds.prototype.add = function(data) {
|
||||
var self = this;
|
||||
var sound = {
|
||||
name: data.name,
|
||||
url: data.url,
|
||||
setting: data.setting,
|
||||
enabled: false
|
||||
};
|
||||
|
||||
this.items[data.name] = sound;
|
||||
this.isEnabled(sound, function(value) {
|
||||
self.setEnabled(sound, value);
|
||||
self.observeSetting(sound);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a sound is
|
||||
* enabled inside mozSettings.
|
||||
*
|
||||
* This is wrapped in a setTimeout
|
||||
* to prevent expensive mozSettings
|
||||
* API from blocking other operations.
|
||||
*
|
||||
* @param {Object} sound
|
||||
* @param {Function} done
|
||||
*/
|
||||
Sounds.prototype.isEnabled = function(sound, done) {
|
||||
setTimeout(function() {
|
||||
var mozSettings = navigator.mozSettings;
|
||||
var key = sound.setting;
|
||||
|
||||
// Requires navigator.mozSettings
|
||||
if (!mozSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
mozSettings
|
||||
.createLock()
|
||||
.get(key)
|
||||
.onsuccess = onSuccess;
|
||||
|
||||
function onSuccess(e) {
|
||||
var result = e.target.result[key];
|
||||
done(result);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Observe mozSettings for changes
|
||||
* on the given settings key.
|
||||
*
|
||||
* @param {Object} sound
|
||||
*
|
||||
*/
|
||||
Sounds.prototype.observeSetting = function(sound) {
|
||||
var mozSettings = navigator.mozSettings;
|
||||
var key = sound.setting;
|
||||
var self = this;
|
||||
if (mozSettings) {
|
||||
mozSettings.addObserver(key, function(e) {
|
||||
self.setEnabled(sound, e.settingValue);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a sounds `enabled` key.
|
||||
*
|
||||
* @param {Object} sound
|
||||
* @param {Boolean} value
|
||||
*
|
||||
*/
|
||||
Sounds.prototype.setEnabled = function(sound, value) {
|
||||
sound.enabled = value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Play a sound by name.
|
||||
*
|
||||
* @param {String} name
|
||||
* @public
|
||||
*/
|
||||
Sounds.prototype.play = function(name) {
|
||||
var sound = this.items[name];
|
||||
if (!sound.audio) {
|
||||
sound.audio = this.createAudio(sound.url);
|
||||
}
|
||||
|
||||
this.playSound(sound);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a function that plays
|
||||
* the given sound when called.
|
||||
*
|
||||
* @param {String} name
|
||||
* @return {Funciton}
|
||||
* @public
|
||||
*/
|
||||
Sounds.prototype.player = function(name) {
|
||||
return (function() { this.play(name); }).bind(this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Play a sound.
|
||||
*
|
||||
* @param {Object} sound
|
||||
*
|
||||
*/
|
||||
Sounds.prototype.playSound = function(sound) {
|
||||
if (sound.enabled) {
|
||||
// cloneNode() lets us play overlapping sounds and is needed when the
|
||||
// user takes pictures very rapidly.
|
||||
// See http://robert.ocallahan.org/2011/11/latency-of-html5-sounds.html
|
||||
sound.audio.cloneNode(true).play();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an audio element.
|
||||
*
|
||||
* @param {String} url
|
||||
* @return {HTMLAudioElement}
|
||||
*/
|
||||
Sounds.prototype.createAudio = function(url) {
|
||||
var audio = new Audio(url);
|
||||
audio.mozAudioChannelType = 'notification';
|
||||
return audio;
|
||||
};
|
||||
|
||||
});
|
||||
410
gaia/apps/camera/js/lib/storage.js
Executable file
410
gaia/apps/camera/js/lib/storage.js
Executable file
|
|
@ -0,0 +1,410 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('storage');
|
||||
var bindAll = require('lib/bind-all');
|
||||
var events = require('evt');
|
||||
var storageSingleton;
|
||||
|
||||
/**
|
||||
* Expose `Storage`
|
||||
*/
|
||||
|
||||
module.exports = Storage;
|
||||
|
||||
/**
|
||||
* Mixin event emitter
|
||||
*/
|
||||
|
||||
events(Storage.prototype);
|
||||
|
||||
/**
|
||||
* Initialize a new `Storage`.
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
function Storage(options) {
|
||||
if (storageSingleton) {
|
||||
return storageSingleton;
|
||||
}
|
||||
storageSingleton = this;
|
||||
bindAll(this);
|
||||
this.maxFileSize = 0;
|
||||
options = options || {};
|
||||
this.dcf = options.dcf;
|
||||
if (this.dcf) {
|
||||
this.dcf.init();
|
||||
}
|
||||
this.require = options.require || require;
|
||||
navigator.mozSettings.addObserver(
|
||||
'device.storage.writable.name',
|
||||
this.onStorageVolumeChanged);
|
||||
this.configure();
|
||||
debug('initialized');
|
||||
}
|
||||
|
||||
Storage.prototype.createFilename = function(storage, type, done) {
|
||||
var self = this;
|
||||
this.require(['asyncStorage', 'lib/dcf'], function(as, dcf) {
|
||||
if (!self.dcf) {
|
||||
self.dcf = dcf;
|
||||
self.dcf.init();
|
||||
}
|
||||
|
||||
self.dcf.createDCFFilename(storage, type, done);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the image Blob to DeviceStorage
|
||||
* then lookup the File reference and
|
||||
* return that in the callback as well
|
||||
* as the resulting paths.
|
||||
*
|
||||
* You always want to forget about the
|
||||
* Blob you told us about and use the
|
||||
* File instead since otherwise you
|
||||
* are wasting precious memory.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @param {String} options.filepath
|
||||
* The path to save the image to.
|
||||
*/
|
||||
Storage.prototype.addPicture = function(blob, options, done) {
|
||||
if (typeof options === 'function') {
|
||||
done = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
done = done || function() {};
|
||||
var filepath = options && options.filepath;
|
||||
var self = this;
|
||||
debug('add picture', filepath);
|
||||
|
||||
// Create a filepath if
|
||||
// one hasn't been given.
|
||||
if (!filepath) {
|
||||
debug('creating filename');
|
||||
this.createFilename(this.picture, 'image', onCreated);
|
||||
} else {
|
||||
onCreated(filepath);
|
||||
}
|
||||
|
||||
function onCreated(filepath) {
|
||||
var req = self.picture.addNamed(blob, filepath);
|
||||
req.onerror = function() { self.emit('error'); };
|
||||
req.onsuccess = function(e) {
|
||||
debug('image stored', filepath);
|
||||
var absolutePath = e.target.result;
|
||||
|
||||
// `addNamed` does not give us a File
|
||||
// handle so we need to get() it again.
|
||||
refetchFile(filepath, absolutePath);
|
||||
};
|
||||
}
|
||||
|
||||
function refetchFile(filepath, absolutePath) {
|
||||
var req = self.picture.get(filepath);
|
||||
req.onerror = function() {
|
||||
self.emit('error');
|
||||
done('Error adding picture to storage');
|
||||
};
|
||||
req.onsuccess = function(e) {
|
||||
debug('image file blob handle retrieved');
|
||||
var fileBlob = e.target.result;
|
||||
done(null, filepath, absolutePath, fileBlob);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new video filepath.
|
||||
*
|
||||
* The CameraControl API will not
|
||||
* automatically create directories
|
||||
* for the new file if they do not
|
||||
* exist.
|
||||
*
|
||||
* So we write a dummy file to the
|
||||
* same directory via DeviceStorage
|
||||
* to ensure that the directory exists
|
||||
* before attempting to record to this
|
||||
* filepath.
|
||||
*
|
||||
* If the request errors it can mean that
|
||||
* there is a file that already exists
|
||||
* in that name. If so, we attempt to delete
|
||||
* it and try to create the filepath again.
|
||||
*
|
||||
* @param {Function} done
|
||||
* @public
|
||||
*/
|
||||
Storage.prototype.createVideoFilepath = function(done) {
|
||||
var videoStorage = this.video;
|
||||
var self = this;
|
||||
|
||||
this.createFilename(this.video, 'video', function(filepath) {
|
||||
var dummyFilepath = getDir(filepath) + '.tmp.3gp';
|
||||
var blob = new Blob([''], { type: 'video/3gpp' });
|
||||
var req = videoStorage.addNamed(blob, dummyFilepath);
|
||||
|
||||
req.onerror = function(e) {
|
||||
debug('Failed to add ' + filepath + ' to DeviceStorage', e);
|
||||
var req = videoStorage.delete(dummyFilepath);
|
||||
req.onerror = function() { done('Error creating video file path'); };
|
||||
req.onsuccess = function() { self.createVideoFilepath(done); };
|
||||
};
|
||||
|
||||
req.onsuccess = function(e) {
|
||||
videoStorage.delete(e.target.result);
|
||||
done(null, filepath);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
Storage.prototype.onStorageChange = function(e) {
|
||||
debug('state change: %s', e.reason);
|
||||
var value = e.reason;
|
||||
|
||||
switch (value) {
|
||||
case 'deleted':
|
||||
// Emit an `itemdeleted` event to
|
||||
// allow parts of the UI to update.
|
||||
var filepath = this.checkFilepath(e.path);
|
||||
this.emit('itemdeleted', { path: filepath });
|
||||
break;
|
||||
case 'available':
|
||||
case 'shared':
|
||||
case 'unavailable':
|
||||
this.setState(value);
|
||||
break;
|
||||
}
|
||||
|
||||
// Check storage
|
||||
// has spare capacity
|
||||
this.check();
|
||||
};
|
||||
|
||||
Storage.prototype.configure = function(storageVolumeName) {
|
||||
var i;
|
||||
var videosStorages;
|
||||
var picturesStorages;
|
||||
// If we had a previous ds for pictures, let's remove the observer
|
||||
// we had set as well before fetching new ds.
|
||||
if (this.picture) {
|
||||
this.picture.removeEventListener('change', this.onStorageChange);
|
||||
}
|
||||
if (!storageVolumeName) {
|
||||
this.video = navigator.getDeviceStorage('videos');
|
||||
this.picture = navigator.getDeviceStorage('pictures');
|
||||
} else { // We select the volumes with the passed name
|
||||
videosStorages = navigator.getDeviceStorages('videos');
|
||||
this.video = videosStorages[0];
|
||||
for (i = 0; i < videosStorages.length; ++i) {
|
||||
if (videosStorages[i].storageName === storageVolumeName) {
|
||||
this.video = videosStorages[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
picturesStorages = navigator.getDeviceStorages('pictures');
|
||||
this.picture = picturesStorages[0];
|
||||
for (i = 0; i < picturesStorages.length; ++i) {
|
||||
if (picturesStorages[i].storageName === storageVolumeName) {
|
||||
this.picture = picturesStorages[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shouldn't happen?
|
||||
if (!this.picture) {
|
||||
this.setState('unavailable');
|
||||
return;
|
||||
}
|
||||
|
||||
this.picture.addEventListener('change', this.onStorageChange);
|
||||
this.emit('volumechanged',{
|
||||
video: this.video,
|
||||
picture: this.picture
|
||||
});
|
||||
};
|
||||
|
||||
Storage.prototype.onStorageVolumeChanged = function(setting) {
|
||||
debug('default storage volume change: %s', setting.settingValue);
|
||||
this.configure(setting.settingValue);
|
||||
};
|
||||
|
||||
Storage.prototype.checkFilepath = function(filepath) {
|
||||
var startString = filepath.indexOf('DCIM/');
|
||||
|
||||
if (startString < -1) { return; }
|
||||
else if (startString > 0) {
|
||||
filepath = filepath.substr(startString);
|
||||
}
|
||||
|
||||
// Check whether filepath is a video poster image or not. If filepath
|
||||
// contains 'VID' and ends with '.jpg', consider it a video poster
|
||||
// image and get the video filepath by changing '.jpg' to '.3gp'
|
||||
if (filepath.indexOf('VID') != -1 &&
|
||||
filepath.lastIndexOf('.jpg') === filepath.length - 4) {
|
||||
filepath = filepath.replace('.jpg', '.3gp');
|
||||
}
|
||||
|
||||
return filepath;
|
||||
};
|
||||
|
||||
Storage.prototype.setState = function(value) {
|
||||
this.state = value;
|
||||
debug('set state: %s', value);
|
||||
this.emit('changed', value);
|
||||
};
|
||||
|
||||
Storage.prototype.setMaxFileSize = function(maxFileSize) {
|
||||
this.maxFileSize = maxFileSize;
|
||||
this.check();
|
||||
debug('max file size set: %d', maxFileSize);
|
||||
};
|
||||
|
||||
/**
|
||||
* Run a full storage check.
|
||||
*
|
||||
* @param {Function} done
|
||||
* @public
|
||||
*/
|
||||
Storage.prototype.check = function(done) {
|
||||
debug('check');
|
||||
|
||||
var self = this;
|
||||
done = done || function() {};
|
||||
|
||||
this.getState(function(result) {
|
||||
self.setState(result);
|
||||
|
||||
if (!self.available()) {
|
||||
onComplete('unhealthy');
|
||||
return;
|
||||
}
|
||||
|
||||
// check file counter in dcf if it already exist.
|
||||
if (self.dcf) {
|
||||
self.dcf.checkFileCounter();
|
||||
}
|
||||
self.isSpace(function(result) {
|
||||
if (!result) { self.setState('nospace'); }
|
||||
onComplete('healthy');
|
||||
});
|
||||
});
|
||||
|
||||
function onComplete(state) {
|
||||
self.emit('checked', state);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if there is enough space to
|
||||
* accomdate the current `maxFileSize`.
|
||||
*
|
||||
* @param {Function} done
|
||||
*/
|
||||
Storage.prototype.isSpace = function(done) {
|
||||
var maxFileSize = this.maxFileSize;
|
||||
this.picture
|
||||
.freeSpace()
|
||||
.onsuccess = function(e) {
|
||||
var freeSpace = e.target.result;
|
||||
var result = freeSpace > maxFileSize;
|
||||
debug('is space: %s', result, freeSpace, maxFileSize);
|
||||
done(result);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current storage state.
|
||||
*
|
||||
* @param {Function} done
|
||||
*/
|
||||
Storage.prototype.getState = function(done) {
|
||||
if (!this.picture) {
|
||||
setTimeout(function() {
|
||||
done('unavailable');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.picture
|
||||
.available()
|
||||
.onsuccess = function(e) {
|
||||
done(e.target.result);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* States if sotrage is available.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
Storage.prototype.available = function() {
|
||||
return this.state === 'available';
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a picture.
|
||||
*
|
||||
* @param {String} filepath
|
||||
*/
|
||||
Storage.prototype.deletePicture = function(filepath, done) {
|
||||
var req = this.picture.delete(filepath);
|
||||
req.onerror = function(e) {
|
||||
var message = 'Failed to delete ' + filepath +
|
||||
' from DeviceStorage:' + e.target.error;
|
||||
console.warn(message);
|
||||
done(message);
|
||||
};
|
||||
|
||||
if (done) {
|
||||
req.onsuccess = function() {
|
||||
done(null);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a video and accompanying
|
||||
* poster image.
|
||||
*
|
||||
* @param {String} filepath
|
||||
*/
|
||||
Storage.prototype.deleteVideo = function(filepath) {
|
||||
var poster = filepath.replace('.3gp', '.jpg');
|
||||
|
||||
this.video.delete(filepath).onerror = function(e) {
|
||||
console.warn(
|
||||
'Failed to delete', filepath,
|
||||
'from DeviceStorage:', e.target.error);
|
||||
};
|
||||
|
||||
this.picture.delete(poster).onerror = function(e) {
|
||||
console.warn(
|
||||
'Failed to delete poster image', poster,
|
||||
'for video', filepath,
|
||||
'from DeviceStorage:', e.target.error);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the directory from a filepath.
|
||||
*
|
||||
* @param {String} filepath
|
||||
* @return {String}
|
||||
*/
|
||||
function getDir(filepath) {
|
||||
var index = filepath.lastIndexOf('/') + 1;
|
||||
return index ? filepath.substring(0, index) : '';
|
||||
}
|
||||
|
||||
});
|
||||
27
gaia/apps/camera/js/lib/string-utils.js
Executable file
27
gaia/apps/camera/js/lib/string-utils.js
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var StringUtils = {};
|
||||
|
||||
StringUtils.toCamelCase = function(str) {
|
||||
|
||||
// Convert SNAKE_CASE strings to hyphenated
|
||||
str = str.toLowerCase().replace(/_/g, '-');
|
||||
return str.replace(/-([a-z])/g, function(g) {
|
||||
return g[1].toUpperCase();
|
||||
});
|
||||
};
|
||||
|
||||
StringUtils.toHyphenate = function(str) {
|
||||
return str.replace(/([a-z][A-Z])/g, function(g) {
|
||||
return g[0] + '-' + g[1].toLowerCase();
|
||||
});
|
||||
};
|
||||
|
||||
StringUtils.lastPathComponent = function(path) {
|
||||
var pathComponents = path.split('/');
|
||||
return pathComponents[pathComponents.length - 1];
|
||||
};
|
||||
|
||||
return StringUtils;
|
||||
});
|
||||
Loading…
Reference in a new issue