/* * 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