Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,708 @@
'use strict';
/* global
CONFIG_MAX_IMAGE_PIXEL_SIZE,
CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT,
CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH,
cropResizeRotate,
Downsample,
getImageSize,
getVideoRotation,
isPhone,
parseJPEGMetadata,
videostorage
*/
/* exported
metadataParser
*/
//
// This file defines a single metadataParsers object. The two
// properties of this object are metadata parsing functions for image
// files and video files, intended for use with the MediaDB class.
//
// This file depends on JPEGMetadataParser.js and blobview.js
//
var metadataParser = (function() {
// If we generate our own thumbnails, aim for this size.
// Calculate needed size from longer side of the screen.
var THUMBNAIL_WIDTH = computeThumbnailWidth();
var THUMBNAIL_HEIGHT = THUMBNAIL_WIDTH;
function computeThumbnailWidth() {
// Make sure this works regardless of current device orientation
var width = window.innerWidth || screen.width;
var height = window.innerHeight || screen.height;
var portraitWidth = Math.max(1, Math.min(width, height));
var landscapeWidth = Math.max(1, Math.max(width, height));
var thumbnailsPerRowPortrait = isPhone ? 3 : 4;
var thumbnailsPerRowLandscape = isPhone ? 4 : 6;
return Math.round(window.devicePixelRatio *
Math.max(portraitWidth / thumbnailsPerRowPortrait,
landscapeWidth / thumbnailsPerRowLandscape));
}
var thumbnailSize = {
width: THUMBNAIL_WIDTH,
height: THUMBNAIL_HEIGHT
};
// Don't try to decode image files of unknown type if bigger than this
var MAX_UNKNOWN_IMAGE_FILE_SIZE = 0.5 * 1024 * 1024; // half a megabyte
// If a jpeg does not have a suitable EXIF preview and has more than
// this number of pixels, then we create an external preview for it
var MAX_PREVIEWLESS_JPEG_SIZE = 5 * 1024 * 1024; // 5 megapixels
// An <img> element for loading images
var offscreenImage = new Image();
// The screen size. Preview images must be at least this big
// Note that we use screen.width instead of window.innerWidth because
// the window size is different when we are invoked for a pick activity
// (non-fullscreen) and when we are invoked normally (fullscreen).
var sw = screen.width * window.devicePixelRatio;
var sh = screen.height * window.devicePixelRatio;
// 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.
// This utility function is used by both the image and video metadata parsers
function createThumbnailFromElement(elt, video, rotation,
mirrored, callback, error) {
try {
// Create a thumbnail image
var canvas = document.createElement('canvas');
canvas.width = THUMBNAIL_WIDTH;
canvas.height = THUMBNAIL_HEIGHT;
var context = canvas.getContext('2d', { willReadFrequently: true });
var eltwidth = elt.width;
var eltheight = 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);
var centerX = Math.floor(THUMBNAIL_WIDTH / 2);
var centerY = Math.floor(THUMBNAIL_HEIGHT / 2);
// If a orientation is specified, rotate/mirroring the canvas context.
if (rotation || mirrored) {
context.save();
// All transformation are applied to the center of the thumbnail.
context.translate(centerX, centerY);
}
if (mirrored) {
context.scale(-1, 1);
}
if (rotation) {
switch (rotation) {
case 90:
context.rotate(Math.PI / 2);
break;
case 180:
context.rotate(Math.PI);
break;
case 270:
context.rotate(-Math.PI / 2);
break;
}
}
if (rotation || mirrored) {
context.translate(-centerX, -centerY);
}
// 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 || mirrored) {
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, .2)';
context.beginPath();
context.arc(THUMBNAIL_WIDTH / 2, THUMBNAIL_HEIGHT / 2,
THUMBNAIL_HEIGHT / 5, 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 / 5;
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(function(blob) {
context = null;
canvas.width = canvas.height = 0;
canvas = null;
callback(blob);
}, 'image/jpeg');
} catch (ex) {
// An error may be thrown when the drawImage decodes a broken/trancated
// image.
// The elt may be a offscreen image. So, the image metadata is parsed, and
// the image data is loaded but not decoded. The drawImage triggers the
// image decoder to decode the image data. And an error may be thrown.
error('createThumbnailFromElement:' + ex.message);
}
}
var VIDEOFILE = /DCIM\/\d{3}MZLLA\/VID_\d{4}\.jpg/;
function metadataParser(file, metadataCallback, metadataError, bigFile) {
// If the file is a poster image for a video file, then we've want
// video metadata, not image metadata
if (VIDEOFILE.test(file.name)) {
videoMetadataParser(file, metadataCallback, metadataError);
return;
}
// Figure out how big the image is if we can. For JPEG files this
// calls the JPEG parser and returns the EXIF preview if there is one.
getImageSize(file, gotImageSize, gotImageSizeError);
function gotImageSizeError(errmsg) {
// The image is not a JPEG, PNG, GIF or BMP file. We may still be
// able to decode and display it but we don't know the image
// size, so we won't even try if the file is too big.
if (file.size > MAX_UNKNOWN_IMAGE_FILE_SIZE) {
metadataError('Ignoring large file ' + file.name);
return;
}
// If the file is too small to be an image, ignore it
if (file.size < 32) {
metadataError('Ignoring small file ' + file.name);
return;
}
// If the error message is anything other than unknown image type
// it means we've got a corrupt image file, or the image metdata parser
// can't handle the file for some reason. Log a warning but keep going
// in case the image is good and the metadata parser is buggy.
if (errmsg !== 'unknown image type') {
console.warn('getImageSize', errmsg, file.name);
}
// If it is not too big create a preview and thumbnail.
createThumbnailAndPreview(file,
metadataCallback,
metadataError,
false,
bigFile,
{});
}
function gotImageSize(metadata) {
//
// If the image is too big, reject it now so we don't have
// memory trouble later.
//
// CONFIG_MAX_IMAGE_PIXEL_SIZE is maximum image resolution we
// can handle. It's from config.js which is generated at build
// time (see build/application-data.js).
//
// For jpeg images we can downsample while decoding which means that
// we can handle images that are quite a bit larger.
//
// getImageSize() distinguishes pjpeg files from jpeg, because
// progressive jpeg files are quite different than regular jpegs.
// Not only do pjpegs not benefit from using #-moz-samplesize, they
// actually require more memory to decode than other images of the same
// resolution, so we actually need to reduce the image size limit for
// this image type.
//
var imagesizelimit = CONFIG_MAX_IMAGE_PIXEL_SIZE;
if (metadata.type === 'jpeg') {
imagesizelimit *= Downsample.MAX_AREA_REDUCTION;
}
else if (metadata.type === 'pjpeg') {
// As a semi-educated guess, we'll say that we can handle pjpegs
// 2/3rds the size of the largest PNG we can handle.
imagesizelimit *= 2 / 3;
}
//
// Even if we can downsample an image while decoding it, we still
// have to read the entire image file. If the file is particularly
// large we might also have memory problems. (See bug 1008834: a 20mb
// 80mp jpeg file will cause an OOM on Tarako even though we can
// decode it at < 2mp). Rather than adding another build-time config
// variable to specify the maximum file size, however, we'll just
// base the file size limit on CONFIG_MAX_IMAGE_PIXEL_SIZE.
// So if that variable is set to 2M, then we might use up to 12Mb of
// memory. 2 * 2M bytes for the image file and 4 bytes times 2M pixels
// for the decoded image. A 4mb file size limit should accomodate
// most JPEG files up to 12 or 16 megapixels.
//
// Note that this size limit is just a guess. If we continue to see
// OOMs with large files then we should change the number 2 below
// to something smaller.
//
var filesizelimit = 2 * CONFIG_MAX_IMAGE_PIXEL_SIZE;
if (metadata.width * metadata.height > imagesizelimit ||
file.size > filesizelimit) {
metadataError('Ignoring high-resolution image ' + file.name);
return;
}
// If the file included a preview image, see if it is big enough
if (metadata.preview) {
// Create a blob that is just the preview image
var previewblob = file.slice(metadata.preview.start,
metadata.preview.end,
'image/jpeg');
// Check to see if the preview is big enough to use in MediaFrame
parseJPEGMetadata(previewblob, previewsuccess, previewerror);
}
else {
// If there wasn't a preview image, then generate a preview and
// thumbnail from the full size image.
useFullsizeImage();
}
function previewerror(msg) {
// The preview isn't a valid jpeg file, so use the full image to
// create a preview and a thumbnail
console.error(msg);
useFullsizeImage();
}
// If we can't find a valid embedded EXIF preview image then we come here
function useFullsizeImage() {
// If the full size image is a JPEG then we'll just skip the
// preview generation because we can downsample and decode it
// at preview size when needed. So in this case, we just need
// to create a thumbnail for the image. On the other hand, if
// this is not a JPEG image then we have to create both a
// thumbnail and an external preview image. Note that progressive
// jpegs will have a type 'pjpeg' and will be handled below
// like png images.
// For jpeg images with image sizes more than 5MP, we need
// to create preview on sdcard to avoid flash while opening image.
// See bug 1181290.
// This constraint can be removed when gecko supports downscale during
// decode by canvas size for offscreen images. This will
// facilitate removing #moz-sample-size and using ddd.
// See bug 1206206.
if (metadata.type === 'jpeg') {
if (metadata.width * metadata.height <= MAX_PREVIEWLESS_JPEG_SIZE) {
// If the jpeg image isn't too big we don't need a preview
// because we can decode it quickly enough at screen size.
// So in this case we just need to create a thumbnail.
cropResizeRotate(file, null, thumbnailSize, null, metadata,
function gotThumbnail(error, thumbnailBlob) {
if (error) {
var msg = 'Failed to create thumbnail for' +
file.name;
console.error(msg);
metadataError(msg);
return;
}
metadata.preview = null;
metadata.thumbnail = thumbnailBlob;
metadataCallback(metadata);
});
}
else {
// Otherwise, this is a high-resolution jpeg that does not
// have an EXIF preview big enough for the screen. We need
// to create both a thumbnail and a preview image but we can
// do that with efficient downsampling using cropResizeRotate().
createJPEGThumbnailAndPreview(file, metadata,
metadataCallback,
metadataError);
}
}
else {
// In this case we have no preview and the image is not a jpeg
// that can be downsampled with #-moz-samplesize
createThumbnailAndPreview(file,
metadataCallback,
metadataError,
false,
bigFile,
metadata);
}
}
function previewsuccess(previewmetadata) {
// Size of the preview image
var pw = previewmetadata.width;
var ph = previewmetadata.height;
// optional configuration specifying minimum size
var mw = CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH;
var mh = CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT;
var bigenough;
// If config.js specifies a minimum required preview size,
// then this preview is big enough if both dimensions are
// larger than that configured minimum. Otherwise, the preview
// is big enough if at least one dimension is >= the screen
// size in both portait and landscape mode.
if (mw && mh) {
bigenough =
Math.max(pw, ph) >= Math.max(mw, mh) &&
Math.min(pw, ph) >= Math.min(mw, mh);
}
else {
bigenough = (pw >= sw || ph >= sh) && (pw >= sh || ph >= sw);
}
// Does the aspect ratio of the preview match the aspect ratio of
// the full-size image? If not we have to assume it is distorted
// and should not be used.
var aspectRatioMatches =
Math.abs(pw / ph - metadata.width / metadata.height) < 0.01;
// If the preview is good, use it to create a thumbnail.
if (bigenough && aspectRatioMatches) {
metadata.preview.width = pw;
metadata.preview.height = ph;
// This is the most common case. We've got a JPEG image with a
// JPEG preview. All we need is a thumbnail, and we can create that
// most efficiently with cropResizeRotate which will use
// #-moz-samplesize to save memory while decoding the image.
// Note that we apply the EXIF orientation information from
// the full size image to the preview image.
previewmetadata.rotation = metadata.rotation;
previewmetadata.mirrored = metadata.mirrored;
cropResizeRotate(previewblob, null, thumbnailSize, null,
previewmetadata,
function gotThumbnailBlob(error, thumbnailBlob) {
if (error) {
previewerror(error);
return;
}
metadata.thumbnail = thumbnailBlob;
metadataCallback(metadata);
});
} else {
// Preview isn't big enough so get one the hard way
useFullsizeImage();
}
}
}
}
// This function is called for JPEG images that do not have a suitable
// EXIF preview image and that have more than MAX_PREVIEWLESS_JPEG_SIZE
// pixels. It creates and saves a preview image and a thumbnail.
// Unlike createThumbnailAndPreview(), it uses cropResizeRotate() which
// downsamples jpeg images while decoding them to save memory.
function createJPEGThumbnailAndPreview(file, metadata,
metadataCallback, metadataError)
{
// Figure out the preview size.
var scale = Math.max(Math.min(sw / metadata.width,
sh / metadata.height, 1),
Math.min(sh / metadata.width,
sw / metadata.height, 1));
var previewSize = {
width: Math.round(metadata.width * scale),
height: Math.round(metadata.height * scale)
};
// Create a preview image.
// Note that we can't use the full metadata object here
// because we don't want the preview image to be rotated.
// We want the same orientation as the original image
cropResizeRotate(file, null, previewSize, null,
{ width: metadata.width, height: metadata.height },
function gotPreview(error, previewBlob) {
if (error) {
var msg = 'Failed to create preview for' +
file.name;
console.error(msg);
metadataError(msg);
return;
}
// Save this preview to the sdcard
savePreview(file, previewBlob, metadataError, function(filename) {
// Record the preview image in the metadata
metadata.preview = {
filename: filename,
width: previewSize.width,
height: previewSize.height
};
// Now create the thumbnail from this smaller preview image
// In this case we need to pass the size of the preview
// and the rotation of the original image.
cropResizeRotate(previewBlob, null, thumbnailSize, null,
{
width: previewSize.width,
height: previewSize.height,
rotation: metadata.rotation
},
function gotThumbnail(error, thumbnailBlob) {
// If thumbnail creation failed report the error
if (error) {
metadataError(error);
return;
}
// Otherwise, remember thumbnail and we're done
metadata.thumbnail = thumbnailBlob;
metadataCallback(metadata);
});
});
});
}
// Load an image from a file into an <img> tag, and then use that
// to get its dimensions and create a thumbnail. Store these values in
// a metadata object, and pass the object to the callback function.
// If anything goes wrong, pass an error message to the error function.
// If it is a large image, create and save a preview for it as well.
//
// We used to call this function for every image. Now that gecko supports
// the #-moz-samplesize media fragment, however, we handle jpeg images
// with cropResizeRotate() for memory efficiency, and this function is
// used only for non-jpeg files.
function createThumbnailAndPreview(file, callback, error, nopreview,
bigFile, metadata) {
var url = URL.createObjectURL(file);
offscreenImage.src = url;
offscreenImage.onerror = function() {
URL.revokeObjectURL(url);
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
error('createThumbnailAndPreview: Image failed to load');
};
offscreenImage.onload = function() {
URL.revokeObjectURL(url);
var iw = offscreenImage.width;
var ih = offscreenImage.height;
// Don't overwrite the metadata in the case we read a previewblob.
if (!nopreview) {
metadata.width = iw;
metadata.height = ih;
}
// If this is a big image, then decoding it takes a lot of memory.
// We set this flag to prevent the user from zooming in on other
// images at the same time because that also takes a lot of memory
// and we don't want to crash with an OOM. If we find one big image
// we assume that there may be others, so the flag remains set until
// the current scan is complete.
//
// XXX: When bug 854795 is fixed, we'll be able to create previews
// for large images without using so much memory, and we can remove
// this flag then.
if (iw * ih > 2 * 1024 * 1024 && bigFile) {
bigFile();
}
// If the image was already thumbnail size, it is its own thumbnail
// and it does not need a preview
if (metadata.width <= THUMBNAIL_WIDTH &&
metadata.height <= THUMBNAIL_HEIGHT) {
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
metadata.thumbnail = file;
callback(metadata);
}
else {
createThumbnailFromElement(
offscreenImage,
false,
metadata.rotation || 0,
metadata.mirrored || false,
gotThumbnail,
error);
}
function gotThumbnail(thumbnail) {
metadata.thumbnail = thumbnail;
// If no preview was requested, or if if the image was less
// than half a megapixel then it does not need a preview
// image, and we can call the callback right away
if (nopreview || metadata.width * metadata.height < 512 * 1024) {
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
callback(metadata);
}
else {
// Otherwise, this was a big image and we need to create a
// preview for it so we can avoid decoding the full size
// image again when possible
createAndSavePreview();
}
}
function createAndSavePreview() {
// Figure out the preview size.
// Make sure the size is big enough for both landscape and portrait
var scale = Math.max(Math.min(sw / iw, sh / ih, 1),
Math.min(sh / iw, sw / ih, 1));
// preview width and height
var pw = Math.round(iw * scale);
var ph = Math.round(ih * scale);
// Create the preview in a canvas
var canvas = document.createElement('canvas');
canvas.width = pw;
canvas.height = ph;
var context = canvas.getContext('2d', { willReadFrequently: true });
context.drawImage(offscreenImage, 0, 0, iw, ih, 0, 0, pw, ph);
canvas.toBlob(function(blob) {
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
canvas.width = canvas.height = 0;
savePreview(file, blob, error, function(filename) {
metadata.preview = {
filename: filename,
width: pw,
height: ph
};
callback(metadata);
});
}, 'image/jpeg');
}
};
}
// Given an image file and a preview blob for it, this function saves
// the preview to a file in .gallery/previews/ and then passes the filename
// to the specified callback. Calls errorCallback if an error occurs.
// This is used by createThumbnailAndPreview() and by
// createJPEGThumbnailAndPreview().
function savePreview(file, previewblob, errorCallback, filenameCallback) {
var storage = navigator.getDeviceStorage('pictures');
var filename;
if (file.name[0] === '/') {
// We expect file.name to be a fully qualified name (perhaps
// something like /sdcard/DCIM/100MZLLA/IMG_0001.jpg).
var slashIndex = file.name.indexOf('/', 1);
if (slashIndex < 0) {
errorCallback('savePreview: Bad filename: \'' + file.name + '\'');
return;
}
filename =
file.name.substring(0, slashIndex) + // storageName (i.e. /sdcard)
'/.gallery/previews' +
file.name.substring(slashIndex); // rest of path (i,e, /DCIM/...)
} else {
// On non-composite storage areas (e.g. desktop), file.name will be
// a relative path.
filename = '.gallery/previews/' + file.name;
}
// Add a '.jpeg' extension to all preview files. This is necessary
// because all previews get saved as jpegs even if the original
// image is not a jpeg. But DeviceStorage uses the filename extension
// to determine the file type.
filename += '.jpeg';
// Delete any existing preview by this name
var delreq = storage.delete(filename);
delreq.onsuccess = delreq.onerror = save;
function save() {
var savereq = storage.addNamed(previewblob, filename);
savereq.onerror = function() {
console.error('Could not save preview image', filename);
};
// Don't actually wait for the save to complete.
filenameCallback(filename);
}
}
function videoMetadataParser(file, metadataCallback, errorCallback) {
var metadata = {};
var videofilename = file.name.replace('.jpg', '.3gp');
metadata.video = videofilename;
var getreq = videostorage.get(videofilename);
getreq.onerror = function() {
errorCallback('cannot get video file: ' + videofilename);
};
getreq.onsuccess = function() {
var videofile = getreq.result;
getVideoRotation(videofile, function(rotation) {
if (typeof rotation === 'number') {
metadata.rotation = rotation;
getVideoThumbnailAndSize();
}
else if (typeof rotation === 'string') {
errorCallback('Video rotation:', rotation);
}
});
};
function getVideoThumbnailAndSize() {
var url = URL.createObjectURL(file);
offscreenImage.src = url;
offscreenImage.onerror = function() {
URL.revokeObjectURL(url);
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
errorCallback('getVideoThumanailAndSize: Image failed to load');
};
offscreenImage.onload = function() {
URL.revokeObjectURL(url);
// We store the unrotated size of the poster image. The video
// itself might be larger or smaller than this, but it will
// have the same orientation and aspect ratio.
metadata.width = offscreenImage.width;
metadata.height = offscreenImage.height;
createThumbnailFromElement(offscreenImage,
true,
metadata.rotation,
false,
function(thumbnail) {
metadata.thumbnail = thumbnail;
offscreenImage.onload = offscreenImage.onerror = null; offscreenImage.removeAttribute('src');
metadataCallback(metadata);
},
errorCallback);
};
}
}
return metadataParser;
}());

View file

@ -0,0 +1 @@
;;var CONFIG_MAX_IMAGE_PIXEL_SIZE=5242880;var CONFIG_MAX_SNAPSHOT_PIXEL_SIZE=5242880;var CONFIG_MAX_PICK_PIXEL_SIZE=0;var CONFIG_MAX_EDIT_PIXEL_SIZE=0;var CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH=0;var CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT=0;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,715 @@
// This file contains Gallery code related to the fullscreen view
'use strict';
/* global
$,
CONFIG_MAX_IMAGE_PIXEL_SIZE,
CONFIG_MAX_PICK_PIXEL_SIZE,
CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT,
CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH,
cropResizeRotate,
currentFileIndex,
currentView,
deleteFile,
Dialogs,
editPhotoIfCardNotFull,
files,
frames:true,
fullscreenButtons,
fullscreenView,
GestureDetector,
getCurrentFile,
getVideoFile,
isPhone,
LAYOUT_MODE,
LazyLoader,
MediaFrame,
NFC,
photodb,
setView,
share,
showFileInformation,
Spinner,
TRANSITION_FRACTION,
TRANSITION_SPEED,
updateFocusThumbnail
*/
/* exported
clearFrames,
showFile
*/
var frames = $('frames');
// These three objects are holders for the previous, current and next
// photos or videos to be displayed. They get swapped around and
// reused when we pan to the next or previous photo: next becomes
// current, current becomes previous etc. See nextFile() and
// previousFile(). Note also that the Frame object is not a DOM
// element. Use currentFrame.container to refer to the section
// element. The frame constructor creates an <img> element, a <video>
// element, and video player controls within the section, and you can refer to
// those as currentFrame.image and currentFrame.video.player and
// currentFrame.video.controls.
var maxImageSize = CONFIG_MAX_IMAGE_PIXEL_SIZE;
var previousFrame = new MediaFrame($('frame1'), true, maxImageSize);
var currentFrame = new MediaFrame($('frame2'), true, maxImageSize);
var nextFrame = new MediaFrame($('frame3'), true, maxImageSize);
if (CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH) {
previousFrame.setMinimumPreviewSize(CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH,
CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT);
currentFrame.setMinimumPreviewSize(CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH,
CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT);
nextFrame.setMinimumPreviewSize(CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH,
CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT);
}
// When this variable is set to true, we ignore any user gestures
// so we don't try to pan or zoom during a frame transition.
var transitioning = false;
// Clicking on the back button will go to the preview view
// Note that tablet doesn't have a back button, it's bind to header instead,
// so we don't try to register click event on a non-existent button
if (fullscreenButtons.back) {
fullscreenButtons.back.onclick = setView.bind(null, LAYOUT_MODE.list);
}
// Clicking the delete button while viewing a single item deletes that item
fullscreenButtons.delete.onclick = deleteSingleItem;
// Clicking the Edit button while viewing a photo switches to edit mode
fullscreenButtons.edit.onclick = function() {
LazyLoader.load(['js/ImageEditor.js',
'js/image_processor_thread.js',
'shared/js/media/crop_resize_rotate.js',
'shared/js/component_utils.js',
'shared/elements/gaia_menu/script.js'
], function() {
editPhotoIfCardNotFull(currentFileIndex);
});
};
// In fullscreen mode, the share button shares the current item
fullscreenButtons.share.onclick = shareSingleItem;
// Clicking the information button will display information about the photo.
fullscreenButtons.info.onclick = function() {
LazyLoader.load(['js/info.js', 'shared/style/confirm.css', 'style/info.css'],
function() {
showFileInformation(files[currentFileIndex]);
});
};
// Use the GestureDetector.js library to handle gestures.
// This will generate tap, pan, swipe and transform events
new GestureDetector(frames).startDetecting();
var frameOffset = 0; // how far are the frames swiped side-to-side?
// Handle gesture events
frames.addEventListener('tap', tapHandler);
frames.addEventListener('dbltap', dblTapHandler);
frames.addEventListener('pan', panHandler);
frames.addEventListener('swipe', swipeHandler);
frames.addEventListener('transform', transformHandler);
frames.addEventListener('wheel', wheelHandler);
currentFrame.video.onfullscreentap =
previousFrame.video.onfullscreentap =
nextFrame.video.onfullscreentap =
function fullscreenRequested(ev) {
setView(LAYOUT_MODE.fullscreen);
resizeFrames();
};
// When displaying a photo or video, a tap hides or shows the toolbar.
// The video player has its own toolbar, so when a video starts playing
// we want to hide the gallery toolbar. And then restore it on pause.
// All three players need this pair of event handlers.
// Note that we're using the onplaying/onpaused fake handlers the
// VideoPlayer object, not the real onplay/onpause handlers of the <video>
// element. This is because VideoPlayer pauses and plays the <video> when
// the user drags on the slider, and we don't want to trigger these handlers
// in that case.
currentFrame.video.onplaying =
previousFrame.video.onplaying =
nextFrame.video.onplaying =
function hideToolbarOnPlay() {
this.isToolbarHidden =
fullscreenView.classList.contains('toolbar-hidden');
if (!this.isToolbarHidden) {
fullscreenView.classList.add('toolbar-hidden');
}
};
currentFrame.video.onpaused =
previousFrame.video.onpaused =
nextFrame.video.onpaused =
function restoreToolbarOnPause() {
this.isToolbarHidden =
fullscreenView.classList.contains('toolbar-hidden');
if (this.isToolbarHidden === true) {
fullscreenView.classList.remove('toolbar-hidden');
}
delete this.isToolbarHidden;
};
// Each of the Frame container elements may be subject to animated
// transitions. So give them transitionend event handlers that
// remove the transition style property when the transition ends.
// This helps prevent unexpected transitions.
function removeTransition(event) {
event.target.style.transition = null;
}
previousFrame.container.addEventListener('transitionend', removeTransition);
currentFrame.container.addEventListener('transitionend', removeTransition);
nextFrame.container.addEventListener('transitionend', removeTransition);
// Clicking the delete button while viewing a single item deletes that item
function deleteSingleItem() {
var msg;
if (files[currentFileIndex].metadata.video) {
msg = 'delete-video?';
}
else {
msg = 'delete-photo?';
}
// We need to disable NFC sharing when showing delete confirmation dialog
NFC.unshare();
Dialogs.confirm({
messageId: msg,
cancelId: 'cancel',
confirmId: 'delete',
danger: true,
bodyClass: 'showing-dialog'
}, function() { // onSuccess
// disable delete, edit and share button to prevent
// operations while delete item
fullscreenButtons.delete.classList.add('disabled');
fullscreenButtons.share.classList.add('disabled');
fullscreenButtons.edit.classList.add('disabled');
deleteFile(currentFileIndex);
// Enable NFC sharing when done deleting and returns to fullscreen view
NFC.share(getCurrentFile);
}, function() { // onCancel
// Enable NFC sharing when cancels delete and returns to fullscreen view
NFC.share(getCurrentFile);
});
}
// In fullscreen mode, the share button shares the current item
function shareSingleItem() {
// This is the item we're sharing
var fileinfo = files[currentFileIndex];
// If the item is a video, just share it
if (fileinfo.metadata.video) {
share([currentFrame.videoblob]);
}
else {
// Otherwise it is an image.
// If it does not have any EXIF orientation, and if we don't need
// to downsample it, then just share it as it is.
if (!fileinfo.metadata.rotation &&
!fileinfo.metadata.mirrored &&
!CONFIG_MAX_PICK_PIXEL_SIZE) {
share([currentFrame.imageblob]);
}
else {
// This is only tricky case. If we are sharing an image that uses
// EXIF orientation for correct display, rotate it before sharing
// so that the recieving app doesn't have to know about EXIF
LazyLoader.load(['shared/js/media/crop_resize_rotate.js'],
shareModifiedImage);
}
}
function shareModifiedImage() {
var metadata = fileinfo.metadata;
var button = fullscreenButtons.share;
button.classList.add('disabled');
Spinner.show();
var maxsize = CONFIG_MAX_PICK_PIXEL_SIZE || CONFIG_MAX_IMAGE_PIXEL_SIZE;
cropResizeRotate(currentFrame.imageblob, null,
maxsize || null, null, metadata,
function(error, rotatedBlob) {
Spinner.hide();
button.classList.remove('disabled');
if (error) {
console.error('Error while rotating image: ', error);
rotatedBlob = currentFrame.imageblob;
}
share([rotatedBlob], currentFrame.imageblob.name);
});
}
}
// In order to distinguish single taps from double taps, we have to
// wait after a tap arrives to make sure that a dbltap event isn't
// coming soon.
var taptimer = null;
function tapHandler(e) {
// Ignore tap event if 1. there is already a timer set, then this
// is the second tap and we're about to get a double tap event
// 2. currentFrame has not yet loaded any image or video.
if (taptimer ||
(!currentFrame.displayingImage && !currentFrame.displayingVideo)) {
return;
}
// If we don't get a second tap soon, then treat this as a single tap
taptimer = setTimeout(function() {
taptimer = null;
singletap(e);
}, GestureDetector.DOUBLE_TAP_TIME);
}
// Dispatch double tap events, but only when displaying a photo
function dblTapHandler(e) {
if (currentFrame.displayingVideo) {
return;
}
clearTimeout(taptimer);
taptimer = null;
doubletapOnPhoto(e);
}
// Resize all the frames' content, if its container's size is changed
function resizeFrames() {
nextFrame.reset();
previousFrame.reset();
currentFrame.reset();
}
// Handle single tap event on frames
// We manage the display of toolbar on the header and footer after user
// tap the image or video. If it's in preview mode, it simply switch to
// fullscreen mode directly.
function singletap(e) {
if (currentView === LAYOUT_MODE.fullscreen) {
if ((currentFrame.displayingImage || currentFrame.video.player.paused) &&
isPhone) {
fullscreenView.classList.toggle('toolbar-hidden');
}
} else if (!isPhone && currentView === LAYOUT_MODE.list &&
!files[currentFileIndex].metadata.video) {
// We don't separate cases by screen size, because we don't show
// preview screen on tiny device.
setView(LAYOUT_MODE.fullscreen);
resizeFrames();
}
}
// Quick zoom in and out with dbltap events
function doubletapOnPhoto(e) {
// Don't allow zooming while we're still scanning for photos and
// have found large photos without previews on the card. Zooming in
// decodes the full-size version of the photo and that can cause OOM
// errors if there is also metadata scanning going on with large images.
// XXX: Remove this when bug 854795 is fixed.
if (photodb.parsingBigFiles) {
return;
}
var scale;
if (currentFrame.fit.scale > currentFrame.fit.baseScale) { // If zoomed in
scale = currentFrame.fit.baseScale / currentFrame.fit.scale; // zoom out
} else { // Otherwise
scale = 2; // zoom in
}
currentFrame.zoom(scale, e.detail.clientX, e.detail.clientY, 200);
}
// Pan the item sideways when the user moves their finger across the screen
function panHandler(event) {
if (transitioning) {
return;
}
var dx = event.detail.relative.dx;
var dy = event.detail.relative.dy;
var oldFrameOffset = frameOffset;
// If the frames are already being shifted in the same direction as
// dx then this just continues the shift. Otherwise, dx might shift
// them back toward the center. If the frames are unshifted to begin
// with or become unshifted after applying dx, then we have got to
// pass dx to the pan() method of the frame, because it might pan
// the image within the frame. But that method returns any dx it
// can't use, and we apply that to shifting the frames.
// If the frames are already shifted and dx is in the same direction, or
// if dx is in the opposite direction but isn't big enough to bring
// the frames back to the center, just adjust the frame positions.
// There is no need to pan the content of the frame in this case.
if ((frameOffset > 0 && dx > 0) ||
(frameOffset < 0 && dx < 0) ||
(frameOffset !== 0 && frameOffset > -dx)) {
frameOffset += dx;
}
else {
// If the frame is shifted, this dx brings it back to center
if (frameOffset !== 0) {
dx += frameOffset;
frameOffset = 0;
}
// Now let the frame pan its content, and add any dx that it doesn't use
// to the frame offset
frameOffset += currentFrame.pan(dx, dy);
}
// Don't swipe past the end of the last item or past the start of the first
// Handle frameOffset reset in RTL when directions are reversed. See 1099458
if (document.documentElement.dir === 'ltr') {
if ((currentFileIndex === 0 && frameOffset > 0) ||
(currentFileIndex === files.length - 1 && frameOffset < 0)) {
frameOffset = 0;
}
} else {
if ((currentFileIndex === 0 && frameOffset < 0) ||
(currentFileIndex === files.length - 1 && frameOffset > 0)) {
frameOffset = 0;
}
}
// If the frameOffset has changed since we started, reposition the frames
if (frameOffset !== oldFrameOffset) {
setFramesPosition();
}
}
// When the user lifts their finger after panning we get this event
function swipeHandler(event) {
if (transitioning) {
return;
}
// If we just panned within a zoomed-in photo, and the frames are not
// shifted at all, then we don't have to do anything here.
if (frameOffset === 0) {
return;
}
// 1 means we're going to the next item -1 means the previous
var direction = (frameOffset < 0) ? 1 : -1;
// If we're in a right-to-left locale, reverse those directions
if (document.documentElement.dir === 'rtl') {
direction *= -1;
}
// Did we pan far enough or swipe fast enough to transition to
// a different item?
var farenough =
Math.abs(frameOffset) > window.innerWidth * TRANSITION_FRACTION;
var velocity = event.detail.vx;
var fastenough = Math.abs(velocity) > TRANSITION_SPEED;
// Make sure that that the speed and pan amount are in the same direction
var samedirection = velocity === 0 || frameOffset / velocity >= 0;
// Is there a next or previous item to transition to?
var fileexists =
(direction === 1 && currentFileIndex + 1 < files.length) ||
(direction === -1 && currentFileIndex > 0);
// If all of these conditions hold, then we'll transition to the
// next photo or the previous photo
var time;
if (direction !== 0 && (farenough || fastenough) &&
samedirection && fileexists) {
// Compute how long the transition should take based on the velocity
var speed = Math.max(Math.abs(velocity), TRANSITION_SPEED);
time = (window.innerWidth - Math.abs(frameOffset)) / speed;
// Transition frames in the appropriate direction
if (direction === 1) {
nextFile(time);
} else {
previousFile(time);
}
}
else if (frameOffset !== 0) {
// Otherwise, just restore the current item by undoing
// the translations we added during panning
time = Math.abs(frameOffset) / TRANSITION_SPEED;
currentFrame.container.style.transition =
nextFrame.container.style.transition =
previousFrame.container.style.transition =
'transform ' + time + 'ms ease';
resetFramesPosition();
// Ignore pan and zoom gestures while the transition happens
transitioning = true;
setTimeout(function() { transitioning = false; }, time);
}
}
// When a screen reader swipes with two fingers
function wheelHandler(event) {
if (event.deltaMode !== event.DOM_DELTA_PAGE || !event.deltaX) {
return;
}
if (event.deltaX > 0) {
nextFile(150);
} else {
previousFile(150);
}
}
// We also support pinch-to-zoom
function transformHandler(e) {
if (transitioning) {
return;
}
// Don't allow zooming while we're still scanning for photos and
// have found large photos without previews on the card. Zooming in
// decodes the full-size version of the photo and that can cause OOM
// errors if there is also metadata scanning going on with large images.
// XXX: Remove this when bug 854795 is fixed.
if (photodb.parsingBigFiles) {
return;
}
currentFrame.zoom(e.detail.relative.scale,
e.detail.midpoint.clientX,
e.detail.midpoint.clientY);
}
// A utility function to display the nth image or video in the specified frame
// Used in showFile(), nextFile() and previousFile().
function setupFrameContent(n, frame) {
// Make sure n is in range
if (n < 0 || n >= files.length) {
frame.clear();
delete frame.filename;
return;
}
var fileinfo = files[n];
// If we're already displaying this file in this frame, then do nothing
if (fileinfo.name === frame.filename) {
return;
}
// Remember what file we're going to display
frame.filename = fileinfo.name;
photodb.getFile(fileinfo.name, function(imagefile) {
if (fileinfo.metadata.video) {
// If this is a video, then the file we just got is the poster image
// and we still have to fetch the actual video
getVideoFile(fileinfo.metadata.video, function(videofile) {
frame.displayVideo(videofile, imagefile,
fileinfo.metadata.width / fileinfo.metadata.height,
fileinfo.metadata.rotation || 0);
});
}
else {
// Otherwise, just display the image
frame.displayImage(
imagefile,
fileinfo.metadata.width,
fileinfo.metadata.height,
fileinfo.metadata.preview,
fileinfo.metadata.rotation,
fileinfo.metadata.mirrored);
}
});
}
var FRAME_BORDER_WIDTH = 3;
function setFramesPosition() {
var width = window.innerWidth + FRAME_BORDER_WIDTH;
currentFrame.container.style.transform =
'translateX(' + frameOffset + 'px)';
if (document.documentElement.dir === 'ltr') {
nextFrame.container.style.transform =
'translateX(' + (frameOffset + width) + 'px)';
previousFrame.container.style.transform =
'translateX(' + (frameOffset - width) + 'px)';
}
else {
// For RTL languages we swap next and previous sides
nextFrame.container.style.transform =
'translateX(' + (frameOffset - width) + 'px)';
previousFrame.container.style.transform =
'translateX(' + (frameOffset + width) + 'px)';
}
// XXX Bug 1021782 add 'current' class to currentFrame
nextFrame.container.classList.remove('current');
previousFrame.container.classList.remove('current');
currentFrame.container.classList.add('current');
// Hide adjacent frames from screen reader
nextFrame.container.setAttribute('aria-hidden', true);
previousFrame.container.setAttribute('aria-hidden', true);
currentFrame.container.removeAttribute('aria-hidden');
}
function resetFramesPosition() {
frameOffset = 0;
setFramesPosition();
}
// Switch from thumbnail list view to single-picture fullscreen view
// and display the specified file.
function showFile(n) {
// Mark what we're focusing on and unmark the old one
updateFocusThumbnail(n);
updateFrames();
// Disable the edit button if this is a video or mediaDB is scanning
if (files[currentFileIndex].metadata.video ||
photodb.scanning) {
fullscreenButtons.edit.classList.add('disabled');
} else {
fullscreenButtons.edit.classList.remove('disabled');
}
// Always bring delete and share button back after show file
fullscreenButtons.delete.classList.remove('disabled');
fullscreenButtons.share.classList.remove('disabled');
}
function updateFrames() {
var n = currentFileIndex;
setupFrameContent(n - 1, previousFrame);
setupFrameContent(n, currentFrame);
setupFrameContent(n + 1, nextFrame);
resetFramesPosition();
}
function clearFrames() {
previousFrame.clear();
currentFrame.clear();
nextFrame.clear();
delete previousFrame.filename;
delete currentFrame.filename;
delete nextFrame.filename;
}
// Transition to the next file, animating it over the specified time (ms).
// This is used when the user pans.
function nextFile(time) {
// If already displaying the last one, do nothing.
if (currentFileIndex === files.length - 1) {
return;
}
// If the current frame is using a <video> element instead of just
// displaying a poster image, reset it back to just the image
if (currentFrame.displayingVideo && currentFrame.video.playerShowing) {
currentFrame.video.init();
}
// Set a flag to ignore pan and zoom gestures during the transition.
transitioning = true;
// Set transitions for the visible frames
var transition = 'transform ' + time + 'ms ease';
currentFrame.container.style.transition = transition;
nextFrame.container.style.transition = transition;
// Cycle the three frames so next becomes current,
// current becomes previous, and previous becomes next.
var tmp = previousFrame;
previousFrame = currentFrame;
currentFrame = nextFrame;
nextFrame = tmp;
updateFocusThumbnail(currentFileIndex + 1);
// Move (transition) the frames to their new position
resetFramesPosition();
// Update the frame for the new next item
setupFrameContent(currentFileIndex + 1, nextFrame);
// When the transition is done, cleanup
currentFrame.container.addEventListener('transitionend', function done(e) {
this.removeEventListener('transitionend', done);
transitioning = false;
// Reposition the item that just transitioned off the screen
// to reset any zooming and panning
previousFrame.reset();
});
// Disable the edit button if this is a video or
// mediaDB is scanning, enable otherwise
if (currentFrame.displayingVideo || photodb.scanning) {
fullscreenButtons.edit.classList.add('disabled');
} else {
fullscreenButtons.edit.classList.remove('disabled');
}
}
// Just like nextFile() but in the other direction
function previousFile(time) {
// if already displaying the first one, do nothing.
if (currentFileIndex === 0) {
return;
}
// If the current frame is using a <video> element instead of just
// displaying a poster image, reset it back to just the image.
if (currentFrame.displayingVideo && currentFrame.video.playerShowing) {
currentFrame.video.init();
}
// Set a flag to ignore pan and zoom gestures during the transition.
transitioning = true;
// Set transitions for the visible frames
var transition = 'transform ' + time + 'ms ease';
previousFrame.container.style.transition = transition;
currentFrame.container.style.transition = transition;
// Transition to the previous item: previous becomes current, current
// becomes next, etc.
var tmp = nextFrame;
nextFrame = currentFrame;
currentFrame = previousFrame;
previousFrame = tmp;
updateFocusThumbnail(currentFileIndex - 1);
// Move (transition) the frames to their new position
resetFramesPosition();
// Preload the new previous item
setupFrameContent(currentFileIndex - 1, previousFrame);
// When the transition is done do some cleanup
currentFrame.container.addEventListener('transitionend', function done(e) {
this.removeEventListener('transitionend', done);
transitioning = false;
// Reset the size and position of the item that just panned off
nextFrame.reset();
});
// Disable the edit button if we're now viewing a video or mediaDB
// is scanning, enable otherwise
if (currentFrame.displayingVideo || photodb.scanning) {
fullscreenButtons.edit.classList.add('disabled');
} else {
fullscreenButtons.edit.classList.remove('disabled');
}
}

View file

@ -0,0 +1,77 @@
;'use strict';var TRANSITION_FRACTION=0.25;var TRANSITION_SPEED=0.75;var thumbnails;var thumbnailList;var fullscreenView=$('fullscreen-view');const LAYOUT_MODE={list:'thumbnailListView',select:'thumbnailSelectView',fullscreen:'fullscreenView',edit:'editView',pick:'pickView',crop:'cropView'};var currentView;ScreenLayout.watch('portrait','(orientation: portrait)');var isPortrait=ScreenLayout.getCurrentLayout('portrait');var isPhone=ScreenLayout.getCurrentLayout('tiny');var fullscreenButtonIds=['back','delete','edit','share','camera','info'];var fullscreenButtons={};for(var i=0;i<fullscreenButtonIds.length;i++){var selector='fullscreen-'+fullscreenButtonIds[i]+'-button';selector+=(isPhone?'-tiny':'-large');fullscreenButtons[fullscreenButtonIds[i]]=document.getElementById(selector);}
var currentFileIndex=0;var editedPhotoIndex;var selectedFileNames=[];var selectedFileNamesToBlobs={};var videostorage=navigator.getDeviceStorage('videos');var justSavedEditedImage=false;function getVideoFile(filename,callback){var req=videostorage.get(filename);req.onsuccess=function(){callback(req.result);};req.onerror=function(){console.error('Failed to get video file',filename);};}
function getCurrentFile(){return new Promise(function(resolve,reject){var fileInfo=files[currentFileIndex];if(fileInfo.metadata.video){getVideoFile(fileInfo.metadata.video,function(file){resolve(file);});}else{photodb.getFile(fileInfo.name,function(file){resolve(file);},function(errmsg){reject(errmsg);});}});}
function compareFilesByDate(a,b){if(a.date<b.date){return 1;}else if(a.date>b.date){return-1;}
return 0;}
function getFileIndex(filename){var fileGroup=thumbnailList.groupMap[filename];if(!fileGroup){console.error('file group does not exist in thumbnail List',filename);return-1;}
var index=0;var thumbnail=thumbnailList.thumbnailMap[filename];index=fileGroup.thumbnails.indexOf(thumbnail);if(index<0){console.error('filename does not exist in thumbnail list',filename);return-1;}
for(var n=0;n<thumbnailList.itemGroups.length;n++){if(thumbnailList.itemGroups[n].groupID===fileGroup.groupID){break;}
index+=thumbnailList.itemGroups[n].getCount();}
return index;}
function fileDeleted(filename){var fileIndex=currentFileIndex;for(var n=0;n<files.length;n++){if(files[n].name===filename){break;}}
if(n>=files.length){return;}
files.splice(n,1)[0];thumbnailList.removeItem(filename);if(n<fileIndex){fileIndex--;}
if(fileIndex>=files.length){fileIndex=files.length-1;}
if(n<editedPhotoIndex){editedPhotoIndex--;}
if(files.length>0&&(currentView===LAYOUT_MODE.fullscreen)){showFile(fileIndex);}else{updateFocusThumbnail(fileIndex);}
if(files.length===0){if(currentView!==LAYOUT_MODE.pick){setView(LAYOUT_MODE.list);}
Overlay.show('emptygallery');}}
function deleteFile(n){if(n<0||n>=files.length){return;}
var fileinfo=files[n];photodb.deleteFile(files[n].name);if(fileinfo.metadata.video){videostorage.delete(fileinfo.metadata.video);}
if(fileinfo.metadata.preview&&fileinfo.metadata.preview.filename){var pictures=navigator.getDeviceStorage('pictures');pictures.delete(fileinfo.metadata.preview.filename);}}
function fileCreated(fileinfo){if(picking&&fileinfo.metadata.video){return;}
photodb.getFileInfo(fileinfo.name,function(fileinfo){var insertPosition;if(Overlay.current==='emptygallery'||Overlay.current==='scanning'){Overlay.hide();}
thumbnailList.addItem(fileinfo);insertPosition=getFileIndex(fileinfo.name);if(insertPosition<0){return;}
files.splice(insertPosition,0,fileinfo);if(currentFileIndex>=insertPosition){currentFileIndex++;}
if(editedPhotoIndex>=insertPosition){editedPhotoIndex++;}
if(currentView===LAYOUT_MODE.fullscreen){if(justSavedEditedImage){var banner=$('edit-copy-save-banner');showFile(insertPosition);document.l10n.setAttributes($('edit-copy-save-status'),'edit-copy-saved');banner.hidden=false;setTimeout(function(){banner.hidden=true;},3000);}else{showFile(currentFileIndex);}}
justSavedEditedImage=false;});}
function scrollToShowThumbnail(n){if(!files[n]){return;}
var selector='img[data-filename="'+files[n].name+'"]';var thumbnail=thumbnails.querySelector(selector);if(thumbnail){var screenTop=thumbnails.scrollTop;var screenBottom=screenTop+thumbnails.clientHeight;var thumbnailTop=thumbnail.offsetTop;var thumbnailBottom=thumbnailTop+thumbnail.offsetHeight;var toolbarHeight=40;screenBottom-=toolbarHeight;if(thumbnailTop<screenTop){thumbnails.scrollTop=thumbnailTop;}
else if(thumbnailBottom>screenBottom){thumbnails.scrollTop=thumbnailBottom-thumbnails.clientHeight+toolbarHeight;}}}
function setView(view){if(currentView===view){return;}
document.body.classList.remove(currentView);document.body.classList.add(view);switch(currentView){case LAYOUT_MODE.select:Array.forEach(thumbnails.querySelectorAll('.selected.thumbnailImage'),function(elt){elt.classList.remove('selected');});if(!isPhone&&currentFileIndex!==-1){showFile(currentFileIndex);}
break;case LAYOUT_MODE.fullscreen:if(!isPhone&&(view===LAYOUT_MODE.list)&&!isPortrait&&currentFileIndex!==-1){resizeFrames();}else{clearFrames();}
break;}
switch(view){case LAYOUT_MODE.list:scrollToShowThumbnail(currentFileIndex);if(currentView===LAYOUT_MODE.fullscreen){NFC.unshare();}
break;case LAYOUT_MODE.fullscreen:resizeFrames();NFC.share(getCurrentFile);break;case LAYOUT_MODE.select:clearSelection();if(!isPhone&&currentFrame.video&&!isPortrait){currentFrame.video.pause();}
break;case LAYOUT_MODE.edit:NFC.unshare();break;}
if(!isPhone){if(view!==LAYOUT_MODE.fullscreen){$('fullscreen-title').setAttribute('data-l10n-id','preview');}else{$('fullscreen-title').setAttribute('data-l10n-id','gallery');}}
currentView=view;}
function thumbnailClickHandler(evt){var target=evt.target;if(!target){return;}
target=target.classList.contains('thumbnail')?target.firstElementChild:target;if(!target||!target.classList.contains('thumbnailImage')){return;}
if(photodb.state!==MediaDB.READY){return;}
var index=getFileIndex(target.dataset.filename);if(picking&&currentView===LAYOUT_MODE.pick&&index>=0){Pick.select(files[index]);}else if(currentView===LAYOUT_MODE.select){updateSelection(target);}else{LazyLoader.load('js/frame_scripts.js',function(){if(isPortrait||isPhone){setView(LAYOUT_MODE.fullscreen);}
showFile(index);});}}
function updateFocusThumbnail(n){var previousIndex=currentFileIndex;currentFileIndex=n;if(isPhone||currentFileIndex===-1){return;}
var newTarget=thumbnailList.thumbnailMap[files[currentFileIndex].name];if(newTarget){newTarget.htmlNode.classList.add('focus');}
if(previousIndex===currentFileIndex){return;}
var oldTarget=files[previousIndex]?thumbnailList.thumbnailMap[files[previousIndex].name]:undefined;if(oldTarget){oldTarget.htmlNode.classList.remove('focus');}}
function clearSelection(){if(!isPhone){clearFrames();}
selectedFileNames=[];selectedFileNamesToBlobs={};$('thumbnails-delete-button').classList.add('disabled');$('thumbnails-share-button').classList.add('disabled');document.l10n.setAttributes($('thumbnails-number-selected'),'number-selected2',{n:0});}
function updateSelection(thumbnail){thumbnail.classList.toggle('selected');var selected=thumbnail.classList.contains('selected');var index=getFileIndex(thumbnail.dataset.filename);if(index<0){return;}
var filename=files[index].name;if(selected){selectedFileNames.push(filename);updateFocusThumbnail(index);if(files[index].metadata.video){getVideoFile(files[index].metadata.video,function(file){selectedFileNamesToBlobs[filename]=file;});}
else{photodb.getFile(filename,function(file){selectedFileNamesToBlobs[filename]=file;});}
if(!isPhone){showFile(currentFileIndex);}}
else{delete selectedFileNamesToBlobs[filename];var i=selectedFileNames.indexOf(filename);if(i!==-1){selectedFileNames.splice(i,1);}
if(currentFileIndex===index&&!isPhone){if(i>0){var lastSelected=selectedFileNames[i-1];var lastSelectedIndex=getFileIndex(lastSelected);updateFocusThumbnail(lastSelectedIndex);showFile(currentFileIndex);}else{clearFrames();}}}
var numSelected=selectedFileNames.length;document.l10n.setAttributes($('thumbnails-number-selected'),'number-selected2',{n:numSelected});if(numSelected===0){$('thumbnails-delete-button').classList.add('disabled');$('thumbnails-share-button').classList.add('disabled');}
else{$('thumbnails-delete-button').classList.remove('disabled');$('thumbnails-share-button').classList.remove('disabled');}}
function launchCameraApp(){fullscreenButtons.camera.classList.add('disabled');$('thumbnails-camera-button').classList.add('disabled');$('overlay-camera-button').classList.add('disabled');var a=new MozActivity({name:'record',data:{type:'photos'}});a.onsuccess=()=>{};window.setTimeout(function(){fullscreenButtons.camera.classList.remove('disabled');$('thumbnails-camera-button').classList.remove('disabled');$('overlay-camera-button').classList.remove('disabled');},2000);}
function deleteSelectedItems(){var selected=thumbnails.querySelectorAll('.selected.thumbnailImage');if(selected.length===0){return;}
Dialogs.confirm({messageId:'delete-n-items?',messageArgs:{n:selected.length},cancelId:'cancel',confirmId:'delete',danger:true,bodyClass:'showing-dialog'},function(){for(var i=0;i<selected.length;i++){selected[i].classList.toggle('selected');deleteFile(getFileIndex(selected[i].dataset.filename));}
clearSelection();});}
function shareSelectedItems(){var blobs=selectedFileNames.map(function(name){return selectedFileNamesToBlobs[name];});share(blobs);}
function share(blobs,blobName){if(blobs.length===0){return;}
var names=[],types=[],fullpaths=[];blobs.forEach(function(blob){var name=blob.name;if(!name&&blobs.length===1){name=blobName;}
fullpaths.push(name);name=name.substring(name.lastIndexOf('/')+1);names.push(name);var type=blob.type;if(type){type=type.substring(0,type.indexOf('/'));}
types.push(type);});var type;if(types.length===1||types.every(function(t){return t===types[0];})){type=types[0]+'/*';}else{type='application/*';}
var a=new MozActivity({name:'share',data:{type:type,number:blobs.length,blobs:blobs,filenames:names,filepaths:fullpaths}});a.onerror=function(e){if(a.error.name==='NO_PROVIDER'){document.l10n.formatValue('share-noprovider').then(alert);}
else{console.warn('share activity error:',a.error.name);}};}
function resizeHandler(){isPortrait=ScreenLayout.getCurrentLayout('portrait');if(currentView===LAYOUT_MODE.list&&isPortrait&&typeof currentFrame!=='undefined'&&currentFrame.video){currentFrame.video.pause();}
if(currentView===LAYOUT_MODE.fullscreen||(!isPhone&&!isPortrait&&(currentView===LAYOUT_MODE.list||currentView===LAYOUT_MODE.select))){resizeFrames();setFramesPosition();}}
function doNotScanInBackgroundHack(photodb){const enoughMB=512;var memoryMB=0;window.addEventListener('visibilitychange',backgroundScanKiller);photodb.addEventListener('scanend',function(){window.removeEventListener('visibilitychange',backgroundScanKiller);});function backgroundScanKiller(){if(!document.hidden||memoryMB>=enoughMB){return;}
if(!navigator.getFeature){exit();}
else{navigator.getFeature('hardware.memory').then(function(mem){memoryMB=mem;if(memoryMB<enoughMB){exit();}});}
function exit(){if(document.hidden&&photodb.scanning){console.warn('[Gallery] exiting to avoid background scan.');setTimeout(function(){if(document.hidden){window.close();}
else{console.warn('[Gallery] now visible again, so not exiting');}},500);}}}}

View file

@ -0,0 +1 @@
;'use strict';var files=[];var picking=(window.location.hash==='#pick');var photodb;

View file

@ -0,0 +1,485 @@
/*
* image_processor.js:
*
* This module defines an ImageProcessor class, which provides the image
* processing primitives required by the Gallery app image editor.
*
* When creating an ImageProcessor, you must specify the maximum number
* of pixels that will be processed in a single batch. When done with
* an ImageProcessor, call destroy() if you want to immediately deallocate
* any resources it uses.
*
* Use the setGamma() method to specify the value to use for gamma correction.
* a value of 1.0 leaves the image unmodified.
*
* Use the setLevels() method to specify the level parameters used for
* image auto enhancement. These parameters are usually computed from a
* histogram and take the form of an object like this:
* {
* red: { min: 20, max:240 },
* green: { min: 30, max:200 },
* blue: { min: 0, max:150 },
* }
*
* Use setMatrix() to specify a 3-row by 4 column matrix used to modify
* each pixel of the image (for converting to black and white or sepia,
* for example). The matrix is represented as an array of 12 floating-point
* numbers.
*
* Call processImage() to process pixels using the gamma, levels and matrix
* previously set with other methods. processImage() expects an ArrayBuffer
* of pixel data in ImageData format (four bytes per pixel in rgba order).
* processImage() returns a Promise that resolves to an ArrayBuffer
* (possibly the same one) when the processing is done.
*
* To compute a histogram for an image, call computeHistogram() with an
* array buffer of rgba pixels. The return value is a Promise that resolves
* to an array buffer. The returned array buffer is 3kb long and should be
* interpreted as 3 arrays each with 256 32-bit integers. These arrays hold
* the counts for the red, green, and blue components.
*
* Even though it implements a Promise-based API, ImageProcessor is fully
* synchronous. The Promises it returns are already resolved.
*
* image_processor_thread.js defines an ImageProcessorThread class that
* implements the same API as ImageProcessor, but does so with asynchronous
* promises in a different worker thread. For responsiveness, it is
* generally better to use ImageProcessorThread instead of ImageProcessor.
*
* An ImageProcessorThread implements the ImageProcessor API by sending
* messsages to a Worker. The worker loads image_processor_worker.js which
* is responsible for receiving the messages sent by image_processor_thread.
* image_processor_worker.js loads this ImageProcessor class into the
* worker context where it can run synchronously without affecting UI
* responsiveness.
*
* Finally, note that this ImageProcessor class contains an
* ImageProcessorEngine module inside of it. This "engine" is a hand-written
* asm.js module that performs the actual pixel value manipulation much
* faster than would be possible with regular JavaScript code.
*/
(function(exports) {
'use strict';
var global = exports; // window or worker context
exports.ImageProcessor = ImageProcessor;
function ImageProcessor(maxpixels) {
// If we're being called directly, then we get maxpixels and initialize now.
// Otherwise, if we're being used in a worker, then the initialize call
// will arrive separately.
if (maxpixels) {
this.initialize(maxpixels);
}
}
// We do this in a separate method because we have to do this in a worker, too
ImageProcessor.prototype.initialize = function(maxpixels) {
this.maxpixels = maxpixels;
this.hasNewParams = true;
this.engine = null;
this.params = {};
this.setGamma(1.0);
this.setLevels(null);
this.setMatrix(null);
};
ImageProcessor.prototype.destroy = function() {
// ImageProcessorThread needs this method, so we define it here, too.
};
ImageProcessor.prototype.setGamma = function setGamma(gamma) {
this.params.gamma = gamma || 1.0;
this.hasNewParams = true;
};
// This could be generalized to take three 256-byte arrays for
// completely general value mapping.
ImageProcessor.prototype.setLevels = function setLevels(levels) {
if (levels) {
this.params.hasLevels = true;
this.params.rmin = levels.red.min;
this.params.rmax = levels.red.max;
this.params.gmin = levels.green.min;
this.params.gmax = levels.green.max;
this.params.bmin = levels.blue.min;
this.params.bmax = levels.blue.max;
}
else {
this.params.hasLevels = false;
// these default values will cause no level correction to be done
this.params.rmin = 0;
this.params.rmax = 0;
this.params.gmin = 0;
this.params.gmax = 0;
this.params.bmin = 0;
this.params.bmax = 0;
}
this.hasNewParams = true;
};
ImageProcessor.prototype.setMatrix = function setMatrix(matrix) {
if (matrix) {
this.params.hasMatrix = true;
this.params.m0 = matrix[0];
this.params.m1 = matrix[1];
this.params.m2 = matrix[2];
this.params.m3 = matrix[3];
this.params.m4 = matrix[4];
this.params.m5 = matrix[5];
this.params.m6 = matrix[6];
this.params.m7 = matrix[7];
this.params.m8 = matrix[8];
this.params.m9 = matrix[9];
this.params.m10 = matrix[10];
this.params.m11 = matrix[11];
}
else {
this.params.hasMatrix = false;
this.params.m0 = 0;
this.params.m1 = 0;
this.params.m2 = 0;
this.params.m3 = 0;
this.params.m4 = 0;
this.params.m5 = 0;
this.params.m6 = 0;
this.params.m7 = 0;
this.params.m8 = 0;
this.params.m9 = 0;
this.params.m10 = 0;
this.params.m11 = 0;
}
this.hasNewParams = true;
};
ImageProcessor.prototype.processImage = function processImage(pixelBuffer) {
// If this is a no-op, just return right away
if (this.params.gamma === 1.0 &&
!this.params.hasLevels &&
!this.params.hasMatrix) {
return Promise.resolve(pixelBuffer);
}
if (this.hasNewParams) {
this.setParams();
}
// These are the pixels that were passed in
var pixels = new Uint8Array(pixelBuffer);
// This is the array we have to copy the pixels into so that the
// asm.js processor can manipulate them.
var heapPixels = new Uint8Array(this.heap,
ImageProcessorEngine.pixelOffset,
pixels.length);
// Copy the input pixels onto the asm.js heap
heapPixels.set(pixels);
// Process the pixels with our asm.js module
this.engine.process(heapPixels.length,
this.params.hasLevels || this.params.gamma !== 1.0,
this.params.hasMatrix);
// Copy the processed pixels from the heap back to the local array
pixels.set(heapPixels);
return Promise.resolve(pixelBuffer);
};
ImageProcessor.prototype.computeHistogram = function(pixelBuffer) {
var pixels = new Uint8Array(pixelBuffer);
var histogramBuffer = new ArrayBuffer(3 * 4 * 256);
var red = new Uint32Array(histogramBuffer, 0, 256);
var green = new Uint32Array(histogramBuffer, 4 * 256, 256);
var blue = new Uint32Array(histogramBuffer, 8 * 256, 256);
// Compute a histogram of the image
// There is not an asm.js version of this becasue it is fast
// enough just like this, especially since we compute the
// histogram of the preview image, not the full-size image.
for(var i = 0; i < pixels.length; i += 4) {
red[pixels[i]]++;
green[pixels[i+1]]++;
blue[pixels[i+2]]++;
}
return Promise.resolve(histogramBuffer);
};
ImageProcessor.prototype.setParams = function setParams() {
if (!this.engine) {
this.createEngine();
}
var p = this.params;
this.engine.setParams(p.gamma,
p.rmin, p.rmax,
p.gmin, p.gmax,
p.bmin, p.bmax,
p.hasMatrix,
p.m0, p.m1, p.m2, p.m3,
p.m4, p.m5, p.m6, p.m7,
p.m8, p.m9, p.m10, p.m11);
this.hasNewParams = false;
};
ImageProcessor.prototype.createEngine = function createEngine() {
var heapSize = ImageProcessorEngine.computeHeapSize(this.maxpixels);
this.heap = new ArrayBuffer(heapSize);
this.engine = ImageProcessorEngine(global, null, this.heap);
};
/*
* This asm.js module does the actual manipulation of pixels.
* asm.js is giving us a significant speed boost here, so be extra
* careful when editing this code to make sure that it still compiles
* correctly as an asm.js module.
*/
function ImageProcessorEngine(stdlib, foreign, heap) {
'use asm';
const bytes = new stdlib.Uint8Array(heap);
const min = stdlib.Math.min;
const max = stdlib.Math.max;
const pow = stdlib.Math.pow;
const GAMMA = 0x0000; // Offsets into the heap
const RED = 0x0100;
const GREEN = 0x0200;
const BLUE = 0x0300;
const M0 = 0x0400;
const M1 = 0x0500;
const M2 = 0x0600;
const M3 = 0x0700;
const M4 = 0x0800;
const M5 = 0x0900;
const M6 = 0x0A00;
const M7 = 0x0B00;
const M8 = 0x0C00;
const M9 = 0x0D00;
const M10 = 0x0E00;
const M11 = 0x0F00;
const PX = 0x1000; // pixels go here
// In setLevels, if the max-min range is smaller than this, then we
// assume that the image is not actually a photo and we don't adjust.
const LEVELS_THRESHOLD = 25.0;
function process(numbytes, doLevelsOrGamma, doMatrix) {
numbytes = numbytes | 0;
doLevelsOrGamma = doLevelsOrGamma | 0;
doMatrix = doMatrix | 0;
var i = 0;
var r = 0, g = 0, b = 0;
var r2 = 0, g2 = 0, b2 = 0;
for(i = 0; (i|0) < (numbytes|0); i = (i + 4)|0) {
// Read the raw red, green and blue values
r = bytes[(PX+i)|0]|0;
g = bytes[(PX+i+1)|0]|0;
b = bytes[(PX+i+2)|0]|0;
// If we have gamma correction or auto levels, do that now
if ((doLevelsOrGamma|0) == 1) {
r = bytes[((RED|0) + (r|0))|0] | 0;
g = bytes[((GREEN|0) + (g|0))|0] | 0;
b = bytes[((BLUE|0) + (b|0))|0] | 0;
}
// Color filter, if there is one specified.
if ((doMatrix|0) == 1) {
r2 = ((bytes[(M0+r)|0]|0) + (bytes[(M1+g)|0]|0) +
(bytes[(M2+b)|0]|0) + (bytes[M3]|0))|0;
g2 = ((bytes[(M4+r)|0]|0) + (bytes[(M5+g)|0]|0) +
(bytes[(M6+b)|0]|0) + (bytes[M7]|0))|0;
b2 = ((bytes[(M8+r)|0]|0) + (bytes[(M9+g)|0]|0) +
(bytes[(M10+b)|0]|0) + (bytes[M11]|0))|0;
r = min(max(r2|0, 0), 255);
g = min(max(g2|0, 0), 255);
b = min(max(b2|0, 0), 255);
}
// store the values back into the image
bytes[(PX+i)|0] = r;
bytes[(PX+i+1)|0] = g;
bytes[(PX+i+2)|0] = b;
}
}
function setParams(gamma,
rmin, rmax, gmin, gmax, bmin, bmax,
hasMatrix,
m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11) {
gamma = +gamma;
rmin = +rmin;
rmax = +rmax;
gmin = +gmin;
gmax = +gmax;
bmin = +bmin;
bmax = +bmax;
hasMatrix = hasMatrix|0;
m0 = +m0;
m1 = +m1;
m2 = +m2;
m3 = +m3;
m4 = +m4;
m5 = +m5;
m6 = +m6;
m7 = +m7;
m8 = +m8;
m9 = +m9;
m10 = +m10;
m11 = +m11;
setGamma(gamma);
setLevels(rmin, rmax, RED);
setLevels(gmin, gmax, GREEN);
setLevels(bmin, bmax, BLUE);
if ((hasMatrix|0) == 1) {
setMatrix(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11);
}
}
function setGamma(gamma) {
gamma = +gamma;
var i = 0;
var x = 0.0, y=0.0;
while((i|0) < 256) {
y = pow(x / 255.0, gamma);
y = 255.0 * y;
y = y + 0.5;
bytes[(GAMMA + i)|0] = ~~y; // float to int
i = (i + 1)|0;
x = x + 1.0;
}
}
function setMatrix(m0, m1, m2, m3,
m4, m5, m6, m7,
m8, m9, m10, m11)
{
m0 = +m0;
m1 = +m1;
m2 = +m2;
m3 = +m3;
m4 = +m4;
m5 = +m5;
m6 = +m6;
m7 = +m7;
m8 = +m8;
m9 = +m9;
m10 = +m10;
m11 = +m11;
var i = 0, x = 0.0;
while((i|0) < 256) {
bytes[(M0 + i)|0] = ~~(m0 * x + 0.5);
bytes[(M1 + i)|0] = ~~(m1 * x + 0.5);
bytes[(M2 + i)|0] = ~~(m2 * x + 0.5);
bytes[(M4 + i)|0] = ~~(m4 * x + 0.5);
bytes[(M5 + i)|0] = ~~(m5 * x + 0.5);
bytes[(M6 + i)|0] = ~~(m6 * x + 0.5);
bytes[(M8 + i)|0] = ~~(m8 * x + 0.5);
bytes[(M9 + i)|0] = ~~(m9 * x + 0.5);
bytes[(M10 + i)|0] = ~~(m10 * x + 0.5);
// constants, no table needed
bytes[M3] = ~~(m3 * 255.0 + 0.5);
bytes[M7] = ~~(m7 * 255.0 + 0.5);
bytes[M11] = ~~(m11 * 255.0 + 0.5);
i = (i + 1) | 0;
x = x + 1.0;
}
}
// This creates tables that already have gamma correction applied.
function setLevels(min, max, offset) {
min = +min;
max = +max;
offset = offset | 0;
var i = 0, x = 0.0;
var imin = 0;
var imax = 0;
var range = 0.0;
var factor = 1.0;
var value = 0;
range = max - min;
imin = ~~min;
imax = ~~max;
if (range < LEVELS_THRESHOLD) {
// If the pixels are all in a narrow range, this is probably not a
// photograph and we shouldn't try to modify it.
while((i|0) < 256) {
bytes[(offset+i)|0] = bytes[(GAMMA+i)|0];
i = (i + 1)|0;
}
}
else {
// Otherwise, spread the pixels out so min goes to 0 and max to 255
factor = 255.0 / range; // Pre-compute so we only have to divide once
while((i|0) < 256) {
if ((i|0) <= (imin|0)) {
value = 0;
}
else if ((i|0) >= (imax|0)) {
value = 255;
}
else {
value = ~~((x - min) * factor + 0.5);
}
bytes[(offset+i)|0] = bytes[(GAMMA+value)|0];
i = (i + 1)|0;
x = x + 1.0;
}
}
}
return {
setParams: setParams,
process: process
};
}
ImageProcessorEngine.gammaOffset = 0x0000;
ImageProcessorEngine.redOffset = 0x0100;
ImageProcessorEngine.greenOffset = 0x0200;
ImageProcessorEngine.blueOffset = 0x0300;
ImageProcessorEngine.matrixOffset = 0x0400;
ImageProcessorEngine.pixelOffset = 0x1000;
ImageProcessorEngine.computeHeapSize = function(maxPixels) {
// This is the number of bytes we need
var numbytes = maxPixels * 4 + ImageProcessorEngine.pixelOffset;
// But we need to round it up to the nearest power of two.
// (Or a multiple of 16mb, but I assume we won't go that high)
numbytes = 1 << Math.ceil(Math.log(numbytes) * Math.LOG2E);
// And we need to allocate at least 64kb
return Math.max(numbytes, 64 * 1024);
};
}(this));

View file

@ -0,0 +1,77 @@
/*
* This module implements the ImageProcessor API by sending messages
* to a worker running the code in image_processor_worker.js. That worker
* has an actual ImageProcessor object that does the image processing.
* Unlike ImageProcessor which is synchronous and returns promises that are
* already resolved, this class is asynchronous.
*
* See the documentation in image_processor.js for details.
*/
(function(exports) {
'use strict';
exports.ImageProcessorThread = ImageProcessorThread;
// Define this as a constant so we can change it in tests.
ImageProcessorThread.WORKER_URL = 'js/image_processor_worker.js';
function ImageProcessorThread(maxPixels) {
this.maxPixels = maxPixels;
this.worker = new Worker(ImageProcessorThread.WORKER_URL);
this.worker.postMessage({
cmd: 'initialize',
arg: maxPixels
});
this.worker.addEventListener('message', this);
this.pendingRequests = [];
}
ImageProcessorThread.prototype.destroy = function() {
this.worker.terminate();
};
ImageProcessorThread.prototype.setGamma = function(gamma) {
this.worker.postMessage({ cmd: 'setGamma', arg: gamma });
};
ImageProcessorThread.prototype.setLevels = function(levels) {
this.worker.postMessage({ cmd: 'setLevels', arg: levels });
};
ImageProcessorThread.prototype.setMatrix = function(matrix) {
this.worker.postMessage({ cmd: 'setMatrix', arg: matrix });
};
ImageProcessorThread.prototype.processImage = function(pixelBuffer) {
return new Promise(function(resolve, reject) {
this.worker.postMessage({ cmd: 'processImage', arg: pixelBuffer },
[pixelBuffer]);
this.pendingRequests.push({ resolve: resolve, reject: reject });
}.bind(this));
};
ImageProcessorThread.prototype.computeHistogram = function(buffer) {
return new Promise(function(resolve, reject) {
this.worker.postMessage({ cmd: 'computeHistogram', arg: buffer },
[buffer]);
this.pendingRequests.push({ resolve: resolve, reject: reject });
}.bind(this));
};
ImageProcessorThread.prototype.handleEvent = function handleEvent(e) {
if (e.type !== 'message') {
return;
}
if (e.data.cmd === 'processImage' || e.data.cmd === 'computeHistogram') {
var request = this.pendingRequests.shift();
if (e.data.result) {
request.resolve(e.data.result);
}
else {
request.reject(e.data.error);
}
}
};
}(this));

View file

@ -0,0 +1,33 @@
/*
* This code runs in a Worker to receive messages sent by
* image_processor_thread.js. It loads image_processor.js, creates an
* ImageProcessor object, and handles the messages it receives by
* calling methods on that object.
*
* See the documentation in image_processor.js for details.
*/
'use strict';
/* global console, ImageProcessor */
importScripts('image_processor.js');
var processor = new ImageProcessor();
self.onmessage = function(message) {
var cmd = message.data.cmd;
var arg = message.data.arg;
try {
var result = processor[cmd](arg);
if (result) { // A promised ArrayBuffer
result.then(function(returnValue) {
postMessage({ cmd: cmd, result: returnValue }, [returnValue]);
});
}
}
catch(e) {
console.error(e, e.stack);
postMessage({ cmd: cmd, error: e.toString() });
}
};

View file

@ -0,0 +1,85 @@
'use strict';
/* exported showFileInformation */
/* global
$,
getCurrentFile,
MediaUtils,
NFC,
videostorage
*/
// Hide the information view again, when clicking on cancel
$('info-close-button').onclick = function hideFileInformation() {
// Enable NFC sharing when user closes info and returns to fullscreen view
NFC.share(getCurrentFile);
$('info-view').classList.add('hidden');
document.body.classList.remove('showing-dialog');
};
function showFileInformation(fileinfo) {
if (fileinfo.metadata.video) {
var req = videostorage.get(fileinfo.metadata.video);
req.onsuccess = function() {
fileinfo.size = req.result.size;
fileinfo.type = req.result.type || 'video/3gp';
populateMediaInfo(fileinfo);
};
} else {
populateMediaInfo(fileinfo);
}
// We need to disable NFC sharing when showing file info view
NFC.unshare();
$('info-view').classList.remove('hidden');
document.body.classList.add('showing-dialog');
function populateMediaInfo(fileinfo) {
MediaUtils.getLocalizedSizeTokens(fileinfo.size).then((args) => {
var data = {
//set the video filename using metadata
'info-name': {
raw: getFileName(fileinfo.metadata.video || fileinfo.name)
},
'info-size': {
id: 'fileSize',
args: args
},
'info-type': {raw: fileinfo.type},
'info-date': {raw: MediaUtils.formatDate(fileinfo.date)},
'info-resolution': {
raw: fileinfo.metadata.width + 'x' + fileinfo.metadata.height
}
};
// Populate info overlay view
MediaUtils.populateMediaInfo(data);
// Hide Resolution for video files. See Bug 1217989
fileinfo.metadata.video ? setFieldVisibility('info-resolution', false) :
setFieldVisibility('info-resolution', true);
});
}
function getFileName(path) {
return path.split('/').pop();
}
function setFieldVisibility(id, visible) {
// Field label in info view
var label = $(id).previousElementSibling;
// If not visible, hide respective field label and
// its value and remove bottom border from the previous field.
if (visible) {
$(id).style.display = 'block';
label.style.display = 'block';
label.previousElementSibling.classList.remove('no-border');
} else {
$(id).style.display = 'none';
label.style.display = 'none';
if ($(id).nextElementSibling === null) {
label.previousElementSibling.classList.add('no-border');
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
;;'use strict';var NFC={shrinkingUI:null,peer:null,share:function share(f,options){if(navigator.mozNfc){if(window.ShrinkingUI&&options){NFC.shrinkingUI=new window.ShrinkingUI(options.foregroundElement,options.backgroundElement);window.addEventListener('shrinking-sent',NFC.sendFile.bind(NFC,f));navigator.mozNfc.onpeerfound=function(evt){if(!evt.peer){return;}
NFC.peer=evt.peer;NFC.shrinkingUI.start();evt.preventDefault();};navigator.mozNfc.onpeerlost=function(){NFC.peer=null;NFC.shrinkingUI.stop();};}else{navigator.mozNfc.onpeerready=function(event){NFC.peer=event.peer;NFC.sendFile(f);};}}},sendFile:function(f){if(!NFC.peer){return;}
if(typeof f==='function'){var promise=f();promise.then(function(file){NFC.peer.sendFile(file);if(NFC.shrinkingUI){NFC.shrinkingUI.stop();}});}
else{NFC.peer.sendFile(f);if(NFC.shrinkingUI){NFC.shrinkingUI.stop();}}},unshare:function unshare(){if(navigator.mozNfc){NFC.peer=null;navigator.mozNfc.onpeerready=null;navigator.mozNfc.onpeerfound=null;navigator.mozNfc.onpeerlost=null;if(NFC.shrinkingUI){NFC.shrinkingUI.stop();NFC.shrinkingUI=null;window.removeEventListener('shrinking-sent',NFC.sendFile);}}}};

View file

@ -0,0 +1,25 @@
;'use strict';(function init(){var activity;var activityData;var blob;var frame;var saved=false;var storage;var title;navigator.mozSetMessageHandler('activity',handleOpenActivity);function $(id){return document.getElementById(id);}
function open(blob){if(activityData.allowSave&&activityData.filename&&checkFilename()){getStorageIfAvailable('pictures',blob.size,function(ds){storage=ds;showSaveButton();});}
getImageSize(blob,success,error);function success(metadata){var pixels=metadata.width*metadata.height;var imagesizelimit=CONFIG_MAX_IMAGE_PIXEL_SIZE;if(blob.type==='image/jpeg'){imagesizelimit*=Downsample.MAX_AREA_REDUCTION;}
var filesizelimit=2*CONFIG_MAX_IMAGE_PIXEL_SIZE;if(pixels>imagesizelimit||blob.size>filesizelimit){displayError('imagetoobig');return;}
if(!metadata.preview||pixels<512*1024){frame.displayImage(blob,metadata.width,metadata.height,null,metadata.rotation,metadata.mirrored);}
else{parseJPEGMetadata(blob.slice(metadata.preview.start,metadata.preview.end,'image/jpeg'),function success(previewmetadata){metadata.preview.width=previewmetadata.width;metadata.preview.height=previewmetadata.height;frame.displayImage(blob,metadata.width,metadata.height,metadata.preview,metadata.rotation,metadata.mirrored);},function error(){frame.displayImage(blob,metadata.width,metadata.height,null,metadata.rotation,metadata.mirrored);});}}
function error(msg){displayError('imageinvalid');}}
function handleOpenActivity(request){activity=request;activityData=activity.source.data;if(!frame){$('header').addEventListener('action',done);$('save').addEventListener('click',save);frame=new MediaFrame($('frame'),false,CONFIG_MAX_IMAGE_PIXEL_SIZE);if(CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH){frame.setMinimumPreviewSize(CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH,CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT);}
var gestureDetector=new GestureDetector(frame.container);gestureDetector.startDetecting();frame.container.addEventListener('dbltap',handleDoubleTap);frame.container.addEventListener('transform',handleTransform);frame.container.addEventListener('pan',handlePan);frame.container.addEventListener('swipe',handleSwipe);window.addEventListener('resize',frame.resize.bind(frame));if(activityData.exitWhenHidden){window.addEventListener('visibilitychange',function(){if(document.hidden){done();}});}
frame.onerror=function invalid(){displayError('imageinvalid');};}
title=baseName(activityData.filename||'');$('filename').textContent=title;blob=activityData.blob;open(blob);NFC.share(blob,{foregroundElement:$('open'),backgroundElement:document.body});}
function checkFilename(){if(activityData.filename.indexOf('.gallery/')!=-1){return false;}
else{var dotIdx=activityData.filename.lastIndexOf('.');if(dotIdx>-1){var ext=activityData.filename.substr(dotIdx+1);return MimeMapper.guessTypeFromExtension(ext)===blob.type;}else{return false;}}}
function displayError(msgid){document.l10n.formatValue(msgid).then(function(val){alert(val);done();});}
function done(){activity.postResult({saved:saved});activity=null;NFC.unshare();}
function handleDoubleTap(e){var scale;if(frame.fit.scale>frame.fit.baseScale){scale=frame.fit.baseScale/frame.fit.scale;}else{scale=2;}
frame.zoom(scale,e.detail.clientX,e.detail.clientY,200);}
function handleTransform(e){frame.zoom(e.detail.relative.scale,e.detail.midpoint.clientX,e.detail.midpoint.clientY);}
function handlePan(e){frame.pan(e.detail.relative.dx,e.detail.relative.dy);}
function handleSwipe(e){var direction=e.detail.direction;var velocity=e.detail.vy;if(direction==='down'&&velocity>2){done();}}
function save(){hideSaveButton();getUnusedFilename(storage,activityData.filename,function(filename){var savereq=storage.addNamed(blob,filename);savereq.onsuccess=function(){saved=filename;showBanner('saved',title);};savereq.onerror=function(e){console.error('Error saving',filename,e);};});}
function showSaveButton(){$('save').classList.remove('hidden');$('filename').textContent=$('filename').textContent;}
function hideSaveButton(){$('save').classList.add('hidden');$('filename').textContent=$('filename').textContent;}
function showBanner(msg,title){document.l10n.setAttributes($('message'),msg,{filename:title});$('banner').hidden=false;setTimeout(function(){$('banner').hidden=true;},3000);}
function baseName(filename){return filename.substring(filename.lastIndexOf('/')+1);}})();

View file

@ -0,0 +1,8 @@
;'use strict';var Overlay={current:null,hide:function hide(){Overlay.current=null;$('overlay').classList.add('hidden');document.body.classList.remove('showing-dialog');},show:function show(id){Overlay.current=id;LazyLoader.load('shared/style/confirm.css',function(){$('overlay-camera-button').classList.add('hidden');$('overlay-cancel-button').classList.add('hidden');$('overlay-menu').classList.add('hidden');document.body.classList.add('showing-dialog');var title,text;switch(id){case null:Overlay.hide();return;case'nocard':title='nocard3-title';text='nocard4-text';if(picking){$('overlay-cancel-button').classList.remove('hidden');$('overlay-menu').classList.remove('hidden');}
break;case'pluggedin':title='pluggedin2-title';text='pluggedin2-text';if(picking){$('overlay-cancel-button').classList.remove('hidden');$('overlay-menu').classList.remove('hidden');}
break;case'scanning':title='scanning-title';text='scanning-text';if(picking){$('overlay-cancel-button').classList.remove('hidden');$('overlay-menu').classList.remove('hidden');}
break;case'emptygallery':title=picking?'emptygallery2-title-pick':'emptygallery2-title';text='emptygallery2-text';$('overlay-menu').classList.remove('hidden');if(picking){$('overlay-cancel-button').classList.remove('hidden');}else{$('overlay-camera-button').classList.remove('hidden');}
break;case'upgrade':title='upgrade-title';text='upgrade-text';if(picking){$('overlay-cancel-button').classList.remove('hidden');$('overlay-menu').classList.remove('hidden');}
break;default:console.warn('Reference to undefined overlay',id);if(picking){$('overlay-cancel-button').classList.remove('hidden');$('overlay-menu').classList.remove('hidden');}
return;}
$('overlay-title').setAttribute('data-l10n-id',title);$('overlay-text').setAttribute('data-l10n-id',text);$('overlay').classList.remove('hidden');});},addEventListener:function(type,listener){$('overlay').addEventListener(type,listener);},removeEventListener:function(type,listener){$('overlay').removeEventListener(type,listener);},};$('overlay-cancel-button').addEventListener('click',function(){$('overlay').dispatchEvent(new CustomEvent('cancel'));});$('overlay-camera-button').addEventListener('click',function(){$('overlay').dispatchEvent(new CustomEvent('camera'));});

View file

@ -0,0 +1,361 @@
/* global $, setView, LAYOUT_MODE, photodb, LazyLoader, Spinner, ImageEditor */
/* global cropResizeRotate, MediaFrame */
/* global CONFIG_MAX_PICK_PIXEL_SIZE, CONFIG_MAX_IMAGE_PIXEL_SIZE */
/* exported Pick */
'use strict';
// XXX: the pick activity could, and probably should be handled with a
// completely different entry point from regular invocations of
// Gallery. If we can modularize the bootstrap/startup code and the
// thumbnail display code enough that we can use it in both entry
// points, it would probably be better to do it that way.
/*
* A pick activity can be in two distinct states:
*
* 1) the picking state where the user is browsing thumbnails.
* Tapping a thumbnail moves to state 2. Tapping the cancel
* button cancels the activity and the app exits.
*
* 2) the preview/cropping phase where the user sees a full-screen
* image and may have the option to crop it and also sees
* cancel and done buttons. Tapping cancel moves back to state 1.
* Tapping Done ends the pick activity and the app exits.
*
* This Pick module defines start(), select(), end(), cancel()
* and restart() methods to handle these state transitions.
* Note that this is not a Pick class, just a module of interacting
* functions for managing picks. The gallery code needs to call
* Pick.start() and Pick.crop().
*/
var Pick = (function() {
var request;
var pickType;
var pickWidth, pickHeight;
var pickedFileInfo;
var pickedFile;
var cropEditor;
// Called when we are first start up with the activity request object
function start(activity) {
request = activity;
pickType = request.source.data.type;
if (request.source.data.width && request.source.data.height) {
pickWidth = request.source.data.width;
pickHeight = request.source.data.height;
}
else {
pickWidth = pickHeight = 0;
}
setView(LAYOUT_MODE.pick);
// re-run the font-fit logic when header is visible
var pickHeading = $('pick-header-title');
pickHeading.textContent = pickHeading.textContent;
// Clicking on the pick back button cancels the pick activity.
$('pick-header').addEventListener('action', cancel);
// In crop view, the back button goes back to pick view
$('crop-top').addEventListener('action', restart);
// In crop view, the Done button crops crops the image and returns
// it to the invoking app;
$('crop-done-button').addEventListener('click', end);
}
// Called when the user selects a thumbnail in pick mode.
function select(fileinfo) {
pickedFileInfo = fileinfo;
// Do we actually want to allow the user to crop the image?
var nocrop = request.source.data.nocrop;
if (nocrop) {
// If we're not cropping show file name in the title bar
var fileName = pickedFileInfo.name.split('/').pop();
$('crop-header').textContent =
fileName.substr(0, fileName.lastIndexOf('.')) || fileName;
}
setView(LAYOUT_MODE.crop);
// Before the picked image is loaded, the done button is disabled
// to avoid users picking a black/empty image.
var doneButton = $('crop-done-button');
doneButton.disabled = true;
// We need all of these for cropping the photo:
// - ImageEditor to display the crop overlay.
// - frame_scripts because it has gesture_detector in it.
// - crop_resize_rotate.js scripts for cropResizeRotate().
LazyLoader.load(['js/frame_scripts.js',
'shared/js/media/crop_resize_rotate.js',
'js/ImageEditor.js'], gotScripts);
// When the scripts we need are loaded, load the picked file we need
function gotScripts() {
photodb.getFile(pickedFileInfo.name, gotFile);
}
// This is called with the file that needs to be cropped
function gotFile(file) {
pickedFile = file;
var previewData = pickedFileInfo.metadata.preview;
if (!previewData) {
// If there is no preview at all, this is a small image and
// it is its own preview. Just crop with the full-size image
startCrop();
}
else if (previewData.filename) {
// If there is an external preview file, use that. This means that
// the EXIF preview was not big enough
var storage = navigator.getDeviceStorage('pictures');
var getreq = storage.get(previewData.filename);
getreq.onsuccess = function() {
startCrop(getreq.result);
};
// If we fail to get the preview file, just use the full-size image
getreq.onerror = function() {
startCrop();
};
}
else {
// Otherwise, use the internal EXIF preview.
// This should be the normal case.
startCrop(pickedFile.slice(previewData.start,
previewData.end,
'image/jpeg'));
}
function startCrop(previewBlob) {
// Before the user can crop the image we have to create a
// preview of the image at the correct size and orientation
// if we do not already have one.
var blob, metadata, outputSize, useSpinner;
if (previewBlob) {
// If there is a preview, use it at full size. If we're using
// a preview we need to pass the size of the preview, but the
// EXIF orientation data from the fullsize image.
blob = previewBlob;
metadata = {
width: previewData.width,
height: previewData.height,
rotation: pickedFileInfo.metadata.rotation,
mirrored: pickedFileInfo.metadata.mirrored
};
outputSize = null;
useSpinner = false;
}
else {
// If there is no preview, use the picked file, but specify a maximum
// size so we don't decode at a size larger than needed.
blob = pickedFile;
metadata = pickedFileInfo.metadata;
var windowSize = window.innerWidth * window.innerHeight *
window.devicePixelRatio * window.devicePixelRatio;
outputSize = Math.min(windowSize,
CONFIG_MAX_PICK_PIXEL_SIZE ||
CONFIG_MAX_IMAGE_PIXEL_SIZE);
useSpinner = metadata.width * metadata.height > outputSize;
}
// Make sure the image is rotated correctly so that it appears
// right side up in the crop UI. Note that we only display a spinner
// here if we have to downsample a large image.
if (useSpinner) {
Spinner.show();
}
cropResizeRotate(blob, null, outputSize, null, metadata,
gotRotatedBlob);
}
function gotRotatedBlob(error, rotatedBlob) {
Spinner.hide();
if (error) {
console.error('Error while rotating image:', error);
rotatedBlob = pickedFile;
}
cropEditor = new ImageEditor(rotatedBlob, $('crop-frame'), {},
cropEditorReady, true);
}
function cropEditorReady() {
// Enable the done button so that users can finish picking image.
doneButton.disabled = false;
// If the initiating app doesn't want to allow the user to crop
// the image, we don't display the crop overlay. But we still use
// this image editor to preview the image.
if (nocrop) {
// Set a fake crop region even though we won't display it
// so that hasBeenCropped() works.
cropEditor.cropOverlayRegion.left = 0;
cropEditor.cropOverlayRegion.top = 0;
cropEditor.cropOverlayRegion.right = cropEditor.dest.w;
cropEditor.cropOverlayRegion.bottom = cropEditor.dest.h;
return;
}
cropEditor.showCropOverlay();
if (pickWidth) {
cropEditor.setCropAspectRatio(pickWidth, pickHeight);
}
else {
cropEditor.setCropAspectRatio(); // free form cropping
}
}
}
}
function end() {
// First, figure out what kind of image to return to the requesting app.
// If the activity request specifically included 'image/jpeg' or
// 'image/png', then we'll use that type. Otherwise, if a generic
// 'image/*' was requested (or if an unsupported type was requested)
// then we use null as the type. This value is passed to
// cropResizeRotate() and will leave the image unchanged if possible
// or will use jpeg if changes are needed.
// EXIF should be preserved.
if (Array.isArray(pickType)) {
if (pickType.indexOf(pickedFileInfo.type) !== -1) {
pickType = pickedFileInfo.type;
}
else if (pickType.indexOf('image/jpeg') !== -1) {
pickType = 'image/jpeg+exif';
}
else if (pickType.indexOf('image/png') !== -1) {
pickType = 'image/png';
}
else {
pickType = null; // Return unchanged or convert to JPEG
}
}
else if (pickType === 'image/*') {
pickType = null; // Return unchanged or convert to JPEG
}
if (pickType && pickType !== 'image/jpeg' &&
pickType !== 'image/jpeg+exif' && pickType !== 'image/png') {
pickType = null; // Return unchanged or convert to JPEG
}
else if (!pickType && pickedFileInfo.type == 'image/jpeg') {
// if we picked a JPEG image, then explicitly force EXIF copy.
pickType = 'image/jpeg+exif';
}
// In order to determine the cropRegion and outputSize arguments to
// cropResizeRotate() below we need to know the actual image size.
// If the image has EXIF rotation, we need to take that into account.
var fullImageWidth, fullImageHeight;
var rotation = pickedFileInfo.metadata.rotation || 0;
if (rotation === 90 || rotation === 270) {
fullImageWidth = pickedFileInfo.metadata.height;
fullImageHeight = pickedFileInfo.metadata.width;
}
else {
fullImageWidth = pickedFileInfo.metadata.width;
fullImageHeight = pickedFileInfo.metadata.height;
}
var cropRegion, cropFraction;
if (request.source.data.nocrop || !cropEditor.hasBeenCropped()) {
cropRegion = null;
cropFraction = 1;
}
else {
// Get the user's crop region from the crop editor
cropRegion = cropEditor.getCropRegion();
cropFraction = cropRegion.width * cropRegion.height;
// Scale to match the actual image size
cropRegion.left = Math.round(cropRegion.left * fullImageWidth);
cropRegion.top = Math.round(cropRegion.top * fullImageHeight);
cropRegion.width = Math.round(cropRegion.width * fullImageWidth);
cropRegion.height = Math.round(cropRegion.height * fullImageHeight);
}
var outputSize;
if (pickWidth && pickHeight) {
outputSize = { width: pickWidth, height: pickHeight };
}
else {
// If no desired size is specified, we have to impose some kind of limit
// so that really big images aren't decoded at full size. If there is no
// build time configuration that specifies the desired maximum pick size,
// check for maximum decode size device can handle based on device memory
// and set outputsize to MediaFrame.maxImageDecodeSize
// else we use half of the configured maximum decode size. Pick
// activities are memory-sensitive because the system app needs to keep
// both the requesting app and the gallery app alive at once.
outputSize =
CONFIG_MAX_PICK_PIXEL_SIZE ||
MediaFrame.maxImageDecodeSize ||
CONFIG_MAX_IMAGE_PIXEL_SIZE >> 1;
// If the pick request specifed a maxFileSizeBytes parameter then
// we'll use this as a hint for the output size. (We make no guarantee
// about the file size of the returned blob, but we try to be close)
// JPEG files typically have about 3 times as many pixels as bytes.
if (request.source.data.maxFileSizeBytes) {
var requestOutputSize =
Math.round(request.source.data.maxFileSizeBytes / cropFraction * 3);
outputSize = Math.min(outputSize, requestOutputSize);
}
}
// show spinner if cropResizeRotate will decode and modify the image
if (cropRegion !== null ||
typeof outputsize === 'object' ||
outputSize < fullImageWidth * fullImageHeight ||
pickedFileInfo.metadata.rotation ||
pickedFileInfo.metadata.mirrored) {
Spinner.show();
}
cropResizeRotate(pickedFile, cropRegion, outputSize, pickType,
pickedFileInfo.metadata,
function(error, blob) {
Spinner.hide();
if (error) {
console.error('while resizing image: ' + error);
blob = pickedFile;
}
// Finally, return the blob to the invoking app
request.postResult({
name: blob.name || pickedFile.name,
type: blob.type,
blob: blob
});
});
}
function cancel() {
request.postError('pick cancelled');
}
// Stop cropping the image and go back to picking mode
function restart() {
pickedFileInfo = pickedFile = null;
if (cropEditor) {
cropEditor.destroy();
cropEditor = null;
}
setView(LAYOUT_MODE.pick);
}
return {
start: start,
select: select,
cancel: cancel,
restart: restart,
end: end
};
}());

View file

@ -0,0 +1 @@
;'use strict';var Spinner={show:function show(){$('spinner').classList.remove('hidden');},hide:function hide(){$('spinner').classList.add('hidden');}};

View file

@ -0,0 +1,17 @@
;(function startup(){'use strict';var firstScanDone=false;showUI();registerEventHandlers();finishDBInitialization();setInitialView();registerActivityHandler();displayThumbnails();doNotScanInBackgroundHack(photodb);function showUI(){document.body.classList.remove('hidden');if(!isPhone){LazyLoader.load('js/frame_scripts.js');}
window.performance.mark('navigationLoaded');}
function registerEventHandlers(){$('thumbnails-select-button').onclick=setView.bind(null,LAYOUT_MODE.select);$('selected-header').addEventListener('action',setView.bind(null,LAYOUT_MODE.list));if(!isPhone){$('fullscreen-toolbar-header').addEventListener('action',function(){setView(LAYOUT_MODE.list);});}
fullscreenButtons.camera.onclick=launchCameraApp;$('thumbnails-camera-button').onclick=launchCameraApp;Overlay.addEventListener('camera',launchCameraApp);$('thumbnails-delete-button').onclick=deleteSelectedItems;$('thumbnails-share-button').onclick=shareSelectedItems;Overlay.addEventListener('cancel',function(){if(picking){Pick.cancel();}});window.onresize=resizeHandler;window.performance.mark('navigationInteractive');}
function finishDBInitialization(){photodb.onupgrading=function(evt){Overlay.show('upgrade');};photodb.onunavailable=function(){if(!picking){if(currentView===LAYOUT_MODE.edit){exitEdit();}
setView(LAYOUT_MODE.list);}else{Pick.restart();}
var why=photodb.state;if(why===MediaDB.NOCARD){Overlay.show('nocard');}
else if(why===MediaDB.UNMOUNTED){Overlay.show('pluggedin');}};photodb.onready=function(){if(Overlay.current==='nocard'||Overlay.current==='pluggedin'||Overlay.current==='upgrade'){Overlay.hide();}};photodb.onscanstart=function onscanstart(){fullscreenButtons.edit.classList.add('disabled');$('throbber').classList.remove('hidden');$('throbber').classList.add('throb');};photodb.onscanend=function onscanend(){fullscreenButtons.edit.classList.remove('disabled');if(Overlay.current==='scanning'){Overlay.show('emptygallery');}
else if(!isPhone&&!currentFrame.displayingImage&&!currentFrame.displayingVideo){showFile(0);}
$('throbber').classList.remove('throb');setTimeout(function(){$('throbber').classList.add('hidden');},100);if(!firstScanDone){firstScanDone=true;window.performance.mark('fullyLoaded');}};photodb.oncardremoved=function oncardremoved(){if(picking){Pick.cancel();return;}
setView(LAYOUT_MODE.list);};photodb.oncreated=function(event){event.detail.forEach(fileCreated);};photodb.ondeleted=function(event){event.detail.forEach(fileDeleted);};switch(photodb.state){case MediaDB.UPGRADING:photodb.onupgrading();break;case MediaDB.READY:photodb.onready();break;case MediaDB.NOCARD:case MediaDB.UNMOUNTED:photodb.onunavailable();break;}}
function setInitialView(){if(picking){setView(LAYOUT_MODE.pick);}
else{setView(LAYOUT_MODE.list);}}
function registerActivityHandler(){navigator.mozSetMessageHandler('activity',function activityHandler(a){var activityName=a.source.name;switch(activityName){case'browse':if(currentView===LAYOUT_MODE.fullscreen){setView(LAYOUT_MODE.list);}
break;case'pick':LazyLoader.load('js/pick.js',function(){Pick.start(a);});break;}});}
function displayThumbnails(){thumbnails=Thumbnails.container;thumbnailList=Thumbnails.list;var placeholder=$('thumbnails-placeholder');placeholder.parentElement.replaceChild(thumbnails,placeholder);thumbnails.addEventListener('click',thumbnailClickHandler);Thumbnails.firstpage.then(function(){window.performance.mark('visuallyLoaded');window.performance.mark('contentInteractive');setTimeout(function(){LazyLoader.load(['shared/elements/gaia-header/dist/gaia-header.js'],function(){var headers=document.querySelectorAll('gaia-header');for(var i=0;i<headers.length;++i){headers[i].removeAttribute('no-font-fit');}});});});Thumbnails.complete.then(function(){if(files.length===0){Overlay.show('scanning');}
window.performance.mark('mediaEnumerated');photodb.addEventListener('ready',function(){photodb.scan();});if(photodb.state===MediaDB.READY){photodb.scan();}});}})();

View file

@ -0,0 +1,9 @@
;'use strict';function ThumbnailDateGroup(item){if(!item){throw new Error('item should not be null or undefined.');}
this.thumbnails=[];this.groupID=ThumbnailDateGroup.getGroupID(item);this.date=item.date;this.header=document.createElement('div');this.header.className='thumbnail-group-header';this.container=document.createElement('div');this.container.className='thumbnail-group-container';this.htmlNode=document.createElement('li');this.htmlNode.appendChild(this.header);this.htmlNode.appendChild(this.container);this.localize();}
ThumbnailDateGroup.getGroupID=function(item){var dateObj=new Date(item.date);var month=dateObj.getMonth()+1;return'group_'+dateObj.getFullYear()+'-'+
(month<10?'0'+month:month);};ThumbnailDateGroup.compareGroupID=function(id1,id2){return id1>id2?1:(id1<id2?-1:0);};ThumbnailDateGroup.prototype.addItem=function(item){if(!item){return;}
var self=this;function getInsertPosition(thumbnail){if(self.thumbnails.length===0||thumbnail.data.date>self.thumbnails[0].data.date){return 0;}
else if(thumbnail.data.date<self.thumbnails[self.thumbnails.length-1].data.date){return self.thumbnails.length;}
else{return MediaUtils.binarySearch(self.thumbnails,thumbnail,function(a,b){return b.data.date-a.data.date;});}}
var thumbnail=new ThumbnailItem(item);var insertPosition=getInsertPosition(thumbnail);this.container.insertBefore(thumbnail.htmlNode,this.container.children[insertPosition]);this.thumbnails.splice(insertPosition,0,thumbnail);return thumbnail;};ThumbnailDateGroup.prototype.getCount=function(){return this.thumbnails.length;};ThumbnailDateGroup.prototype.removeItem=function(thumbnail){var idx=this.thumbnails.indexOf(thumbnail);if(idx<0){return;}
this.thumbnails.splice(idx,1);URL.revokeObjectURL(thumbnail.imgNode.src);this.container.removeChild(thumbnail.htmlNode);};ThumbnailDateGroup.formatter=new Intl.DateTimeFormat(navigator.languages,{month:'long',year:'numeric',});ThumbnailDateGroup.resetFormatter=()=>{ThumbnailDateGroup.formatter=new Intl.DateTimeFormat(navigator.languages,{month:'long',year:'numeric',});};ThumbnailDateGroup.prototype.localize=function(){var date=new Date(this.date);this.header.textContent=ThumbnailDateGroup.formatter.format(date);this.thumbnails.forEach(function(thumbnail){thumbnail.localize();});};

View file

@ -0,0 +1,3 @@
;'use strict';function ThumbnailItem(fileData){if(!fileData){throw new Error('fileData should not be null or undefined.');}
this.data=fileData;this.htmlNode=document.createElement('div');this.htmlNode.classList.add('thumbnail');this.htmlNode.setAttribute('role','button');this.htmlNode.setAttribute('tabindex',0);this.imgNode=document.createElement('img');this.imgNode.alt='';this.imgNode.classList.add('thumbnailImage');this.imgNode.dataset.filename=fileData.name;var url=URL.createObjectURL(fileData.metadata.thumbnail);this.imgNode.src=url;this.htmlNode.appendChild(this.imgNode);this.localize();}
ThumbnailItem.formatter=new Intl.DateTimeFormat(navigator.languages,{hour:'numeric',minute:'numeric',month:'long',day:'numeric',year:'numeric',});ThumbnailItem.resetFormatter=()=>{ThumbnailItem.formatter=new Intl.DateTimeFormat(navigator.languages,{hour:'numeric',minute:'numeric',month:'long',day:'numeric',year:'numeric',});};ThumbnailItem.prototype.localize=function(){var date=new Date(this.data.date);var descId=!this.data.metadata.video?'imageDated':'videoDated';document.l10n.setAttributes(this.imgNode,descId,{timeStamp:ThumbnailItem.formatter.format(date)});};

View file

@ -0,0 +1,11 @@
;'use strict';function ThumbnailList(groupClass,container){if(!groupClass||!container){throw new Error('group class or container cannot be null or undefined');}
this.thumbnailMap={};this.groupMap={};this.itemGroups=[];this.count=0;this.groupClass=groupClass;this.container=container;}
ThumbnailList.prototype.addItem=function(item){if(!item){return null;}
if(this.thumbnailMap[item.name]){return this.thumbnailMap[item.name];}
var self=this;function createItemGroup(item,before){var group=new self.groupClass(item);self.container.insertBefore(group.htmlNode,before?before.htmlNode:null);return group;}
function getItemGroup(item){var groupID=self.groupClass.getGroupID(item);var i;for(i=0;i<self.itemGroups.length;i++){if(self.itemGroups[i].groupID===groupID){return self.itemGroups[i];}else if(self.groupClass.compareGroupID(self.itemGroups[i].groupID,groupID)<0){break;}}
var createdGroup=createItemGroup(item,self.itemGroups[i]);self.itemGroups.splice(i,0,createdGroup);return createdGroup;}
var group=getItemGroup(item);var thumbnail=group.addItem(item);this.groupMap[item.name]=group;this.thumbnailMap[item.name]=thumbnail;this.count++;return thumbnail;};ThumbnailList.prototype.removeItem=function(filename){if(!this.thumbnailMap[filename]){return;}
var group=this.groupMap[filename];group.removeItem(this.thumbnailMap[filename]);if(!group.getCount()){this.container.removeChild(group.htmlNode);this.itemGroups.splice(this.itemGroups.indexOf(group),1);}
this.count--;delete this.groupMap[filename];delete this.thumbnailMap[filename];};ThumbnailList.prototype.reset=function(){for(var name in this.thumbnailMap){this.groupMap[name].removeItem(this.thumbnailMap[name]);}
this.container.innerHTML='';this.thumbnailMap={};this.itemGroups=[];this.groupMap={};this.count=0;};ThumbnailList.prototype.localize=function(){this.itemGroups.forEach(function(group){group.localize();});};

View file

@ -0,0 +1,10 @@
;(function(exports){'use strict';var thumbnails=exports.Thumbnails={};thumbnails.container=document.createElement('ul');thumbnails.container.id='thumbnails';thumbnails.list=new ThumbnailList(ThumbnailDateGroup,thumbnails.container);document.addEventListener('DOMRetranslated',()=>{ThumbnailDateGroup.resetFormatter();ThumbnailItem.resetFormatter();thumbnails.list.localize();});var PAGE_SIZE=15;photodb=new MediaDB('pictures',metadataParserWrapper,{version:2,autoscan:false,batchHoldTime:2000,batchSize:3});var metadataParserLoaded=false;function metadataParserWrapper(file,onsuccess,onerror,bigFile){if(metadataParserLoaded){metadataParser(file,onsuccess,onerror,bigFile);return;}
LazyLoader.load(['js/metadata_scripts.js','shared/js/media/crop_resize_rotate.js'],function(){metadataParserLoaded=true;metadataParser(file,onsuccess,onerror,bigFile);});}
var firstPageResolver;var completionResolver;thumbnails.firstpage=new Promise(function(resolve,reject){firstPageResolver=resolve;thumbnails.complete=new Promise(function(resolve,reject){completionResolver=resolve;createThumbnails();});});function createThumbnails(){if(photodb.state===MediaDB.READY||photodb.state===MediaDB.ENUMERABLE){enumerateDB();}
else{photodb.addEventListener('enumerable',enumerateDB);}}
function enumerateDB(){var batch=[];var batchsize=PAGE_SIZE;var firstPageDisplayed=false;photodb.enumerate('date',null,'prev',function(fileinfo){if(fileinfo){if(picking&&fileinfo.metadata.video){return;}
var metadata=fileinfo.metadata;if(metadata&&metadata.preview&&metadata.preview.filename){metadata.preview.width=Math.floor(metadata.preview.width);metadata.preview.height=Math.floor(metadata.preview.height);}
batch.push(fileinfo);if(batch.length>=batchsize){flush();batchsize*=2;}}
else{done();}});function flush(){batch.forEach(thumb);batch.length=0;if(!firstPageDisplayed){firstPageDisplayed=true;firstPageResolver();}}
function thumb(fileinfo){files.push(fileinfo);thumbnails.list.addItem(fileinfo);}
function done(){flush();completionResolver();}}}(window));

View file

@ -0,0 +1 @@
;'use strict';function $(id){return document.getElementById(id);}