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.
131
gaia/apps/gallery/build/build.js
Executable 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
|
|
@ -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>
|
||||
1977
gaia/apps/gallery/js/ImageEditor.js
Executable file
708
gaia/apps/gallery/js/MetadataParser.js
Executable 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
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
485
gaia/apps/gallery/js/image_processor.js
Executable 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));
|
||||
77
gaia/apps/gallery/js/image_processor_thread.js
Executable 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));
|
||||
33
gaia/apps/gallery/js/image_processor_worker.js
Executable 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
152
gaia/apps/gallery/js/thumbnail_date_group.js
Executable 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(); });
|
||||
};
|
||||
66
gaia/apps/gallery/js/thumbnail_item.js
Executable 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)
|
||||
});
|
||||
};
|
||||
114
gaia/apps/gallery/js/thumbnail_list.js
Executable 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(); });
|
||||
};
|
||||
150
gaia/apps/gallery/js/thumbnails.js
Executable 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
|
|
@ -0,0 +1,4 @@
|
|||
/* exported $ */
|
||||
'use strict';
|
||||
|
||||
function $(id) { return document.getElementById(id); }
|
||||
18
gaia/apps/gallery/jsdoc.json
Executable 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"
|
||||
}
|
||||
}
|
||||
134
gaia/apps/gallery/locales/gallery.en-US.properties
Executable 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}}
|
||||
84
gaia/apps/gallery/manifest.webapp
Executable 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
|
|
@ -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
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"webide": {
|
||||
"prepackage": {
|
||||
"command": "make",
|
||||
"env": ["APP=gallery"],
|
||||
"cwd": "../.."
|
||||
},
|
||||
"packageDir": "../../build_stage/gallery/"
|
||||
}
|
||||
}
|
||||
194
gaia/apps/gallery/style/VideoPlayer.css
Executable 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;
|
||||
}
|
||||
97
gaia/apps/gallery/style/VideoPlayer_tablet.css
Executable 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;
|
||||
}
|
||||
}
|
||||
922
gaia/apps/gallery/style/gallery.css
Executable 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));
|
||||
}
|
||||
709
gaia/apps/gallery/style/gallery_tablet.css
Executable 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)));
|
||||
}
|
||||
}
|
||||
BIN
gaia/apps/gallery/style/icons/gallery_126.png
Executable file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
gaia/apps/gallery/style/icons/gallery_142.png
Executable file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
gaia/apps/gallery/style/icons/gallery_189.png
Executable file
|
After Width: | Height: | Size: 5 KiB |
BIN
gaia/apps/gallery/style/icons/gallery_284.png
Executable file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
gaia/apps/gallery/style/icons/gallery_84.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
gaia/apps/gallery/style/images/actionicon_gallery_edit_effects_30x30.png
Executable file
|
After Width: | Height: | Size: 434 B |
BIN
gaia/apps/gallery/style/images/actionicon_gallery_edit_effects_30x30@1.5x.png
Executable file
|
After Width: | Height: | Size: 561 B |
BIN
gaia/apps/gallery/style/images/actionicon_gallery_edit_effects_30x30@2.25x.png
Executable file
|
After Width: | Height: | Size: 847 B |
BIN
gaia/apps/gallery/style/images/actionicon_gallery_edit_effects_30x30@2x.png
Executable file
|
After Width: | Height: | Size: 736 B |
|
After Width: | Height: | Size: 387 B |
|
After Width: | Height: | Size: 601 B |
|
After Width: | Height: | Size: 880 B |
|
After Width: | Height: | Size: 750 B |
BIN
gaia/apps/gallery/style/images/actionicon_gallery_edit_enhance_30x30.png
Executable file
|
After Width: | Height: | Size: 282 B |
BIN
gaia/apps/gallery/style/images/actionicon_gallery_edit_enhance_30x30@1.5x.png
Executable file
|
After Width: | Height: | Size: 381 B |
BIN
gaia/apps/gallery/style/images/actionicon_gallery_edit_enhance_30x30@2.25x.png
Executable file
|
After Width: | Height: | Size: 709 B |
BIN
gaia/apps/gallery/style/images/actionicon_gallery_edit_enhance_30x30@2x.png
Executable file
|
After Width: | Height: | Size: 599 B |
BIN
gaia/apps/gallery/style/images/actionicon_gallery_edit_enhance_on_30x30.png
Executable file
|
After Width: | Height: | Size: 364 B |
BIN
gaia/apps/gallery/style/images/actionicon_gallery_edit_enhance_on_30x30@1.5x.png
Executable file
|
After Width: | Height: | Size: 536 B |
|
After Width: | Height: | Size: 764 B |
BIN
gaia/apps/gallery/style/images/actionicon_gallery_edit_enhance_on_30x30@2x.png
Executable file
|
After Width: | Height: | Size: 670 B |
|
After Width: | Height: | Size: 382 B |
|
After Width: | Height: | Size: 542 B |
|
After Width: | Height: | Size: 791 B |
|
After Width: | Height: | Size: 694 B |
|
After Width: | Height: | Size: 395 B |
|
After Width: | Height: | Size: 392 B |
|
After Width: | Height: | Size: 565 B |
|
After Width: | Height: | Size: 496 B |
BIN
gaia/apps/gallery/style/images/fullscreen.png
Executable file
|
After Width: | Height: | Size: 584 B |
BIN
gaia/apps/gallery/style/images/fullscreen_pressed.png
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
gaia/apps/gallery/style/images/gradient@2.25x.png
Executable file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
gaia/apps/gallery/style/images/gradient@2x.png
Executable file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
gaia/apps/gallery/style/images/iconaction_camera_30x30.png
Executable file
|
After Width: | Height: | Size: 336 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_30x30@1.5x.png
Executable file
|
After Width: | Height: | Size: 587 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_30x30@2.25x.png
Executable file
|
After Width: | Height: | Size: 836 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_30x30@2x.png
Executable file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
gaia/apps/gallery/style/images/iconaction_camera_crop1x1_30x30.png
Executable file
|
After Width: | Height: | Size: 229 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_crop1x1_30x30@1.5x.png
Executable file
|
After Width: | Height: | Size: 462 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_crop1x1_30x30@2.25x.png
Executable file
|
After Width: | Height: | Size: 513 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_crop1x1_30x30@2x.png
Executable file
|
After Width: | Height: | Size: 580 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_crop2x3_30x30.png
Executable file
|
After Width: | Height: | Size: 360 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_crop2x3_30x30@1.5x.png
Executable file
|
After Width: | Height: | Size: 614 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_crop2x3_30x30@2.25x.png
Executable file
|
After Width: | Height: | Size: 936 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_crop2x3_30x30@2x.png
Executable file
|
After Width: | Height: | Size: 865 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_crop3x2_30x30.png
Executable file
|
After Width: | Height: | Size: 332 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_crop3x2_30x30@1.5x.png
Executable file
|
After Width: | Height: | Size: 636 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_crop3x2_30x30@2.25x.png
Executable file
|
After Width: | Height: | Size: 931 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_crop3x2_30x30@2x.png
Executable file
|
After Width: | Height: | Size: 878 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_editphoto_30x30.png
Executable file
|
After Width: | Height: | Size: 323 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_editphoto_30x30@1.5x.png
Executable file
|
After Width: | Height: | Size: 479 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_editphoto_30x30@2.25x.png
Executable file
|
After Width: | Height: | Size: 783 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_editphoto_30x30@2x.png
Executable file
|
After Width: | Height: | Size: 701 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_freecrop_30x30.png
Executable file
|
After Width: | Height: | Size: 233 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_freecrop_30x30@1.5x.png
Executable file
|
After Width: | Height: | Size: 456 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_freecrop_30x30@2.25x.png
Executable file
|
After Width: | Height: | Size: 543 B |
BIN
gaia/apps/gallery/style/images/iconaction_camera_freecrop_30x30@2x.png
Executable file
|
After Width: | Height: | Size: 498 B |
BIN
gaia/apps/gallery/style/images/iconaction_delete_30x30.png
Executable file
|
After Width: | Height: | Size: 286 B |
BIN
gaia/apps/gallery/style/images/iconaction_delete_30x30@1.5x.png
Executable file
|
After Width: | Height: | Size: 359 B |
BIN
gaia/apps/gallery/style/images/iconaction_delete_30x30@2.25x.png
Executable file
|
After Width: | Height: | Size: 472 B |
BIN
gaia/apps/gallery/style/images/iconaction_delete_30x30@2x.png
Executable file
|
After Width: | Height: | Size: 865 B |
BIN
gaia/apps/gallery/style/images/iconaction_done_30x30.png
Executable file
|
After Width: | Height: | Size: 319 B |
BIN
gaia/apps/gallery/style/images/iconaction_done_30x30@1.5x.png
Executable file
|
After Width: | Height: | Size: 715 B |
BIN
gaia/apps/gallery/style/images/iconaction_done_30x30@2.25x.png
Executable file
|
After Width: | Height: | Size: 552 B |
BIN
gaia/apps/gallery/style/images/iconaction_done_30x30@2x.png
Executable file
|
After Width: | Height: | Size: 547 B |
BIN
gaia/apps/gallery/style/images/iconaction_media_info_30x30.png
Executable file
|
After Width: | Height: | Size: 408 B |
BIN
gaia/apps/gallery/style/images/iconaction_media_info_30x30@1.5x.png
Executable file
|
After Width: | Height: | Size: 576 B |
BIN
gaia/apps/gallery/style/images/iconaction_media_info_30x30@2.25x.png
Executable file
|
After Width: | Height: | Size: 866 B |
BIN
gaia/apps/gallery/style/images/iconaction_media_info_30x30@2x.png
Executable file
|
After Width: | Height: | Size: 821 B |