From f5bac2f1e1a51b83d215a07f9d5d87db337873f3 Mon Sep 17 00:00:00 2001 From: Daniel Narvaez Date: Wed, 06 Feb 2013 14:30:36 +0000 Subject: Get the build to work --- diff --git a/.gitignore b/.gitignore index 3bedf6b..ce9669c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ docs/*.html +profile diff --git a/Makefile b/Makefile index a0a6cdb..6d8d6a0 100644 --- a/Makefile +++ b/Makefile @@ -49,46 +49,12 @@ endif REPORTER?=Spec -GAIA_APP_SRCDIRS?=apps test_apps showcase_apps +GAIA_APP_SRCDIRS?=apps GAIA_INSTALL_PARENT?=/data/local ADB_REMOUNT?=0 GAIA_ALL_APP_SRCDIRS=$(GAIA_APP_SRCDIRS) -ifeq ($(MAKECMDGOALS), demo) -GAIA_DOMAIN=thisdomaindoesnotexist.org -GAIA_APP_SRCDIRS=apps showcase_apps -else ifeq ($(MAKECMDGOALS), dogfood) -DOGFOOD=1 -PRODUCTION=1 -B2G_SYSTEM_APPS=1 -else ifeq ($(MAKECMDGOALS), production) -PRODUCTION=1 -B2G_SYSTEM_APPS=1 -endif - -# PRODUCTION is also set for user and userdebug B2G builds -ifeq ($(PRODUCTION), 1) -GAIA_APP_SRCDIRS=apps -ADB_REMOUNT=1 -endif - -ifeq ($(MAKECMDGOALS), dogfood) -GAIA_APP_SRCDIRS=apps dogfood_apps -endif - -ifeq ($(B2G_SYSTEM_APPS), 1) -GAIA_INSTALL_PARENT=/system/b2g -endif - -ifneq ($(GAIA_OUTOFTREE_APP_SRCDIRS),) - $(shell mkdir -p outoftree_apps \ - $(foreach dir,$(GAIA_OUTOFTREE_APP_SRCDIRS),\ - $(foreach appdir,$(wildcard $(dir)/*),\ - && ln -sf $(appdir) outoftree_apps/))) - GAIA_APP_SRCDIRS += outoftree_apps -endif - GAIA_LOCALES_PATH?=locales LOCALES_FILE?=shared/resources/languages.json GAIA_LOCALE_SRCDIRS=shared $(GAIA_APP_SRCDIRS) @@ -180,7 +146,7 @@ TEST_DIRS ?= $(CURDIR)/tests # Generate profile/ -profile: multilocale applications-data preferences app-makefiles test-agent-config offline extensions profile/settings.json +profile: multilocale applications-data preferences app-makefiles offline extensions profile/settings.json @echo "Profile Ready: please run [b2g|firefox] -profile $(CURDIR)$(SEP)profile" LANG=POSIX # Avoiding sort order differences between OSes @@ -240,11 +206,8 @@ webapp-manifests: @#cat profile/webapps/webapps.json # Generate profile/webapps/APP/application.zip -webapp-zip: stamp-commit-hash +webapp-zip: ifneq ($(DEBUG),1) - @rm -rf apps/system/camera - @cp -r apps/camera apps/system/camera - @rm apps/system/camera/manifest.webapp @mkdir -p profile/webapps @$(call run-js-command, webapp-zip) endif @@ -357,96 +320,6 @@ common-install: cd $(TEST_AGENT_DIR) && npm install . -.PHONY: update-common -update-common: common-install - # integration tests - rm -f tests/vendor/marionette.js - cp $(TEST_AGENT_DIR)/node_modules/marionette-client/marionette.js tests/js/vendor/ - - # common testing tools - mkdir -p $(TEST_COMMON)/vendor/test-agent/ - mkdir -p $(TEST_COMMON)/vendor/chai/ - rm -Rf tools/xpcwindow - rm -f $(TEST_COMMON)/vendor/test-agent/test-agent*.js - rm -f $(TEST_COMMON)/vendor/chai/*.js - cp -R $(TEST_AGENT_DIR)/node_modules/xpcwindow tools/xpcwindow - rm -R tools/xpcwindow/vendor/ - cp $(TEST_AGENT_DIR)/node_modules/test-agent/test-agent.js $(TEST_COMMON)/vendor/test-agent/ - cp $(TEST_AGENT_DIR)/node_modules/test-agent/test-agent.css $(TEST_COMMON)/vendor/test-agent/ - cp $(TEST_AGENT_DIR)/node_modules/chai/chai.js $(TEST_COMMON)/vendor/chai/ - -# Create the json config file -# for use with the test agent GUI -test-agent-config: test-agent-bootstrap-apps - @rm -f $(TEST_AGENT_CONFIG) - @touch $(TEST_AGENT_CONFIG) - @rm -f /tmp/test-agent-config; - @# Build json array of all test files - @for d in ${GAIA_APP_SRCDIRS}; \ - do \ - find $$d -name '*_test.js' | sed "s:$$d/::g" >> /tmp/test-agent-config; \ - done; - @echo '{"tests": [' >> $(TEST_AGENT_CONFIG) - @cat /tmp/test-agent-config | \ - sed 's:\(.*\):"\1":' | \ - sed -e ':a' -e 'N' -e '$$!ba' -e 's/\n/,\ - /g' >> $(TEST_AGENT_CONFIG); - @echo ' ]}' >> $(TEST_AGENT_CONFIG); - @echo "Finished: test ui config file: $(TEST_AGENT_CONFIG)" - @rm -f /tmp/test-agent-config - -.PHONY: test-agent-bootstrap-apps -test-agent-bootstrap-apps: - @for d in `find -L ${GAIA_APP_SRCDIRS} -mindepth 1 -maxdepth 1 -type d` ;\ - do \ - mkdir -p $$d/test/unit ; \ - mkdir -p $$d/test/integration ; \ - cp -f $(TEST_COMMON)/test/boilerplate/_proxy.html $$d/test/unit/_proxy.html; \ - cp -f $(TEST_COMMON)/test/boilerplate/_sandbox.html $$d/test/unit/_sandbox.html; \ - done - @echo "Finished: bootstrapping test proxies/sandboxes"; - -# Temp make file method until we can switch -# over everything in test -ifneq ($(strip $(APP)),) -APP_TEST_LIST=$(shell find apps/$(APP)/test/unit -name '*_test.js') -endif -.PHONY: test-agent-test -test-agent-test: -ifneq ($(strip $(APP)),) - @echo 'Running tests for $(APP)'; - @$(TEST_AGENT_DIR)/node_modules/test-agent/bin/js-test-agent test --reporter $(REPORTER) $(APP_TEST_LIST) -else - @echo 'Running all tests'; - @$(TEST_AGENT_DIR)/node_modules/test-agent/bin/js-test-agent test --reporter $(REPORTER) -endif - -.PHONY: test-agent-server -test-agent-server: common-install - $(TEST_AGENT_DIR)/node_modules/test-agent/bin/js-test-agent server -c ./$(TEST_AGENT_DIR)/test-agent-server.js --http-path . --growl - -.PHONY: marionette -marionette: -#need the profile - test -d $(GAIA)/profile || $(MAKE) profile -ifneq ($(PYTHON_MAJOR), 2) - @echo "Python 2.7.x is needed for the marionette client. You can set the PYTHON_27 variable to your python2.7 path." && exit 1 -endif -ifneq ($(PYTHON_MINOR), 7) - @echo "Python 2.7.x is needed for the marionette client. You can set the PYTHON_27 variable to your python2.7 path." && exit 1 -endif -ifeq ($(strip $(MC_DIR)),) - @echo "Please have the MC_DIR environment variable point to the top of your mozilla-central tree." && exit 1 -endif -#if B2G_BIN is defined, we will run the b2g binary, otherwise, we assume an instance is running -ifneq ($(strip $(B2G_BIN)),) - cd $(MC_DIR)/testing/marionette/client/marionette && \ - sh venv_test.sh $(PYTHON_27) --address=$(MARIONETTE_HOST):$(MARIONETTE_PORT) --b2gbin=$(B2G_BIN) $(TEST_DIRS) -else - cd $(MC_DIR)/testing/marionette/client/marionette && \ - sh venv_test.sh $(PYTHON_27) --address=$(MARIONETTE_HOST):$(MARIONETTE_PORT) $(TEST_DIRS) -endif - ############################################################################### # Utils # ############################################################################### @@ -460,20 +333,6 @@ lint: @gjslint --nojsdoc -r apps -e 'homescreen/everything.me,sms/js/ext,pdfjs/content,pdfjs/test,email/js/ext,music/js/ext,calendar/js/ext' @gjslint --nojsdoc -r shared/js -# Generate a text file containing the current changeset of Gaia -# XXX I wonder if this should be a replace-in-file hack. This would let us -# let us remove the update-offline-manifests target dependancy of the -# default target. -stamp-commit-hash: - @(if [ -e gaia_commit_override.txt ]; then \ - cp gaia_commit_override.txt apps/settings/resources/gaia_commit.txt; \ - elif [ -d ./.git ]; then \ - git log -1 --format="%H%n%at" HEAD > apps/settings/resources/gaia_commit.txt; \ - else \ - echo 'Unknown Git commit; build date shown here.' > apps/settings/resources/gaia_commit.txt; \ - date +%s >> apps/settings/resources/gaia_commit.txt; \ - fi) - # Erase all the indexedDB databases on the phone, so apps have to rebuild them. delete-databases: @echo 'Stopping b2g' diff --git a/apps/system/camera/index.html b/apps/system/camera/index.html deleted file mode 100644 index c06bf41..0000000 --- a/apps/system/camera/index.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - Camera - - - - - - - -
- - -
- -
- -
- - -
- - - 00:00 -
-
- - - - - -
-
-
-
-
- - - -
-
- - - - - - - - - - - - - - diff --git a/apps/system/camera/js/camera.js b/apps/system/camera/js/camera.js deleted file mode 100644 index 25f12ae..0000000 --- a/apps/system/camera/js/camera.js +++ /dev/null @@ -1,982 +0,0 @@ -'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 deleted file mode 100644 index d428801..0000000 --- a/apps/system/camera/js/filmstrip.js +++ /dev/null @@ -1,568 +0,0 @@ -/* - * 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 or