Publish Vulpes Preview 15 sources for desktop, Android and Tundra

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

361
gaia/apps/gallery/js/pick.js Executable file
View file

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