From 821413607a0718156f9d25d895e89b1c3d37aa8b Mon Sep 17 00:00:00 2001 From: Daniel Narvaez Date: Wed, 06 Feb 2013 13:49:14 +0000 Subject: Copy various bits from gaia --- (limited to 'apps/system/camera/js/filmstrip.js') 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 or