Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/apps/system/camera
diff options
context:
space:
mode:
Diffstat (limited to 'apps/system/camera')
-rw-r--r--apps/system/camera/index.html66
-rw-r--r--apps/system/camera/js/camera.js982
-rw-r--r--apps/system/camera/js/filmstrip.js568
-rw-r--r--apps/system/camera/locales/camera.ar.properties19
-rw-r--r--apps/system/camera/locales/camera.en-US.properties19
-rw-r--r--apps/system/camera/locales/camera.fr.properties19
-rw-r--r--apps/system/camera/locales/camera.zh-TW.properties20
-rw-r--r--apps/system/camera/locales/locales.ini11
-rw-r--r--apps/system/camera/resources/sounds/shutter.oggbin0 -> 15807 bytes
-rw-r--r--apps/system/camera/style/VideoPlayer.css152
-rw-r--r--apps/system/camera/style/camera.css314
-rw-r--r--apps/system/camera/style/filmstrip.css187
-rw-r--r--apps/system/camera/style/icons/60/Camera.pngbin0 -> 5909 bytes
-rw-r--r--apps/system/camera/style/icons/Camera.pngbin0 -> 5909 bytes
-rw-r--r--apps/system/camera/style/images/actionicon_cancel.pngbin0 -> 1423 bytes
-rw-r--r--apps/system/camera/style/images/camera.pngbin0 -> 1654 bytes
-rw-r--r--apps/system/camera/style/images/delete.pngbin0 -> 472 bytes
-rw-r--r--apps/system/camera/style/images/flash_auto.pngbin0 -> 1880 bytes
-rw-r--r--apps/system/camera/style/images/flash_off.pngbin0 -> 1613 bytes
-rw-r--r--apps/system/camera/style/images/flash_on.pngbin0 -> 1673 bytes
-rw-r--r--apps/system/camera/style/images/flash_torch.pngbin0 -> 1880 bytes
-rw-r--r--apps/system/camera/style/images/grid.pngbin0 -> 1127 bytes
-rw-r--r--apps/system/camera/style/images/hud_button_underlay.pngbin0 -> 548 bytes
-rw-r--r--apps/system/camera/style/images/hud_button_underlay_focus.pngbin0 -> 954 bytes
-rw-r--r--apps/system/camera/style/images/play_overlay.pngbin0 -> 2498 bytes
-rw-r--r--apps/system/camera/style/images/share.pngbin0 -> 855 bytes
-rw-r--r--apps/system/camera/style/images/stop.pngbin0 -> 3233 bytes
-rw-r--r--apps/system/camera/style/images/toggle_back.pngbin0 -> 2106 bytes
-rw-r--r--apps/system/camera/style/images/toggle_front.pngbin0 -> 2249 bytes
-rw-r--r--apps/system/camera/style/images/ui/gradient.pngbin0 -> 3713 bytes
-rw-r--r--apps/system/camera/style/images/ui/pattern.pngbin0 -> 6851 bytes
-rw-r--r--apps/system/camera/style/images/video.pngbin0 -> 1360 bytes
-rw-r--r--apps/system/camera/style/images/video_pause_button.pngbin0 -> 1722 bytes
-rw-r--r--apps/system/camera/style/images/video_play_button.pngbin0 -> 3862 bytes
-rw-r--r--apps/system/camera/style/images/video_play_focus.pngbin0 -> 1373 bytes
-rw-r--r--apps/system/camera/style/images/video_play_normal.pngbin0 -> 1361 bytes
-rw-r--r--apps/system/camera/test/unit/_proxy.html49
-rw-r--r--apps/system/camera/test/unit/_sandbox.html28
38 files changed, 2434 insertions, 0 deletions
diff --git a/apps/system/camera/index.html b/apps/system/camera/index.html
new file mode 100644
index 0000000..c06bf41
--- /dev/null
+++ b/apps/system/camera/index.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="pragma" content="no-cache">
+ <title>Camera</title>
+ <link rel="resource" type="application/l10n" href="locales/locales.ini" />
+ <link rel="resource" type="application/l10n" href="/shared/locales/date.ini" />
+ <link type="text/css" rel="stylesheet" href="style/camera.css"/>
+ <link type="text/css" rel="stylesheet" href="style/filmstrip.css"/>
+ <link type="text/css" rel="stylesheet" href="style/VideoPlayer.css"/>
+ </head>
+ <body>
+ <div id="focus-ring"></div>
+ <video id="viewfinder" autoplay></video>
+
+ <div id="hud">
+ <a id="toggle-camera" class="hidden"></an>
+ <a id="toggle-flash" class="hidden"></a>
+ </div>
+
+ <div id="controls">
+ <a id="switch-button" name="Switch source" class="hidden" disabled="disabled"><span></span></a>
+ <a id="capture-button" name="Capture" disabled="disabled"><span></span></a>
+ <div id="misc-button">
+ <a id="gallery-button" class="hidden" name="View Gallery"><span></span></a>
+ <a id="cancel-pick" class="hidden"><span></span></a>
+ <span id="video-timer">00:00</span>
+ </div>
+ </div>
+
+ <div id="overlay" class="hidden">
+ <div id="overlay-content">
+ <h1 id="overlay-title"></h1>
+ <p id="overlay-text"><p>
+ </div>
+ </div>
+
+ <!-- see filmstrip.js and filmstrip.css for these elements -->
+ <div id="filmstrip" class="hidden">
+ <a id="filmstrip-gallery-button" class="hidden button"></a>
+ </div>
+ <div id="preview" class="offscreen">
+ <div id="frame-container"> <!-- media frame rotates inside this -->
+ <div id="media-frame"></div> <!-- image or video here -->
+ </div>
+ <footer id="preview-controls"> <!-- camera, delete, share buttons -->
+ <a id="camera-button" class="button"></a>
+ <a id="delete-button" class="button"></a>
+ <a id="share-button" class="button"></a>
+ </footer>
+ </div>
+
+ <script type="text/javascript" src="/shared/js/l10n.js"></script>
+ <script type="text/javascript" src="/shared/js/l10n_date.js"></script>
+ <script type="text/javascript" src="/shared/js/async_storage.js"></script>
+ <script type="text/javascript" src="/shared/js/blobview.js"></script>
+ <script type="text/javascript" src="/shared/js/media/jpeg_metadata_parser.js"></script>
+ <script type="text/javascript" src="/shared/js/media/get_video_rotation.js"></script>
+ <script type="text/javascript" src="/shared/js/media/video_player.js"></script>
+ <script type="text/javascript" src="/shared/js/media/media_frame.js"></script>
+ <script type="text/javascript" src="/shared/js/gesture_detector.js"></script>
+ <script type="text/javascript" src="js/camera.js"></script>
+ <script type="text/javascript" src="js/filmstrip.js"></script>
+ </body>
+</html>
diff --git a/apps/system/camera/js/camera.js b/apps/system/camera/js/camera.js
new file mode 100644
index 0000000..25f12ae
--- /dev/null
+++ b/apps/system/camera/js/camera.js
@@ -0,0 +1,982 @@
+'use strict';
+
+// Utility functions
+function padLeft(num, length) {
+ var r = String(num);
+ while (r.length < length) {
+ r = '0' + r;
+ }
+ return r;
+}
+
+
+// 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
+var DCFApi = (function() {
+
+ var api = {};
+
+ 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'}
+ };
+
+ api.init = function() {
+
+ asyncStorage.getItem(dcfConfig.key, function(value) {
+
+ dcfConfigLoaded = true;
+ dcfConfig.seq = value ? value : defaultSeq;
+
+ // We have a previous call to createDCFFilename that is waiting for
+ // a response, fire it again
+ if (deferredArgs) {
+ var args = deferredArgs;
+ api.createDCFFilename(args.storage, args.type, args.callback);
+ deferredArgs = null;
+ }
+ });
+ };
+
+ api.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 filepath = 'DCIM/' + dcfConfig.seq.dir + dcfConfig.postFix + '/';
+ var filename = dcfConfig.prefix[type] +
+ padLeft(dcfConfig.seq.file, 4) + '.' +
+ dcfConfig.ext[type];
+
+ // 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 + filename);
+
+ // 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() {
+ api.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);
+ });
+ };
+ };
+
+ return api;
+
+})();
+
+var Camera = {
+ _cameras: null,
+ _camera: 0,
+ _captureMode: null,
+ _recording: false,
+
+ // In secure mode the user cannot browse to the gallery
+ _secureMode: window.parent !== window,
+ _currentOverlay: null,
+
+ CAMERA: 'camera',
+ VIDEO: 'video',
+
+ _videoTimer: null,
+ _videoStart: null,
+ _videoPath: null,
+
+ _autoFocusSupported: 0,
+ _manuallyFocused: false,
+
+ _timeoutId: 0,
+ _cameraObj: null,
+
+ _photosTaken: [],
+ _cameraProfile: null,
+
+ _resumeViewfinderTimer: null,
+ _waitingToGenerateThumb: false,
+
+ _styleSheet: document.styleSheets[0],
+ _orientationRule: null,
+ _phoneOrientation: 0,
+
+ _pictureStorage: null,
+ _videoStorage: null,
+ _storageState: null,
+
+ STORAGE_INIT: 0,
+ STORAGE_AVAILABLE: 1,
+ STORAGE_NOCARD: 2,
+ STORAGE_UNMOUNTED: 3,
+ STORAGE_CAPACITY: 4,
+
+ _pictureSize: null,
+ _previewPaused: false,
+ _previewActive: false,
+
+ PREVIEW_PAUSE: 500,
+ FILMSTRIP_DURATION: 5000, // show filmstrip for 5s before fading
+
+ _flashModes: [],
+ _currentFlashMode: 0,
+
+ _config: {
+ fileFormat: 'jpeg'
+ },
+
+ get _previewConfig() {
+ delete this._previewConfig;
+ return this._previewConfig = {
+ width: document.body.clientHeight,
+ height: document.body.clientWidth
+ };
+ },
+
+ _previewConfigVideo: {
+ profile: 'cif',
+ rotation: 0,
+ width: 352,
+ height: 288
+ },
+
+ _shutterKey: 'camera.shutter.enabled',
+ _shutterSound: null,
+ _shutterSoundEnabled: true,
+
+ PROMPT_DELAY: 2000,
+
+ _watchId: null,
+ _position: null,
+
+ _pendingPick: null,
+
+ // The minimum available disk space to start recording a video.
+ RECORD_SPACE_MIN: 1024 * 1024 * 2,
+
+ // Number of bytes left on disk to let us stop recording.
+ RECORD_SPACE_PADDING: 1024 * 1024 * 1,
+
+ // Maximum image resolution for still photos taken with camera
+ MAX_IMAGE_RES: 1600 * 1200, // Just under 2 megapixels
+
+ get overlayTitle() {
+ return document.getElementById('overlay-title');
+ },
+
+ get overlayText() {
+ return document.getElementById('overlay-text');
+ },
+
+ get overlay() {
+ return document.getElementById('overlay');
+ },
+
+ get viewfinder() {
+ return document.getElementById('viewfinder');
+ },
+
+ get switchButton() {
+ return document.getElementById('switch-button');
+ },
+
+ get captureButton() {
+ return document.getElementById('capture-button');
+ },
+
+ get galleryButton() {
+ return document.getElementById('gallery-button');
+ },
+
+ get videoTimer() {
+ return document.getElementById('video-timer');
+ },
+
+ get focusRing() {
+ return document.getElementById('focus-ring');
+ },
+
+ get toggleButton() {
+ return document.getElementById('toggle-camera');
+ },
+
+ get toggleFlashBtn() {
+ return document.getElementById('toggle-flash');
+ },
+
+ // We have seperated init and delayedInit as we want to make sure
+ // that on first launch we dont interfere and load the camera
+ // previewStream as fast as possible, once the previewStream is
+ // active we do the rest of the initialisation.
+ init: function() {
+ this.setCaptureMode(this.CAMERA);
+ this.loadCameraPreview(this._camera, this.delayedInit.bind(this));
+ },
+
+ delayedInit: function camera_delayedInit() {
+ // If we don't have any pending messages, show the usual UI
+ // Otherwise, determine which buttons to show once we get our
+ // activity message
+ if (!navigator.mozHasPendingMessage('activity')) {
+ this.galleryButton.classList.remove('hidden');
+ this.switchButton.classList.remove('hidden');
+ this.enableButtons();
+ }
+
+ // Dont let the phone go to sleep while the camera is
+ // active, user must manually close it
+ if (navigator.requestWakeLock) {
+ navigator.requestWakeLock('screen');
+ }
+
+ this.setToggleCameraStyle();
+
+ // We lock the screen orientation and deal with rotating
+ // the icons manually
+ var css = '#switch-button span, #capture-button span, ' +
+ '#gallery-button span { -moz-transform: rotate(0deg); }';
+ var insertId = this._styleSheet.cssRules.length - 1;
+ this._orientationRule = this._styleSheet.insertRule(css, insertId);
+ window.addEventListener('deviceorientation', this.orientChange.bind(this));
+
+ this.toggleButton.addEventListener('click', this.toggleCamera.bind(this));
+ this.toggleFlashBtn.addEventListener('click', this.toggleFlash.bind(this));
+ this.viewfinder.addEventListener('click', this.toggleFilmStrip.bind(this));
+
+ this.switchButton
+ .addEventListener('click', this.toggleModePressed.bind(this));
+ this.captureButton
+ .addEventListener('click', this.capturePressed.bind(this));
+ this.galleryButton
+ .addEventListener('click', this.galleryBtnPressed.bind(this));
+
+ if (!navigator.mozCameras) {
+ this.captureButton.setAttribute('disabled', 'disabled');
+ return;
+ }
+
+ if (this._secureMode) {
+ this.galleryButton.setAttribute('disabled', 'disabled');
+ }
+
+ this._shutterSound = new Audio('./resources/sounds/shutter.ogg');
+ this._shutterSound.mozAudioChannelType = 'publicnotification';
+
+ if ('mozSettings' in navigator) {
+ var req = navigator.mozSettings.createLock().get(this._shutterKey);
+ req.onsuccess = (function onsuccess() {
+ this._shutterSoundEnabled = req.result[this._shutterKey];
+ }).bind(this);
+
+ navigator.mozSettings.addObserver(this._shutterKey, (function(e) {
+ this._shutterSoundEnabled = e.settingValue;
+ }).bind(this));
+ }
+
+ this._storageState = this.STORAGE_INIT;
+
+ this._pictureStorage = navigator.getDeviceStorage('pictures');
+ this._videoStorage = navigator.getDeviceStorage('videos'),
+
+ this._pictureStorage
+ .addEventListener('change', this.deviceStorageChangeHandler.bind(this));
+
+ navigator.mozSetMessageHandler('activity', function(activity) {
+ var name = activity.source.name;
+ if (name === 'pick') {
+ Camera.initPick(activity);
+ }
+ else {
+ // We got another activity. Perhaps we were launched from gallery
+ // So show our usual buttons
+ Camera.galleryButton.classList.remove('hidden');
+ Camera.switchButton.classList.remove('hidden');
+ }
+ Camera.enableButtons();
+ });
+
+ DCFApi.init();
+ },
+
+ enableButtons: function camera_enableButtons() {
+ if (!this._pendingPick) {
+ this.switchButton.removeAttribute('disabled');
+ }
+ this.captureButton.removeAttribute('disabled');
+ },
+
+ disableButtons: function camera_disableButtons() {
+ this.switchButton.setAttribute('disabled', 'disabled');
+ this.captureButton.setAttribute('disabled', 'disabled');
+ },
+
+ // When inside an activity the user cannot switch between
+ // the gallery or video recording.
+ initPick: function camera_initPick(activity) {
+ this._pendingPick = activity;
+
+ // Hide the gallery and switch buttons, leaving only the shutter
+ this.galleryButton.classList.add('hidden');
+ this.switchButton.classList.add('hidden');
+
+ // Display the cancel button and add an event listener for it
+ var cancelButton = document.getElementById('cancel-pick');
+ cancelButton.classList.remove('hidden');
+ cancelButton.onclick = this.cancelPick.bind(this);
+ },
+
+ cancelPick: function camera_cancelPick() {
+ if (this._pendingPick) {
+ this._pendingPick.postError('pick cancelled');
+ }
+ this._pendingPick = null;
+ },
+
+ toggleModePressed: function camera_toggleCaptureMode(e) {
+ if (e.target.getAttribute('disabled')) {
+ return;
+ }
+
+ var newMode = (this.captureMode === this.CAMERA) ? this.VIDEO : this.CAMERA;
+ this.disableButtons();
+ this.setCaptureMode(newMode);
+
+ function gotPreviewStream(stream) {
+ this.viewfinder.mozSrcObject = stream;
+ this.viewfinder.play();
+ this.enableButtons();
+ }
+ if (this.captureMode === this.CAMERA) {
+ this._cameraObj.getPreviewStream(this._previewConfig,
+ gotPreviewStream.bind(this));
+ } else {
+ this._previewConfigVideo.rotation = this._phoneOrientation;
+ this._cameraObj.getPreviewStreamVideoMode(this._previewConfigVideo,
+ gotPreviewStream.bind(this));
+ }
+ },
+
+ toggleCamera: function camera_toggleCamera() {
+ this._camera = 1 - this._camera;
+ this.loadCameraPreview(this._camera, this.enableButtons.bind(this));
+ this.setToggleCameraStyle();
+ },
+
+ setToggleCameraStyle: function camera_setToggleCameraStyle() {
+ var modeName = this._camera === 0 ? 'back' : 'front';
+ this.toggleButton.setAttribute('data-mode', modeName);
+ },
+
+ toggleFlash: function camera_toggleFlash() {
+ if (this._currentFlashMode === this._flashModes.length - 1) {
+ this._currentFlashMode = 0;
+ } else {
+ this._currentFlashMode = this._currentFlashMode + 1;
+ }
+ this.setFlashMode();
+ },
+
+ setFlashMode: function camera_setFlashMode() {
+ var flashModeName = this._flashModes[this._currentFlashMode];
+ this.toggleFlashBtn.setAttribute('data-mode', flashModeName);
+ this._cameraObj.flashMode = flashModeName;
+ },
+
+ toggleRecording: function camera_toggleRecording() {
+ if (this._recording) {
+ this.stopRecording();
+ return;
+ }
+
+ this.startRecording();
+ },
+
+ startRecording: function camera_startRecording() {
+ var captureButton = this.captureButton;
+ var switchButton = this.switchButton;
+
+ var onerror = function() {
+ handleError('error-recording');
+ }
+ var onsuccess = (function onsuccess() {
+ document.body.classList.add('capturing');
+ captureButton.removeAttribute('disabled');
+ this._recording = true;
+ this.startRecordingTimer();
+
+ // Hide the filmstrip to prevent the users from
+ // entering the preview mode after Camera starts recording
+ if (Filmstrip.isShown())
+ Filmstrip.hide();
+
+ // User closed app while recording was trying to start
+ if (document.mozHidden) {
+ this.stopRecording();
+ }
+ }).bind(this);
+
+ var handleError = (function handleError(id) {
+ this.enableButtons();
+ alert(navigator.mozL10n.get(id + '-title') + '. ' +
+ navigator.mozL10n.get(id + '-text'));
+ }).bind(this);
+
+ this.disableButtons();
+
+ var startRecording = (function startRecording(freeBytes) {
+ if (freeBytes < this.RECORD_SPACE_MIN) {
+ handleError('nospace');
+ return;
+ }
+
+ var config = {
+ rotation: this._phoneOrientation,
+ maxFileSizeBytes: freeBytes - this.RECORD_SPACE_PADDING
+ };
+ this._cameraObj.startRecording(config,
+ this._videoStorage, this._videoPath,
+ onsuccess, onerror);
+ }).bind(this);
+
+ DCFApi.createDCFFilename(this._videoStorage, 'video', (function(path, name) {
+ this._videoPath = path + name;
+
+ // The CameraControl API will not automatically create directories
+ // for the new file if they do not exist, so write a dummy file
+ // to the same directory via DeviceStorage to ensure that the directory
+ // exists before recording starts.
+ var dummyblob = new Blob([''], {type: 'video/3gpp'});
+ var dummyfilename = path + '.' + name;
+ var req = this._videoStorage.addNamed(dummyblob, dummyfilename);
+ req.onerror = onerror;
+ req.onsuccess = (function fileCreated() {
+ this._videoStorage.delete(dummyfilename); // No need to wait for success
+ // Determine the number of bytes available on disk.
+ var spaceReq = this._videoStorage.freeSpace();
+ spaceReq.onerror = onerror;
+ spaceReq.onsuccess = function() {
+ startRecording(spaceReq.result);
+ }
+ }).bind(this);
+ }).bind(this));
+ },
+
+ startRecordingTimer: function camera_startRecordingTimer() {
+ this._videoStart = new Date().getTime();
+ this.videoTimer.textContent = this.formatTimer(0);
+ this._videoTimer =
+ window.setInterval(this.updateVideoTimer.bind(this), 1000);
+ },
+
+ updateVideoTimer: function camera_updateVideoTimer() {
+ var videoLength =
+ Math.round((new Date().getTime() - this._videoStart) / 1000);
+ this.videoTimer.textContent = this.formatTimer(videoLength);
+ },
+
+ stopRecording: function camera_stopRecording() {
+ this._cameraObj.stopRecording();
+ this._recording = false;
+ window.clearInterval(this._videoTimer);
+ this.enableButtons();
+ document.body.classList.remove('capturing');
+
+ // XXX
+ // I need some way to know when the camera is done writing this file
+ // currently I'm sending this to the filmstrip which is trying to
+ // determine its rotation and fails sometimes if the file is not
+ // yet complete. For now, I just defer for a second, but
+ // there ought to be a better way.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=817367
+ // Maybe I'll get a device storage callback... check this.
+ var videofile = this._videoPath;
+ setTimeout(function() {
+ Filmstrip.addVideo(videofile);
+ Filmstrip.show(Camera.FILMSTRIP_DURATION);
+ }, 1000);
+ },
+
+ formatTimer: function camera_formatTimer(time) {
+ var minutes = Math.floor(time / 60);
+ var seconds = Math.round(time % 60);
+ if (minutes < 60) {
+ return padLeft(minutes, 2) + ':' + padLeft(seconds, 2);
+ }
+ return '';
+ },
+
+ capturePressed: function camera_doCapture(e) {
+ if (e.target.getAttribute('disabled')) {
+ return;
+ }
+
+ if (this.captureMode === this.CAMERA) {
+ this.prepareTakePicture();
+ } else {
+ this.toggleRecording();
+ }
+ },
+
+ galleryBtnPressed: function camera_galleryBtnPressed() {
+ // 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._secureMode)
+ return;
+
+ // Launch the gallery with an activity
+ var a = new MozActivity({
+ name: 'browse',
+ data: {
+ type: 'photos'
+ }
+ });
+ },
+
+ orientChange: function camera_orientChange(e) {
+ // Orientation is 0 starting at 'natural portrait' increasing
+ // going clockwise
+ var orientation = (e.beta > 45) ? 180 :
+ (e.beta < -45) ? 0 :
+ (e.gamma < -45) ? 90 :
+ (e.gamma > 45) ? 270 : 0;
+
+ if (orientation !== this._phoneOrientation) {
+ var rule = this._styleSheet.cssRules[this._orientationRule];
+ // PLEASE DO SOMETHING KITTENS ARE DYING
+ // Setting MozRotate to 90 or 270 causes element to disappear
+ rule.style.MozTransform = 'rotate(' + -(orientation + 1) + 'deg)';
+ this._phoneOrientation = orientation;
+
+ Filmstrip.setOrientation(orientation);
+ }
+ },
+
+ setCaptureMode: function camera_setCaptureMode(mode) {
+ if (this.captureMode) {
+ document.body.classList.remove(this.captureMode);
+ }
+ this.captureMode = mode;
+ document.body.classList.add(mode);
+ },
+
+ toggleFilmStrip: function camera_toggleFilmStrip(ev) {
+ // We will just ignore
+ // because the filmstrip shouldn't be shown
+ // while Camera is recording
+ if (this._recording)
+ return;
+
+ if (Filmstrip.isShown())
+ Filmstrip.hide();
+ else
+ Filmstrip.show();
+ },
+
+ loadCameraPreview: function camera_loadCameraPreview(camera, callback) {
+
+ this.viewfinder.mozSrcObject = null;
+ this._timeoutId = 0;
+
+ var viewfinder = this.viewfinder;
+ var style = viewfinder.style;
+ var width = document.body.clientHeight;
+ var height = document.body.clientWidth;
+
+ style.top = ((width / 2) - (height / 2)) + 'px';
+ style.left = -((width / 2) - (height / 2)) + 'px';
+
+ var transform = 'rotate(90deg)';
+ var rotation;
+ if (camera == 1) {
+ /* backwards-facing camera */
+ transform += ' scale(-1, 1)';
+ rotation = 0;
+ } else {
+ /* forwards-facing camera */
+ rotation = 0;
+ }
+
+ style.MozTransform = transform;
+ style.width = width + 'px';
+ style.height = height + 'px';
+
+ this._cameras = navigator.mozCameras.getListOfCameras();
+ var options = {camera: this._cameras[this._camera]};
+
+ function gotPreviewScreen(stream) {
+ viewfinder.mozSrcObject = stream;
+ viewfinder.play();
+
+ if (callback) {
+ callback();
+ }
+
+ this._previewActive = true;
+ this.checkStorageSpace();
+ setTimeout(this.initPositionUpdate.bind(this), this.PROMPT_DELAY);
+ }
+
+ function gotCamera(camera) {
+ this._cameraObj = camera;
+ this._config.rotation = rotation;
+ this._autoFocusSupported =
+ camera.capabilities.focusModes.indexOf('auto') !== -1;
+ this._pictureSize =
+ this.pickPictureSize(camera.capabilities.pictureSizes);
+ this.enableCameraFeatures(camera.capabilities);
+ camera.onShutter = (function() {
+ if (this._shutterSoundEnabled) {
+ this._shutterSound.play();
+ }
+ }).bind(this);
+ camera.onRecorderStateChange = this.recordingStateChanged.bind(this);
+ if (this.captureMode === this.CAMERA) {
+ camera.getPreviewStream(this._previewConfig, gotPreviewScreen.bind(this));
+ } else {
+ this._previewConfigVideo.rotation = this._phoneOrientation;
+ this._cameraObj.getPreviewStreamVideoMode(this._previewConfigVideo,
+ gotPreviewScreen.bind(this));
+ }
+ }
+
+ // If there is already a camera, we would have to release it first.
+ if (this._cameraObj) {
+ this.release(function camera_release_callback() {
+ navigator.mozCameras.getCamera(options, gotCamera.bind(this));
+ });
+ } else {
+ navigator.mozCameras.getCamera(options, gotCamera.bind(this));
+ }
+ },
+
+ recordingStateChanged: function(msg) {
+ if (msg === 'FileSizeLimitReached') {
+ this.stopRecording();
+ alert(navigator.mozL10n.get('size-limit-reached'));
+ }
+ },
+
+ enableCameraFeatures: function camera_enableCameraFeatures(capabilities) {
+ if (this._cameras.length > 1) {
+ this.toggleButton.classList.remove('hidden');
+ } else {
+ this.toggleButton.classList.add('hidden');
+ }
+
+ this._flashModes = capabilities.flashModes;
+ if (this._flashModes) {
+ this.setFlashMode();
+ this.toggleFlashBtn.classList.remove('hidden');
+ } else {
+ this.toggleFlashBtn.classList.add('hidden');
+ }
+ },
+
+ startPreview: function camera_startPreview() {
+ this.viewfinder.play();
+ this.loadCameraPreview(this._camera, this.enableButtons.bind(this));
+ this._previewActive = true;
+ },
+
+ stopPreview: function camera_stopPreview() {
+ if (this._recording) {
+ this.stopRecording();
+ }
+ this.disableButtons();
+ this.viewfinder.pause();
+ this._previewActive = false;
+ this.viewfinder.mozSrcObject = null;
+ this.release();
+ },
+
+ resumePreview: function camera_resumePreview() {
+ this._cameraObj.resumePreview();
+ this._previewActive = true;
+ this.enableButtons();
+ },
+
+ restartPreview: function camera_restartPreview() {
+ this._resumeViewfinderTimer =
+ window.setTimeout(this.resumePreview.bind(this), this.PREVIEW_PAUSE);
+ },
+
+ takePictureSuccess: function camera_takePictureSuccess(blob) {
+ this._manuallyFocused = false;
+ this.hideFocusRing();
+ this.restartPreview();
+ DCFApi.createDCFFilename(this._pictureStorage, 'image', (function(path, name) {
+ var addreq = this._pictureStorage.addNamed(blob, path + name);
+ addreq.onsuccess = (function() {
+ if (this._pendingPick) {
+ // XXX: https://bugzilla.mozilla.org/show_bug.cgi?id=806503
+ // We ought to just be able to pass this blob to the activity.
+ // But there seems to be a bug with blob lifetimes and activities.
+ // So we'll get a new blob back out of device storage to ensure
+ // that we've got a file-backed blob instead of a memory-backed blob.
+ var getreq = this._pictureStorage.get(path + name);
+ getreq.onsuccess = (function() {
+ this._pendingPick.postResult({
+ type: 'image/jpeg',
+ blob: getreq.result
+ });
+ this.cancelPick();
+ }).bind(this);
+
+ return;
+ }
+
+ Filmstrip.addImage(path + name, blob);
+ Filmstrip.show(Camera.FILMSTRIP_DURATION);
+ this.checkStorageSpace();
+
+ }).bind(this);
+
+ addreq.onerror = function() {
+ alert(navigator.mozL10n.get('error-saving-title') + '. ' +
+ navigator.mozL10n.get('error-saving-text'));
+ };
+ }).bind(this));
+ },
+
+ hideFocusRing: function camera_hideFocusRing() {
+ this.focusRing.removeAttribute('data-state');
+ },
+
+ checkStorageSpace: function camera_checkStorageSpace() {
+ if (this.updateOverlay()) {
+ return;
+ }
+
+ // The first time we're called, we need to make sure that there
+ // is an sdcard and that it is mounted. (Subsequently the device
+ // storage change handler will track that.)
+ if (this._storageState === this.STORAGE_INIT) {
+ this._pictureStorage.available().onsuccess = (function(e) {
+ this.updateStorageState(e.target.result);
+ this.updateOverlay();
+ // Now call the parent method again, so that if the sdcard is
+ // available we will actually verify that there is enough space on it
+ this.checkStorageSpace();
+ }.bind(this));
+ return;
+ }
+
+ // Now verify that there is enough space to store a picture
+ // 4 bytes per pixel plus some room for a header should be more
+ // than enough for a JPEG image.
+ var MAX_IMAGE_SIZE =
+ (this._pictureSize.width * this._pictureSize.height * 4) + 4096;
+
+ this._pictureStorage.freeSpace().onsuccess = (function(e) {
+ // XXX
+ // If we ever enter this out-of-space condition, it looks like
+ // this code will never be able to exit. The user will have to
+ // quit the app and start it again. Just deleting files will
+ // not be enough to get back to the STORAGE_AVAILABLE state.
+ // To fix this, we need an else clause here, and also a change
+ // in the updateOverlay() method.
+ if (e.target.result < MAX_IMAGE_SIZE) {
+ this._storageState = this.STORAGE_CAPACITY;
+ }
+ this.updateOverlay();
+ }).bind(this);
+ },
+
+ deviceStorageChangeHandler: function camera_deviceStorageChangeHandler(e) {
+ switch (e.reason) {
+ case 'available':
+ case 'unavailable':
+ case 'shared':
+ this.updateStorageState(e.reason);
+ break;
+ }
+ this.checkStorageSpace();
+ },
+
+ updateStorageState: function camera_updateStorageState(state) {
+ switch (state) {
+ case 'available':
+ this._storageState = this.STORAGE_AVAILABLE;
+ break;
+ case 'unavailable':
+ this._storageState = this.STORAGE_NOCARD;
+ break;
+ case 'shared':
+ this._storageState = this.STORAGE_UNMOUNTED;
+ break;
+ }
+ },
+
+ updateOverlay: function camera_updateOverlay() {
+ if (this._storageState === this.STORAGE_INIT) {
+ return false;
+ }
+
+ if (this._storageState === this.STORAGE_AVAILABLE) {
+ // Preview may have previously been paused if storage
+ // was not available
+ if (!this._previewActive && !document.mozHidden) {
+ this.startPreview();
+ }
+ this.showOverlay(null);
+ return false;
+ }
+
+ switch (this._storageState) {
+ case this.STORAGE_NOCARD:
+ this.showOverlay('nocard');
+ break;
+ case this.STORAGE_UNMOUNTED:
+ this.showOverlay('pluggedin');
+ break;
+ case this.STORAGE_CAPACITY:
+ this.showOverlay('nospace2');
+ break;
+ }
+ if (this._previewActive) {
+ this.stopPreview();
+ }
+ return true;
+ },
+
+ prepareTakePicture: function camera_takePicture() {
+ this.disableButtons();
+ this.focusRing.setAttribute('data-state', 'focusing');
+ if (this._autoFocusSupported && !this._manuallyFocused) {
+ this._cameraObj.autoFocus(this.autoFocusDone.bind(this));
+ } else {
+ this.takePicture();
+ }
+ },
+
+ autoFocusDone: function camera_autoFocusDone(success) {
+ if (!success) {
+ this.enableButtons();
+ this.focusRing.setAttribute('data-state', 'fail');
+ window.setTimeout(this.hideFocusRing.bind(this), 1000);
+ return;
+ }
+ this.focusRing.setAttribute('data-state', 'focused');
+ this.takePicture();
+ },
+
+ takePicture: function camera_takePicture() {
+ this._config.rotation = this._phoneOrientation;
+ this._config.pictureSize = this._pictureSize;
+ if (this._position) {
+ this._config.position = this._position;
+ }
+ this._cameraObj
+ .takePicture(this._config, this.takePictureSuccess.bind(this));
+ },
+
+ showOverlay: function camera_showOverlay(id) {
+ this._currentOverlay = id;
+
+ if (id === null) {
+ this.overlay.classList.add('hidden');
+ return;
+ }
+
+ this.overlayTitle.textContent = navigator.mozL10n.get(id + '-title');
+ this.overlayText.textContent = navigator.mozL10n.get(id + '-text');
+ this.overlay.classList.remove('hidden');
+ },
+
+ pickPictureSize: function camera_pickPictureSize(pictureSizes) {
+ var maxRes = this.MAX_IMAGE_RES;
+ var size = pictureSizes.reduce(function(acc, size) {
+ var mp = size.width * size.height;
+ return (mp > acc.width * acc.height && mp <= maxRes) ? size : acc;
+ }, {width: 0, height: 0});
+
+ if (size.width === 0 && size.height === 0) {
+ return pictureSizes[0];
+ } else {
+ return size;
+ }
+ },
+
+ initPositionUpdate: function camera_initPositionUpdate() {
+ if (this._watchId || document.mozHidden) {
+ return;
+ }
+ this._watchId = navigator.geolocation
+ .watchPosition(this.updatePosition.bind(this));
+ },
+
+ updatePosition: function camera_updatePosition(position) {
+ this._position = {
+ timestamp: position.timestamp,
+ altitude: position.coords.altitude,
+ latitude: position.coords.latitude,
+ longitude: position.coords.longitude
+ };
+ },
+
+ cancelPositionUpdate: function camera_cancelPositionUpdate() {
+ navigator.geolocation.clearWatch(this._watchId);
+ this._watchId = null;
+ },
+
+ release: function camera_release(callback) {
+ if (!this._cameraObj)
+ return;
+
+ this._cameraObj.release(function cameraReleased() {
+ Camera._cameraObj = null;
+ if (callback)
+ callback.call(Camera);
+ }, function releaseError() {
+ console.warn('Camera: failed to release hardware?');
+ if (callback)
+ callback.call(Camera);
+ });
+ }
+};
+
+Camera.init();
+
+document.addEventListener('mozvisibilitychange', function() {
+ if (document.mozHidden) {
+ Camera.stopPreview();
+ Camera.cancelPick();
+ Camera.cancelPositionUpdate();
+ if (this._secureMode) // If the lockscreen is locked
+ Filmstrip.clear(); // then forget everything when closing camera
+ } else {
+ Camera.startPreview();
+ }
+});
+
+window.addEventListener('beforeunload', function() {
+ window.clearTimeout(Camera._timeoutId);
+ delete Camera._timeoutId;
+ Camera.viewfinder.mozSrcObject = null;
+});
diff --git a/apps/system/camera/js/filmstrip.js b/apps/system/camera/js/filmstrip.js
new file mode 100644
index 0000000..d428801
--- /dev/null
+++ b/apps/system/camera/js/filmstrip.js
@@ -0,0 +1,568 @@
+/*
+ * filmstrip.js: filmstrip, thumbnails and previews for the camera.
+ */
+
+'use strict';
+
+var Filmstrip = (function() {
+
+ // This array holds all the data we need for image and video previews
+ var items = [];
+ var currentItemIndex;
+
+ // Maximum number of thumbnails in the filmstrip
+ var MAX_THUMBNAILS = 5;
+ var THUMBNAIL_WIDTH = 46; // size of each thumbnail
+ var THUMBNAIL_HEIGHT = 46;
+
+ // Timer for auto-hiding the filmstrip
+ var hideTimer = null;
+
+ // Document elements we care about
+ var filmstrip = document.getElementById('filmstrip');
+ var preview = document.getElementById('preview');
+ var frameContainer = document.getElementById('frame-container');
+ var mediaFrame = document.getElementById('media-frame');
+ var cameraButton = document.getElementById('camera-button');
+ var shareButton = document.getElementById('share-button');
+ var deleteButton = document.getElementById('delete-button');
+ var filmstripGalleryButton =
+ document.getElementById('filmstrip-gallery-button');
+
+ // Offscreen elements for generating thumbnails with
+ var offscreenImage = new Image();
+ var offscreenVideo = document.createElement('video');
+
+ // Set up event handlers
+ cameraButton.onclick = returnToCameraMode;
+ deleteButton.onclick = deleteCurrentItem;
+ shareButton.onclick = shareCurrentItem;
+ filmstripGalleryButton.onclick = Camera.galleryBtnPressed;
+ mediaFrame.addEventListener('dbltap', handleDoubleTap);
+ mediaFrame.addEventListener('transform', handleTransform);
+ mediaFrame.addEventListener('pan', handlePan);
+ mediaFrame.addEventListener('swipe', handleSwipe);
+
+ // Generate gesture events
+ var gestureDetector = new GestureDetector(mediaFrame);
+ gestureDetector.startDetecting();
+
+ // Create the MediaFrame for previews
+ var frame = new MediaFrame(mediaFrame);
+
+ // Start off with it positioned correctly.
+ setOrientation(Camera._phoneOrientation);
+
+ // If we're running in secure mode, we never want the user to see the
+ // gallery button or the share button.
+ filmstrip.removeChild(filmstripGalleryButton);
+ shareButton.parentNode.removeChild(shareButton);
+
+ function isShown() {
+ return !filmstrip.classList.contains('hidden');
+ }
+
+ function hide() {
+ filmstrip.classList.add('hidden');
+ if (hideTimer) {
+ clearTimeout(hideTimer);
+ hideTimer = null;
+ }
+ }
+
+ /*
+ * With a time, show the filmstrip and then hide it after the time is up.
+ * Without time, show until hidden.
+ * Tapping in the camera toggles it. And if toggled on, it will be on
+ * without a timer.
+ * It is always on when a preview is shown.
+ * After recording a photo or video, it is shown for 5 seconds.
+ * And it is also shown for 5 seconds after leaving preview mode.
+ */
+ function show(time) {
+ filmstrip.classList.remove('hidden');
+ if (hideTimer) {
+ clearTimeout(hideTimer);
+ hideTimer = null;
+ }
+ if (time)
+ hideTimer = setTimeout(hide, time);
+ }
+
+ filmstrip.onclick = function(event) {
+ var target = event.target;
+ if (!target || !target.classList.contains('thumbnail'))
+ return;
+
+ var index = parseInt(target.dataset.index);
+ previewItem(index);
+ // If we're showing previews be sure we're showing the filmstrip
+ // with no timeout and be sure that the viewfinder video is paused.
+ show();
+ Camera.viewfinder.pause();
+ // If there is a preview shown, we want the gallery button in
+ // the filmstrip
+ filmstripGalleryButton.classList.remove('hidden');
+ };
+
+ function previewItem(index) {
+ // Don't redisplay the item if it is already displayed
+ if (currentItemIndex === index)
+ return;
+
+ var item = items[index];
+
+ if (item.isImage) {
+ frame.displayImage(item.blob, item.width, item.height, item.preview);
+ }
+ else if (item.isVideo) {
+ frame.displayVideo(item.blob, item.width, item.height, item.rotation);
+ }
+
+ preview.classList.remove('offscreen');
+ currentItemIndex = index;
+
+ // Highlight the border of the thumbnail we're previewing
+ // and clear the highlight on all others
+ items.forEach(function(item, itemindex) {
+ if (itemindex === index)
+ item.element.classList.add('previewed');
+ else
+ item.element.classList.remove('previewed');
+ });
+ }
+
+ function returnToCameraMode() {
+ Camera.viewfinder.play(); // Restart the viewfinder
+ show(Camera.FILMSTRIP_DURATION); // Fade the filmstrip after a delay
+ // hide the gallery button in the filmstrip
+ filmstripGalleryButton.classList.add('hidden');
+ preview.classList.add('offscreen');
+ frame.clear();
+ if (items.length > 0)
+ items[currentItemIndex].element.classList.remove('previewed');
+ currentItemIndex = null;
+ }
+
+ function deleteCurrentItem() {
+ var item = items[currentItemIndex];
+ var msg, storage, filename;
+
+ if (item.isImage) {
+ msg = navigator.mozL10n.get('delete-photo?');
+ storage = Camera._pictureStorage;
+ filename = item.filename;
+ }
+ else {
+ msg = navigator.mozL10n.get('delete-video?');
+ storage = Camera._videoStorage;
+ filename = item.filename;
+ }
+
+ // The system app is not allowed to use confirm, I think
+ // so if we're running in secure mode, just delete the file without
+ // confirmation
+ if (Camera._secureMode || confirm(msg)) {
+ // Remove the item from the array of items
+ items.splice(currentItemIndex, 1);
+
+ // Remove the thumbnail image from the filmstrip
+ filmstrip.removeChild(item.element);
+ URL.revokeObjectURL(item.element.src);
+
+ // Renumber the item elements
+ items.forEach(function(item, index) {
+ item.element.dataset.index = index;
+ });
+
+ // If there are no more items, go back to the camera
+ if (items.length === 0) {
+ returnToCameraMode();
+ }
+ else {
+ // Otherwise, switch the frame to display the next item. But if
+ // we just deleted the last item, then we'll need to display the
+ // previous item.
+ var newindex = currentItemIndex;
+ if (newindex >= items.length)
+ newindex = items.length - 1;
+ currentItemIndex = null;
+ previewItem(newindex);
+ }
+
+ // Actually delete the file
+ storage.delete(filename).onerror = function(e) {
+ console.warn('Failed to delete', filename,
+ 'from DeviceStorage:', e.target.error);
+ }
+ }
+ }
+
+ function shareCurrentItem() {
+ if (Camera._secureMode)
+ return;
+ var item = items[currentItemIndex];
+ var type = item.isImage ? 'image/*' : 'video/*';
+ var nameonly = item.filename.substring(item.filename.lastIndexOf('/') + 1);
+ var activity = new MozActivity({
+ name: 'share',
+ data: {
+ type: type,
+ number: 1,
+ blobs: [item.blob],
+ filenames: [nameonly],
+ filepaths: [item.filename] /* temporary hack for bluetooth app */
+ }
+ });
+ activity.onerror = function(e) {
+ console.warn('Share activity error:', activity.error.name);
+ }
+ }
+
+ function handleDoubleTap(e) {
+ if (!items[currentItemIndex].isImage)
+ return;
+
+ var scale;
+ if (frame.fit.scale > frame.fit.baseScale)
+ scale = frame.fit.baseScale / frame.fit.scale;
+ else
+ scale = 2;
+
+ // If the phone orientation is 0 (unrotated) then the gesture detector's
+ // event coordinates match what's on the screen, and we use them to
+ // specify a point to zoom in or out on. For other orientations we could
+ // calculate the correct point, but instead just use the midpoint.
+ var x, y;
+ if (Camera._phoneOrientation === 0) {
+ x = e.detail.clientX;
+ y = e.detail.clientY;
+ }
+ else {
+ x = mediaFrame.offsetWidth / 2;
+ y = mediaFrame.offsetHeight / 2;
+ }
+
+ frame.zoom(scale, x, y, 200);
+ }
+
+ function handleTransform(e) {
+ if (!items[currentItemIndex].isImage)
+ return;
+
+ // If the phone orientation is 0 (unrotated) then the gesture detector's
+ // event coordinates match what's on the screen, and we use them to
+ // specify a point to zoom in or out on. For other orientations we could
+ // calculate the correct point, but instead just use the midpoint.
+ var x, y;
+ if (Camera._phoneOrientation === 0) {
+ x = e.detail.midpoint.clientX;
+ y = e.detail.midpoint.clientY;
+ }
+ else {
+ x = mediaFrame.offsetWidth / 2;
+ y = mediaFrame.offsetHeight / 2;
+ }
+
+ frame.zoom(e.detail.relative.scale, x, y);
+ }
+
+ function handlePan(e) {
+ if (!items[currentItemIndex].isImage)
+ return;
+
+ // The gesture detector event does not take our CSS rotation into
+ // account, so we have to pan by a dx and dy that depend on how
+ // the MediaFrame is rotated
+ var dx, dy;
+ switch (Camera._phoneOrientation) {
+ case 0:
+ dx = e.detail.relative.dx;
+ dy = e.detail.relative.dy;
+ break;
+ case 90:
+ dx = -e.detail.relative.dy;
+ dy = e.detail.relative.dx;
+ break;
+ case 180:
+ dx = -e.detail.relative.dx;
+ dy = -e.detail.relative.dy;
+ break;
+ case 270:
+ dx = e.detail.relative.dy;
+ dy = -e.detail.relative.dx;
+ break;
+ }
+
+ frame.pan(dx, dy);
+ }
+
+ function handleSwipe(e) {
+ // Because the stuff around the media frame does not change position
+ // when the phone is rotated, we don't alter these directions based
+ // on orientation. To dismiss the preview, the user always swipes toward
+ // the filmstrip.
+
+ switch (e.detail.direction) {
+ case 'up': // close the preview if the swipe is fast enough
+ if (e.detail.vy < -1)
+ returnToCameraMode();
+ break;
+ case 'left': // go to next image if fast enough
+ if (e.detail.vx < -1 && currentItemIndex < items.length - 1)
+ previewItem(currentItemIndex + 1);
+ break;
+ case 'right': // go to previous image if fast enough
+ if (e.detail.vx > 1 && currentItemIndex > 0)
+ previewItem(currentItemIndex - 1);
+ break;
+ }
+ }
+
+ function addImage(filename, blob) {
+ parseJPEGMetadata(blob, function getPreviewBlob(metadata) {
+ if (metadata.preview) {
+ var previewBlob = blob.slice(metadata.preview.start,
+ metadata.preview.end,
+ 'image/jpeg');
+
+ offscreenImage.src = URL.createObjectURL(previewBlob);
+ offscreenImage.onload = function() {
+ createThumbnailFromElement(offscreenImage, false, 0,
+ function(thumbnail) {
+ addItem({
+ isImage: true,
+ filename: filename,
+ thumbnail: thumbnail,
+ blob: blob,
+ width: metadata.width,
+ height: metadata.height,
+ preview: metadata.preview
+ });
+ });
+ URL.revokeObjectURL(offscreenImage.src);
+ offscreenImage.onload = null;
+ offscreenImage.src = null;
+ };
+ }
+ }, function logerr(msg) { console.warn(msg); });
+ }
+
+ function addVideo(filename) {
+ var request = Camera._videoStorage.get(filename);
+ request.onerror = function() {
+ console.warn('addVideo:', filename, request.error.name);
+ };
+ request.onsuccess = function() {
+ var blob = request.result;
+ getVideoRotation(blob, function(rotation) {
+ if (typeof rotation !== 'number') {
+ console.warn('Unexpected rotation:', rotation);
+ rotation = 0;
+ }
+
+ var url = URL.createObjectURL(blob);
+
+ offscreenVideo.preload = 'metadata';
+ offscreenVideo.style.width = THUMBNAIL_WIDTH + 'px';
+ offscreenVideo.style.height = THUMBNAIL_HEIGHT + 'px';
+ offscreenVideo.src = url;
+
+ offscreenVideo.onerror = function() {
+ URL.revokeObjectURL(url);
+ offscreenVideo.onerror = null;
+ offscreenVideo.onloadedmetadata = null;
+ offscreenVideo.removeAttribute('src');
+ offscreenVideo.load();
+ console.warn('not a video file', filename);
+ }
+
+ offscreenVideo.onloadedmetadata = function() {
+ createThumbnailFromElement(offscreenVideo, true, rotation,
+ function(thumbnail) {
+ addItem({
+ isVideo: true,
+ filename: filename,
+ thumbnail: thumbnail,
+ blob: blob,
+ width: offscreenVideo.videoWidth,
+ height: offscreenVideo.videoHeight,
+ rotation: rotation
+ });
+ });
+ URL.revokeObjectURL(url);
+ offscreenVideo.onerror = null;
+ offscreenVideo.onloadedmetadata = null;
+ offscreenVideo.removeAttribute('src');
+ offscreenVideo.load();
+ };
+ });
+ };
+ }
+
+ // Add a thumbnail to the filmstrip.
+ // The details object contains everything we need to know
+ // to display the thumbnail and preview the image or video
+ function addItem(item) {
+ // Thumbnails go from most recent to least recent.
+ items.unshift(item);
+
+ // Create an image element for this new thumbnail and display it
+ item.element = new Image();
+ item.element.src = URL.createObjectURL(item.thumbnail);
+ item.element.classList.add('thumbnail');
+ filmstrip.insertBefore(item.element, filmstrip.firstElementChild);
+
+ // If we have too many thumbnails now, remove the oldest one from
+ // the array, and remove its element from the filmstrip and release
+ // its blob url
+ if (items.length > MAX_THUMBNAILS) {
+ var oldest = items.pop();
+ filmstrip.removeChild(oldest.element);
+ URL.revokeObjectURL(oldest.element.src);
+ }
+
+ // Now update the index associated with each of the remaining elements
+ // so that the click event handle knows which one it clicked on
+ items.forEach(function(item, index) {
+ item.element.dataset.index = index;
+ });
+ }
+
+ // Remove all items from the filmstrip. Don't delete the files, but
+ // forget all of our state. This also exits preview mode if we're in it.
+ function clear() {
+ if (!preview.classList.contains('offscreen'))
+ returnToCameraMode();
+ items.forEach(function(item) {
+ filmstrip.removeChild(item.element);
+ URL.revokeObjectURL(item.element.src);
+ });
+ items.length = 0;
+ }
+
+ // 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.
+ function createThumbnailFromElement(elt, video, rotation, callback) {
+ // Create a thumbnail image
+ var canvas = document.createElement('canvas');
+ var context = canvas.getContext('2d');
+ canvas.width = THUMBNAIL_WIDTH;
+ canvas.height = THUMBNAIL_HEIGHT;
+ var eltwidth = video ? elt.videoWidth : elt.width;
+ var eltheight = video ? elt.videoHeight : elt.height;
+ var scalex = canvas.width / eltwidth;
+ var scaley = canvas.height / eltheight;
+
+ // Take the larger of the two scales: we crop the image to the thumbnail
+ var scale = Math.max(scalex, scaley);
+
+ // Calculate the region of the image that will be copied to the
+ // canvas to create the thumbnail
+ var w = Math.round(THUMBNAIL_WIDTH / scale);
+ var h = Math.round(THUMBNAIL_HEIGHT / scale);
+ var x = Math.round((eltwidth - w) / 2);
+ var y = Math.round((eltheight - h) / 2);
+
+ // If a rotation is specified, rotate the canvas context
+ if (rotation) {
+ context.save();
+ switch (rotation) {
+ case 90:
+ context.translate(THUMBNAIL_WIDTH, 0);
+ context.rotate(Math.PI / 2);
+ break;
+ case 180:
+ context.translate(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
+ context.rotate(Math.PI);
+ break;
+ case 270:
+ context.translate(0, THUMBNAIL_HEIGHT);
+ context.rotate(-Math.PI / 2);
+ break;
+ }
+ }
+
+ // Draw that region of the image into the canvas, scaling it down
+ context.drawImage(elt, x, y, w, h,
+ 0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
+
+ // Restore the default rotation so the play arrow comes out correctly
+ if (rotation) {
+ context.restore();
+ }
+
+ // If this is a video, superimpose a translucent play button over
+ // the captured video frame to distinguish it from a still photo thumbnail
+ if (video) {
+ // First draw a transparent gray circle
+ context.fillStyle = 'rgba(0, 0, 0, .3)';
+ context.beginPath();
+ context.arc(THUMBNAIL_WIDTH / 2, THUMBNAIL_HEIGHT / 2,
+ THUMBNAIL_HEIGHT / 3, 0, 2 * Math.PI, false);
+ context.fill();
+
+ // Now outline the circle in white
+ context.strokeStyle = 'rgba(255,255,255,.6)';
+ context.lineWidth = 2;
+ context.stroke();
+
+ // And add a white play arrow.
+ context.beginPath();
+ context.fillStyle = 'rgba(255,255,255,.6)';
+ // The height of an equilateral triangle is sqrt(3)/2 times the side
+ var side = THUMBNAIL_HEIGHT / 3;
+ var triangle_height = side * Math.sqrt(3) / 2;
+ context.moveTo(THUMBNAIL_WIDTH / 2 + triangle_height * 2 / 3,
+ THUMBNAIL_HEIGHT / 2);
+ context.lineTo(THUMBNAIL_WIDTH / 2 - triangle_height / 3,
+ THUMBNAIL_HEIGHT / 2 - side / 2);
+ context.lineTo(THUMBNAIL_WIDTH / 2 - triangle_height / 3,
+ THUMBNAIL_HEIGHT / 2 + side / 2);
+ context.closePath();
+ context.fill();
+ }
+
+ canvas.toBlob(callback, 'image/jpeg');
+ }
+
+ function setOrientation(orientation) {
+ preview.dataset.orientation = orientation;
+ filmstrip.dataset.orientation = orientation;
+ mediaFrame.dataset.orientation = orientation;
+
+ // When we rotate the media frame, we also have to change its size
+ var containerWidth = frameContainer.offsetWidth;
+ var containerHeight = frameContainer.offsetHeight;
+ if (orientation === 0 || orientation === 180) {
+ mediaFrame.style.width = containerWidth + 'px';
+ mediaFrame.style.height = containerHeight + 'px';
+ mediaFrame.style.top = 0 + 'px';
+ mediaFrame.style.left = 0 + 'px';
+ }
+ else {
+ mediaFrame.style.width = containerHeight + 'px';
+ mediaFrame.style.height = containerWidth + 'px';
+ mediaFrame.style.top = ((containerHeight - containerWidth) / 2) + 'px';
+ mediaFrame.style.left = ((containerWidth - containerHeight) / 2) + 'px';
+ }
+
+ // And rotate so this new size fills the screen
+ mediaFrame.style.transform = 'rotate(-' + orientation + 'deg)';
+
+ // And we have to resize the frame (and its video player)
+ frame.resize();
+ frame.video.setPlayerSize();
+ }
+
+ return {
+ isShown: isShown,
+ hide: hide,
+ show: show,
+ addImage: addImage,
+ addVideo: addVideo,
+ clear: clear,
+ setOrientation: setOrientation
+ };
+}());
diff --git a/apps/system/camera/locales/camera.ar.properties b/apps/system/camera/locales/camera.ar.properties
new file mode 100644
index 0000000..9ca7690
--- /dev/null
+++ b/apps/system/camera/locales/camera.ar.properties
@@ -0,0 +1,19 @@
+nospace2-title = لا توجد مساحة كافية على بطاقة الذاكرة
+nospace2-text = حرِّر مساحة بحذف ملفات.
+
+error-saving-title = لم يتم حفظ الصورة
+error-saving-text = حدث خطأ منع الكاميرا من حفظ الصورة.
+
+error-recording-title = لم يتم تصوير الفيديو
+error-recording-text = هنالك خطأ منع الكاميرا من تصوير الفيديو
+
+nocard-title = لم يتم العثور على بطاقة الذاكرة
+nocard-text = أدخل بطاقة الذاكرة لأخذ صور.
+
+pluggedin-title = لا يمكن استخدام الكاميرا والجوَّال موصول.
+pluggedin-text = إفصل الجوَّال لعرض الصور.
+
+size-limit-reached = لا توجد مساحة كافية على بطاقة الذاكرة
+
+delete-photo? = حذف الصورة ؟
+delete-video? = حذف الفيديو؟
diff --git a/apps/system/camera/locales/camera.en-US.properties b/apps/system/camera/locales/camera.en-US.properties
new file mode 100644
index 0000000..90b318d
--- /dev/null
+++ b/apps/system/camera/locales/camera.en-US.properties
@@ -0,0 +1,19 @@
+nospace2-title = Not enough space on memory card
+nospace2-text = Try freeing up space by deleting media.
+
+error-saving-title = Picture not saved
+error-saving-text = An error prevented Camera from saving the picture.
+
+error-recording-title = Video not recorded
+error-recording-text = An error prevented Camera from recording the video.
+
+nocard-title = No memory card found
+nocard-text = Insert a memory card to take pictures.
+
+pluggedin-title = Camera can not be used while plugged in
+pluggedin-text = Unplug the phone to view pictures.
+
+size-limit-reached = You have run out of space on your SD card.
+
+delete-photo? = Delete photo?
+delete-video? = Delete video?
diff --git a/apps/system/camera/locales/camera.fr.properties b/apps/system/camera/locales/camera.fr.properties
new file mode 100644
index 0000000..56d7d3b
--- /dev/null
+++ b/apps/system/camera/locales/camera.fr.properties
@@ -0,0 +1,19 @@
+nospace2-title = Espace libre sur la carte mémoire insuffisant
+nospace2-text = Essayez de libérer de l’espace en supprimant des médias.
+
+error-saving-title = Photo non sauvegardée
+error-saving-text = Une erreur a empêché l’application Photo de sauvegarder la photo.
+
+error-recording-title = Vidéo non enregistrée
+error-recording-text = Une erreur a empêché l’application Photo d’enregistrer la vidéo.
+
+nocard-title = Aucune carte mémoire trouvée
+nocard-text = Pour prendre des photos, insérez une carte mémoire.
+
+pluggedin-title = L’application Photo ne peut pas être utilisée tant que le téléphone est branché
+pluggedin-text = Pour afficher les photos, débranchez le téléphone.
+
+size-limit-reached = Vous n’avez pas assez d’espace sur votre carte SD.
+
+delete-photo? = Supprimer la photo ?
+delete-video? = Supprimer la vidéo ?
diff --git a/apps/system/camera/locales/camera.zh-TW.properties b/apps/system/camera/locales/camera.zh-TW.properties
new file mode 100644
index 0000000..9cf042a
--- /dev/null
+++ b/apps/system/camera/locales/camera.zh-TW.properties
@@ -0,0 +1,20 @@
+nospace2-title = 記憶卡的空間不足
+nospace2-text = 刪除媒體檔案以釋放可用空間。
+
+error-saving-title = 相片未儲存
+error-saving-text = 發生錯誤,相機無法儲存相片。
+
+error-recording-title = 影片未紀錄
+error-recording-text = 有錯誤發生使照相機無法錄製影片。
+
+nocard-title = 找不到記憶卡
+nocard-text = 插入記憶卡以拍照。
+
+pluggedin-title = 連接到電腦時無法使用相機
+pluggedin-text = 拔線以檢視照片。
+
+size-limit-reached = 您已用盡 SD 卡上的空間。
+
+delete-photo? = 刪除相片?
+delete-video? = 刪除影片?
+
diff --git a/apps/system/camera/locales/locales.ini b/apps/system/camera/locales/locales.ini
new file mode 100644
index 0000000..93f5cbc
--- /dev/null
+++ b/apps/system/camera/locales/locales.ini
@@ -0,0 +1,11 @@
+@import url(camera.en-US.properties)
+
+[ar]
+@import url(camera.ar.properties)
+
+[fr]
+@import url(camera.fr.properties)
+
+[zh-TW]
+@import url(camera.zh-TW.properties)
+
diff --git a/apps/system/camera/resources/sounds/shutter.ogg b/apps/system/camera/resources/sounds/shutter.ogg
new file mode 100644
index 0000000..f0c67d6
--- /dev/null
+++ b/apps/system/camera/resources/sounds/shutter.ogg
Binary files differ
diff --git a/apps/system/camera/style/VideoPlayer.css b/apps/system/camera/style/VideoPlayer.css
new file mode 100644
index 0000000..bc2b23c
--- /dev/null
+++ b/apps/system/camera/style/VideoPlayer.css
@@ -0,0 +1,152 @@
+/* styles for the video element itself */
+.videoPlayer {
+ position: absolute;
+ left: 0; /* we position it with a transform */
+ top:0;
+ transform-origin: 0 0;
+}
+
+/* video player controls */
+.videoPlayerControls {
+ position: absolute;
+ left: 0px;
+ right: 0px;
+ top: 0px;
+ bottom: 0px;
+ margin: 0;
+ padding: 0;
+}
+
+.videoPlayerPlayButton {
+ position: absolute;
+ width: 106px;
+ height: 106px;
+ left: calc(50% - 53px);
+ top: calc(50% - 53px);
+ background: url("images/video_play_button.png") center no-repeat,
+ url("images/video_play_normal.png") center no-repeat;
+ border-width: 0;
+}
+
+.videoPlayerPlayButton:active {
+ background: url("images/video_play_button.png") center no-repeat,
+ url("images/video_play_focus.png") center no-repeat
+}
+
+.videoPlayerPlayButton.hidden {
+ opacity: 0;
+}
+
+.videoPlayerFooter {
+ position: absolute;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
+ height: 50px;
+ margin: 0;
+ padding: 0;
+ background-color: rgba(0, 0, 0, 0.3);
+ overflow: hidden;
+ opacity: 1;
+ transition: opacity 0.5s;
+ font-family: "MozTT", sans-serif;
+ -moz-user-select: none;
+}
+
+.videoPlayerFooter.hidden {
+ opacity: 0;
+ pointer-events: none;
+}
+
+.videoPlayerPauseButton {
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ padding: 0;
+ margin: 0;
+ background: url("images/video_pause_button.png") center no-repeat,
+ rgba(0,0,0,.5);
+ border-radius: 53px;
+ border: solid #ccc 3px;
+ top: -25px;
+ left: 10px;
+}
+
+.videoPlayerPauseButton:active {
+ background: url("images/video_pause_button.png") center no-repeat,
+ url("images/video_play_focus.png") center no-repeat
+}
+
+button::-moz-focus-inner {
+ padding: 0;
+ border: none;
+}
+
+/* time slider */
+.videoPlayerSlider {
+ position: absolute;
+ left: 110px;
+ top: 0px;
+ right: 0px;
+ height: 100%;
+}
+
+.videoPlayerSlider > span {
+ display: block;
+ width: 45px;
+ position: absolute;
+ color: white;
+ height: 100%;
+ line-height: 50px;
+ text-align: center;
+ font-size: 15px;
+}
+
+.videoPlayerElapsedText {
+ left: 10px;
+}
+
+.videoPlayerDurationText {
+ right: 10px;
+}
+
+.videoPlayerProgress {
+ position: absolute;
+ top: 0px;
+ left: 70px;
+ right: 70px;
+ height: 100%;
+}
+
+.videoPlayerProgress > div {
+ position: absolute;
+ pointer-events: none;
+}
+
+.videoPlayerElapsedBar, .videoPlayerBackgroundBar {
+ height: 4px;
+ width: 0%;
+ top: 50%;
+ margin-top: -2px;
+ border-radius: 6px;
+}
+
+.videoPlayerElapsedBar {
+ background-color: #0ac;
+}
+
+.videoPlayerBackgroundBar {
+ background-color: #333;
+ width: 100%;
+}
+
+.videoPlayerPlayHead {
+ display: block;
+ height: 20px;
+ width: 25px;
+ border-radius: 25px;
+ background-color: white;
+ top: 50%;
+ margin: -10px 0 0 -12px;
+}
+
diff --git a/apps/system/camera/style/camera.css b/apps/system/camera/style/camera.css
new file mode 100644
index 0000000..6a1ca02
--- /dev/null
+++ b/apps/system/camera/style/camera.css
@@ -0,0 +1,314 @@
+html, body {
+ font-family: "MozTT", sans-serif;
+ font-size: 10px;
+ height: 100%;
+ width: 100%;
+ padding: 0;
+ margin: 0;
+ overflow: hidden;
+ background-color: black;
+}
+
+#viewfinder {
+ position: absolute;
+ z-index: 25;
+}
+
+#controls {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ height: 45px;
+ z-index: 50;
+ background-color: rgba(0, 0, 0, 0.8);
+ overflow: hidden;
+}
+
+#switch-button, #capture-button, #misc-button {
+ position: absolute;
+}
+
+#switch-button, #misc-button {
+ height: 45px;
+ width: 33%;
+}
+
+#switch-button span,
+#capture-button span,
+#gallery-button span,
+#cancel-pick span
+{
+ -moz-transition: 0.2s ease-in-out;
+ pointer-events: none;
+ background-position: center center;
+ background-repeat: no-repeat;
+ display: block;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-left: -15px;
+ margin-top: -15px;
+ width: 30px;
+ height: 30px;
+}
+
+#switch-button {
+ left: 66%;
+}
+
+#misc-button {
+ text-align: center;
+ left: 0;
+}
+
+#video-timer {
+ position:relative;
+ top:50%;
+ margin-top:-0.5em;
+}
+
+#gallery-button {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+#gallery-button.hidden {
+ display:none;
+}
+
+#gallery-button span {
+ background-image: url(images/grid.png);
+}
+
+#gallery-button[disabled=disabled] {
+ display: none;
+}
+
+#cancel-pick {
+ display:block;
+ width: 100%;
+ height: 100%;
+}
+
+#cancel-pick.hidden {
+ display:none
+}
+
+#cancel-pick span {
+ background-image: url(images/actionicon_cancel.png);
+}
+
+#capture-button[disabled=disabled] {
+ opacity: 0.5;
+}
+
+#switch-button[disabled=disabled] {
+ opacity: 0.5;
+}
+
+#capture-button {
+ background-color: #03a2b4;
+ border-radius: 100px;
+ left: 33%;
+ height: 100px;
+ width: 33%;
+ top: -28px;
+}
+
+#video-timer {
+ display: none;
+ color: white;
+}
+
+/* Specific to when we are capturing video */
+.capturing #video-timer {
+ display: block;
+}
+
+.capturing #gallery-button {
+ display: none;
+}
+
+.capturing #capture-button {
+ background-color: #d3361c;
+}
+
+.video.capturing #capture-button span {
+ background-image: url(images/stop.png);
+}
+
+/* Swap the camera and video icons depending on mode */
+.video #switch-button span {
+ background-image: url(images/camera.png);
+}
+
+.camera #switch-button span {
+ background-image: url(images/video.png);
+}
+
+.camera #capture-button span {
+ background-image: url(images/camera.png);
+}
+
+.video #capture-button span {
+ background-image: url(images/video.png);
+}
+
+#focus-ring {
+ position: absolute;
+ z-index: 100;
+ display: none;
+ width: 50px;
+ height: 50px;
+ border-radius: 50px;
+ top: 50%;
+ left: 50%;
+ margin-top: -30px;
+ margin-left: -30px;
+}
+
+#focus-ring[data-state=focused] {
+ border: 4px solid rgba(0, 255, 0, 0.3);
+ display: block;
+}
+
+#focus-ring[data-state=focusing] {
+ border: 4px solid rgba(0, 0, 0, 0.8);
+ display: block;
+}
+
+#focus-ring[data-state=fail] {
+ border: 4px solid rgba(255, 0, 0, 0.3);
+ display: block;
+}
+
+/*
+ * The overlay is where we display messages like Scanning, No Videos,
+ * No SD card and SD Card in Use along with instructions for resolving
+ * the issue. The user can't interact with the app while the overlay
+ * is displayed.
+ */
+#overlay {
+ /* it takes up the whole screen */
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+
+ /* almost transparent gray */
+ background-color: rgba(0, 0, 0, 0.4);
+ z-index: 100;
+}
+
+/*
+ * The overlay content area holds the text of the overlay.
+ * It has borders and a less transparent background so that
+ * the overlay text shows up more clearly
+ */
+#overlay-content {
+ background:
+ url(images/ui/pattern.png) repeat left top,
+ url(images/ui/gradient.png) no-repeat left top;
+ background-size: auto auto, 100% 100%;
+ /* We can't use shortand with background size because is not implemented yet:
+ https://bugzilla.mozilla.org/show_bug.cgi?id=570326; */
+ overflow: hidden;
+ position: absolute;
+ z-index: 100;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ font-family: "MozTT", Sans-serif;
+ font-size: 0;
+ /* Using font-size: 0; we avoid the unwanted visual space (about 3px)
+ created by white-spaces and break lines in the code betewen inline-block elements */
+ color: #fff;
+ padding: 110px 25px 0px 25px;
+}
+
+#overlay-title {
+ font-weight: normal;
+ font-size: 1.9rem;
+ color: #fff;
+ margin: 0 5px -10px 5px;
+}
+
+#overlay-text {
+ padding: 10px 5px 0 5px;
+ border-top: 1px solid #686868;
+ font-weight: 300;
+ font-size: 2.5rem;
+ color: #ebebeb;
+}
+
+.hidden {
+ display: none;
+}
+
+#hud {
+ position: absolute;
+ top: 20px;
+ height: 75px;
+ left: 0;
+ right: 0;
+ z-index: 50;
+}
+
+#hud a {
+ position: absolute;
+ z-index: 50;
+ height: 75px;
+ width: 75px;
+ border: 0;
+ background-position: center center;
+ background-repeat: no-repeat;
+ background-image: url(images/hud_button_underlay.png);
+}
+
+#hud a:after {
+ content: " ";
+ display: block;
+ position: relative;
+ z-index: 60;
+ height: 75px;
+ width: 75px;
+ background: transparent;
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+
+#hud a:active {
+ background-image: url(images/hud_button_underlay_focus.png);
+}
+
+#toggle-camera {
+ right: 20px;
+}
+
+#toggle-flash {
+ left: 20px;
+}
+
+#toggle-camera[data-mode=back]:after {
+ background-image: url(images/toggle_front.png);
+}
+#toggle-camera[data-mode=front]:after {
+ background-image: url(images/toggle_back.png);
+}
+
+#toggle-flash[data-mode=on]:after {
+ background-image: url(images/flash_on.png);
+}
+#toggle-flash[data-mode=off]:after {
+ background-image: url(images/flash_off.png);
+}
+#toggle-flash[data-mode=auto]:after {
+ background-image: url(images/flash_auto.png);
+}
+#toggle-flash[data-mode=torch]:after {
+ background-image: url(images/flash_torch.png);
+}
diff --git a/apps/system/camera/style/filmstrip.css b/apps/system/camera/style/filmstrip.css
new file mode 100644
index 0000000..fd32dc6
--- /dev/null
+++ b/apps/system/camera/style/filmstrip.css
@@ -0,0 +1,187 @@
+#filmstrip {
+ transition: 0.2s ease-in-out;
+ position: absolute;
+ z-index: 100;
+ left: 0;
+ right: 0;
+ height: 50px;
+ /*
+ * the background must be solid for preview mode because otherwise some
+ * of the frozen viewfinder shows through. If it really need to be translucent
+ * in camera mode, we'll have to change it with javascript
+ */
+ background: black;
+}
+
+#filmstrip.hidden {
+ transform: translateY(-50px);
+}
+
+img.thumbnail {
+ position: relative;
+ width: 46px;
+ height: 46px;
+ border: 2px solid white;
+ margin-right: 4px;
+ float: left; /* XXX: do we need this? */
+ -moz-user-select: none;
+ transition: 0.2s ease-in-out;
+}
+
+img.thumbnail.previewed { /* if the thumbnail is being previewed */
+ border: 2px solid #0ac;
+}
+
+/*
+ * Make the thumbnails rotate with the phone
+ */
+#filmstrip[data-orientation="90"] img.thumbnail {
+ transform: rotate(-90deg);
+}
+#filmstrip[data-orientation="180"] img.thumbnail {
+ transform: rotate(-180deg);
+}
+#filmstrip[data-orientation="270"] img.thumbnail {
+ transform: rotate(-270deg);
+}
+
+/* this is where we display image and video previews */
+#preview {
+ position: absolute;
+ left: 0;
+ width: 100%;
+ top: 50px;
+ bottom: 0px;
+ padding: 0;
+ margin: 0;
+ border-width: 0;
+ background: #000; /* opaque */
+ z-index: 100; /* on top of all the camera stuff */
+ transition: transform 0.5s linear;
+ overflow: hidden;
+ transform-origin: 0 0;
+}
+
+#preview.offscreen {
+ transform: translateY(-100%) scale(.125) translateX(50%);
+}
+
+#frame-container {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ bottom: 40px;
+ padding: 0px;
+ margin: 0px;
+ overflow: hidden;
+ -moz-user-select: none;
+}
+
+#media-frame {
+ position: absolute;
+ /* size, position, and rotation are set based on the phone orientation */
+}
+
+#media-frame > img {
+ top: 0px; /* javascript modifies this position with a transform */
+ left: 0px;
+ position: absolute;
+ border-width: 0px;
+ padding: 0px;
+ margin: 0px;
+ transform-origin: 0px 0px;
+ pointer-events: none;
+ -moz-user-select: none;
+}
+
+/*
+ * these styes apply when we're swapping out a preview image to replace
+ * it with a full-resolution image, but the full image isn't ready yet.
+ * This happens when the user starts to zoom in on the image. We need
+ * some sort of simple visual effect to fill ~500ms of dead time so the
+ * user doesn't think the app has frozen up
+ */
+#media-frame > img.swapping {
+ opacity: 0.8;
+ outline: dashed #0ac 4px;
+ outline-offset: -4px;
+}
+
+#media-frame > video {
+ transform-origin: 0px 0px;
+}
+
+#preview-controls {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0px;
+ height: 40px;
+ background-color: rgba(0, 0, 0, 0.8);
+ z-index: 100; /* above the dynamically inserted frame elements */
+}
+
+a.button {
+ display: block;
+ padding: 0;
+ margin: 0;
+ border-width: 0;
+ background-position: center center;
+ background-repeat: no-repeat;
+ transition: 0.2s ease-in-out;
+}
+
+a.button:active, a.button:focus {
+ outline: none;
+}
+
+a.button.hidden {
+ display: none;
+}
+
+#camera-button {
+ position: absolute;
+ left: 0;
+ width: 33%;
+ height: 100%;
+ background-image: url(images/camera.png);
+}
+
+#share-button {
+ position: absolute;
+ left: 33%;
+ width: 33%;
+ height: 100%;
+ background-image: url(images/share.png);
+}
+
+#delete-button {
+ position: absolute;
+ left: 67%;
+ width: 33%;
+ height: 100%;
+ background-image: url(images/delete.png);
+}
+
+#filmstrip-gallery-button {
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 50px;
+ height: 50px;
+ background-image: url(images/grid.png);
+}
+
+/*
+ * Make the button icons rotate with the phone
+ */
+#preview[data-orientation="90"] a.button {
+ transform: rotate(-90deg);
+}
+#preview[data-orientation="180"] a.button {
+ transform: rotate(-180deg);
+}
+#preview[data-orientation="270"] a.button {
+ transform: rotate(-270deg);
+}
diff --git a/apps/system/camera/style/icons/60/Camera.png b/apps/system/camera/style/icons/60/Camera.png
new file mode 100644
index 0000000..f27f506
--- /dev/null
+++ b/apps/system/camera/style/icons/60/Camera.png
Binary files differ
diff --git a/apps/system/camera/style/icons/Camera.png b/apps/system/camera/style/icons/Camera.png
new file mode 100644
index 0000000..f27f506
--- /dev/null
+++ b/apps/system/camera/style/icons/Camera.png
Binary files differ
diff --git a/apps/system/camera/style/images/actionicon_cancel.png b/apps/system/camera/style/images/actionicon_cancel.png
new file mode 100644
index 0000000..5e73322
--- /dev/null
+++ b/apps/system/camera/style/images/actionicon_cancel.png
Binary files differ
diff --git a/apps/system/camera/style/images/camera.png b/apps/system/camera/style/images/camera.png
new file mode 100644
index 0000000..85a80f5
--- /dev/null
+++ b/apps/system/camera/style/images/camera.png
Binary files differ
diff --git a/apps/system/camera/style/images/delete.png b/apps/system/camera/style/images/delete.png
new file mode 100644
index 0000000..0f2450e
--- /dev/null
+++ b/apps/system/camera/style/images/delete.png
Binary files differ
diff --git a/apps/system/camera/style/images/flash_auto.png b/apps/system/camera/style/images/flash_auto.png
new file mode 100644
index 0000000..3018d8d
--- /dev/null
+++ b/apps/system/camera/style/images/flash_auto.png
Binary files differ
diff --git a/apps/system/camera/style/images/flash_off.png b/apps/system/camera/style/images/flash_off.png
new file mode 100644
index 0000000..0fc7112
--- /dev/null
+++ b/apps/system/camera/style/images/flash_off.png
Binary files differ
diff --git a/apps/system/camera/style/images/flash_on.png b/apps/system/camera/style/images/flash_on.png
new file mode 100644
index 0000000..c7983d1
--- /dev/null
+++ b/apps/system/camera/style/images/flash_on.png
Binary files differ
diff --git a/apps/system/camera/style/images/flash_torch.png b/apps/system/camera/style/images/flash_torch.png
new file mode 100644
index 0000000..3018d8d
--- /dev/null
+++ b/apps/system/camera/style/images/flash_torch.png
Binary files differ
diff --git a/apps/system/camera/style/images/grid.png b/apps/system/camera/style/images/grid.png
new file mode 100644
index 0000000..b53bcf2
--- /dev/null
+++ b/apps/system/camera/style/images/grid.png
Binary files differ
diff --git a/apps/system/camera/style/images/hud_button_underlay.png b/apps/system/camera/style/images/hud_button_underlay.png
new file mode 100644
index 0000000..5853adb
--- /dev/null
+++ b/apps/system/camera/style/images/hud_button_underlay.png
Binary files differ
diff --git a/apps/system/camera/style/images/hud_button_underlay_focus.png b/apps/system/camera/style/images/hud_button_underlay_focus.png
new file mode 100644
index 0000000..c3542bc
--- /dev/null
+++ b/apps/system/camera/style/images/hud_button_underlay_focus.png
Binary files differ
diff --git a/apps/system/camera/style/images/play_overlay.png b/apps/system/camera/style/images/play_overlay.png
new file mode 100644
index 0000000..2a56d04
--- /dev/null
+++ b/apps/system/camera/style/images/play_overlay.png
Binary files differ
diff --git a/apps/system/camera/style/images/share.png b/apps/system/camera/style/images/share.png
new file mode 100644
index 0000000..6a56f19
--- /dev/null
+++ b/apps/system/camera/style/images/share.png
Binary files differ
diff --git a/apps/system/camera/style/images/stop.png b/apps/system/camera/style/images/stop.png
new file mode 100644
index 0000000..b358cc5
--- /dev/null
+++ b/apps/system/camera/style/images/stop.png
Binary files differ
diff --git a/apps/system/camera/style/images/toggle_back.png b/apps/system/camera/style/images/toggle_back.png
new file mode 100644
index 0000000..5e767b4
--- /dev/null
+++ b/apps/system/camera/style/images/toggle_back.png
Binary files differ
diff --git a/apps/system/camera/style/images/toggle_front.png b/apps/system/camera/style/images/toggle_front.png
new file mode 100644
index 0000000..b67507f
--- /dev/null
+++ b/apps/system/camera/style/images/toggle_front.png
Binary files differ
diff --git a/apps/system/camera/style/images/ui/gradient.png b/apps/system/camera/style/images/ui/gradient.png
new file mode 100644
index 0000000..b288545
--- /dev/null
+++ b/apps/system/camera/style/images/ui/gradient.png
Binary files differ
diff --git a/apps/system/camera/style/images/ui/pattern.png b/apps/system/camera/style/images/ui/pattern.png
new file mode 100644
index 0000000..af03f56
--- /dev/null
+++ b/apps/system/camera/style/images/ui/pattern.png
Binary files differ
diff --git a/apps/system/camera/style/images/video.png b/apps/system/camera/style/images/video.png
new file mode 100644
index 0000000..5c41986
--- /dev/null
+++ b/apps/system/camera/style/images/video.png
Binary files differ
diff --git a/apps/system/camera/style/images/video_pause_button.png b/apps/system/camera/style/images/video_pause_button.png
new file mode 100644
index 0000000..b0224f8
--- /dev/null
+++ b/apps/system/camera/style/images/video_pause_button.png
Binary files differ
diff --git a/apps/system/camera/style/images/video_play_button.png b/apps/system/camera/style/images/video_play_button.png
new file mode 100644
index 0000000..56dba6b
--- /dev/null
+++ b/apps/system/camera/style/images/video_play_button.png
Binary files differ
diff --git a/apps/system/camera/style/images/video_play_focus.png b/apps/system/camera/style/images/video_play_focus.png
new file mode 100644
index 0000000..1bb0537
--- /dev/null
+++ b/apps/system/camera/style/images/video_play_focus.png
Binary files differ
diff --git a/apps/system/camera/style/images/video_play_normal.png b/apps/system/camera/style/images/video_play_normal.png
new file mode 100644
index 0000000..0cabf3d
--- /dev/null
+++ b/apps/system/camera/style/images/video_play_normal.png
Binary files differ
diff --git a/apps/system/camera/test/unit/_proxy.html b/apps/system/camera/test/unit/_proxy.html
new file mode 100644
index 0000000..2102451
--- /dev/null
+++ b/apps/system/camera/test/unit/_proxy.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>Serve the tests</title>
+
+ <script type="text/javascript" charset="utf-8">
+ (function(window){
+ var Loader = window.CommonResourceLoader = {},
+ host = document.location.host,
+ domain = host.replace(/(^[\w\d-]+\.)?([\w\d]+\.[a-z]+)/, 'test-agent.$2');
+
+ Loader.domain = document.location.protocol + '//' + domain;
+ Loader.url = function(url){
+ return this.domain + url;
+ }
+
+ Loader.script = function(url, doc){
+ doc = doc || document;
+ doc.write('<script type="application/javascript;version=1.8" src="' + this.url(url) + '"><\/script>');
+ return this;
+ };
+ }(this));
+ </script>
+
+ <style type="text/css" media="all">
+ html,body,iframe {
+ height: 100%;
+ width: 100%;
+ }
+ </style>
+
+</head>
+<body>
+
+<!-- Test Agent UI will be loaded in here -->
+<div id="test-agent-ui">
+</div>
+
+<script type="text/javascript" charset="utf-8">
+CommonResourceLoader.
+ script('/common/test/test_url_resolver.js').
+ script('/common/vendor/test-agent/test-agent.js').
+ script('/common/test/agent_proxy.js');
+</script>
+</body>
+</html>
+
+
diff --git a/apps/system/camera/test/unit/_sandbox.html b/apps/system/camera/test/unit/_sandbox.html
new file mode 100644
index 0000000..70d0efa
--- /dev/null
+++ b/apps/system/camera/test/unit/_sandbox.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>Tests</title>
+ <link rel="stylesheet" type="text/css" href="/vendor/mocha/mocha.css" />
+ <style type="text/css" media="all">
+ iframe {
+ border: none;
+ padding: 0px;
+ }
+ </style>
+ <script type="text/javascript" charset="utf-8">
+ </script>
+</head>
+
+<body>
+
+<div id="mocha">
+</div>
+
+<div id="test">
+</div>
+
+</body>
+</html>
+
+