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

131
gaia/apps/gallery/build/build.js Executable file
View file

@ -0,0 +1,131 @@
'use strict';
/* jshint node: true */
var utils = require('utils');
var GalleryAppBuilder = function() {
};
GalleryAppBuilder.prototype.DEFAULT_VALUE = {
maxImagePixelSize: 5 * 1024 * 1024,
maxSnapshotPixelSize: 5 * 1024 * 1024,
maxPickPixelSize: 0,
maxEditPixelSize: 0
};
GalleryAppBuilder.prototype.setOptions = function(options) {
this.options = options;
this.sharedPath = utils.gaia.getInstance(options).sharedFolder.path;
};
GalleryAppBuilder.prototype.concatenatedScripts = function() {
var frameScriptsPaths = [
utils.joinPath(this.sharedPath, 'js', 'gesture_detector.js'),
utils.joinPath(this.sharedPath, 'js', 'format.js'),
utils.joinPath(this.sharedPath, 'js', 'media', 'video_player.js'),
utils.joinPath(this.sharedPath, 'js', 'media', 'media_frame.js'),
utils.joinPath(this.options.APP_DIR, 'js', 'frames.js')
];
var frameScriptsPath = utils.joinPath(this.options.STAGE_APP_DIR, 'js',
'frame_scripts.js');
utils.concatenatedScripts(frameScriptsPaths, frameScriptsPath);
var metadataScriptsPaths = [
utils.joinPath(this.sharedPath, 'js', 'blobview.js'),
utils.joinPath(this.sharedPath, 'js', 'media', 'jpeg_metadata_parser.js'),
utils.joinPath(this.sharedPath, 'js', 'media', 'get_video_rotation.js'),
utils.joinPath(this.sharedPath, 'js/media', 'image_size.js'),
utils.joinPath(this.options.APP_DIR, 'js', 'MetadataParser.js')
];
var metadataScriptsPath = utils.joinPath(this.options.STAGE_APP_DIR, 'js',
'metadata_scripts.js');
utils.concatenatedScripts(metadataScriptsPaths, metadataScriptsPath);
};
GalleryAppBuilder.prototype.customizeMaximumImageSize = function(options) {
// On low-memory devices like Tarako, we need different default values
if (options.GAIA_MEMORY_PROFILE === 'low') {
this.DEFAULT_VALUE.maxImagePixelSize = 2 * 1024 * 1024;
this.DEFAULT_VALUE.maxSnapshotPixelSize = 1600 * 1200; // Tarako camera size
// Reduce the size of images returned by pick activities because
// we have to rotate many images on Tarako
this.DEFAULT_VALUE.maxPickPixelSize = 800 * 600;
// On Tarako devices we can't reliably edit full-size photos.
// This setting reduces them to quarter size before editing.
// This should make the editor UI more responsive and less prone
// to crashing. But it also means that edited photos will be .5
// megapixel instead of 2 megapixel
this.DEFAULT_VALUE.maxEditPixelSize = 800 * 600;
}
else if (options.GAIA_MEMORY_PROFILE === '256') {
this.DEFAULT_VALUE.maxImagePixelSize = 3 * 1024 * 1024;
}
var distDir = options.GAIA_DISTRIBUTION_DIR;
var customize = JSON.parse(utils.getDistributionFileContent('gallery',
this.DEFAULT_VALUE, distDir));
var content =
'//\n' +
'// This file is automatically generated: DO NOT EDIT.\n' +
'//\n'+
'// The default value of these variables depends on the\n'+
'// GAIA_MEMORY_PROFILE environment variable. Set\n'+
'// GAIA_MEMORY_PROFILE=low when building Gaia to get default\n'+
'// values suitable for low-memory devices.\n'+
'//\n'+
'// To customize these values, create a gallery.json file in the\n' +
'// distribution directory with content like this:\n' + '//\n' +
'// {\n' +
'// "maxImagePixelSize": 6000000,\n' +
'// "maxSnapshotPixelSize": 4000000,\n' +
'// "maxPickPixelSize": 480000,\n' +
'// "maxEditPixelSize": 480000 }\n' +
'// }\n' +
'//\n' +
'// Optionally, you can also define variables to specify the\n' +
'// minimum EXIF preview size that will be displayed as a\n' +
'// full-screen preview by adding a property like this:\n' +
'//\n' +
'// "requiredEXIFPreviewSize": { "width": 640, "height": 480}\n' +
'//\n' +
'// If you do not specify this property then EXIF previews will only\n' +
'// be used if they are big enough to fill the screen in either\n' +
'// width or height in both landscape and portrait mode.\n' +
'//\n' +
'var CONFIG_MAX_IMAGE_PIXEL_SIZE = ' +
customize.maxImagePixelSize + ';\n' +
'var CONFIG_MAX_SNAPSHOT_PIXEL_SIZE = ' +
customize.maxSnapshotPixelSize + ';\n' +
'var CONFIG_MAX_PICK_PIXEL_SIZE = ' +
customize.maxPickPixelSize + ';\n' +
'var CONFIG_MAX_EDIT_PIXEL_SIZE = ' +
customize.maxEditPixelSize + ';\n';
if (customize.requiredEXIFPreviewSize) {
content +=
'var CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH = ' +
customize.requiredEXIFPreviewSize.width + ';\n' +
'var CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT = ' +
customize.requiredEXIFPreviewSize.height + ';\n';
} else {
content +=
'var CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH = 0;\n' +
'var CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT = 0;\n';
}
var file = utils.getFile(this.options.STAGE_APP_DIR, 'js', 'config.js');
utils.writeContent(file, content);
};
GalleryAppBuilder.prototype.execute = function(options) {
this.setOptions(options);
this.concatenatedScripts();
this.customizeMaximumImageSize(options);
};
exports.execute = function(options) {
utils.copyToStage(options);
(new GalleryAppBuilder()).execute(options);
};

315
gaia/apps/gallery/index.html Executable file
View file

@ -0,0 +1,315 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8" />
<meta name="theme-color" content="#242d33">
<title>Gallery</title>
<!-- Stylesheets -->
<link rel="stylesheet" href="style/gallery.css" type="text/css" />
<link rel="stylesheet" href="style/VideoPlayer.css" type="text/css" />
<link rel="stylesheet" href="style/VideoPlayer_tablet.css" type="text/css" media="(min-height: 768px) and (min-width: 768px)" />
<link rel="stylesheet" href="shared/style/progress_activity.css"/>
<!-- Reponsive design for tiny & large device -->
<link rel="stylesheet" href="style/gallery_tablet.css" type="text/css" media="(min-height: 768px) and (min-width: 768px)" />
<link rel="stylesheet" type="text/css" href="shared/style/status.css">
<!-- These file are loaded lazily but listed here to get packaged
<link rel="stylesheet" href="shared/style/confirm.css" type="text/css" />
<script defer src="shared/js/component_utils.js"></script>
<script defer src="shared/elements/gaia_menu/script.js"></script>
-->
<!-- Localization -->
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/gallery.{locale}.properties">
<link rel="localization" href="/shared/locales/date/date.{locale}.properties">
<link rel="localization" href="/shared/locales/media/media.{locale}.properties">
<link rel="localization" href="/shared/elements/gaia_menu/locales/gaia_menu.{locale}.properties">
<!-- This batch of scripts is the critical startup code that we
want to run as soon as possible. Thumbnails.js creates a MediaDB
instance and starts enumerating it and building the
thumbnails. The files that come before it are its
dependencies. The rest of the application startup is at the end
of the scripts in startup.js. I have tried converting these to
non-deferred, thinking that that will get them running faster,
but in practice (on a quad core nexus 5), the app starts more
quickly if everything is deferred. -->
<script defer src="shared/js/intl/l20n.js"></script>
<script defer src="shared/js/mediadb.js"></script>
<script defer src="js/globals.js"></script>
<script defer src="js/thumbnail_item.js"></script>
<script defer src="js/thumbnail_date_group.js"></script>
<script defer src="js/thumbnail_list.js"></script>
<script defer src="js/thumbnails.js"></script>
<!-- Shared code -->
<script defer src="shared/js/lazy_loader.js"></script>
<script defer src="shared/js/media/media_utils.js"></script>
<script defer src="shared/js/screen_layout.js"></script>
<script defer src="shared/js/media/downsample.js"></script>
<script defer src="shared/js/dialogs.js"></script>
<!-- This script is lazy loaded, but listed here so it gets packaged -->
<!-- <script defer src="shared/js/media/crop_resize_rotate.js"></script> -->
<!-- <script defer src="shared/js/media/jpeg-exif.js"></script> -->
<!-- <script defer src="shared/elements/gaia-header/dist/gaia-header.js"></script> -->
<!-- Web Components -->
<script defer src="shared/elements/config.js"></script>
<link rel="stylesheet" type="text/css" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/gaia-theme.css">
<link rel="stylesheet" type="text/css" href="shared/elements/gaia-icons/gaia-icons.css" />
<link rel="stylesheet" type="text/css" href="shared/elements/gaia-icons/bidi-helper.css" />
<!-- More application code. startup.js launches the app -->
<script defer src="js/config.js"></script>
<script defer src="js/utils.js"></script>
<script defer src="js/spinner.js"></script>
<script defer src="js/overlay.js"></script>
<script defer src="js/nfc.js"></script>
<script defer src="js/gallery.js"></script>
<script defer src="js/startup.js"></script>
</head>
<body role="application" class="theme-media">
<!-- This section holds the thumbnails list and the headers and footers -->
<!-- for all views that display the thumbnails -->
<section role="region" id="thumbnail-views" class="skin-dark">
<!-- This header only shown on tablet view -->
<gaia-header ignore-dir id="thumbnails-list-top" class="thumbnails-list" title-start="0" title-end="0" no-font-fit>
<h1 id="thumbnail-list-title" data-l10n-id="gallery"></h1>
</gaia-header>
<gaia-header ignore-dir id="selected-header" class="thumbnails-select" action="close" title-start="50" title-end="0" no-font-fit>
<h1 id="thumbnails-number-selected"></h1>
</gaia-header>
<gaia-header ignore-dir id="pick-header" class="thumbnails-pick" action="close" title-start="50" title-end="0" no-font-fit>
<h1 id="pick-header-title" data-l10n-id="pickoneimage2"></h1>
</gaia-header>
<!-- This is a placeholder element marking where the list of
thumbnails should be inserted into the document. When the
real list of thumbnails is inserted, it will have id
"thumbnails". This list of thumbnails is displayed in
normal thumbnail list mode, in select mode, and in pick
mode, and its styling depends on what mode we're in. -->
<ul id="thumbnails-placeholder"></ul>
<footer class="thumbnails-list" role="toolbar">
<a id="thumbnails-camera-button" tabindex="0" class="button" data-icon="camera" data-l10n-id="camera-button" role="button"></a>
<a id="thumbnails-select-button" tabindex="0" class="button" data-icon="select" data-l10n-id="select-multiple-button" role="button"></a>
</footer>
<footer class="thumbnails-select" role="toolbar">
<a id="thumbnails-delete-button" tabindex="0" class="button" data-icon="delete" data-l10n-id="delete-button" role="button"></a>
<a id="thumbnails-share-button" tabindex="0" class="button" data-icon="share" data-l10n-id="share-button" role="button"></a>
</footer>
</section>
<section role="region" id="fullscreen-view" class="skin-dark">
<gaia-header ignore-dir id="fullscreen-toolbar-header" action="back" class="large-only" title-start="50" no-font-fit>
<h1 data-l10n-id="gallery" id="fullscreen-title"></h1>
<a id="fullscreen-camera-button-large">
<span data-icon="camera"></span>
</a>
<a id="fullscreen-edit-button-large">
<span data-icon="edit-image"></span>
</a>
<a id="fullscreen-share-button-large">
<span data-icon="share"></span>
</a>
<a id="fullscreen-info-button-large">
<span data-icon="info"></span>
</a>
<a id="fullscreen-delete-button-large">
<span data-icon="delete"></span>
</a>
</gaia-header>
<button id="fullscreen-back-button-tiny" data-l10n-id="back-button" type="reset" class="tiny-only" data-icon="back"></button>
<div id="frames">
<!-- the current photo plus next and previous waiting in the wings -->
<section id="frame1" class="frame"></section>
<section id="frame2" class="frame"></section>
<section id="frame3" class="frame"></section>
</div>
<section role="status" id="edit-copy-save-banner" class="banner" hidden>
<p id="edit-copy-save-status"></p>
</section>
<footer id="fullscreen-toolbar" class="tiny-only" role="toolbar">
<a id="fullscreen-camera-button-tiny" tabindex="0" class="button" data-icon="camera" data-l10n-id="camera-button" role="button"></a>
<a id="fullscreen-edit-button-tiny" tabindex="0" class="button" data-icon="edit-image" data-l10n-id="edit-button" role="button"></a>
<a id="fullscreen-share-button-tiny" tabindex="0" class="button" data-icon="share" data-l10n-id="share-button" role="button"></a>
<a id="fullscreen-info-button-tiny" tabindex="0" class="button" data-icon="info" data-l10n-id="info-button" role="button"></a>
<a id="fullscreen-delete-button-tiny" tabindex="0" class="button" data-icon="delete" data-l10n-id="delete-button" role="button"></a>
</footer>
</section>
<form id="info-view" role="dialog" data-type="confirm" class="hidden">
<section>
<dl>
<dt data-l10n-id="name-label"></dt>
<dd id="info-name" class="value"></dd>
<dt data-l10n-id="size-label"></dt>
<dd id="info-size" class="value"></dd>
<dt data-l10n-id="image-type-label"></dt>
<dd id="info-type" class="value"></dd>
<dt data-l10n-id="date-taken-label"></dt>
<dd id="info-date" class="value"></dd>
<dt data-l10n-id="resolution-label"></dt>
<dd id="info-resolution" class="value"></dd>
</dl>
</section>
<menu>
<button type="button" id="info-close-button"
data-l10n-id="close-button" class="full"></button>
</menu>
</form>
<section role="region" id="crop-view" class="skin-dark">
<gaia-header ignore-dir id="crop-top" action="back" title-start="50" no-font-fit>
<h1 id="crop-header" data-l10n-id="cropimage"></h1>
<button id="crop-done-button" data-l10n-id="done">done</button>
</gaia-header>
<div id="crop-frame">
<img id="crop-image"></img>
</div>
</section>
<section role="region" id="edit-view" class="skin-dark">
<gaia-header ignore-dir id="edit-header" action="close" title-start="50" no-font-fit>
<h1 id="edit-title" data-l10n-id="edit"></h1>
<button id="edit-save-button" data-l10n-id="save">Save</button>
<button id="edit-tool-apply-button" data-l10n-id="apply" hidden>Apply</button>
</gaia-header>
<!-- Indicates progress when saving -->
<progress id="save-progress" value="0" max="120" class="hidden"></progress>
<!-- preview of edited photo goes here -->
<!-- canvas elements are dynamically inserted here -->
<div id="edit-preview-area">
</div>
<!-- this holds different options for different editing modes -->
<!-- only one set of options will be visible at a time -->
<div id="edit-options">
<div id="exposure-slider" class="edit-options-bar hidden" role="slider" aria-valuemax="3" aria-valuemin="-3" aria-valuenow="0">
<div id="sliderbar">
<hr id="sliderline"/>
<div id="sliderthumb"></div>
</div>
<div id="sliderlabels">
<div id="slider-3" class="sliderlabel" data-label="-3"></div>
<div id="slider-2" class="sliderlabel" data-label="-2"></div>
<div id="slider-1" class="sliderlabel" data-label="-1"></div>
<div id="slider0" class="sliderlabel" data-label="0"></div>
<div id="slider1" class="sliderlabel" data-label="+1"></div>
<div id="slider2" class="sliderlabel" data-label="+2"></div>
<div id="slider3" class="sliderlabel" data-label="+3"></div>
</div>
</div>
<div id="edit-crop-options" class="edit-options-bar hidden" role="radiogroup" data-l10n-id="crop-options">
<a id="edit-crop-aspect-free" tabindex="0" class="selected radio button" role="radio" aria-checked="true" data-l10n-id="crop-aspect-free"></a>
<a id="edit-crop-aspect-portrait" tabindex="0" class="radio button" role="radio" data-l10n-id="crop-aspect-portrait"></a>
<a id="edit-crop-aspect-landscape" tabindex="0" class="radio button" role="radio" data-l10n-id="crop-aspect-landscape"></a>
<a id="edit-crop-aspect-square" tabindex="0" class="radio button" role="radio" data-l10n-id="crop-aspect-square"></a>
<a id="edit-crop-aspect-more" tabindex="0" class="radio button" role="radio" data-icon="more" data-l10n-id="crop-aspect-more"></a>
</div>
<div id="edit-effect-options" class="edit-options-bar hidden" role="radiogroup" data-l10n-id="effect-options">
<a id="edit-effect-none" tabindex="0" class="selected radio button bgimage"
data-effect="none" role="radio" data-l10n-id="effect-none"></a>
<a id="edit-effect-bw" tabindex="0" class="radio button filter-bw bgimage"
data-effect="bw" role="radio" data-l10n-id="effect-bw"></a>
<a id="edit-effect-sepia" tabindex="0" class="radio button filter-sepia bgimage"
data-effect="sepia" role="radio" data-l10n-id="effect-sepia"></a>
<a id="edit-effect-bluesteel" tabindex="0" class="radio button filter-bluesteel bgimage"
data-effect="bluesteel" role="radio" data-l10n-id="effect-bluesteel"></a>
<a id="edit-effect-faded" tabindex="0" class="radio button filter-faded bgimage"
data-effect="faded" role="radio" data-l10n-id="effect-faded"></a>
</div>
</div>
<section role="status" id="edit-enhance-banner" class="banner" hidden>
<p id="edit-enhance-status"></p>
</section>
<!-- buttons for selecting the type of edit to perform -->
<footer id="edit-toolbar" role="toolbar">
<a id="edit-exposure-button" tabindex="0" class="button" data-icon="brightness" data-l10n-id="exposure-button" role="button"></a>
<a id="edit-crop-button" tabindex="0" class="button" data-icon="crop" data-l10n-id="crop-button" role="button"></a>
<a id="edit-effect-button" tabindex="0" class="button" data-l10n-id="effect-button" role="button"></a>
<a id="edit-enhance-button" tabindex="0" class="button" data-l10n-id="enhance-button" role="button" aria-pressed="false"></a>
</footer>
</section>
<!-- A thin indeterminate progress bar to indicate scanning -->
<!-- set class to "throb" to make it go, or hidden" to make it stop -->
<div id="throbber" class="hidden"></div>
<!-- See spinner.js -->
<!-- a spinner that we display in the center of the screen when -->
<!-- doing time-consuming operations like rotating images -->
<progress id="spinner" class="hidden"></progress>
<!-- See overlay.js -->
<!-- display messages like "No Photos" and "No SD Card" here -->
<form id="overlay" role="dialog" data-type="confirm" class="hidden">
<section id="overlay-content">
<h1 id="overlay-title">tre</h1>
<p id="overlay-text">ter</p>
</section>
<menu id="overlay-menu" class="hidden">
<button type="button" id="overlay-camera-button" class="hidden full"
data-l10n-id="overlay-camera-button">Go to Camera</button>
<button type="button" id="overlay-cancel-button" data-l10n-id="cancel" class="full">Cancel</button>
</menu>
</form>
<form id="confirm-dialog" role="dialog" data-type="confirm" class="hidden">
<section>
<p id="confirm-msg"></p>
</section>
<menu>
<button id="confirm-cancel"></button>
<button id="confirm-ok"></button>
</menu>
</form>
<gaia-menu id="aspect-ratio-options-view" hidden>
<button id="edit-crop-aspect-3x4" data-l10n-id="crop-aspect-3x4">3:4</button>
<button id="edit-crop-aspect-4x3" data-l10n-id="crop-aspect-4x3">4:3</button>
<button id="edit-crop-aspect-16x9" data-l10n-id="crop-aspect-16x9">16:9</button>
</gaia-menu>
<!-- Some SVG filters we use -->
<svg id="filters" width="0" height="0">
<filter id="filter-bw">
<feColorMatrix type="matrix"
values="0.65 0.25 0.10 0 0
0.65 0.25 0.10 0 0
0.65 0.25 0.10 0 0
0 0 0 1 0"/>
</filter>
<filter id="filter-sepia">
<feColorMatrix type="matrix"
values="0.393 0.769 0.189 0 0
0.349 0.686 0.168 0 0
0.272 0.534 0.131 0 0
0 0 0 1 0"/>
</filter>
<filter id="filter-bluesteel">
<feColorMatrix type="matrix"
values="1 0.25 0.65 0 0
0.1 1 0.65 0 0
0.1 0.25 1 0.1 0
0 0 0 1 0"/>
</filter>
<filter id="filter-faded">
<feColorMatrix type="matrix"
values="1 0.2 0.2 0.03 0
0.2 0.7 0.2 0.05 0
0.1 0 0.8 0 0
0 0 0 1 0"/>
</filter>
</svg>
</body>
</html>

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;
}());

715
gaia/apps/gallery/js/frames.js Executable file
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');
}
}

830
gaia/apps/gallery/js/gallery.js Executable file
View file

@ -0,0 +1,830 @@
'use strict';
/* global
$,
clearFrames,
currentFrame,
Dialogs,
files,
LazyLoader,
Pick,
picking,
MediaDB,
MozActivity,
NFC,
Overlay,
photodb,
resizeFrames,
ScreenLayout,
setFramesPosition,
showFile
*/
/* exported
compareFilesByDate,
deleteSelectedItems,
doNotScanInBackgroundHack,
fileCreated,
fileDeleted,
fullscreenView,
launchCameraApp,
resizeHandler,
shareSelectedItems,
thumbnailClickHandler,
TRANSITION_FRACTION,
TRANSITION_SPEED
*/
/*
* This app displays photos and videos that are stored on the phone.
*
* Its starts with a thumbnail view in which small versions of all photos
* and videos are displayed. Tapping on a thumbnail shows the image
* or video at the full size of the screen and swiping left or right moves to
* the next or previous image or video.
*
* The app supports two-finger "pinch" gestures to zoom in and out on an
* image. When zoomed, a one finger swipe gesture pans within the zoomed
* image, and only moves to the next or previous image once you reach the
* edge of the currently displayed image.
*
* To make transitions between photos smooth, the app preloads the next
* and previous image or video and positions them off-screen to the right and
* left of the currently displayed image.
*
* Image and videos are displayed in "frames" which are managed by
* the Frame.js abstraction. A Frame object includes a video player UI
* (from VideoPlayer.js) and also includes the code that manage zooming
* and panning within an image.
*/
//
// Tuneable parameters
//
// Pan this % of width to transition from one item to the next
var TRANSITION_FRACTION = 0.25;
// This is the speed of our default transitions in pixels/ms.
// Swipe faster than this to transition faster. But we'll
// never go slower (except slide show transitions).
var TRANSITION_SPEED = 0.75;
// UI elements
var thumbnails;
var thumbnailList;
var fullscreenView = $('fullscreen-view');
// These are the top-level view class which are defined in
// gallery_tablet.css
// This object is used by setView()
// Layout Mode Transition:
// list <-> selection
// list <-> fullscreen <-> edit/crop
// (activity) pick <-> crop
const LAYOUT_MODE = {
list: 'thumbnailListView',
select: 'thumbnailSelectView',
fullscreen: 'fullscreenView',
edit: 'editView',
pick: 'pickView',
crop: 'cropView'
};
var currentView;
// Register orientation watcher in ScreenLayout
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; // What file is currently displayed
var editedPhotoIndex;
// In thumbnailSelectView, we allow the user to select thumbnails.
// These variables hold the names of the selected files, and map those
// names to the corresponding File objects
var selectedFileNames = [];
var selectedFileNamesToBlobs = {};
// We manage videos through their poster images, which are photos and so get
// listed in the photodb above. But when we need to access the actual video
// file, we have to get that from a device storage object for videos.
var videostorage = navigator.getDeviceStorage('videos');
// Flag that indicates that we've edited a picture and just saved it
var justSavedEditedImage = false;
// Pass the filename of the poster image and get the video file back
function getVideoFile(filename, callback) {
// We get videos directly through the video device storage
var req = videostorage.get(filename);
req.onsuccess = function() {
callback(req.result);
};
req.onerror = function() {
console.error('Failed to get video file', filename);
};
}
// Return a Promise that resolves to the File object of the currently
// displayed image or video.
// XXX This function is temporarily added here as part of NFC
// refactoring, but will be moved into the model module.
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 {
// share photo
photodb.getFile(fileInfo.name, function(file) {
resolve(file);
}, function(errmsg) {
reject(errmsg);
});
}
});
}
// This comparison function is used for sorting arrays and doing binary
// search on the resulting sorted arrays.
function compareFilesByDate(a, b) {
if (a.date < b.date) {
return 1; // larger (newer) dates come first
} else if (a.date > b.date) {
return -1;
}
return 0;
}
// getFileIndex return position of a file thumbnail in gallery view
// It first find thumbnail's position within its own group
// then add the sizes of previous groups
function getFileIndex(filename) {
// Get the group of the file
var fileGroup = thumbnailList.groupMap[filename];
if (!fileGroup) {
console.error('file group does not exist in thumbnail List', filename);
return -1;
}
var index = 0;
// Get the thumbnailItem of the file
var thumbnail = thumbnailList.thumbnailMap[filename];
// Get the index of the file in its own group
index = fileGroup.thumbnails.indexOf(thumbnail);
if (index < 0) {
console.error('filename does not exist in thumbnail list', filename);
return -1;
}
// Add to index the sizes of previous groups
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;
// Find the deleted file in our files array
for (var n = 0; n < files.length; n++) {
if (files[n].name === filename) {
break;
}
}
if (n >= files.length) { // It was a file we didn't know about
return;
}
// Remove the image from the array
files.splice(n, 1)[0];
// Remove the corresponding thumbnail
thumbnailList.removeItem(filename);
// Adjust currentFileIndex, too, if we have to.
if (n < fileIndex) {
fileIndex--;
}
// If we remove the last item in files[],
// we need to show the previous image, not the next image.
if (fileIndex >= files.length) {
fileIndex = files.length - 1;
}
if (n < editedPhotoIndex) {
editedPhotoIndex--;
}
// If we're in fullscreen mode or has preview screen, then the only way
// this function gets called is when we delete the currently displayed photo.
// This means that we need to redisplay.
if (files.length > 0 && (currentView === LAYOUT_MODE.fullscreen)) {
showFile(fileIndex);
} else {
updateFocusThumbnail(fileIndex);
}
// If there are no more photos show the "no pix" overlay
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;
}
// Delete the file from the MediaDB. This removes the db entry and
// deletes the file in device storage. This will generate an change
// event which will call imageDeleted()
var fileinfo = files[n];
photodb.deleteFile(files[n].name);
// If it is a video, however, we can't just delete the poster image, but
// must also delete the video file.
if (fileinfo.metadata.video) {
videostorage.delete(fileinfo.metadata.video);
}
// If the metdata parser saved a preview image for this photo,
// delete that, too.
if (fileinfo.metadata.preview && fileinfo.metadata.preview.filename) {
// We use raw device storage here instead of MediaDB because that is
// what MetadataParser.js uses for saving the preview.
var pictures = navigator.getDeviceStorage('pictures');
pictures.delete(fileinfo.metadata.preview.filename);
}
}
function fileCreated(fileinfo) {
// If the new file is a video and we're handling an image pick activity
// then we won't display the new file.
if (picking && fileinfo.metadata.video) {
return;
}
// The fileinfo object that MediaDB sends us has a thumbnail blob in it,
// fresh from the metadata parser. This blob has been stored in the db, but
// the copy we have is still a memory-backed blob. We need to be using a
// file backed blob here so that we don't leak memory if the user scans a
// fresh sdcard full of hundreds of photos. So we go get the db record out
// of the database. It should be an exact copy of what we already have,
// except that the thumbnail will be file-backed instead of memory-backed.
photodb.getFileInfo(fileinfo.name, function(fileinfo) {
var insertPosition;
// If we were showing the 'no pictures' overlay, hide it
if (Overlay.current === 'emptygallery' || Overlay.current === 'scanning') {
Overlay.hide();
}
// Create a thumbnailItem for this image and insert it at the right spot
thumbnailList.addItem(fileinfo);
insertPosition = getFileIndex(fileinfo.name);
if (insertPosition < 0) {
return;
}
// Insert the image info into the array
files.splice(insertPosition, 0, fileinfo);
if (currentFileIndex >= insertPosition) {
currentFileIndex++;
}
if (editedPhotoIndex >= insertPosition) {
editedPhotoIndex++;
}
// Redisplay the current photo if we're in photo view. The current
// photo should not change, but the content of the next or previous frame
// might. This call will only make changes if the filename to display
// in a frame has actually changed.
if (currentView === LAYOUT_MODE.fullscreen) {
if (justSavedEditedImage) {
var banner = $('edit-copy-save-banner');
// Show latest saved file inserted at index insertPosition
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;
});
}
// Make the thumbnail for image n visible
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; // compute this dynamically?
// Adjust the screen bottom up to be above the overlaid footer
screenBottom -= toolbarHeight;
if (thumbnailTop < screenTop) { // If thumbnail is above screen
thumbnails.scrollTop = thumbnailTop; // scroll up to show it.
}
else if (thumbnailBottom > screenBottom) { // If thumbnail is below screen
thumbnails.scrollTop = // scroll down to show it
thumbnailBottom - thumbnails.clientHeight + toolbarHeight;
}
}
}
function setView(view) {
if (currentView === view) {
return;
}
// define each view's layout based on data-view of body
document.body.classList.remove(currentView);
document.body.classList.add(view);
// Do any necessary cleanup of the view we're exiting
switch (currentView) {
case LAYOUT_MODE.select:
// Clear the selection, if there is one
Array.forEach(thumbnails.querySelectorAll('.selected.thumbnailImage'),
function(elt) { elt.classList.remove('selected'); });
// On large devices we need to display the new current file after deletion
// But if we just deleted the last file then we don't do this
if (!isPhone && currentFileIndex !== -1) {
showFile(currentFileIndex);
}
break;
case LAYOUT_MODE.fullscreen:
if (!isPhone && (view === LAYOUT_MODE.list) && !isPortrait &&
currentFileIndex !== -1) {
// we'll reuse and resize the fullscreen window
// when go back to thumbnailList mode from fullscreen
// and also does editView in landscape
resizeFrames();
} else {
// Clear the frames to release the memory they're holding and
// so that we don't see a flash of the old image when we return
// to fullscreen view
clearFrames();
}
break;
}
switch (view) {
case LAYOUT_MODE.list:
// If we're going to fullscreen, then we were just viewing a photo
// or video, so make sure its thumbnail is fully on the screen.
// XXX: do we need to defer this?
scrollToShowThumbnail(currentFileIndex);
if (currentView === LAYOUT_MODE.fullscreen) {
// only do it when we back from fullscreen.
NFC.unshare();
}
break;
case LAYOUT_MODE.fullscreen:
resizeFrames();
NFC.share(getCurrentFile);
break;
case LAYOUT_MODE.select:
clearSelection();
// When entering select view, we pause the video
if (!isPhone && currentFrame.video && !isPortrait) {
currentFrame.video.pause();
}
break;
case LAYOUT_MODE.edit:
NFC.unshare();
break;
}
// We reuse the fullscreen dom for preview and fullscreen dom,
// so the title must be changed while switching
if (!isPhone) {
if (view !== LAYOUT_MODE.fullscreen) {
$('fullscreen-title').setAttribute('data-l10n-id', 'preview');
} else {
$('fullscreen-title').setAttribute('data-l10n-id', 'gallery');
}
}
// Remember the current view
currentView = view;
}
//
// Event handlers
//
// Clicking on a thumbnail does different things depending on the view.
// 1. On pickView -> go to cropMode
// 2. On large/selectView -> update preview image
// 3. On tiny/large with listView -> go to fullscreen image
function thumbnailClickHandler(evt) {
var target = evt.target;
if (!target) {
return;
}
// Bug 1106877 - Handle tap for clicks in gray area of containing
// div for thumbnail images smaller than thumbnail container.
target = target.classList.contains('thumbnail') ?
target.firstElementChild : target;
if (!target || !target.classList.contains('thumbnailImage')) {
return;
}
// If the MediaDB is not fully ready yet, then ignore the event
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);
});
}
}
// On large screen, we outline the picture we're focusing on and
// update the currentFileIndex.
function updateFocusThumbnail(n) {
var previousIndex = currentFileIndex;
currentFileIndex = n;
if (isPhone || currentFileIndex === -1) {
return;
}
// If file is delted on select mode, the currentFileIndex may
// be the same as previousIndex. We need to hightlight it again.
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) {
// Clear preview screen on large device.
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 }
);
}
// When we enter thumbnail selection mode, or when the selection changes
// we call this function to update the message the top of the screen and to
// enable or disable the Delete and Share buttons
function updateSelection(thumbnail) {
// First, update the visual appearance of the element
thumbnail.classList.toggle('selected');
// Now update the list of selected filenames and filename->blob map
// based on whether we selected or deselected the thumbnail
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);
// currentFileIndex point to the last selected fileIndex
updateFocusThumbnail(index);
if (files[index].metadata.video) {
getVideoFile(files[index].metadata.video, function(file) {
selectedFileNamesToBlobs[filename] = file;
});
}
else {
// We get photos through the photo db
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) {
// show the last selected image of selectedFileNames.
var lastSelected = selectedFileNames[i - 1];
var lastSelectedIndex = getFileIndex(lastSelected);
updateFocusThumbnail(lastSelectedIndex);
showFile(currentFileIndex);
} else {
// If selectedFileNames is empty, we clear preview
// screen.
clearFrames();
}
}
}
// Now update the UI based on the number of selected thumbnails
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'
}
});
// Assign an onsuccess function to the activity so jshint doesn't complain.
a.onsuccess = () => {};
// Wait 2000ms before re-enabling the Camera buttons to prevent
// hammering them and causing a crash (Bug 957709)
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() { // onSuccess
// deleteFile is O(n), so this loop is O(n*n). If used with really large
// selections, it might have noticably bad performance. If so, we
// can write a more efficient deleteFiles() function.
for (var i = 0; i < selected.length; i++) {
selected[i].classList.toggle('selected');
deleteFile(getFileIndex(selected[i].dataset.filename));
}
clearSelection();
});
}
// Clicking on the share button in select mode shares all selected images
// Note: when sharing multiple items, we make no attempt to handle EXIF
// orientation in photos. So on devices (like Tarako) that rely on EXIF
// orientation in the camera app, we may be leaking images that will not
// work well on the web.
function shareSelectedItems() {
var blobs = selectedFileNames.map(function(name) {
return selectedFileNamesToBlobs[name];
});
share(blobs);
}
// Initiate a share activity for all of the Blobs in the blobs array.
// Usually these blobs are File objects, with names. But when sharing a
// single image, we sometimes pass an in-memory blob to handle EXIF orientation
// issues. In that case, use the second name argument for the unnamed blob
function share(blobs, blobName) {
if (blobs.length === 0) {
return;
}
var names = [], types = [], fullpaths = [];
// Get the file name (minus path) and type of each blob
blobs.forEach(function(blob) {
var name = blob.name;
// Special case for blobs that are not File objects
if (!name && blobs.length === 1) {
name = blobName;
}
// We try to fix Bug 814323 by using
// current workaround of bluetooth transfer
// so we will pass both filenames and fullpaths
// The fullpaths can be removed after Bug 811615 is fixed
fullpaths.push(name);
// Discard the path, we just want the base name
name = name.substring(name.lastIndexOf('/') + 1);
names.push(name);
// And we just want the first component of the type "image" or "video"
var type = blob.type;
if (type) {
type = type.substring(0, type.indexOf('/'));
}
types.push(type);
});
// If there is just one type, or if all types are the same, then use
// that type plus '/*'. Otherwise, use 'application/*'
// If all the blobs are image we use 'image/*'. If all are videos
// we use 'video/*'. Otherwise, 'application/*'.
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);
}
};
}
// This happens when the user rotates the phone.
// When we used mozRequestFullscreen, it would also happen
// when we entered or left fullscreen mode.
// As a workaround for Bug 961636, use resize event handler
// in place of screenlayoutchange event handler.
function resizeHandler() {
isPortrait = ScreenLayout.getCurrentLayout('portrait');
// In list view when video is playing, if user rotate screen from
// landscape to portrait, the video pause.
// Check if currentFrame is undefined for cases where frame_script.js
// is not loaded e.g. if a user rotates phone
// in list mode before opening an image.
if (currentView === LAYOUT_MODE.list && isPortrait &&
typeof currentFrame !== 'undefined' && currentFrame.video) {
currentFrame.video.pause();
}
// We'll need to resize and reposition frames for below cases, since
// the size of container has been changed.
if (currentView === LAYOUT_MODE.fullscreen ||
(!isPhone && !isPortrait &&
(currentView === LAYOUT_MODE.list ||
currentView === LAYOUT_MODE.select))) {
resizeFrames();
// We also have to reposition the frames to get the next and previous
// frames the correct distance away from the current frame
setFramesPosition();
}
}
/*
* This is a temporary workaround to bug 1039943: when the user launches
* the gallery and then switches to another app gallery's scanning and
* thumbnail generation process can slow down foreground apps.
*
* For now, we address this by simply making the app exit if it goes to the
* background while scanning and if the device has 256mb or less of memory.
*
* Bug 1046995 should fix this issue in a better way and when it does,
* we should remove this function and the code that invokes it.
*/
function doNotScanInBackgroundHack(photodb) {
const enoughMB = 512; // How much memory is enough to not do this hack?
var memoryMB = 0; // How much memory do we have?
// Listen for visibilitychange events that happen when we go to the background
window.addEventListener('visibilitychange', backgroundScanKiller);
// Stop listening for those events when the scan is complete
photodb.addEventListener('scanend', function() {
window.removeEventListener('visibilitychange', backgroundScanKiller);
});
// This is what we do when we go to the background
function backgroundScanKiller() {
// If we're coming back to the foreground or if we already know that
// we have enough memory, then do nothing here.
if (!document.hidden || memoryMB >= enoughMB) {
return;
}
// If we can't query our memory (i.e. this is the 1.4 Dolphin release)
// then assume that we have low memory and exit
if (!navigator.getFeature) {
exit();
}
else {
// Otherwise we're in release 2.0 or later and can actually query
// how much memory we have.
navigator.getFeature('hardware.memory').then(function(mem) {
memoryMB = mem;
if (memoryMB < enoughMB) {
exit();
}
});
}
function exit() {
// If we are still hidden and still scanning, then log a message
// wait a bit for the log to be flushed, and then close the application
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);
}
}
}
}

16
gaia/apps/gallery/js/globals.js Executable file
View file

@ -0,0 +1,16 @@
/* exported files, picking, photodb */
'use strict';
// This array holds information about all the image and video files we
// know about. Each array element is an object that includes a
// filename and metadata. The array is initially filled when we
// enumerate the mediadb, and has elements added and removed when we
// receive create and delete events from the media databases.
var files = [];
// If we're doing a pick activity, our URL will end with '#pick'
var picking = (window.location.hash === '#pick');
// The MediaDB object that manages the filesystem and the database of metadata
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() });
}
};

85
gaia/apps/gallery/js/info.js Executable file
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');
}
}
}
}

85
gaia/apps/gallery/js/nfc.js Executable file
View file

@ -0,0 +1,85 @@
/* exported NFC */
'use strict';
/*
* This module helps to manage NFC sharing in an app.
* Call share() when the UI displays an object that can be shared as a file.
* Pass the file or blob object itself or a function that returns a Promise
* that resolves to the file.
* When the UI changes so that no sharable object is visible, call unshare().
*/
var NFC = {
shrinkingUI: null,
peer: null,
share: function share(f, options) {
if (navigator.mozNfc) {
// This is a workaround for bug 1163957 which is only applied to the open
// activity. The complete solution should be done in bug 1109495.
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();
// Without calling preventDefault, gecko will fire onpeerlost right
// after onpeerfound since it think gallery app would not handle it.
evt.preventDefault();
};
navigator.mozNfc.onpeerlost = function() {
NFC.peer = null;
NFC.shrinkingUI.stop();
};
} else {
// Enable NFC sharing by defining a "peerready" callback that
// calls sendFile() to transfer the current image or video to the peer
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() {
// Disable NFC sharing simply by removing the callback
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);
}
}
}
};

314
gaia/apps/gallery/js/open.js Executable file
View file

@ -0,0 +1,314 @@
'use strict';
/* global
CONFIG_MAX_IMAGE_PIXEL_SIZE,
CONFIG_REQUIRED_EXIF_PREVIEW_HEIGHT,
CONFIG_REQUIRED_EXIF_PREVIEW_WIDTH,
Downsample,
GestureDetector,
getImageSize,
getStorageIfAvailable,
getUnusedFilename,
MediaFrame,
MimeMapper,
NFC,
parseJPEGMetadata
*/
(function init() {
var activity; // The activity object we're handling
var activityData; // The data sent by the initiating app
var blob; // The blob we'll be displaying and maybe saving
var frame; // The MediaFrame that displays the image
var saved = false; // Did we save the file?
var storage; // The DeviceStorage object used for saving
var title; // What we call the image in the titlebar and banner
// Register a handler to receive the Activity object
navigator.mozSetMessageHandler('activity', handleOpenActivity);
function $(id) { return document.getElementById(id); }
// Display the specified blob, unless it is too big to display
function open(blob) {
// If the app that initiated this activity wants us to do allow the
// user to save this blob as a file, and if device storage is available
// and if there is enough free space, then display a save button.
if (activityData.allowSave && activityData.filename && checkFilename()) {
getStorageIfAvailable('pictures', blob.size, function(ds) {
storage = ds;
showSaveButton();
});
}
// Figure out how big (in pixels) the image is.
// For JPEG images, this also gets us the preview image if there is one.
getImageSize(blob, success, error);
// Called if we get an image size
function success(metadata) {
var pixels = metadata.width * metadata.height;
//
// 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 so we can
// handle images that are quite a bit larger
//
var imagesizelimit = CONFIG_MAX_IMAGE_PIXEL_SIZE;
if (blob.type === 'image/jpeg') {
imagesizelimit *= Downsample.MAX_AREA_REDUCTION;
}
//
// 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
//
var filesizelimit = 2 * CONFIG_MAX_IMAGE_PIXEL_SIZE;
if (pixels > imagesizelimit || blob.size > filesizelimit) {
displayError('imagetoobig');
return;
}
// If there was no EXIF preview, or if the image is not very big,
// display the full-size image.
if (!metadata.preview || pixels < 512 * 1024) {
frame.displayImage(blob,
metadata.width,
metadata.height,
null,
metadata.rotation,
metadata.mirrored);
}
else {
// If we found an EXIF preview, and can determine its size, then
// we can display it instead of the big image and save a lot of
// memory.
parseJPEGMetadata(blob.slice(metadata.preview.start,
metadata.preview.end,
'image/jpeg'),
function success(previewmetadata) {
//
// If we parsed the preview image, add its
// dimensions to the metdata.preview
// object, and then let the MediaFrame
// object display the preview instead of
// the full-size image.
//
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() {
//
// If we couldn't parse the preview image,
// just display full-size.
//
frame.displayImage(blob,
metadata.width,
metadata.height,
null,
metadata.rotation,
metadata.mirrored);
});
}
}
// Called if getImageSize parsing fails.
function error(msg) {
//
// This wasn't a JPEG, PNG, GIF, or BMP image and we can't figure
// out the size of the image. Without the size, we can't pass the
// image to MediaFrame.displayImage().
//
// XXX: Currently, we know how to get the sizes of all the image
// types that this activity is registered to handle. If we add new
// image types to manifest.webapp, then we should update
// shared/js/media/image_size.js to compute the size or we should
// add code here to load the image into an offscreen <img> to
// determine its size (but only if the file size is not too large).
//
displayError('imageinvalid');
}
}
function handleOpenActivity(request) {
activity = request;
activityData = activity.source.data;
// Set up the UI, if it is not already set up
if (!frame) {
// Hook up the buttons
$('header').addEventListener('action', done);
$('save').addEventListener('click', save);
// And register event handlers for gestures
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();
}
});
}
// Report errors if we're passed an invalid image
frame.onerror = function invalid() {
displayError('imageinvalid');
};
}
// Display the filename in the header, if there was one
title = baseName(activityData.filename || '');
$('filename').textContent = title;
blob = activityData.blob;
open(blob);
NFC.share(blob, {
foregroundElement: $('open'),
backgroundElement: document.body
});
}
function checkFilename() {
// Hide save button for file names having hidden
// .gallery/ directories. See Bug 992426
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() {
// Hides the save button: we can only save once
hideSaveButton();
getUnusedFilename(storage, activityData.filename, function(filename) {
var savereq = storage.addNamed(blob, filename);
savereq.onsuccess = function() {
// Remember that it has been saved so we can pass this back
// to the invoking app
saved = filename;
// And tell the user
showBanner('saved', title);
};
savereq.onerror = function(e) {
// XXX we don't report this to the user because it is hard to
// localize.
console.error('Error saving', filename, e);
};
});
}
function showSaveButton() {
$('save').classList.remove('hidden');
// XXX work around bug 870619
$('filename').textContent = $('filename').textContent;
}
function hideSaveButton() {
$('save').classList.add('hidden');
// XXX work around bug 870619
$('filename').textContent = $('filename').textContent;
}
function showBanner(msg, title) {
document.l10n.setAttributes($('message'), msg, {filename: title});
$('banner').hidden = false;
setTimeout(function() {
$('banner').hidden = true;
}, 3000);
}
// Strip directories and just return the base filename
function baseName(filename) {
return filename.substring(filename.lastIndexOf('/') + 1);
}
})();

129
gaia/apps/gallery/js/overlay.js Executable file
View file

@ -0,0 +1,129 @@
/* global $, LazyLoader, picking */
/* exported Overlay */
'use strict';
/*
* This module handles the display of Gallery app overlays: these are full
* screen dialogs that are displayed when the app cannot be used transiently
* or permanenty (when there is no sdcard, e.g.)
*
* Different overlays are identified by with a string. Pass the id of an
* overlay to Overlay.show() to show that overlay. Call Overlay.hide() to
* hide the current overlay. Overlay.current holds the id of the current
* overlay, or null if no overlay is displayed.
*
* Supported ids include:
*
* nocard: no sdcard is installed in the phone
* pluggedin: the sdcard is being used by USB mass storage
* emptygallery: no pictures found
* scanning: scanning the sdcard for photo's, but none found yet
*
* Localization is done using the specified id with "-title" and "-text"
* suffixes.
*
* Some overlays have a cancel button (for cancelling a pick activity) and
* some have a camera button (for switching to the Camera) app. When those
* buttons are clicked, we dispatch synthetic "cancel" and "camera" events.
* Use the addEventListener() and removeEventListener() functions to manage
* listeners for these events.
*/
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() {
// hide any special elements
$('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);
},
};
// Register handlers on the button elements that dispatch higher-level
// events on the overlay itself.
$('overlay-cancel-button').addEventListener('click', function() {
$('overlay').dispatchEvent(new CustomEvent('cancel'));
});
$('overlay-camera-button').addEventListener('click', function() {
$('overlay').dispatchEvent(new CustomEvent('camera'));
});

361
gaia/apps/gallery/js/pick.js Executable file
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
};
}());

14
gaia/apps/gallery/js/spinner.js Executable file
View file

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

311
gaia/apps/gallery/js/startup.js Executable file
View file

@ -0,0 +1,311 @@
/* global LazyLoader, Pick, Thumbnails, $, thumbnails */
/* global MediaDB, photodb, picking, setView, LAYOUT_MODE, currentView */
/* global Overlay, isPhone, doNotScanInBackgroundHack, fullscreenButtons */
/* global launchCameraApp, deleteSelectedItems, shareSelectedItems */
/* global resizeHandler, currentFrame, showFile, fileCreated, fileDeleted */
/* global files, thumbnailClickHandler, exitEdit */
(function startup() {
'use strict';
var firstScanDone = false; // Have we completed our first scan yet?
showUI(); // We're localized, so unhide
registerEventHandlers(); // Hook up UI elements
finishDBInitialization(); // Finish MediaDB initialization
setInitialView(); // Start off in thumbnail list mode
registerActivityHandler(); // Handle pick and browse activities
displayThumbnails(); // Get the thumbnails displayed
doNotScanInBackgroundHack(photodb); // Remove as part of bug 1046995
function showUI() {
// <body> children are hidden until the UI is translated
document.body.classList.remove('hidden');
// load frame_script.js for preview mode and show loading background
if (!isPhone) {
LazyLoader.load('js/frame_scripts.js');
}
// Tell performance monitors that our chrome is visible
window.performance.mark('navigationLoaded');
}
function registerEventHandlers() {
// Clicking on the select button goes to thumbnail select mode
$('thumbnails-select-button').onclick =
setView.bind(null, LAYOUT_MODE.select);
// Clicking on the cancel button goes from thumbnail select mode
// back to thumbnail list mode
$('selected-header').addEventListener('action',
setView.bind(null, LAYOUT_MODE.list));
if (!isPhone) {
$('fullscreen-toolbar-header').addEventListener('action', function() {
setView(LAYOUT_MODE.list);
});
}
// The camera buttons should launch the camera app
fullscreenButtons.camera.onclick = launchCameraApp;
$('thumbnails-camera-button').onclick = launchCameraApp;
Overlay.addEventListener('camera', launchCameraApp);
// Clicking on the delete button in thumbnail select mode deletes all
// selected items
$('thumbnails-delete-button').onclick = deleteSelectedItems;
// Clicking on the share button in select mode shares all selected images
$('thumbnails-share-button').onclick = shareSelectedItems;
Overlay.addEventListener('cancel', function() {
if (picking) {
Pick.cancel();
}
});
// Handle resize events
window.onresize = resizeHandler;
// All our buttons and UI elements are hooked up, so we've reached
// the "interactive" mark.
window.performance.mark('navigationInteractive');
}
// We created the MediaDB in thumbnails.js. Now we need to finish
// setting it up.
function finishDBInitialization() {
// Display a dialog if the DB needs to upgrade before becoming ready
photodb.onupgrading = function(evt) {
Overlay.show('upgrade');
};
// This is called when DeviceStorage becomes unavailable because the
// sd card is removed or because it is mounted for USB mass storage
// This may be called before onready if it is unavailable to begin with
// We don't need one of these handlers for the video db, since both
// will get the same event at more or less the same time.
photodb.onunavailable = function() {
// Switch back to the thumbnail view unless it is a pick activity.
// If we were viewing or editing an image it might not be there
// anymore when the MediaDB becomes available again.
if (!picking) {
// If we are editing image, call exitEdit to revoke blob URLs
// and close the editor object. This is a known case where the
// user can lose their work. See Bug 1148347
if (currentView === LAYOUT_MODE.edit) {
exitEdit();
}
setView(LAYOUT_MODE.list);
} else {
Pick.restart();
}
// If storage becomes unavailble (e.g. the user starts a USB Mass Storage
// Lock the user out of the app, and tell them why
var why = photodb.state;
if (why === MediaDB.NOCARD) {
Overlay.show('nocard');
}
else if (why === MediaDB.UNMOUNTED) {
Overlay.show('pluggedin');
}
};
photodb.onready = function() {
// Hide the nocard or pluggedin overlay if it is displayed
if (Overlay.current === 'nocard' || Overlay.current === 'pluggedin' ||
Overlay.current === 'upgrade') {
Overlay.hide();
}
// We also scan the file system for new files every time we get
// a ready event. That code is in a different event handler that
// is not registered until we are notified the Thumbnails.created
// promise resolves.
};
photodb.onscanstart = function onscanstart() {
// Prevents user to edit images when scanning pictures from storage
fullscreenButtons.edit.classList.add('disabled');
// Show the scanning indicator
$('throbber').classList.remove('hidden');
$('throbber').classList.add('throb');
};
photodb.onscanend = function onscanend() {
// Allows the user to edit images when scanning is finished
fullscreenButtons.edit.classList.remove('disabled');
if (Overlay.current === 'scanning') {
Overlay.show('emptygallery');
}
else if (!isPhone && !currentFrame.displayingImage &&
!currentFrame.displayingVideo) {
// focus on latest one if client hasn't clicked any of
// them
showFile(0);
}
// Hide the scanning indicator
// setTimeout() is to workaround Bug 1166500
$('throbber').classList.remove('throb');
setTimeout(function() { $('throbber').classList.add('hidden'); }, 100);
// If this was the first scan after startup, then tell
// performance monitors that the app is finally fully loaded and stable.
if (!firstScanDone) {
firstScanDone = true;
window.performance.mark('fullyLoaded');
}
};
// On devices with internal and external device storage, this handler is
// triggered when the user removes the sdcard. MediaDB remains usable
// and we'll get a bunch of deleted events for the files that are no longer
// available. But we need to listen to this event so we can switch back
// to the list of thumbnails. We don't want to be left viewing or editing
// a photo that is no longer available.
photodb.oncardremoved = function oncardremoved() {
// If the user pulls the sdcard while trying to pick an image, give up
if (picking) {
Pick.cancel();
return;
}
setView(LAYOUT_MODE.list);
};
// One or more files was created (or was just discovered by a scan)
photodb.oncreated = function(event) {
event.detail.forEach(fileCreated);
};
// One or more files were deleted (or were just discovered missing)
photodb.ondeleted = function(event) {
event.detail.forEach(fileDeleted);
};
// If the mediadb is not still in its initial OPENING state then we
// missed a "ready" or "unavailable" or "upgrading" event, and need
// to call the appropriate handler now. Note that we don't start scanning
// until after the ready event arrives and we enumerate the db, so it
// is not possible for us to miss a scanstart or scanend events. And if
// we miss a created or deleted event, the relevant files will be found
// during the enumeration, so this is all we have to look for now
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() {
// Start off in thumbnail list mode or in pick mode
if (picking) {
setView(LAYOUT_MODE.pick);
}
else {
setView(LAYOUT_MODE.list);
}
}
// Register a handler for pick and browse activities
function registerActivityHandler() {
navigator.mozSetMessageHandler('activity', function activityHandler(a) {
var activityName = a.source.name;
switch (activityName) {
case 'browse':
// The user is probably coming to us from the camera, and she probably
// wants to see the list of thumbnails. If the gallery was already
// running when the browse activity was started, then we might not be
// displaying the thumbnails. If we're currently displaying a single
// image, switch to the thumbnails. But if the user left the gallery
// in the middle of an edit or in the middle of making a selection,
// then returning to the thumbnail list would cause her to lose work,
// so in those cases we don't change anything and let the gallery
// resume where the user left it. See Bug 846220.
if (currentView === LAYOUT_MODE.fullscreen) {
setView(LAYOUT_MODE.list);
}
break;
case 'pick':
LazyLoader.load('js/pick.js', function() { Pick.start(a); });
break;
}
});
}
// Get the thumbnails displayed
function displayThumbnails() {
// thumbnails.js started created thumbanils offscreen.
// We're ready now to deal with them
// Copy stuff to global variables
// jshint ignore:start
thumbnails = Thumbnails.container; // The element that holds the thumbnail
thumbnailList = Thumbnails.list; // The object that represents them
// jshint ignore:end
// Now insert the thumbnails into the document.
var placeholder = $('thumbnails-placeholder');
placeholder.parentElement.replaceChild(thumbnails, placeholder);
// Handle clicks on the thumbnails
thumbnails.addEventListener('click', thumbnailClickHandler);
// When the first page of thumbnails is diplayed, we can emit
// our 'visually complete' mark for startup time comparison
Thumbnails.firstpage.then(function() {
// Tell performance monitors that "above the fold" content is displayed
// and is ready to interact with.
window.performance.mark('visuallyLoaded');
window.performance.mark('contentInteractive');
// Remove the attribute to trigger gaia header font-fit logic, defer it
// til now to postpone the overhead. Add setTimeout to lazyload
// gaia-header.js aynchronously.
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');
}
});
});
});
// When all the thumbnails have been created, we can start a scan
Thumbnails.complete.then(function() {
if (files.length === 0) { // If we didn't find anything
Overlay.show('scanning');
}
// Send a custom mark to performance monitors to note that we're done
// enumerating the database at this point. We won't send the final
// fullyLoaded marker until we're completely stable and have
// finished scanning.
window.performance.mark('mediaEnumerated');
// Now that we've enumerated all the photos and videos we already know
// about it is time to go and scan the filesystem for new ones. If the
// MediaDB is fully ready, we can scan now. Either way, we always want
// to scan every time we get a new 'ready' event.
photodb.addEventListener('ready', function() { photodb.scan(); });
if (photodb.state === MediaDB.READY) { // if already ready then scan now
photodb.scan();
}
});
}
})();

View file

@ -0,0 +1,152 @@
'use strict';
/* global
MediaUtils,
ThumbnailItem
*/
/**
* ThumbnailDateGroup is a grouping mechanism supported in gallery app. It
* groups file data by its year and month. The grouping
* identity is based on the groupID. Once the groupID is different, we view
* two groups as different groups. ThumbnailDateGroup also sorts the added file
* data descendant. We can just put a item into group and let it sort the list.
*
* CONSTRUCTOR:
* To create a group, we need to supply one argument:
* item: the first data object for this group. It will be used for
* rendering the information about this group.
*
* API:
* addItem(): add a new item to this group. ThumbnailDateGroup will create a
* ThumbnailItem for the supplied file data and insert it to UI.
* args:
* item: the file data object.
* return: ThumbnailItem object
* removeItem(): remove a ThumbnailItem from this group. It also removes the
* UI from this group.
* thumbnail: ThumbnailItem object to remove.
* getCount(): returns the total items in this group
*
* Properties:
* htmlNode: the HTML DOM node of this group.
* groupID: the ID of this group.
* thumbnails: all ThumbnailItem objects under this group.
*
* Static API:
* getGroupID: calculate the group id for file object.
* item: the file data object.
* return: an id string.
* compareGroupID: Compare group ids in format group_yyyy-mm.
* The format is chosen carefully so that string comparison
* returns the same result as numeric comparison
*
*/
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;
// Create the DOM elements for this thumbnail group
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) {
// id is group_yyyy-mm. this id will be used as a key
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);
};
// Instance methods
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) {
// This file is the first or is newer than the first one.
// This is the most common case for new files.
return 0;
}
else if (thumbnail.data.date <
self.thumbnails[self.thumbnails.length - 1].data.date) {
// This file is older than the last one.
// This is the most common case when we enumerate the database.
return self.thumbnails.length;
}
else {
// Otherwise we have to search for the right insertion spot
return MediaUtils.binarySearch(self.thumbnails,
thumbnail,
function(a, b) {
return b.data.date - a.data.date;
});
}
}
var thumbnail = new ThumbnailItem(item);
// calculate the position.
var insertPosition = getInsertPosition(thumbnail);
// put into DOM tree.
this.container.insertBefore(thumbnail.htmlNode,
this.container.children[insertPosition]);
// insert thumbnail view into array
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);
// Localize each of the group's thumbnails.
this.thumbnails.forEach(function(thumbnail) { thumbnail.localize(); });
};

View file

@ -0,0 +1,66 @@
'use strict';
/**
* ThumbnailItem is view object for a single gallery item data. It renders file
* in listitem object.
*
* CONSTRUCTOR:
* To create a ThumbnailItem objet requires the following argument:
* fileData: the file data object from mediadb.
*
* Properties:
* htmlNode: the HTML DOM node for this thumbnail item. It is rendered at the
* creation of object.
* data: the file data object bound with this thumbnail item.
*/
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;
// We revoke this url in imageDeleted
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,114 @@
'use strict';
/**
* ThumbnailList is the class reponsible for rendering all gallery content in to
* list. It uses GroupClass to group and sort file data as ThumbnailItem.
*
* Constructor:
* groupClass: the grouping class.
* container: the HTML DOM object for containing everything.
*
* API:
* addItem: add a file data and render it.
* item: the file data to add.
* removeItem: remove a ThumbnailItem.
* filename: the filename of file data.
* reset: clears all the internal data structure.
*
* Properties:
* thumbnailMap: a mapping of filename to ThumbnailItem.
* groupMap: a mapping of filename to thumbnail group.
* itemGroups: an sorted array of thumbnail group.
* count: the total ThumbnailItem in this list.
* groupClass: the grouping class this list used.
* container: the HTML DOM element containing this list.
*/
function ThumbnailList(groupClass, container) {
if (!groupClass || !container) {
throw new Error('group class or container cannot be null or undefined');
}
// the filename to ThumbnailItem map
this.thumbnailMap = {};
// the filename to group
this.groupMap = {};
// the group list
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) {
// existing ID is less than current groupID, stop searching/
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,150 @@
/* exported Thumbnails */
/* global ThumbnailList, ThumbnailDateGroup, MediaDB, LazyLoader */
/* global ThumbnailItem, photodb, files, picking, metadataParser */
(function(exports) {
'use strict';
// This is the object we export
var thumbnails = exports.Thumbnails = {};
// This is the container element for the thumbnails. We put them here
// so that we can start creating them before the DOM is loaded. Once
// the DOM is loaded and we've been localized, we'll insert this into
// the document in the right spot.
thumbnails.container = document.createElement('ul');
thumbnails.container.id = 'thumbnails';
// This is the ThumbnailList object that represents the list of thumbnails
// contained in the thumbnailContainer.
thumbnails.list = new ThumbnailList(ThumbnailDateGroup, thumbnails.container);
// Bug 1135256: Having a listener in ThumbnailList prevents them from
// being garbage collected since ready() holds a strong reference, so
// we have a singleton listener here.
document.addEventListener('DOMRetranslated', () => {
ThumbnailDateGroup.resetFormatter();
ThumbnailItem.resetFormatter();
thumbnails.list.localize();
});
// How many thumbnails are visible on a page.
// Batch sizes are based on this.
var PAGE_SIZE = 15;
// Create the MediaDB object so it can start initializing the DB right away
photodb = new MediaDB('pictures', // jshint ignore:line
metadataParserWrapper, {
version: 2,
autoscan: false, // We're going to call scan() explicitly
batchHoldTime: 2000, // Batch files during scanning
batchSize: 3 // Max batch size when scanning
});
var metadataParserLoaded = false;
// This is defined here only because we need it defined when we call
// the MediaDB() constructor. It will not be called until we start
// scanning, so it does not matter that it uses things that are not
// defined yet.
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);
});
}
// These variables will hold the functions that will resolve the promises
var firstPageResolver;
var completionResolver;
// Create promises to store in the thumbnails object so that
// the code that has not loaded yet can find out when we're done.
thumbnails.firstpage = new Promise(function(resolve, reject) {
firstPageResolver = resolve;
thumbnails.complete = new Promise(function(resolve, reject) {
completionResolver = resolve;
// Now that the promises are ready, start creating thumbnails
createThumbnails();
});
});
function createThumbnails() {
// If the db is enumerable (or fully ready), start enumerating it.
// Otherwise wait until it becomes ready enumerable.
if (photodb.state === MediaDB.READY ||
photodb.state === MediaDB.ENUMERABLE) {
enumerateDB();
}
else {
photodb.addEventListener('enumerable', enumerateDB);
}
}
function enumerateDB() {
// Temporary arrays to hold enumerated files
var batch = [];
var batchsize = PAGE_SIZE;
var firstPageDisplayed = false;
photodb.enumerate('date', null, 'prev', function(fileinfo) {
if (fileinfo) {
// For a pick activity, don't display videos
if (picking && fileinfo.metadata.video) {
return;
}
// Bug 1003036 fixed an issue where explicitly created preview
// images could be saved with fractional sizes. We don't do that
// anymore, but we still need to clean up existing bad data here.
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;
// Resolve the thumbnails.firstpage promise to tell the UI
// that "above the fold" content is ready to display
firstPageResolver();
}
}
function thumb(fileinfo) {
files.push(fileinfo); // remember the file
// Create the thumbnail view for this file
// and insert it at the right spot
thumbnails.list.addItem(fileinfo);
}
function done() {
flush();
// Resolve the thumbnails.complete promise
completionResolver();
}
}
}(window));

4
gaia/apps/gallery/js/utils.js Executable file
View file

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

18
gaia/apps/gallery/jsdoc.json Executable file
View file

@ -0,0 +1,18 @@
{
"name": "gallery",
"tags": {
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["apps/gallery/js/**/*.js"]
},
"plugins": [
"plugins/markdown",
"plugins/summarize"
],
"opts": {
"private": true,
"recurse": true,
"destination": "docs/gallery"
}
}

View file

@ -0,0 +1,134 @@
number-selected2 = {[ plural(n) ]}
number-selected2[zero] = Select
number-selected2[one] = {{ n }} selected
number-selected2[two] = {{ n }} selected
number-selected2[few] = {{ n }} selected
number-selected2[many] = {{ n }} selected
number-selected2[other] = {{ n }} selected
delete-photo? = Delete photo?
delete-video? = Delete video?
# An "item" means a photo or a video
# The app currently doesn't use the [zero] case
delete-n-items? = {[ plural(n) ]}
delete-n-items?[zero] = Nothing selected
delete-n-items?[one] = Delete selected item?
delete-n-items?[two] = Delete {{ n }} items?
delete-n-items?[few] = Delete {{ n }} items?
delete-n-items?[many] = Delete {{ n }} items?
delete-n-items?[other] = Delete {{ n }} items?
pickoneimage2 = Select
cropimage = Crop
cancel = Cancel
delete = Delete
ok = OK
edit = Edit
save = Save
done = Done
exposure = Exposure
crop = Crop
effects = Effects
borders = Borders
enhance-on = Auto-enhance on
enhance-off = Auto-enhance off
apply = Apply
filters = Filters
original = Original
blackandwhite = B&W
sepia = Sepia
freeform = Freeform
portrait = Portrait
landscape = Landscape
square = Square
emptygallery2-title = No photos or videos
emptygallery2-text = Use the Camera app to get started.
emptygallery2-title-pick = No photos
nocard3-title = No memory card
nocard4-text = Insert a memory card to use this app.
pluggedin2-title = Memory card in use
pluggedin2-text = Unplug the phone to use this app.
scanning-title = Scanning
scanning-text = Looking for photos and videos.
upgrade-title = Database Upgrade
upgrade-text = Please wait while Gallery updates its database.
share-noprovider = Can not share this image file type.
memorycardfull = Can not edit photos: Memory card is full.
imagetoobig = Image is too large to display on this device.
imageinvalid = Image is invalid or corrupt and cannot be displayed.
saved = {{filename}} saved to Gallery
edit-copy-saved = New copy saved.
overlay-camera-button = Go to Camera
name-label = Name
size-label = Size
image-type-label = Image Type
date-taken-label = Date Taken
resolution-label = Resolution
close-button = Close
byteUnit-B = Bytes
byteUnit-KB = KB
byteUnit-MB = MB
byteUnit-GB = GB
gallery = Gallery
preview = Preview
videoDated.ariaLabel = Video dated {{ timeStamp }}
imageDated.ariaLabel = Image dated {{ timeStamp }}
# Full screen back-button
back-button.ariaLabel = Back
# Edit crop buttons
crop-options.ariaLabel = Crop
crop-aspect-free.ariaLabel = Custom aspect ratio
crop-aspect-portrait.ariaLabel = Portrait aspect ratio
crop-aspect-landscape.ariaLabel = Landscape aspect ratio
crop-aspect-square.ariaLabel = Square aspect ratio
crop-aspect-more.ariaLabel = More aspect ratios
crop-aspect-3x4 = 3:4 portrait
crop-aspect-4x3 = 4:3 landscape
crop-aspect-16x9 = 16:9 landscape
# Edit effect buttons
effect-options.ariaLabel = Effect
effect-none.ariaLabel = None
effect-bw.ariaLabel = Black and white
effect-sepia.ariaLabel = Sepia
effect-bluesteel.ariaLabel = Blue steel
effect-faded.ariaLabel = Faded
# Edit buttons
exposure-button.ariaLabel = Exposure
crop-button.ariaLabel = Crop
effect-button.ariaLabel = Effects
enhance-button.ariaLabel = Enhance
# Footer buttons
camera-button.ariaLabel = Camera
select-multiple-button.ariaLabel = Select multiple
edit-button.ariaLabel = Edit
share-button.ariaLabel = Share
info-button.ariaLabel = Information
delete-button.ariaLabel = Delete
# Edit image preview
editImagePreview.ariaLabel = Edit image preview
# Localization note (fileSize): {{size}} is a floating number with 2 decimal places
fileSize = {{size}} {{unit}}

View file

@ -0,0 +1,84 @@
{
"name": "Gallery",
"description": "Gaia Gallery",
"type": "certified",
"launch_path": "/index.html",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"fullscreen": true,
"permissions": {
"themeable": {},
"device-storage:pictures": {
"access": "readwrite"
},
"device-storage:videos": {
"access": "readwrite"
},
"audio-channel-content": {},
"nfc-share": {},
"nfc": {},
"feature-detection": {},
"moz-extremely-unstable-and-will-change-webcomponents": {}
},
"activities": {
"browse": {
"filters": {
"type": "photos"
},
"disposition": "window"
},
"pick": {
"filters": {
"type": [
"image/*",
"image/jpeg",
"image/png"
]
},
"disposition": "inline",
"returnValue": true,
"href": "/index.html#pick"
},
"open": {
"filters": {
"type": [
"image/jpeg",
"image/png",
"image/gif",
"image/bmp"
]
},
"disposition": "inline",
"returnValue": true,
"href": "/open.html"
}
},
"locales": {
"ar": {
"name": "إستديو",
"description": "Gaia إستديو"
},
"en-US": {
"name": "Gallery",
"description": "Gaia Gallery"
},
"fr": {
"name": "Galerie",
"description": "Galerie de Gaia"
},
"zh-TW": {
"name": "相簿",
"description": "Gaia 相簿"
}
},
"default_locale": "en-US",
"icons": {
"84": "/style/icons/gallery_84.png",
"126": "/style/icons/gallery_126.png",
"142": "/style/icons/gallery_142.png",
"189": "/style/icons/gallery_189.png",
"284": "/style/icons/gallery_284.png"
}
}

53
gaia/apps/gallery/open.html Executable file
View file

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8" />
<meta name="theme-color" content="#242d33">
<title>Gallery</title>
<link rel="stylesheet" href="style/open.css" type="text/css" />
<link rel="stylesheet" href="shared/style/status.css" type="text/css" />
<link rel="stylesheet" href="shared/style/shrinking_ui.css" type="text/css" />
<!-- Localization -->
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/gallery.{locale}.properties">
<link rel="localization" href="/shared/locales/media/media.{locale}.properties">
<link rel="localization" href="/shared/locales/shrinking_ui/shrinking_ui.{locale}.properties">
<!-- Shared code -->
<script type="text/javascript" src="shared/js/intl/l20n.js"></script>
<script type="text/javascript" defer src="shared/js/gesture_detector.js"></script>
<script type="text/javascript" defer src="shared/js/media/downsample.js"></script>
<script type="text/javascript" defer src="shared/js/media/media_frame.js"></script>
<script type="text/javascript" defer src="shared/js/blobview.js"></script>
<script type="text/javascript" defer src="shared/js/mime_mapper.js"></script>
<script type="text/javascript" defer src="shared/js/media/jpeg_metadata_parser.js"></script>
<script type="text/javascript" defer src="shared/js/media/image_size.js"></script>
<script type="text/javascript" defer src="shared/js/device_storage/get_storage_if_available.js"></script>
<script type="text/javascript" defer src="shared/js/device_storage/get_unused_filename.js"></script>
<script type="text/javascript" defer src="shared/js/shrinking_ui.js"></script>
<!-- Specific code -->
<script type="text/javascript" defer src="js/config.js"></script>
<script type="text/javascript" defer src="js/nfc.js"></script>
<script type="text/javascript" defer src="js/open.js"></script>
<!-- Web Components -->
<script defer src="shared/elements/config.js"></script>
<link rel="stylesheet" type="text/css" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/gaia-theme.css">
<link rel="stylesheet" type="text/css" href="shared/elements/gaia-icons/gaia-icons.css">
<script defer src="shared/elements/gaia-header/dist/gaia-header.js"></script>
</head>
<body role="application" class="theme-media">
<section id="open" role="region" class="skin-dark">
<gaia-header ignore-dir id="header" action="back">
<h1 id="filename"></h1>
<button id="save" data-l10n-id="save" class="hidden"></button>
</gaia-header>
<div id="frame"></div>
</section>
<section id="banner" role="status" hidden>
<p id="message"></p>
</section>
</body>
</html>

10
gaia/apps/gallery/package.json Executable file
View file

@ -0,0 +1,10 @@
{
"webide": {
"prepackage": {
"command": "make",
"env": ["APP=gallery"],
"cwd": "../.."
},
"packageDir": "../../build_stage/gallery/"
}
}

View file

@ -0,0 +1,194 @@
.videoPlayer:focus {
outline: none;
}
/* video player controls */
.videoPlayerPlayButton:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 9.2rem;
height: 9.2rem;
margin: -4.6rem 0 0 -4.6rem;
border-radius: 50%;
background-color: rgba(0,0,0, .65);
}
.videoPlayerPlayButton {
position: absolute;
width: 10rem;
height: 10rem;
left: calc(50% - 5rem);
top: calc(50% - 5rem);
border-radius: 50%;
border-width: 0;
}
.videoPlayerPlayButton:after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 10rem;
height: 10rem;
border-radius: 50%;
background-image: url("images/video_play_button.png");
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: 10rem;
}
.videoPlayerPlayButton:active:before {
background-color: rgba(13,108,128, .8);
}
.videoPlayerPlayButton.hidden {
opacity: 0;
visibility: hidden;
}
.videoPlayerFooter {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 5rem;
margin: 0;
padding: 0;
background-color: rgba(0, 0, 0, 0.3);
overflow: hidden;
opacity: 1;
transition: opacity var(--transition-duration), visibility 0s;
visibility: visible;
-moz-user-select: none;
}
.videoPlayerFooter.hidden {
opacity: 0;
pointer-events: none;
visibility: hidden;
transition-delay: 0s, 0.5s;
}
.videoPlayerPauseButton {
position: absolute;
width: 10rem;
height: 10rem;
padding: 0;
margin: 0;
background: url("images/video_pause_button.png") no-repeat center center / 3rem,
rgba(0,0,0,.5);
border-radius: 5.3rem;
border: solid #ccc 0.3rem;
top: -2.5rem;
left: 1rem;
}
.videoPlayerPauseButton:active {
background: url("images/video_pause_button.png") no-repeat center center / 3rem,
url("images/video_play_focus.png") no-repeat center center / 10.6rem;
}
button::-moz-focus-inner {
padding: 0;
border: none;
}
/* time slider */
.videoPlayerSlider {
position: absolute;
left: 11rem;
top: 0;
right: 0;
height: 100%;
}
.videoPlayerSlider > span {
display: block;
width: 4.5rem;
position: absolute;
color: white;
height: 100%;
line-height: 5rem;
text-align: center;
font-size: 1.5rem;
}
.videoPlayerElapsedText {
left: 1rem;
}
html[dir="rtl"] .videoPlayerElapsedText {
right: 1rem;
left: unset;
}
.videoPlayerDurationText {
right: 1rem;
}
html[dir="rtl"] .videoPlayerDurationText {
left: 1rem;
right: unset;
}
.videoPlayerProgress {
position: absolute;
top: 0;
left: 7rem;
right: 7rem;
height: 100%;
}
.videoPlayerProgress > div {
position: absolute;
pointer-events: none;
}
.videoPlayerElapsedBar, .videoPlayerBackgroundBar {
height: 0.4rem;
width: 0;
top: 50%;
margin-top: -0.2rem;
border-radius: 0.6rem;
}
.videoPlayerElapsedBar {
background-color: #0ac;
}
.videoPlayerBackgroundBar {
background-color: #333;
width: 100%;
}
.videoPlayerPlayHead {
display: block;
height: 2rem;
width: 2.5rem;
border-radius: 2.5rem;
background-color: white;
top: 50%;
margin-top: -1rem;
-moz-margin-start: -1.2rem;
}
.videoPlayerFullscreenButton {
position: absolute;
display: none;
border: none;
width: 8rem;
height: 8rem;
padding: 3.4rem;
margin: 0;
background: url("images/fullscreen.png") no-repeat center center / 4.6rem,
transparent;
right: 0;
}
.videoPlayerFullscreenButton:active {
background: url("images/fullscreen_pressed.png") no-repeat center center / 4.6rem,
transparent;
}

View file

@ -0,0 +1,97 @@
.videoPlayerSlider > span {
font-size: 1.8rem;
line-height: 8rem;
}
.videoPlayerFooter {
background-color: transparent;
height: 8rem;
}
.videoPlayerSlider {
left: 10rem;
}
.videoPlayerSlider .videoPlayerProgress {
left: 7rem;
right: 19rem;
}
.videoPlayerPauseButton {
top: 0;
left: 0;
height: 8rem;
width: 10rem;
background: url("images/video_pause_button_46x46.png") no-repeat center center / 4.6rem,
transparent;
border: none;
border-radius: 0;
}
.videoPlayerPauseButton:active {
background: url("images/video_pause_pressed_46x46.png") no-repeat center center / 4.6rem,
transparent;
}
.videoPlayerSlider .videoPlayerDurationText {
font-size: 1.8rem;
right: 8rem;
width: 10rem;
}
.videoPlayerFullscreenButton {
display: block;
background-color: transparent;
}
@media (orientation: landscape) {
.videoPlayerSlider .videoPlayerElapsedText {
text-align: left;
left: 0;
width: 6rem;
}
body.fullscreenView .videoPlayerPauseButton {
width: 19rem;
}
body.fullscreenView .videoPlayerSlider {
left: 20rem;
}
body.fullscreenView.videoPlayerSlider .videoPlayerProgress {
left: 26rem;
right: 12rem;
}
body.fullscreenView .videoPlayerSlider .videoPlayerDurationText {
text-align: left;
right: 0;
width: 10rem;
}
}
@media (orientation: portrait) {
.videoPlayerElapsedText {
width: 6rem;
}
.videoPlayerPauseButton {
width: 12rem;
}
.videoPlayerSlider {
left: 14rem;
}
.videoPlayerSlider .videoPlayerProgress {
right: 10rem;
}
.videoPlayerSlider .videoPlayerDurationText {
text-align: left;
right: 0;
width: 8rem;
}
}

View file

@ -0,0 +1,922 @@
* { -moz-box-sizing: border-box;
/**
By default do not allow any user text selection, only enable
for certain parts on a per-element/region area
*/
-moz-user-select: none;
}
html {
height: 100%;
width: 100%;
padding: 0;
margin: 0;
overflow: hidden;
font-size: 10px;
}
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
color: #fff;
font-size: 1rem;
background-color: #0E1214;
overflow: hidden;
}
section {
margin: 0;
padding: 0;
overflow: hidden;
font-size: 1.4rem;
}
.large-only {
display: none !important;
}
.hidden {
display: none;
}
/* hide sections from screen reader when modal dialogs are showing */
body.showing-dialog > section {
visibility: hidden;
}
/* All of the main views fill the screen */
#fullscreen-view, #edit-view, #crop-view, #thumbnail-views {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
font-size: 2.1rem;
/* display will be determined by LAYOUT_MODE */
display: none;
}
/* Hide unresolved gaia-header elements */
gaia-header:unresolved {
visibility:hidden;
}
#thumbnail-views > gaia-header {
position: absolute;
width: 100%;
top: 0;
}
/* Bug 1111735 - !important is needed because of the shadow CSS polyfill */
.thumbnails-list, .thumbnails-select, .thumbnails-pick {
/* display will be determined by LAYOUT_MODE */
display: none !important;
}
body.thumbnailListView .thumbnails-list,
body.thumbnailSelectView .thumbnails-select,
body.pickView .thumbnails-pick {
display: block !important;
}
body.thumbnailListView #thumbnail-views,
body.thumbnailSelectView #thumbnail-views,
body.pickView #thumbnail-views,
body.fullscreenView #fullscreen-view,
body.editView #edit-view,
body.cropView #crop-view {
display: block;
}
/* Header in default thumbnail list in tablets only, see gallery_tablet.css */
body.thumbnailListView #thumbnails-list-top {
display: none !important;
}
/*
* The overlay content area holds the text of the overlay.
* It has borders and a less transparent background so that
* the overlay text shows up more clearly
*/
#overlay-content {
background:
url(images/ui/pattern.png) repeat left top,
url(images/ui/gradient.png) no-repeat left top;
background-size: auto auto, 100% 100%;
/* We can't use shortand with background size because is not implemented yet:
https://bugzilla.mozilla.org/show_bug.cgi?id=570326; */
overflow: hidden;
position: absolute;
z-index: 1;
top: 0;
left: 0;
right: 0;
bottom: 0;
font-size: 0;
/* Using font-size: 0; we avoid the unwanted visual space (about 3px)
created by white-spaces and break lines in the code betewen inline-block elements */
color: #fff;
padding: 11rem 2.5rem 0 2.5rem;
}
#overlay-title {
font-weight: normal;
font-size: 1.9rem;
color: #fff;
margin: 0 0.5rem 0 0.5rem;
padding: 0;
}
#overlay-text {
padding: 1rem 0.5rem 0 0.5rem;
border-top: 0.1rem solid #686868;
font-weight: 300;
font-size: 2.5rem;
color: #ebebeb;
}
#overlay-menu {
z-index: 1;
}
/* For overriding the style from BB */
#overlay-menu.hidden {
display: none;
}
#overlay-menu button.hidden {
display: none;
}
#overlay-menu button.full {
margin: 0;
}
/*
* Thumbnails are implemented as <li> elements in a <ul> with background-image
* set to display the image. We use background-size: cover to automatically
* resize the image as needed.
*/
#thumbnails {
position: absolute;
left: 0;
width: 100%;
top: 0;
bottom: 0;
/* top and bottom are set depending on view below */
padding: 0;
margin: 0;
overflow-y: scroll;
overflow-x: hidden;
/*
* the body background is black, but remind the UA of that here to
* keep the thumbnail view opaque if possible.
*/
background-color: #0E1214;
will-change: scroll-position;
/*
* set it visible/hidden by LAYOUT_MODE
*/
visibility: hidden;
pointer-events: none;
}
/*
* The thumbnail list appears in regular browsing mode, in selection mode,
* and in pick mode.
*/
body.thumbnailSelectView #thumbnails,
body.pickView #thumbnails,
body.thumbnailListView #thumbnails {
visibility: visible;
pointer-events: auto;
bottom: 0;
}
body.thumbnailSelectView #thumbnails,
body.pickView #thumbnails {
top: 5rem;
}
/*
* Give the very last thumbnail a large bottom margin so that it doesn't
* hide behind the overlaid toolbar in thumbnail list and select view.
*/
body.thumbnailSelectView #thumbnails > li:last-child .thumbnail:last-child,
body.thumbnailListView #thumbnails > li:last-child .thumbnail:last-child {
margin-bottom: 4.5rem;
}
.thumbnail-group-header {
font-size: 1.6rem;
font-weight: 400;
color: #8a9699;
-moz-padding-start: 0.5rem;
height: 4rem;
line-height: 4rem;
width:100%;
float:left;
}
/* Bug 968311 - Keep Gallery in scrollable
* background color. Need in all four
* containers below.
*/
#thumbnails > li,
#thumbnails > li > ul,
.thumbnail-group-header,
.thumbnail-group-container {
background-color: #0E1214;
}
.thumbnail-group-container {
padding: 0;
}
#thumbnails>li {
/* no list item bullets */
list-style-type: none;
}
#thumbnails>li:first-child .thumbnail-group-header {
border-top: 0.1rem solid;
border-color:rgba(255,255,255, 0.1);
}
.thumbnail {
-moz-margin-end: 0.1rem;
margin-bottom: 0.1rem;
padding: 0;
/* default to left-to-right, but overridden for rtl languages below */
float: left;
/* don't let the user select the items */
-moz-user-select: none;
background-color: #444;
/*
* Here's the thumbnail size for portrait mode. We have 3 thumbnails
* for each row. The size for landscape mode is handled below by
* media query.
* Padding-bottom is used here in order to set div height with respect
* to device width.
*/
width: calc((100% - 0.2rem) / 3 );
height: 0;
padding-bottom: calc((100% - 0.2rem) / 3 );
outline: none;
}
.thumbnail:nth-child(3n) {
-moz-margin-end: 0;
}
.thumbnailImage{
width: 100%;
}
.thumbnailImage.selected {
outline: solid #00aacc 0.3rem;
outline-offset: -0.2rem;
}
/* RTL modifications */
html[dir=rtl] #thumbnails-number-selected {
direction: rtl;
}
@media (orientation: landscape) {
/* 4 thumbnails for each row in landscape mode. */
.thumbnail {
width: calc((100% - 0.3rem) / 4);
padding-bottom: calc((100% - 0.3rem) / 4);
}
.thumbnail:nth-child(3n) {
-moz-margin-end: 0.1rem;
}
.thumbnail:nth-child(4n) {
-moz-margin-end: 0;
}
}
#fullscreen-back-button-tiny {
/*
these particular sizes, positions, and colors are chosen
to line up with the focus mode button of the camera app
*/
position: absolute;
top: 2rem;
left: 1.4rem;
width: 5rem;
height: 5rem;
font-size: 0;
border-radius: 50%;
border: none;
background: rgba(95, 95, 95, 0.3);
color: inherit;
transition: opacity 0.2s linear;
z-index: 10;
}
/**
* 1. Make sure icon is aligned inside circle
*/
#fullscreen-back-button-tiny:before {
margin-left: -0.3rem; /* 1 */
}
#fullscreen-back-button-tiny:active {
background-color: #008eab;
}
#frames {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
.frame {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
-moz-user-select: none;
will-change: scroll-position;
}
.frame > .image-view {
top: 0; /* javascript modifies this position with a transform */
left: 0;
position: absolute;
border-width: 0;
padding: 0;
margin: 0;
pointer-events: none;
-moz-user-select: none;
transition: opacity 100ms ease-in;
}
.frame > video {
transform-origin: 0 0;
}
/* localization */
body.hidden *[data-l10n-id] {
visibility: hidden;
}
*[dir=rtl] .thumbnail {
float: right;
}
/* the indeterminate progress bar displayed while scanning */
#throbber {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 0.3rem;
border: none;
overflow: hidden;
background-size: 1rem;
background-image: repeating-linear-gradient(135deg,
#000 0%, #000 42%,
#32D1F2 42%, #32D1F2 57%,
#000 57%, #000 100%);
z-index: 10; /* has to be higher than pick-top */
}
#throbber.throb {
animation: throb 1s steps(10) infinite;
}
@keyframes throb {
from { background-position: 0 0; }
to { background-position: -1rem 0; }
}
#spinner {
position: absolute;
top: calc(50% - 1.5rem);
left: calc(50% - 1.5rem);
z-index: 1000;
}
/* override progress_activity.css */
#spinner.hidden {
display: none;
}
/* buttons */
a.button {
display: block;
padding: 0;
margin: 0;
border-width: 0;
background-position: center center;
background-repeat: no-repeat;
background-size: 3rem;
/* for buttons that display text */
font-size: 1.4rem;
text-align: center;
line-height: 4.5rem;
}
a.button:active, a.button:focus {
outline: none;
}
a.button.disabled {
opacity: 0.4;
pointer-events: none;
}
.icon.icon-done {
background: url('images/iconaction_done_30x30.png') no-repeat center center / 3rem !important;
}
footer {
background: rgba(0, 0, 0, 0.75);
height: 4.5rem;
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 1;
box-shadow: 0 -0.2rem 0.5rem rgba(50, 50, 50, 0.75);
}
.icon.icon-done {
background-image: url('images/iconaction_done_30x30.png') !important;
}
#thumbnails-camera-button {
position: absolute;
width: 50%;
height: 100%;
left: 0;
border-right: 0.1rem solid #000;
}
#thumbnails-select-button {
position: absolute;
width: 50%;
height: 100%;
left: 50%;
}
#thumbnails-delete-button {
position: absolute;
width: 50%;
height: 100%;
left: 0;
border-right: 0.1rem solid #000;
}
#thumbnails-share-button {
position: absolute;
width: 50%;
height: 100%;
left: 50%;
}
#fullscreen-toolbar {
height: 4.5rem;
position: absolute;
bottom: 0;
left: 0;
right: 0;
font-size: 0;
visibility: visible;
direction: ltr; /* buttons in toolbar are not directional and stays LTR */
/**
Bug 991367 - This is a temporary workaround for an issue where
the `tap` event on the video player "Pause" button causes a
`click` event to bleed through to the "Camera" button on the
toolbar. To get around this issue, we add 0.1s delay before
restoring the toolbar from hidden state and transition the
`visibility` property as well.
*/
transition: visibility 0.2s linear 0.1s, transform 0.2s linear 0.1s;
}
/* only for large screen */
#fullscreen-toolbar-header {
display: none;
}
#fullscreen-toolbar-header > menu {
display: none;
}
#fullscreen-toolbar > a {
display: inline-block;
width: 20%;
height: 100%;
}
#fullscreen-view.toolbar-hidden #fullscreen-toolbar {
pointer-events: none;
visibility: hidden;
transform: translate(0, 4.5rem);
transition-delay: 0s;
}
#fullscreen-view.toolbar-hidden #fullscreen-back-button-tiny {
opacity: 0;
pointer-events: none;
}
#pick-header {
/*
* we want the user to be able to push the close button even if
* an overlay is displayed, so be sure that this titlebar/toolbar
* is above the overlay
*/
z-index: 2;
}
/* like .frame */
#crop-frame {
position: absolute;
top: 5rem;
bottom: 0;
left: 0;
width: 100%;
padding: 0;
margin: 0;
overflow: hidden;
-moz-user-select: none;
}
#crop-image {
top: 0; /* javascript modifies this position with a transform */
left: 0;
position: relative;
border-width: 0;
padding: 0;
margin: 0;
transform-origin: 0 0;
pointer-events: none;
-moz-user-select: none;
}
#edit-preview-area {
position: absolute;
left: 0;
right: 0;
top: 5rem;
bottom: 4.5rem; /* 4.5rem for edit-toolbar */
}
#edit-preview-canvas {
position: absolute;
top: 1.5rem;
left: 2.5rem;
width: calc(100% - 5rem); /* I can't do right: 2.5rem here. Bug? */
height: calc(100% - 3rem); /* can't just set bottom: 1.5rem here */
}
#edit-crop-canvas {
position: absolute;
left: 0;
right: 0;
width: 100%;
height: 100%;
}
#edit-options {
position: absolute;
left: 0;
width: 100%;
height: 5rem;
bottom: 0;
background: url(images/options-background.png);
}
/* The progress has higer selectivity css in progress.css to control display.
* The hidden attribute in progress is not working in this case. We need to use
* css to override it. */
#save-progress.hidden {
display: none;
}
/* All the effect options radio buttons share these styles */
#edit-effect-options a.radio.button {
position: absolute;
display: block;
top: 0;
height: 100%;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
margin: 0;
padding: 0;
border-width: 0;
border-right: solid black 0.1rem;
overflow: hidden;
}
#edit-effect-options a.selected.radio.button {
outline: solid #00aacc 0.2rem;
outline-offset: -0.2rem;
}
#edit-crop-options a.button {
position: absolute;
display: block;
top: 0;
bottom: 0;
width: 20%;
margin: 0;
padding: 0;
border-width: 0;
border-right: solid black 0.1rem;
overflow: hidden;
font-size: 1.4rem;
background-position: center;
}
#edit-crop-options a.selected.radio.button {
outline: solid #00aacc 0.2rem;
outline-offset: -0.2rem;
}
#edit-crop-aspect-free {
left: 0;
background-image: url('images/iconaction_camera_freecrop_30x30.png');
}
#edit-crop-aspect-portrait {
left: 20%;
background-image: url('images/iconaction_camera_crop2x3_30x30.png');
}
#edit-crop-aspect-landscape {
left: 40%;
background-image: url('images/iconaction_camera_crop3x2_30x30.png');
}
#edit-crop-aspect-square {
left: 60%;
background-image: url('images/iconaction_camera_crop1x1_30x30.png');
}
#edit-crop-aspect-more {
left: 80%;
}
/* instead of using images inside the buttons above, just draw boxes */
#edit-free-frame {
margin-left: 0.5rem;
margin-top: 0.5rem;
width: 3rem;
height: 2.5rem;
border: dotted white 0.1rem;
}
#edit-portrait-frame {
margin-left: 1.1rem;
margin-top: 0.6rem;
width: 2.6rem;
height: 3.9rem;
border: solid white 0.1rem;
}
#edit-landscape-frame {
margin-left: 0.5rem;
margin-top: 1.2rem;
width: 3.9rem;
height: 2.6rem;
border: solid white 0.1rem;
}
#edit-square-frame {
margin-left: 0.8rem;
margin-top: 0.9rem;
width: 3.2rem;
height: 3.2rem;
border: solid white 0.1rem;
}
/* All the effects radio buttons share these styles */
#edit-effect-options > a.radio.button {
width: 20%;
padding-top: 1.5rem;
}
#edit-effect-none {
left:0;
}
#edit-effect-bw {
left:20%
}
#edit-effect-sepia {
left:40%
}
#edit-effect-bluesteel {
left:60%
}
#edit-effect-faded {
left:80%
}
a.filter-bw {
filter: url(../index.html#filter-bw);
}
a.filter-sepia {
filter: url(../index.html#filter-sepia);
}
a.filter-bluesteel {
filter: url(../index.html#filter-bluesteel);
}
a.filter-faded {
filter: url(../index.html#filter-faded);
}
#edit-toolbar {
height: 4.5rem;
position: absolute;
bottom: 0;
left: 0;
right: 0;
font-size: 0;
transition: transform 0.2s linear;
direction: ltr; /* buttons in toolbar are not directional and stays LTR */
}
/* all buttons in the toolbar share these styles */
#edit-toolbar > a {
display: inline-block;
vertical-align: top;
width: 25%;
height: 100%;
}
#edit-toolbar > a.selected {
color: #00CAF2;
}
#edit-exposure-button {
left: 0;
}
#edit-crop-button {
left: 20%;
}
#edit-effect-button {
left: 40%;
}
#edit-border-button {
left: 60%;
}
#edit-enhance-button {
left: 80%;
}
/* Override the styles from shared/styles/status.css */
#edit-copy-save-banner.banner, #edit-enhance-banner.banner {
position: absolute;
left: 0;
bottom: 4.5rem;
background-color: #0E1214;
}
#edit-copy-save-banner > p, #edit-enhance-banner > p {
text-align: center;
}
#edit-effect-button {
background-image: url('images/actionicon_gallery_edit_effects_30x30.png');
}
#edit-border-button {
background-image: url('images/actionicon_gallery_edit_borders_30x30.png');
}
#edit-enhance-button {
background-image: url('images/actionicon_gallery_edit_enhance_30x30.png');
}
#edit-enhance-button.on {
background-image: url('images/actionicon_gallery_edit_enhance_on_30x30.png');
}
/*
* All of these slider styles are for the exposure compensation slider
* If this app ever needs another slider, we'll have lots of name
* collisions. Ideally, this slider stuff should all be packaged up
* into a reusable component. But this is one-off for now
*/
#exposure-slider {
position: absolute;
left: 0;
width: 100%;
height: 5rem;
font-size: 1.4rem;
}
#sliderbar {
position: relative;
height: 2.6rem;
padding: 0;
margin: 0;
background: url(images/slider-background.png);
}
#sliderline {
position: absolute;
height: 0.5rem;
top: 1rem;
left: 3.5rem;
right: 3.5rem;
margin: 0;
border-radius: 3px;
background: url(images/slider-line.png) repeat-x;
border: none;
}
#sliderthumb {
position: absolute;
width: 3.6rem;
height: 2.6rem;
-moz-margin-start: calc(3.9rem - 1.8rem);
background: url(images/slider-scrubber.png) no-repeat center center / 100%;
font: 0/0 a;
}
#sliderthumb.active {
background-image: url(images/slider-scrubber-active.png);
}
#sliderlabels {
position: relative;
height: 3rem;
padding: 0;
margin: 0;
color: white;
}
.sliderlabel {
position: absolute;
top: -0.2rem;
height: 0.8rem;
background: url(images/slider-marker.png) no-repeat top left;
color: #8a9699;
font-size: 1.5rem;
}
html[dir="rtl"] .sliderlabel {
background: url(images/slider-marker.png) no-repeat top right;
}
.sliderlabel:after {
content: attr(data-label);
position: relative;
text-align: center;
top: 0.9rem;
width: 1rem;
margin: 0 calc(-50% + 0.1rem);
/* Slider labels are numbers */
direction: ltr;
unicode-bidi: -moz-isolate;
}
#slider-3 {
-moz-margin-start: 3.9rem;
}
#slider-2 {
-moz-margin-start: calc(3.9rem + (100% - 2 * 3.9rem) / 6);
}
#slider-1 {
-moz-margin-start: calc(3.9rem + (100% - 2 * 3.9rem) * 2 / 6);
}
#slider0 {
-moz-margin-start: calc(3.9rem + (100% - 2 * 3.9rem) * 3 / 6);
color: #fff;
}
#slider1 {
-moz-margin-start: calc(3.9rem + (100% - 2 * 3.9rem) * 4 / 6);
}
#slider2 {
-moz-margin-start: calc(3.9rem + (100% - 2 * 3.9rem) * 5 / 6);
}
#slider3 {
-moz-margin-start: calc(3.9rem + (100% - 2 * 3.9rem));
}

View file

@ -0,0 +1,709 @@
/* separator between thumbnail list and preview picture */
body.thumbnailListView #fullscreen-toolbar-header:before {
content: "";
position: absolute;
left: 0;
top: 0;
z-index: -1;
height: 5rem;
width: 0.2rem;
background: url("images/ui/tablet_section_divider.png") no-repeat left;
}
/* prevent user click on the preview in select mode */
body.thumbnailSelectView #fullscreen-view {
pointer-events: none;
}
.tiny-only {
display: none !important;
}
footer {
background: rgba(28, 33, 36, 0.9);
height: 6rem;
}
/* Frames */
#frames {
top: 5rem;
height: calc(100% - 5rem);
background-color: #1c2124;
}
.frame {
top: 0.2rem;
left: 0.2rem;
height: calc(100% - 0.4rem);
width: calc(100% - 0.4rem);
}
body.fullscreenView #frames {
height: 100%;
}
body.fullscreenView #frames .frame {
top: 0;
left: 0;
height: 100%;
width: 100%;
}
body.fullscreenView #fullscreen-view.toolbar-hidden #fullscreen-toolbar-header {
transform: translateY(-5rem);
}
/* player_mask for player footer */
body.thumbnailListView #fullscreen-view.toolbar-hidden:after {
content: '';
background: url("images/player_mask.png") repeat-x center center / 0.1rem;
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 8rem;
pointer-events: none;
}
body.thumbnailListView #frames:before {
content: '';
background: url("images/ui/pane_divider.png") no-repeat center center / 0.2rem;
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 0.2rem;
pointer-events: none;
}
body.fullscreenView .videoPlayerFooter {
background: url("images/player_mask.png") repeat-x center center / 0.1rem;
}
/* Thumbnails */
body.thumbnailListView #thumbnails {
top: 5rem;
}
#thumbnails.offscreen {
top: 5rem;
visibility: visible;
}
/* Header in default thumbnail list appears in tablets only */
body.thumbnailListView #thumbnail-list-top {
display: block !important;
}
#thumbnails.list {
top: 5rem;
bottom: 0;
}
.thumbnail.selected {
outline: 0.2rem solid #0AC;
box-shadow: 0 0 0 4px rgb(0, 202, 242) inset;
}
/* Fullscreen */
body.fullscreenView #fullscreen-toolbar-header > menu {
display: block;
}
#fullscreen-back-button-large {
display: none;
}
/* back button only shows in fullscreen */
body.fullscreenView #fullscreen-back-button-large {
display: block;
}
#fullscreen-toolbar {
display: none;
}
#fullscreen-view.toolbar-hidden #fullscreen-toolbar {
transform: translate(0, 6rem);
}
#fullscreen-camera-button-large > .icon-camera {
background-image: url('images/iconaction_camera_30x30.png');
}
#fullscreen-edit-button-large > .icon-picedit {
background-image: url('images/iconaction_camera_editphoto_30x30.png');
}
#fullscreen-share-button-large > .icon-share {
background-image: url('images/iconaction_share_30x30.png');
}
#fullscreen-info-button-large > .icon-info {
background-image: url('images/iconaction_media_info_30x30.png');
}
#fullscreen-delete-button-large > .icon-delete {
background-image: url('images/iconaction_delete_30x30.png');
}
#fullscreen-view a.disabled {
opacity: 0.4;
pointer-events: none;
}
/* Edit */
#save-progress {
position: absolute;
top: 5rem;
z-index: 10;
}
#edit-preview-area {
background-color: #1c2124;
/* 100% - header 5rem
* - margin between edit options and edit button 0.2rem
* - edit button 6rem
* - edit option 14.4rem
*/
height: calc(100% - 5rem - 0.2rem - 6rem - 14.4rem);
}
/* Edit - toolbar */
#edit-toolbar {
height: 6rem;
}
#edit-toolbar > .button {
font-size: 1.8rem;
line-height: 7rem;
}
#edit-toolbar a.selected.radio.button {
outline: none;
outline-offset: 0;
}
#edit-options {
background: url("images/slider-bg.png") repeat-x scroll 0 0 #1c2124;
bottom: 6.2rem;
height: 14.4rem;
}
#edit-effect-options > a.radio.button {
padding-top: 0;
}
#edit-effect-none:before,
#edit-border-none:before {
content: "";
position: absolute;
left: 0;
display: block;
background: url("images/selection_mask_L.png") repeat-y scroll 0 0 transparent;
height: 100%;
width: 5rem;
}
#edit-border-thick-black:after,
#edit-effect-faded:after {
content: "";
display: block;
position: absolute;
right: 0;
background: url("images/selection_mask_R.png") repeat-y scroll 0 0 transparent;
height: 100%;
width: 5rem;
}
/* Edit - slider */
#exposure-slider {
top: 7rem;
height: 10rem;
font-size: 1.8rem;
}
#sliderthumb {
width: 5.4rem;
top: -5.4rem;
height: 8rem;
left: calc(100% - 3rem);
padding-top: 3.6rem;
font-size: 1.8rem;
text-align: center;
background: url("images/slider-indicator.png") no-repeat scroll center center transparent;
}
#sliderthumb.active {
background: url("images/slider-indicator.png") no-repeat scroll center center transparent;
}
#sliderbar {
position: static;
height: 1.9rem;
background: none;
}
#sliderline {
top: 0;
background: white;
height: 0.4rem;
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.5);
}
#sliderlabels {
top: -1.5rem;
height: auto;
}
.sliderlabel {
height: 1rem;
font-size: 1.8rem;
}
.sliderlabel:after {
top: 1.5rem;
}
/*
* It replaces 'save' text with image and also keep the press effect from
* building block.
*/
#crop-done-button:after,
#edit-save-button:after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url('images/iconaction_done_30x30.png') no-repeat center center / 3rem;
}
#crop-done-button,
#edit-save-button {
font-size: 0;
}
/* Edit - effect buttons */
#edit-exposure-button {
background: url("images/actionicon_gallery_edit_exposure_30x30.png") no-repeat center/3rem, url("images/tab.png") repeat-x center;
}
#edit-exposure-button.selected {
background: url("images/actionicon_gallery_edit_exposure_30x30.png") no-repeat center/3rem, url("images/tab_selected.png") repeat-x center;
}
#edit-crop-button {
background: url("images/actionicon_gallery_edit_crop_30x30.png") no-repeat center/3rem, url("images/tab.png") repeat-x center;
}
#edit-crop-button.selected {
background: url("images/actionicon_gallery_edit_crop_30x30.png") no-repeat center/3rem, url("images/tab_selected.png") repeat-x center;
}
#edit-effect-button {
background: url("images/actionicon_gallery_edit_effects_30x30.png") no-repeat center/3rem, url("images/tab.png") repeat-x center;
}
#edit-effect-button.selected {
background: url("images/actionicon_gallery_edit_effects_30x30.png") no-repeat center/3rem, url("images/tab_selected.png") repeat-x center;
}
#edit-border-button {
background: url("images/actionicon_gallery_edit_borders_30x30.png") no-repeat center/3rem, url("images/tab.png") repeat-x center;
}
#edit-border-button.selected {
background: url("images/actionicon_gallery_edit_borders_30x30.png") no-repeat center/3rem, url("images/tab_selected.png") repeat-x center;
}
#edit-enhance-button {
background: url("images/actionicon_gallery_edit_enhance_selected_30x30.png") no-repeat center/3rem, url("images/tab.png") repeat-x center;
}
#edit-enhance-button.on {
background: url("images/actionicon_gallery_edit_enhance_on_selected_30x30.png") no-repeat center/3rem, url("images/tab.png") repeat-x center;
}
#edit-enhance-button.selected {
background: url("images/actionicon_gallery_edit_enhance_selected_30x30.png") no-repeat center/3rem, url("images/tab_selected.png") repeat-x center;
}
#edit-enhance-button.selected.on {
background: url("images/actionicon_gallery_edit_enhance_on_selected_30x30.png") no-repeat center/3rem, url("images/tab_selected.png") repeat-x center;
}
/*
* Dialog. Since delete dialog in select mode and loading dialog
* are managed by gallery app itself, we need aditional style here.
* We'll modify it until 934326 is landed.
*/
#overlay,
#confirm-dialog {
background: rgb(51, 51, 51);
}
#overlay section,
#confirm-dialog section {
position: absolute;
padding: 1rem 6rem 0;
left: calc((100% - 68rem) / 2);
top: calc((100% - 53rem) / 2);
width: 68rem;
height: 46rem;
box-shadow: 0 0 1rem #000;
border: 0.1rem #000;
font-size: 2rem;
background-color: #333333;
}
#overlay > section > p {
padding: 1rem 1rem 0;
margin: 0;
}
#confirm-dialog > section > p {
margin-top: 20rem;
}
#overlay > section > h1 {
line-height: 6rem;
margin: 1rem 0 0;
}
#overlay > menu,
#confirm-dialog > menu {
position: absolute;
left: calc((100% - 68rem) / 2);
top: calc((100% - 53rem) / 2 + 46rem);
width: 68rem;
background: #2d2d2d;
padding: 1.5rem 14rem;
height: 7rem;
bottom: 0;
box-shadow: 0 0 1rem #000;
}
#overlay:after,
#confirm-dialog:after {
content: '';
position: absolute;
width: 68rem;
top: calc((100% - 53rem) / 2 + 45rem);
left: calc((100% - 68rem) / 2);
height: 1rem;
background-color: #333;
}
/* video player */
body.fullscreenView .videoPlayerFullscreenButton,
body.thumbnailSelectView .videoPlayerFullscreenButton {
display: none;
}
/* We don't want the controller of videoplayer hide in preview mode */
body.thumbnailListView #fullscreen-view.toolbar-hidden .videoPlayerFooter.hidden {
display: block;
opacity: 1;
}
/* XXX: Below css will move to building block til bug905065 is closed */
/*
* 1. Introduced by bug 1059748
* In bug 1019841 we introduced !important for .large-only styles to hide
* tablet layout on phone devices, which will cause regression on tablet.
* We have to use !important here to override styles from phone layout.
*/
#fullscreen-toolbar-header {
display: block !important; /* 1 */
}
@media (min-width: 768px) and (min-height: 768px) {
section[role="region"].skin-organic > header:first-child h1,
.skin-organic section[role="region"] > header:first-child h1,
section[role="region"].skin-dark > header:first-child,
.skin-dark section[role="region"] > header:first-child {
color: white;
}
section[role="region"] > header:first-child h1 {
font-size: 2.4rem;
line-height: 5rem;
color: #fff;
padding: 0 1rem 0 2rem;
}
section[role="region"] > header:first-child menu[type="toolbar"] a,
section[role="region"] > header:first-child menu[type="toolbar"] button {
width: 8.2rem;
}
section[role="region"].skin-dark > header:first-child menu[type="toolbar"] a:after,
section[role="region"].skin-dark > header:first-child menu[type="toolbar"] button:after,
section[role="region"].skin-organic > header:first-child menu[type="toolbar"] a:after,
section[role="region"].skin-organic > header:first-child menu[type="toolbar"] button:after {
content: "";
position: absolute;
left: 0;
top: 0;
z-index: -1;
height: 5rem;
width: 8.2rem;
background: url("images/ui/tablet_button_separator.png") no-repeat left / 0.2rem transparent;
}
section[role="region"].skin-dark > header:first-child > a:not([aria-disabled="true"]):active:after,
section[role="region"].skin-dark > header:first-child > button:not(:disabled):active:after,
section[role="region"].skin-dark > header:first-child > a:not([aria-disabled="true"]):hover:after,
section[role="region"].skin-dark > header:first-child > button:not(:disabled):hover:after,
section[role="region"].skin-dark > header:first-child menu[type="toolbar"] a:not([aria-disabled="true"]):hover,
section[role="region"].skin-dark > header:first-child menu[type="toolbar"] button:not(:disabled):hover,
section[role="region"].skin-dark > header:first-child menu[type="toolbar"] a:not([aria-disabled="true"]):active,
section[role="region"].skin-dark > header:first-child menu[type="toolbar"] button:not(:disabled):active,
section[role="region"].skin-organic > header:first-child > a:not([aria-disabled="true"]):active:after,
section[role="region"].skin-organic > header:first-child > button:not(:disabled):active:after,
section[role="region"].skin-organic > header:first-child > a:not([aria-disabled="true"]):hover:after,
section[role="region"].skin-organic > header:first-child > button:not(:disabled):hover:after,
section[role="region"].skin-organic > header:first-child menu[type="toolbar"] a:not([aria-disabled="true"]):hover,
section[role="region"].skin-organic > header:first-child menu[type="toolbar"] button:not(:disabled):hover,
section[role="region"].skin-organic > header:first-child menu[type="toolbar"] a:not([aria-disabled="true"]):active,
section[role="region"].skin-organic > header:first-child menu[type="toolbar"] button:not(:disabled):active {
background: url("images/ui/tablet_header_pressed.png") repeat-x center / 0.2rem transparent !important;
}
section[role="region"] > header:first-child > button,
section[role="region"] > header:first-child > a {
position: relative;
width: 5rem;
height: 5rem;
background: none;
float: left;
overflow: visible;
margin: 0;
}
section[role="region"] > header:first-child > button:after,
section[role="region"] > header:first-child > a:after {
width: 5.2rem;
height: 5rem;
}
section[role="region"] > header:first-child .icon {
width: 5rem;
}
section[role="region"] > header:first-child .icon.icon-back,
section[role="region"].skin-organic > header:first-child .icon.icon-back,
.skin-organic section[role="region"] > header:first-child .icon.icon-back,
section[role="region"].skin-dark > header:first-child .icon.icon-back,
.skin-dark section[role="region"] > header:first-child .icon.icon-back {
background-image: url("images/back.png");
}
section[role="region"] > header:first-child .icon.icon-close,
section[role="region"].skin-organic > header:first-child .icon.icon-close,
.skin-organic section[role="region"] > header:first-child .icon.icon-close,
section[role="region"].skin-dark > header:first-child .icon.icon-close,
.skin-dark section[role="region"] > header:first-child .icon.icon-close {
background-image: url("images/actionicon_cancel.png");
}
section[role="region"].skin-dark > header:first-child,
.skin-dark section[role="region"] > header:first-child,
section[role="region"].skin-organic > header:first-child,
.skin-organic section[role="region"] > header:first-child {
background: url("images/ui/tablet_header.png") repeat-x scroll 0 0 / auto 100% transparent;
}
section[role="region"].skin-dark > header:first-child a:after,
.skin-dark section[role="region"] > header:first-child a:after,
section[role="region"].skin-dark > header:first-child button:after,
.skin-dark section[role="region"] > header:first-child button:after,
section[role="region"].skin-organic > header:first-child a:after,
.skin-organic section[role="region"] > header:first-child a:after,
section[role="region"].skin-organic > header:first-child button:after,
.skin-organic section[role="region"] > header:first-child button:after {
background: url("images/ui/tablet_button_separator.png") no-repeat right / 0.2rem transparent;
}
}
section[role="region"] > header:first-child menu[type="toolbar"] a:after,
section[role="region"] > header:first-child menu[type="toolbar"] button:after {
content: "";
position: absolute;
left: 0;
top: 0;
z-index: -1;
height: 5rem;
width: 8.2rem;
background: url("images/ui/tablet_button_separator.png") no-repeat left / 0.2rem transparent;
}
@media (orientation: landscape) {
/* we have both thumbnail list and preview together when in landscape */
body.thumbnailListView #fullscreen-view,
body.thumbnailSelectView #fullscreen-view {
display: block;
width: calc(100% - 40rem);
left: 40rem;
}
body.thumbnailListView #thumbnail-views,
body.thumbnailSelectView #thumbnail-views {
visibility: visible;
width: 40rem;
}
/* outline the picture we're focusing on in preview mode */
body.thumbnailListView .thumbnail.focus {
outline: solid #00aacc 0.3rem;
outline-offset: -0.2rem;
}
/* rewrite thumbnail's original size */
.thumbnail {
width: calc((100% - 0.4rem) / 3);
padding: 0 0 calc((100% - 0.4rem) / 3);
margin-right: 0.2rem;
margin-bottom: 0.2rem;
}
.thumbnail:nth-child(4n) {
margin-right: 0.2rem;
}
.thumbnail:nth-child(3n) {
margin-right: 0;
}
/* we don't have preview screen when in picking */
body.pickView #thumbnails {
width: 100%;
}
body.pickView #thumbnails .thumbnail {
width: calc((100% - 1rem) / 6);
padding: 0 0 calc((100% - 1em) / 6);
margin-bottom: 0.2rem;
}
body.pickView #thumbnails .thumbnail:nth-child(3n) {
margin-right: 0.2rem;
}
body.pickView #thumbnails .thumbnail:nth-child(4n) {
margin-right: 0.2rem;
}
body.pickView #thumbnails .thumbnail:nth-child(6n) {
margin-right: 0;
}
/*
* slider
* We have 7 steps located from the center of sliderline
*/
#sliderline {
width: 90rem;
left: calc(50% - 45rem);
}
#slider-3 {
left: calc(50% - 45rem);
}
#slider-2 {
left: calc(50% - 45rem + (100% - 2 * (50% - 45rem)) / 6);
}
#slider-1 {
left: calc(50% - 45rem + (100% - 2 * (50% - 45rem)) * 2 / 6);
}
#slider0 {
left: calc(50% - 45rem + (100% - 2 * (50% - 45rem)) * 3 / 6);
color: #fff;
}
#slider1 {
left: calc(50% - 45rem + (100% - 2 * (50% - 45rem)) * 4 / 6);
}
#slider2 {
left: calc(50% - 45rem + (100% - 2 * (50% - 45rem)) * 5 / 6);
}
#slider3 {
left: calc(50% - 45rem + (100% - 2 * (50% - 45rem)));
}
/* video player */
body:not(.fullscreenView) .videoPlayerPauseButton {
width: 9rem;
}
body:not(.fullscreenView) .videoPlayerSlider {
left: 10rem;
}
body:not(.fullscreenView) .videoPlayerProgress {
left: 7rem;
right: 15rem;
}
body:not(.fullscreenView) .videoPlayerSlider .videoPlayerDurationText {
text-align: center;
right: 8rem;
width: 5rem;
}
}
@media (orientation: portrait) {
.thumbnail:nth-child(3n) {
margin-right: 0.2rem;
}
.thumbnail {
width: calc((100% - 0.6rem) / 4);
padding: 0 0 calc((100% - 0.6rem) / 4);
margin-right: 0.2rem;
margin-bottom: 0.2rem;
}
.thumbnail:nth-child(4n) {
margin-right: 0;
}
/* slider */
#sliderline {
width: 60rem;
left: calc(50% - 30rem);
}
#slider-3 {
left: calc(50% - 30rem);
}
#slider-2 {
left: calc(50% - 30rem + (100% - 2 * (50% - 30rem)) / 6);
}
#slider-1 {
left: calc(50% - 30rem + (100% - 2 * (50% - 30rem)) * 2 / 6);
}
#slider0 {
left: calc(50% - 30rem + (100% - 2 * (50% - 30rem)) * 3 / 6);
color: #fff;
}
#slider1 {
left: calc(50% - 30rem + (100% - 2 * (50% - 30rem)) * 4 / 6);
}
#slider2 {
left: calc(50% - 30rem + (100% - 2 * (50% - 30rem)) * 5 / 6);
}
#slider3 {
left: calc(50% - 30rem + (100% - 2 * (50% - 30rem)));
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

Some files were not shown because too many files have changed in this diff Show more