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:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
148
gaia/apps/camera/js/views/confirm.js
Executable file
148
gaia/apps/camera/js/views/confirm.js
Executable file
|
|
@ -0,0 +1,148 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:confirm');
|
||||
var addPanAndZoomHandlers = require('lib/panzoom');
|
||||
var orientation = require('lib/orientation');
|
||||
var MediaFrame = require('MediaFrame');
|
||||
var bind = require('lib/bind');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'confirm',
|
||||
|
||||
initialize: function() {
|
||||
this.on('destroy', this.onDestroy);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
|
||||
// Get elements
|
||||
this.els.mediaFrame = this.find('.js-media-frame');
|
||||
this.els.retake = this.find('.js-retake');
|
||||
this.els.select = this.find('.js-select');
|
||||
|
||||
// Disable buttons on this view by default
|
||||
// until an image/video is displayed
|
||||
this.disableButtons();
|
||||
|
||||
// Initialize the MediaFrame component
|
||||
this.setupMediaFrame();
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.els.retake, 'click', this.onButtonClick);
|
||||
bind(this.els.select, 'click', this.onButtonClick);
|
||||
return this;
|
||||
},
|
||||
|
||||
setupMediaFrame: function() {
|
||||
this.mediaFrame = new MediaFrame(this.els.mediaFrame, true,
|
||||
this.maxPreviewSize);
|
||||
this.mediaFrame.video.onloading = this.onVideoLoading;
|
||||
this.mediaFrame.video.onplaying = this.onVideoPlaying;
|
||||
addPanAndZoomHandlers(this.mediaFrame);
|
||||
window.addEventListener('resize', this.onResize);
|
||||
return this;
|
||||
},
|
||||
|
||||
onVideoLoading: function() {
|
||||
this.emit('loadingvideo', 'loadingVideo');
|
||||
},
|
||||
|
||||
onVideoPlaying: function() {
|
||||
this.emit('playingvideo');
|
||||
},
|
||||
|
||||
clearMediaFrame: function() {
|
||||
this.mediaFrame.clear();
|
||||
this.disableButtons();
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this.el.classList.add('hidden');
|
||||
orientation.lock();
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this.el.classList.remove('hidden');
|
||||
orientation.unlock();
|
||||
},
|
||||
|
||||
disableButtons: function() {
|
||||
this.els.retake.setAttribute('disabled', true);
|
||||
this.els.select.setAttribute('disabled', true);
|
||||
},
|
||||
|
||||
enableButtons: function() {
|
||||
this.els.retake.removeAttribute('disabled');
|
||||
this.els.select.removeAttribute('disabled');
|
||||
},
|
||||
|
||||
showImage: function(image) {
|
||||
this.enableButtons();
|
||||
this.mediaFrame.displayImage(
|
||||
image.blob,
|
||||
image.width,
|
||||
image.height,
|
||||
image.preview,
|
||||
image.rotation,
|
||||
image.mirrored);
|
||||
return this;
|
||||
},
|
||||
|
||||
showVideo: function(video) {
|
||||
this.enableButtons();
|
||||
this.mediaFrame.displayVideo(
|
||||
video.blob,
|
||||
video.poster.blob,
|
||||
video.poster.width / video.poster.height,
|
||||
video.rotation);
|
||||
return this;
|
||||
},
|
||||
|
||||
template: function() {
|
||||
/*jshint maxlen:false*/
|
||||
return '<div class="confirm-media-frame js-media-frame"></div>' +
|
||||
'<footer id="confirm-controls" class="confirm-controls">' +
|
||||
'<button class="retake-button js-retake" data-l10n-id="retake-button" ' +
|
||||
'name="retake"></button>' +
|
||||
'<button class="select-button test-confirm-select js-select" ' +
|
||||
'data-l10n-id="select-button" name="select"></button></footer>';
|
||||
},
|
||||
|
||||
onButtonClick: function(event) {
|
||||
var el = event.currentTarget;
|
||||
var name = el.getAttribute('name');
|
||||
this.emit('click:' + name);
|
||||
},
|
||||
|
||||
onResize: function() {
|
||||
this.mediaFrame.resize();
|
||||
if (this.mediaFrame.displayingVideo) {
|
||||
this.mediaFrame.video.setPlayerSize();
|
||||
}
|
||||
},
|
||||
|
||||
onDestroy: function() {
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
this.mediaFrame.clear();
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
344
gaia/apps/camera/js/views/controls.js
vendored
Executable file
344
gaia/apps/camera/js/views/controls.js
vendored
Executable file
|
|
@ -0,0 +1,344 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:controls');
|
||||
var debounce = require('lib/debounce');
|
||||
var bind = require('lib/bind');
|
||||
var View = require('view');
|
||||
var Drag = require('drag');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'controls',
|
||||
className: 'test-controls',
|
||||
|
||||
initialize: function(options) {
|
||||
this.drag = options && options.drag; // test hook
|
||||
this.once('inserted', this.setupSwitch);
|
||||
this.render();
|
||||
},
|
||||
|
||||
switchPositions: {
|
||||
left: 'picture',
|
||||
right: 'video',
|
||||
picture: 'left',
|
||||
video: 'right'
|
||||
},
|
||||
|
||||
// {node}: {data-l10n-id} pairs used for localization.
|
||||
elsL10n: {
|
||||
cancel: 'controls-button-close',
|
||||
thumbnail: 'preview-button',
|
||||
capture: 'capture-button'
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
|
||||
// Get nodes
|
||||
this.els.switchHandle = this.find('.js-switch-handle');
|
||||
this.els.thumbnail = this.find('.js-thumbnail');
|
||||
this.els.capture = this.find('.js-capture');
|
||||
this.els.cancel = this.find('.js-cancel');
|
||||
this.els.switch = this.find('.js-switch');
|
||||
this.els.icons = {
|
||||
camera: this.find('.js-icon-camera'),
|
||||
video: this.find('.js-icon-video')
|
||||
};
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
/**
|
||||
* Respond to click events on the buttons
|
||||
* other than the switch, which is a special
|
||||
* case.
|
||||
*
|
||||
* We 'debouce' the callback to defend
|
||||
* against button-bashing.
|
||||
*
|
||||
* @return {ControlsView} for chaining
|
||||
* @private
|
||||
*/
|
||||
bindEvents: function() {
|
||||
this.onButtonClick = debounce(this.onButtonClick, 300, true);
|
||||
bind(this.els.thumbnail, 'click', this.onButtonClick);
|
||||
bind(this.els.capture, 'click', this.onButtonClick);
|
||||
bind(this.els.cancel, 'click', this.onButtonClick);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Create the draggable switch.
|
||||
*
|
||||
* We debouce the tapped callback to
|
||||
* defend against button-bashing.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
setupSwitch: function() {
|
||||
debug('setup dragger');
|
||||
|
||||
// Wait until the document is complete
|
||||
// to avoid any forced sync reflows.
|
||||
if (document.readyState !== 'complete') {
|
||||
window.addEventListener('load', this.setupSwitch);
|
||||
debug('deferred switch setup till after load');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer existing drag (test hook)
|
||||
this.drag = this.drag || new Drag({
|
||||
handle: this.els.switchHandle,
|
||||
container: this.els.switch
|
||||
});
|
||||
|
||||
this.drag.on('tapped', debounce(this.onSwitchTapped, 300, true));
|
||||
this.drag.on('ended', this.drag.snapToClosestEdge);
|
||||
this.drag.on('translate', this.onSwitchTranslate);
|
||||
this.drag.on('snapped', this.onSwitchSnapped);
|
||||
|
||||
this.drag.updateDimensions();
|
||||
this.updateSwitchPosition();
|
||||
|
||||
// Tidy up
|
||||
window.removeEventListener('load', this.setupSwitch);
|
||||
},
|
||||
|
||||
setCaptureLabel: function(recording) {
|
||||
this.els.capture.setAttribute('data-l10n-id',
|
||||
recording ? 'stop-capture-button' : 'capture-button');
|
||||
},
|
||||
|
||||
onSwitchSnapped: function(edges) {
|
||||
var mode = this.switchPositions[edges.x];
|
||||
var changed = mode !== this.get('mode');
|
||||
if (changed) { this.onSwitchChanged(); }
|
||||
},
|
||||
|
||||
onSwitchChanged: function() {
|
||||
this.emit('modechanged');
|
||||
},
|
||||
|
||||
onSwitchTapped: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
debug('switch tapped');
|
||||
this.onSwitchChanged();
|
||||
},
|
||||
|
||||
onSwitchTranslate: function(e) {
|
||||
this.setSwitchIcon(e.position.ratio.x);
|
||||
},
|
||||
|
||||
setSwitchIcon: function(ratio) {
|
||||
var skew = 2;
|
||||
var ratioSkewed = ratio * skew;
|
||||
var camera = Math.max(0, 1 - ratioSkewed);
|
||||
var video = Math.max(0, -1 + ratioSkewed);
|
||||
this.els.icons.camera.style.opacity = camera;
|
||||
this.els.icons.video.style.opacity = video;
|
||||
debug('set switch icon camera: %s, video: %s', camera, video);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set view screen reader visibility. In some cases, though the view is behind
|
||||
* an overlay and not hidden off screen, it still needs to be
|
||||
* hidden/inaccessible from the screen reader.
|
||||
*/
|
||||
setScreenReaderVisible: function(visible) {
|
||||
this.el.setAttribute('aria-hidden', !visible);
|
||||
},
|
||||
|
||||
onButtonClick: function(e) {
|
||||
e.stopPropagation();
|
||||
debug('button click');
|
||||
var name = e.currentTarget.getAttribute('name');
|
||||
this.emit('click:' + name, e);
|
||||
},
|
||||
|
||||
suspendModeSwitch: function(suspended) {
|
||||
if (suspended) {
|
||||
this.set('switch-toggle-disabled');
|
||||
} else {
|
||||
this.unset('switch-toggle-disabled');
|
||||
}
|
||||
},
|
||||
|
||||
setMode: function(mode) {
|
||||
debug('set mode: %s', mode);
|
||||
this.set('mode', mode);
|
||||
this.switchPosition = this.switchPositions[mode];
|
||||
var ratio = { left: 0, right: 1 }[this.switchPosition];
|
||||
this.updateSwitchPosition();
|
||||
this.setSwitchIcon(ratio);
|
||||
// Set appropriate mode switch label for screen reader.
|
||||
this.els.switch.setAttribute('data-l10n-id', mode + '-mode-button');
|
||||
debug('mode set pos: %s', this.switchPosition);
|
||||
},
|
||||
|
||||
updateSwitchPosition: function() {
|
||||
debug('updateSwitchPosition');
|
||||
if (!this.drag) { return; }
|
||||
this.drag.set({ x: this.switchPosition });
|
||||
debug('updated switch position: %s', this.switchPosition);
|
||||
},
|
||||
|
||||
setThumbnail: function(blob) {
|
||||
if (!this.els.image) {
|
||||
this.els.image = new Image();
|
||||
this.els.image.classList.add('test-thumbnail');
|
||||
this.els.thumbnail.appendChild(this.els.image);
|
||||
this.set('thumbnail', true);
|
||||
} else {
|
||||
window.URL.revokeObjectURL(this.els.image.src);
|
||||
}
|
||||
|
||||
this.els.image.src = window.URL.createObjectURL(blob);
|
||||
debug('thumbnail set');
|
||||
},
|
||||
|
||||
removeThumbnail: function() {
|
||||
if (this.els.image) {
|
||||
this.els.thumbnail.removeChild(this.els.image);
|
||||
window.URL.revokeObjectURL(this.els.image.src);
|
||||
this.els.image = null;
|
||||
}
|
||||
|
||||
this.set('thumbnail', false);
|
||||
},
|
||||
|
||||
/**
|
||||
* NOTE: The below functions are a first
|
||||
* attempt at replacing the default View
|
||||
* `.set()`, `.enable()` and `.disable()` APIs
|
||||
* to avoid having to use attributes to style
|
||||
* state in our CSS.
|
||||
*/
|
||||
|
||||
set: function(key, value) {
|
||||
if (typeof key !== 'string') { return; }
|
||||
if (arguments.length === 1) { value = true; }
|
||||
if (!value) { return this.unset(key); }
|
||||
|
||||
var attr = 'data-' + key;
|
||||
var oldValue = this.el.getAttribute(attr);
|
||||
var oldClass = oldValue && classFrom(key, oldValue);
|
||||
var newClass = classFrom(key, value);
|
||||
|
||||
if (oldClass) { this.el.classList.remove(oldClass); }
|
||||
if (newClass) { this.el.classList.add(newClass); }
|
||||
|
||||
this.el.setAttribute(attr, value);
|
||||
debug('remove: %s, add: %s', oldClass, newClass);
|
||||
debug('attr key: %s, value: %s', attr, value);
|
||||
},
|
||||
|
||||
get: function(key) {
|
||||
var attr = 'data-' + key;
|
||||
return this.el.getAttribute(attr);
|
||||
},
|
||||
|
||||
unset: function(key) {
|
||||
var attr = 'data-' + key;
|
||||
var value = this.el.getAttribute(attr);
|
||||
this.el.classList.remove(classFrom(key, value));
|
||||
this.el.removeAttribute(attr);
|
||||
},
|
||||
|
||||
enable: function(key) {
|
||||
this.set(key ? key + '-enabled' : 'enabled');
|
||||
this.unset(key ? key + '-disabled' : 'disabled');
|
||||
},
|
||||
|
||||
disable: function(key) {
|
||||
this.set(key ? key + '-disabled' : 'disabled');
|
||||
this.unset(key ? key + '-enabled' : 'enabled');
|
||||
},
|
||||
|
||||
/**
|
||||
* Localize the template based on a list of localizable elements - elsL10n. In
|
||||
* case the template is loaded before l10n is ready, localize will peform the
|
||||
* initial localization.
|
||||
*/
|
||||
localize: function() {
|
||||
// Switch mode label depends on the mode that is currently set.
|
||||
var mode = this.get('mode') || 'picture';
|
||||
this.els.switch.setAttribute('data-l10n-id', mode + '-mode-button');
|
||||
},
|
||||
|
||||
template: function() {
|
||||
/*jshint maxlen:false*/
|
||||
return '<div class="controls-left">' +
|
||||
'<div class="controls-button controls-thumbnail-button test-thumbnail js-thumbnail rotates" ' +
|
||||
'name="thumbnail" role="button" data-l10n-id="preview-button"></div>' +
|
||||
'<div class="controls-button controls-cancel-pick-button test-cancel-pick rotates js-cancel" ' +
|
||||
'name="cancel" data-icon="close" role="button" data-l10n-id="controls-button-close"></div>' +
|
||||
'</div>' +
|
||||
'<div class="controls-middle">' +
|
||||
'<div class="capture-button test-capture rotates js-capture" name="capture" ' +
|
||||
'data-l10n-id="capture-button" role="button">' +
|
||||
'<div class="circle outer-circle"></div>' +
|
||||
'<div class="circle inner-circle"></div>' +
|
||||
'<div class="center" data-icon="camera" aria-hidden="true"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="controls-right">' +
|
||||
'<div class="mode-switch test-switch" name="switch">' +
|
||||
'<div class="inner js-switch" role="button">' +
|
||||
'<div class="mode-switch_bg-icon rotates" data-icon="camera" aria-hidden="true"></div>' +
|
||||
'<div class="mode-switch_bg-icon rotates" data-icon="video" aria-hidden="true"></div>' +
|
||||
'<div class="mode-switch_handle js-switch-handle" aria-hidden="true">' +
|
||||
'<div class="mode-switch_current-icon camera rotates js-icon-camera" data-icon="camera" aria-hidden="true"></div>' +
|
||||
'<div class="mode-switch_current-icon video rotates js-icon-video" data-icon="video" aria-hidden="true"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Examples:
|
||||
*
|
||||
* classFrom('recording', true); //=> 'recording'
|
||||
* classFrom('flash', 'on'); //=> 'flash-on'
|
||||
* classFrom('recording', false); //=> ''
|
||||
* classFrom('recording'); //=> 'recording'
|
||||
* classFrom('recording', 'true'); //=> 'recording'
|
||||
* classFrom('recording', 'false'); //=> ''
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {*} value
|
||||
* @return {String}
|
||||
*/
|
||||
function classFrom(key, value) {
|
||||
value = detectBooleans(value);
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? key : '';
|
||||
} else if (value) {
|
||||
return key + '-' + value ;
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
function detectBooleans(value) {
|
||||
if (typeof value === 'boolean') { return value; }
|
||||
else if (value === 'true') { return true; }
|
||||
else if (value === 'false') { return false; }
|
||||
else { return value; }
|
||||
}
|
||||
|
||||
});
|
||||
83
gaia/apps/camera/js/views/countdown.js
Executable file
83
gaia/apps/camera/js/views/countdown.js
Executable file
|
|
@ -0,0 +1,83 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:countdown');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
/**
|
||||
* Countdown
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
module.exports = View.extend({
|
||||
name: 'countdown',
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
this.els.count = this.find('.js-count');
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this;
|
||||
},
|
||||
|
||||
set: function(time) {
|
||||
|
||||
// Update the number
|
||||
this.els.count.textContent = time;
|
||||
|
||||
// Trigger the shrink animation
|
||||
this.el.classList.remove('shrink');
|
||||
this.reflow = this.el.offsetTop;
|
||||
this.el.classList.add('shrink');
|
||||
|
||||
debug('set time: %s, near: %s', time);
|
||||
return this;
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this.el.classList.remove('hidden');
|
||||
this.el.classList.add('visible');
|
||||
},
|
||||
|
||||
hide: function(callback) {
|
||||
this.el.classList.remove('visible');
|
||||
this.el.classList.add('hidden');
|
||||
setTimeout(callback, 200);
|
||||
return this;
|
||||
},
|
||||
|
||||
setImmanent: function(value) {
|
||||
this.el.classList.toggle('immanent', value);
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.els.count.textContent = '';
|
||||
this.el.classList.remove('immanent', 'shrink');
|
||||
return this;
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div class="countdown_circle-1"></div>' +
|
||||
'<div class="countdown_circle-2"></div>' +
|
||||
'<div class="countdown_count">' +
|
||||
'<div class="rotates js-count"></div>' +
|
||||
'</div>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
38
gaia/apps/camera/js/views/face.js
Executable file
38
gaia/apps/camera/js/views/face.js
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'face',
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
this.el.classList.add('js-face');
|
||||
},
|
||||
|
||||
setPosition: function(x, y) {
|
||||
this.el.style.left = x + 'px';
|
||||
this.el.style.top = y + 'px';
|
||||
},
|
||||
|
||||
setDiameter: function(diameter) {
|
||||
this.el.style.width = diameter + 'px';
|
||||
this.el.style.height = diameter + 'px';
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
74
gaia/apps/camera/js/views/faces.js
Executable file
74
gaia/apps/camera/js/views/faces.js
Executable file
|
|
@ -0,0 +1,74 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var FaceView = require('views/face');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'faces',
|
||||
faces: [],
|
||||
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
this.el.innerHTML = this.template();
|
||||
this.FaceView = options.FaceView || FaceView;
|
||||
},
|
||||
|
||||
// It creates the DOM elements that will display circles
|
||||
// around the detected faces.
|
||||
createFaces: function(maxNumberFaces) {
|
||||
var faceView;
|
||||
var i;
|
||||
for(i = 0; i < maxNumberFaces; ++i) {
|
||||
faceView = new this.FaceView();
|
||||
faceView.hide();
|
||||
this.faces.push(faceView);
|
||||
faceView.appendTo(this.el);
|
||||
}
|
||||
},
|
||||
|
||||
render: function(faces) {
|
||||
var self = this;
|
||||
this.hideFaces();
|
||||
if (faces.length > this.faces.length) {
|
||||
this.createFaces(faces.length - this.faces.length);
|
||||
}
|
||||
faces.forEach(function(face, index) {
|
||||
var faceView = self.faces[index];
|
||||
self.renderFace(face, faceView);
|
||||
});
|
||||
},
|
||||
|
||||
renderFace: function(face, faceView) {
|
||||
// Maximum diameter is 300px as in the visual spec
|
||||
var diameter = Math.min(300, face.diameter);
|
||||
faceView.setPosition(face.x, face.y);
|
||||
faceView.setDiameter(diameter);
|
||||
faceView.show();
|
||||
},
|
||||
|
||||
hideFaces: function() {
|
||||
this.faces.forEach(function(faceView) {
|
||||
faceView.hide();
|
||||
});
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
var self = this;
|
||||
this.faces.forEach(function(faceView) {
|
||||
self.el.removeChild(faceView.el);
|
||||
});
|
||||
this.faces = [];
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
74
gaia/apps/camera/js/views/focus.js
Executable file
74
gaia/apps/camera/js/views/focus.js
Executable file
|
|
@ -0,0 +1,74 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:focus');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'focus',
|
||||
fadeTime: 500,
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
this.setFocusState('none');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
delete this.template; // Clean up
|
||||
debug('rendered');
|
||||
return this;
|
||||
},
|
||||
|
||||
setFocusState: function(state) {
|
||||
this.set('state', state);
|
||||
if (state !== 'focusing') {
|
||||
this.fadeOut();
|
||||
}
|
||||
},
|
||||
|
||||
setFocusMode: function(mode) {
|
||||
this.reset();
|
||||
this.set('mode', mode);
|
||||
},
|
||||
|
||||
setPosition: function(x, y) {
|
||||
if (this.fadeOutTimer) {
|
||||
clearTimeout(this.fadeOutTimer);
|
||||
}
|
||||
this.el.style.left = x + 'px';
|
||||
this.el.style.top = y + 'px';
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.el.style.left = '50%';
|
||||
this.el.style.top = '50%';
|
||||
this.set('state', 'none');
|
||||
},
|
||||
|
||||
fadeOut: function() {
|
||||
var self = this;
|
||||
this.fadeOutTimer = setTimeout(hide, this.fadeTime);
|
||||
function hide() {
|
||||
self.reset();
|
||||
}
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div class="focus_locking" data-icon="focus-locking" ' +
|
||||
'aria-hidden="true"></div>' +
|
||||
'<div class="focus_locked" data-icon="focus-locked" aria-hidden="true">' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
102
gaia/apps/camera/js/views/hud.js
Executable file
102
gaia/apps/camera/js/views/hud.js
Executable file
|
|
@ -0,0 +1,102 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:hud');
|
||||
var bind = require('lib/bind');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'hud',
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
this.els.flash = this.find('.js-flash');
|
||||
this.els.camera = this.find('.js-camera');
|
||||
this.els.settings = this.find('.js-settings');
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.els.flash, 'click', this.onFlashClick);
|
||||
bind(this.els.camera, 'click', this.onCameraClick);
|
||||
bind(this.els.settings, 'click', this.onSettingsClick, true);
|
||||
return this;
|
||||
},
|
||||
|
||||
_setLabel: function(element, mode) {
|
||||
if (mode) {
|
||||
this.els[element].setAttribute('data-l10n-id', mode.title + '-button');
|
||||
} else {
|
||||
this.els[element].removeAttribute('data-l10n-id');
|
||||
this.els[element].removeAttribute('aria-label');
|
||||
}
|
||||
},
|
||||
|
||||
setFlashModeLabel: function(mode) {
|
||||
this._setLabel('flash', mode);
|
||||
},
|
||||
|
||||
setFlashMode: function(mode) {
|
||||
if (!mode) { return; }
|
||||
this.els.flash.dataset.icon = mode.icon;
|
||||
this.setFlashModeLabel(mode);
|
||||
},
|
||||
|
||||
setCameraLabel: function(camera) {
|
||||
this._setLabel('camera', camera);
|
||||
},
|
||||
|
||||
setCamera: function(camera) {
|
||||
if (!camera) { return; }
|
||||
this.els.camera.dataset.icon = camera.icon;
|
||||
this.setCameraLabel(camera);
|
||||
},
|
||||
|
||||
setMenuLabel: function() {
|
||||
this._setLabel('settings', { title: 'menu' });
|
||||
},
|
||||
|
||||
onFlashClick: function(event) {
|
||||
event.stopPropagation();
|
||||
this.emit('click:flash');
|
||||
},
|
||||
|
||||
onCameraClick: function(event) {
|
||||
event.stopPropagation();
|
||||
this.emit('click:camera');
|
||||
},
|
||||
|
||||
onSettingsClick: function(event) {
|
||||
event.stopPropagation();
|
||||
this.emit('click:settings');
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div role="button" class="hud_btn hud_camera rotates ' +
|
||||
'test-camera-toggle js-camera"></div>' +
|
||||
'<div role="button" class="hud_btn hud_flash rotates test-flash-button ' +
|
||||
'js-flash"></div>' +
|
||||
'<div role="button" class="hud_btn hud_settings rotates ' +
|
||||
'test-settings-toggle js-settings" data-icon="menu" ' +
|
||||
'data-l10n-id="menu-button"></div>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
45
gaia/apps/camera/js/views/indicators.js
Executable file
45
gaia/apps/camera/js/views/indicators.js
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:indicators');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'indicators',
|
||||
tag: 'ul',
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this;
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<li class="indicator_countdown rotates" data-icon="self-timer" ' +
|
||||
'data-l10n-id="self-timer-indicator"></li>' +
|
||||
'<li class="indicator_hdr rotates" data-icon="hdr" ' +
|
||||
'data-l10n-id="hdr-indicator"></li>' +
|
||||
'<li class="indicator_geolocation rotates" data-icon="location" ' +
|
||||
'data-l10n-id="location-indicator"></li>' +
|
||||
'<li class="indicator_battery rotates" data-icon="battery-3" ' +
|
||||
'data-l10n-id="battery-low-indicator"></li>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
38
gaia/apps/camera/js/views/loading-screen.js
Executable file
38
gaia/apps/camera/js/views/loading-screen.js
Executable file
|
|
@ -0,0 +1,38 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
var debug = require('debug')('view:loading-screen');
|
||||
var View = require('view');
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'loading-screen',
|
||||
fadeTime: 300,
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template;
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this;
|
||||
},
|
||||
|
||||
show: function(done) {
|
||||
this.reflow = this.el.offsetTop;
|
||||
View.prototype.show.call(this);
|
||||
},
|
||||
|
||||
hide: function(done) {
|
||||
View.prototype.hide.call(this);
|
||||
if (done) { setTimeout(done, this.fadeTime); }
|
||||
},
|
||||
|
||||
template: '<progress></progress>'
|
||||
});
|
||||
|
||||
});
|
||||
152
gaia/apps/camera/js/views/notification.js
Executable file
152
gaia/apps/camera/js/views/notification.js
Executable file
|
|
@ -0,0 +1,152 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var mix = require('lib/mixin');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name:'notification',
|
||||
tag: 'ul',
|
||||
time: 3000,
|
||||
|
||||
initialize: function() {
|
||||
this.counter = 0;
|
||||
this.hash = {};
|
||||
// For accessibility purposes list/listitem semantics is not necessary.
|
||||
// Instead notifications are presented as status ARIA live regions.
|
||||
this.el.setAttribute('role', 'presentation');
|
||||
},
|
||||
|
||||
/**
|
||||
* Display a new notification.
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - `text {L10nId}`
|
||||
* L10nId may be:
|
||||
* a string -> l10nId
|
||||
* an object -> {id: l10nId, args: l10nArgs}
|
||||
* an object -> {html: string}
|
||||
* read more at: http://mzl.la/1A76Fby
|
||||
* - `className {String}`
|
||||
* - `persistent {Boolean}`
|
||||
*
|
||||
* @param {Object} options
|
||||
* @return {Number} id for clearing
|
||||
* @public
|
||||
*/
|
||||
display: function(options) {
|
||||
var item = mix({}, options);
|
||||
var id = ++this.counter;
|
||||
var self = this;
|
||||
|
||||
item.el = document.createElement('li');
|
||||
item.el.className = options.className || '';
|
||||
|
||||
var span = document.createElement('span');
|
||||
if (typeof(options.text) === 'string') {
|
||||
span.setAttribute('data-l10n-id', options.text);
|
||||
} else {
|
||||
if (options.text.html) {
|
||||
span.innerHTML = options.text.html;
|
||||
}
|
||||
}
|
||||
item.el.appendChild(span);
|
||||
if (options.attrs) { this.setAttributes(item.el, options.attrs); }
|
||||
// Notification should have the semantics of the status ARIA live region.
|
||||
item.el.setAttribute('role', 'status');
|
||||
// Making notfication assertive, so it is spoken right away and not queued.
|
||||
item.el.setAttribute('aria-live', 'assertive');
|
||||
this.el.appendChild(item.el);
|
||||
|
||||
// Remove last temporary
|
||||
// notification in the way
|
||||
this.clear(this.temporary);
|
||||
|
||||
// Remove non-persistent
|
||||
// messages after 3s
|
||||
if (!item.persistent) {
|
||||
this.temporary = id;
|
||||
this.hide(this.persistent);
|
||||
item.clearTimeout = setTimeout(function() {
|
||||
self.clear(id);
|
||||
}, this.time);
|
||||
}
|
||||
|
||||
// Remove previous persistent
|
||||
if (item.persistent) {
|
||||
this.clear(this.persistent);
|
||||
this.persistent = id;
|
||||
}
|
||||
|
||||
// Store and return
|
||||
this.hash[id] = item;
|
||||
return id;
|
||||
},
|
||||
|
||||
setAttributes: function(el, attrs) {
|
||||
for (var key in attrs) { el.setAttribute(key, attrs[key]); }
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear notfication by id.
|
||||
*
|
||||
* Remove the notification from the DOM,
|
||||
* clear any existing `clearTimeout` that
|
||||
* may have been installed on creation.
|
||||
*
|
||||
* @param {Number} item
|
||||
* @public
|
||||
*/
|
||||
clear: function(id) {
|
||||
var item = this.hash[id];
|
||||
if (!item || item.cleared) { return; }
|
||||
|
||||
this.el.removeChild(item.el);
|
||||
clearTimeout(item.clearTimeout);
|
||||
item.cleared = true;
|
||||
|
||||
// Clear references
|
||||
if (item === this.temporary) { this.temporary = null; }
|
||||
if (item === this.persistent) { this.persistent = null; }
|
||||
delete this.hash[id];
|
||||
|
||||
// Show persistent notification
|
||||
// (if there still is one)
|
||||
this.show(this.persistent);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide a notification.
|
||||
*
|
||||
* @param {Number} id
|
||||
* @private
|
||||
*/
|
||||
hide: function(id) {
|
||||
var item = this.hash[id];
|
||||
if (!item) { return; }
|
||||
item.el.classList.add('hidden');
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a hidden notification.
|
||||
*
|
||||
* @param {Number} id
|
||||
* @private
|
||||
*/
|
||||
show: function(id) {
|
||||
var item = this.hash[id];
|
||||
if (!item) { return; }
|
||||
item.el.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
115
gaia/apps/camera/js/views/overlay.js
Executable file
115
gaia/apps/camera/js/views/overlay.js
Executable file
|
|
@ -0,0 +1,115 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:overlay');
|
||||
var View = require('view');
|
||||
var bind = require('lib/bind');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
className: 'overlay',
|
||||
|
||||
initialize: function(options) {
|
||||
var data = this.getData(options.type);
|
||||
if (!data) { return; }
|
||||
|
||||
this.data('type', options.type);
|
||||
this.data('closable', options.closable);
|
||||
this.render(data);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
|
||||
// Inject HTML
|
||||
this.el.innerHTML = this.template(data);
|
||||
|
||||
// Pick out elements
|
||||
this.els.buttons = {
|
||||
close: this.find('.js-close-btn')
|
||||
};
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
rendered: function() {
|
||||
return this.el.innerHTML.length > 0;
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.els.buttons.close, 'click', this.onButtonClick);
|
||||
return this;
|
||||
},
|
||||
|
||||
template: function(data) {
|
||||
/*jshint maxlen:false*/
|
||||
return '<form role="dialog" data-type="confirm">' +
|
||||
'<section>' +
|
||||
'<h1 class="overlay-title" data-l10n-id="' + data.title + '"></h1>' +
|
||||
'<p id="overlay-text" data-l10n-id="' + data.body + '"></p>' +
|
||||
'</section>' +
|
||||
'<menu class="overlay-menu-close">' +
|
||||
'<button class="full js-close-btn" type="button" ' +
|
||||
'data-l10n-id="' + data.closeButtonText + '" name="close-btn">' +
|
||||
'</button>' +
|
||||
'</menu>' +
|
||||
'</form>';
|
||||
},
|
||||
|
||||
getData: function(type) {
|
||||
var data = {};
|
||||
|
||||
switch (type) {
|
||||
case 'unavailable':
|
||||
data.title = 'nocard2-title';
|
||||
data.body = 'nocard3-text';
|
||||
break;
|
||||
case 'nospace':
|
||||
data.title = 'nospace2-title';
|
||||
data.body = 'nospace2-text';
|
||||
break;
|
||||
case 'shared':
|
||||
data.title = 'pluggedin2-title';
|
||||
data.body = 'pluggedin2-text';
|
||||
break;
|
||||
case 'shutdown':
|
||||
data.title = 'battery-shutdown-title';
|
||||
data.body = 'battery-shutdown-text';
|
||||
break;
|
||||
case 'request-fail':
|
||||
data.title = 'camera-unavailable-title';
|
||||
data.body = 'camera-unavailable-text';
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
data.closeButtonText = 'close-button';
|
||||
return data;
|
||||
},
|
||||
|
||||
data: function(key, value) {
|
||||
switch (arguments.length) {
|
||||
case 1: return this.el.getAttribute('data-' + key);
|
||||
case 2: this.el.setAttribute('data-' + key, value);
|
||||
}
|
||||
},
|
||||
|
||||
onButtonClick: function(event) {
|
||||
var el = event.currentTarget;
|
||||
var name = el.getAttribute('name');
|
||||
this.emit('click:' + name);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
377
gaia/apps/camera/js/views/preview-gallery.js
Executable file
377
gaia/apps/camera/js/views/preview-gallery.js
Executable file
|
|
@ -0,0 +1,377 @@
|
|||
/* jshint -W098 */
|
||||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:preview-gallery');
|
||||
var addPanAndZoomHandlers = require('lib/panzoom');
|
||||
var orientation = require('lib/orientation');
|
||||
var MediaFrame = require('MediaFrame');
|
||||
var bind = require('lib/bind');
|
||||
var attach = require('attach');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* `<gaia-header>` used in template
|
||||
*/
|
||||
|
||||
require('gaia-header');
|
||||
|
||||
/**
|
||||
* Constants
|
||||
*/
|
||||
|
||||
var SWIPE_DISTANCE_THRESHOLD = window.innerWidth / 3; // pixels
|
||||
var SWIPE_VELOCITY_THRESHOLD = 1.0; // pixels/ms
|
||||
|
||||
var SWIPE_DURATION = 150; // How long to animate the swipe
|
||||
var FADE_IN_DURATION = 450; // How long to animate the fade in after swipe
|
||||
|
||||
/**
|
||||
* Locals
|
||||
*/
|
||||
|
||||
return View.extend({
|
||||
name: 'preview-gallery',
|
||||
className: 'offscreen',
|
||||
|
||||
initialize: function() {
|
||||
debug('initialized');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
this.els.frameContainer = this.find('.js-frame-container');
|
||||
this.els.previewMenu = this.find('.js-preview-menu');
|
||||
this.els.mediaFrame = this.find('.js-media-frame');
|
||||
this.els.countText = this.find('.js-count-text');
|
||||
this.els.options = this.find('.js-options');
|
||||
this.els.header = this.find('.js-header');
|
||||
this.els.share = this.find('.js-share');
|
||||
|
||||
// We're appending new elements to DOM so to make sure headers are
|
||||
// properly resized and centered, we emmit a lazyload event.
|
||||
// This will be removed when the gaia-header web component lands.
|
||||
window.dispatchEvent(new CustomEvent('lazyload', {
|
||||
detail: this.el
|
||||
}));
|
||||
|
||||
// Configure the MediaFrame component
|
||||
this.configure();
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.el, 'tap', this.onTap);
|
||||
bind(this.els.header, 'action', this.firer('click:back'));
|
||||
bind(this.els.options, 'click', this.onButtonClick);
|
||||
bind(this.els.share, 'click', this.onButtonClick);
|
||||
// Our screen reader synthesizes wheel events when the user swipes with two
|
||||
// fingers in either direction. Those events always have their deltaMode set
|
||||
// to DOM_DELTA_PAGE, and the delta value can either be -1, 0 or 1.
|
||||
bind(this.els.mediaFrame, 'wheel', this.onFrameWheel);
|
||||
return this;
|
||||
},
|
||||
|
||||
configure: function() {
|
||||
this.currentIndex = this.lastIndex = 0;
|
||||
|
||||
this.frame = new MediaFrame(this.els.mediaFrame, true, this.maxPreviewSize);
|
||||
this.frame.video.onloading = this.onVideoLoading;
|
||||
this.frame.video.onplaying = this.onVideoPlaying;
|
||||
this.frame.video.onpaused = this.onVideoPaused;
|
||||
|
||||
addPanAndZoomHandlers(this.frame, this.swipeCallback);
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div class="preview-menu js-preview-menu">' +
|
||||
'<gaia-header class="js-header" action="back" ignore-dir>' +
|
||||
'<h1 data-l10n-id="preview">Preview</h1>' +
|
||||
'<button class="preview-share-icon js-share"' +
|
||||
'name="share" data-icon="share" ' +
|
||||
'data-l10n-id="share-button"></button>' +
|
||||
'<button class="preview-option-icon ' +
|
||||
'js-options" name="options" data-icon="more" ' +
|
||||
'data-l10n-id="more-button"></button>' +
|
||||
'</gaia-header>' +
|
||||
'</div>' +
|
||||
'<div class="frame-container js-frame-container">' +
|
||||
'<div class="media-frame js-media-frame"></div>' +
|
||||
'</div>' +
|
||||
'<div class="count-text js-count-text"></div>';
|
||||
},
|
||||
|
||||
onTap: function() {
|
||||
if (this.videoPlaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
var isShown = this.els.previewMenu.classList.contains('visible');
|
||||
if (isShown) {
|
||||
this.previewMenuFadeOut();
|
||||
} else {
|
||||
this.previewMenuFadeIn();
|
||||
}
|
||||
},
|
||||
|
||||
previewMenuFadeIn: function() {
|
||||
this.els.previewMenu.classList.add('visible');
|
||||
},
|
||||
|
||||
previewMenuFadeOut: function() {
|
||||
this.els.previewMenu.classList.remove('visible');
|
||||
},
|
||||
|
||||
onFrameWheel: function(event) {
|
||||
if (event.deltaMode !== event.DOM_DELTA_PAGE || event.deltaY) {
|
||||
return;
|
||||
}
|
||||
// If the video is playing, switch to preview.
|
||||
if (this.videoPlaying) {
|
||||
this.onVideoPaused();
|
||||
}
|
||||
if (event.deltaX > 0) {
|
||||
this.emit('swipe', 'left');
|
||||
} else if (event.deltaX < 0) {
|
||||
this.emit('swipe', 'right');
|
||||
}
|
||||
},
|
||||
|
||||
swipeCallback: function(swipeAmount, swipeVelocity) {
|
||||
var self = this;
|
||||
|
||||
if (swipeVelocity === undefined) {
|
||||
// If this is not the end of the gesture, then just move the media
|
||||
// frame left or right by the specified amount.
|
||||
this.els.frameContainer.style.transform =
|
||||
'translate(' + swipeAmount + 'px, 0)';
|
||||
return;
|
||||
}
|
||||
|
||||
// If we were passed a velocity, then this is the end of the gesture
|
||||
// and we have to figure out whether we are going to go to the
|
||||
// next item, the previous item, or remain on the current item.
|
||||
var direction, translation;
|
||||
|
||||
// Should we move to the previous item?
|
||||
if (swipeAmount > 0 && swipeVelocity > 0 && // Same sign and
|
||||
this.currentIndex > 1 && // has previous item and
|
||||
(swipeAmount > SWIPE_DISTANCE_THRESHOLD || // distance big enough or
|
||||
swipeVelocity > SWIPE_VELOCITY_THRESHOLD)) // speed fast enough
|
||||
{
|
||||
direction = 'right';
|
||||
translation = '100%';
|
||||
}
|
||||
// Should we move to the next item?
|
||||
else if (swipeAmount <= 0 && swipeVelocity <= 0 &&
|
||||
this.currentIndex < this.lastIndex &&
|
||||
(swipeAmount < -SWIPE_DISTANCE_THRESHOLD ||
|
||||
swipeVelocity < -SWIPE_VELOCITY_THRESHOLD)) {
|
||||
direction = 'left';
|
||||
translation = '-100%';
|
||||
}
|
||||
|
||||
// If we're not moving either left or right, just animate the
|
||||
// item to its starting point and we're done.
|
||||
if (!direction) {
|
||||
animate('transform', 'translate(0,0)', SWIPE_DURATION);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we get here, we are going to slide the current item off the screen
|
||||
// and display the next or previous item.
|
||||
|
||||
// First, stop the video if there is one and it is playing.
|
||||
if (this.videoPlaying) {
|
||||
this.onVideoPaused();
|
||||
}
|
||||
|
||||
// Now animate the item off the screen
|
||||
animate('transform', 'translate(' + translation + ', 0)',
|
||||
SWIPE_DURATION,
|
||||
function() {
|
||||
// Once we're off screen ask the controller to
|
||||
// switch to the new image.
|
||||
self.emit('swipe', direction);
|
||||
|
||||
// On the next redraw, put the frame back on the screen
|
||||
// but make it opaque
|
||||
window.requestAnimationFrame(function() {
|
||||
// Next, make the frame invisible
|
||||
// and put it back on the screen
|
||||
self.els.frameContainer.style.opacity = 0;
|
||||
self.els.frameContainer.style.transform = 'translate(0,0)';
|
||||
|
||||
// Then on the frame after that, animate the opacity to
|
||||
// make it visible again.
|
||||
window.requestAnimationFrame(function() {
|
||||
animate('opacity', 1, FADE_IN_DURATION);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// A helper function to animate the specified CSS property to the
|
||||
// specified value for the specified duration. When the animation
|
||||
// is complete, reset everything and call the done callback
|
||||
function animate(property, value, duration, done) {
|
||||
var e = self.els.frameContainer;
|
||||
e.addEventListener('transitionend', onTransitionEnd);
|
||||
e.style.transitionProperty = property;
|
||||
e.style.transitionDuration = duration + 'ms';
|
||||
e.style[property] = value;
|
||||
|
||||
function onTransitionEnd() {
|
||||
e.removeEventListener('transitionend', onTransitionEnd);
|
||||
delete e.style.transitionProperty;
|
||||
delete e.style.transitionDuration;
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onButtonClick: function(e, el) {
|
||||
if (this.videoPlaying) { return; }
|
||||
|
||||
el = el || e.currentTarget;
|
||||
var name = el.getAttribute('name');
|
||||
if (this.optionsMenuContainer) {
|
||||
this.hideOptionsMenu();
|
||||
}
|
||||
this.emit('click:' + name, e);
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
open: function() {
|
||||
window.addEventListener('resize', this.onResize);
|
||||
orientation.unlock();
|
||||
this.previewMenuFadeIn();
|
||||
this.el.classList.remove('offscreen');
|
||||
},
|
||||
|
||||
close: function() {
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
orientation.lock();
|
||||
this.previewMenuFadeOut();
|
||||
this.el.classList.add('offscreen');
|
||||
this.frame.clear();
|
||||
},
|
||||
|
||||
updateCountText: function(current, total) {
|
||||
this.currentIndex = current;
|
||||
this.lastIndex = total;
|
||||
this.els.countText.textContent = current + '/' + total;
|
||||
|
||||
// Ensure that the information about the number of thumbnails is provided to
|
||||
// the screen reader.
|
||||
this.els.mediaFrame.setAttribute('data-l10n-id', 'media-frame');
|
||||
this.els.mediaFrame.setAttribute('data-l10n-args', JSON.stringify(
|
||||
{ total: total, current: current }));
|
||||
},
|
||||
|
||||
onResize: function() {
|
||||
// And we have to resize the frame (and its video player)
|
||||
this.frame.resize();
|
||||
if (this.frame.displayingVideo) {
|
||||
this.frame.video.setPlayerSize();
|
||||
}
|
||||
},
|
||||
|
||||
showImage: function(image) {
|
||||
this.frame.displayImage(
|
||||
image.blob,
|
||||
image.width,
|
||||
image.height,
|
||||
image.preview,
|
||||
image.rotation,
|
||||
image.mirrored);
|
||||
},
|
||||
|
||||
showVideo: function(video) {
|
||||
this.frame.displayVideo(
|
||||
video.blob,
|
||||
video.poster.blob,
|
||||
video.poster.width / video.poster.height,
|
||||
video.rotation);
|
||||
},
|
||||
|
||||
onVideoLoading: function() {
|
||||
this.emit('loadingvideo', 'loadingVideo');
|
||||
},
|
||||
|
||||
onVideoPlaying: function() {
|
||||
if (this.videoPlaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoPlaying = true;
|
||||
this.previewMenuFadeOut();
|
||||
|
||||
this.emit('playingvideo');
|
||||
},
|
||||
|
||||
onVideoPaused: function() {
|
||||
if (!this.videoPlaying) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoPlaying = false;
|
||||
this.previewMenuFadeIn();
|
||||
},
|
||||
|
||||
showOptionsMenu: function() {
|
||||
this.optionsMenuContainer = document.createElement('div');
|
||||
this.optionsMenuContainer.innerHTML = this.optionTemplate();
|
||||
this.el.appendChild(this.optionsMenuContainer);
|
||||
|
||||
this.el.classList.add('action-menu');
|
||||
|
||||
this.menu = this.find('.js-menu');
|
||||
|
||||
// We add the event listner for menu items and cancel buttons
|
||||
var cancelButton = this.find('.js-cancel');
|
||||
bind(cancelButton, 'click', this.hideOptionsMenu);
|
||||
if (this.menu) {
|
||||
attach.on(this.menu, 'click', '.js-btn', this.onButtonClick);
|
||||
}
|
||||
},
|
||||
|
||||
hideOptionsMenu: function() {
|
||||
if (this.optionsMenuContainer) {
|
||||
this.el.classList.remove('action-menu');
|
||||
this.optionsMenuContainer.parentElement.removeChild(
|
||||
this.optionsMenuContainer);
|
||||
this.optionsMenuContainer = null;
|
||||
}
|
||||
},
|
||||
|
||||
optionTemplate: function() {
|
||||
return '<form class="visible" data-type="action"' +
|
||||
'role="dialog" data-z-index-level="action-menu">' +
|
||||
'<header data-l10n-id="options">Options</header>' +
|
||||
'<menu class="js-menu">' +
|
||||
'<button class="js-btn" name="gallery" data-l10n-id="open-gallery">' +
|
||||
'Open Gallery' +
|
||||
'</button>' +
|
||||
'<button class="js-btn" name="delete" data-l10n-id="delete">' +
|
||||
'Delete' +
|
||||
'</button>' +
|
||||
'<button class="js-cancel" data-action="cancel" data-l10n-id="cancel">' +
|
||||
'Cancel' +
|
||||
'</button>' +
|
||||
'</menu>' +
|
||||
'</form>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
30
gaia/apps/camera/js/views/recording-timer.js
Executable file
30
gaia/apps/camera/js/views/recording-timer.js
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:recording-timer');
|
||||
var formatTimer = require('lib/format-timer');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'recording-timer',
|
||||
|
||||
initialize: function() {
|
||||
this.value(0);
|
||||
this.el.setAttribute('role', 'timer');
|
||||
},
|
||||
|
||||
value: function(value) {
|
||||
this.el.textContent = formatTimer(value);
|
||||
debug('set value: %s', value);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
108
gaia/apps/camera/js/views/setting-options.js
Executable file
108
gaia/apps/camera/js/views/setting-options.js
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:setting-options');
|
||||
var attach = require('attach');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'setting-options',
|
||||
|
||||
initialize: function(options) {
|
||||
this.model = options.model;
|
||||
this.l10n = options.l10n || document.l10n;
|
||||
this.on('destroy', this.onDestroy);
|
||||
attach(this.el, 'click', 'li', this.onOptionClick);
|
||||
attach(this.el, 'click', '.js-back', this.firer('click:back'));
|
||||
this.model.on('change:selected', this.onSelectedChange);
|
||||
debug('initialized');
|
||||
},
|
||||
|
||||
onDestroy: function() {
|
||||
this.model.off('change:selected', this.onSelectedChange);
|
||||
},
|
||||
|
||||
onOptionClick: function(event, el) {
|
||||
var key = el.getAttribute('data-key');
|
||||
this.emit('click:option', key, this.model);
|
||||
},
|
||||
|
||||
onSelectedChange: function(key) {
|
||||
var next = this.els[key];
|
||||
this.els.selected.classList.remove('selected');
|
||||
next.classList.add('selected');
|
||||
this.els.selected = next;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var data = this.model.get();
|
||||
this.selectedKey = data.selected;
|
||||
this.el.innerHTML = this.template(data);
|
||||
this.els.ul = this.find('.js-list');
|
||||
|
||||
// we need to pass a boolean flag indicating if the
|
||||
// options should be localized
|
||||
var localizable = data.optionsLocalizable === false ? false : true;
|
||||
data.options.forEach(this.renderOption.bind(this, localizable));
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this;
|
||||
},
|
||||
|
||||
renderOption: function(localizable, option) {
|
||||
var li = document.createElement('li');
|
||||
var isSelected = option.key === this.selectedKey;
|
||||
|
||||
if (localizable) {
|
||||
li.setAttribute('data-l10n-id', option.title);
|
||||
this.l10n.formatValue(option.title).then((title) => {
|
||||
li.setAttribute('aria-label', title);
|
||||
});
|
||||
} else {
|
||||
li.textContent = option.title;
|
||||
li.setAttribute('aria-label', option.title);
|
||||
}
|
||||
li.setAttribute('data-key', option.key);
|
||||
// The settings options list is a listbox (list of actionable items) thus
|
||||
// each iteam must have an 'option' role.
|
||||
li.setAttribute('role', 'option');
|
||||
li.className = 'setting-option';
|
||||
li.dataset.icon = 'tick';
|
||||
this.els.ul.appendChild(li);
|
||||
this.els[option.key] = li;
|
||||
|
||||
if (isSelected) {
|
||||
li.classList.add('selected');
|
||||
// Make sure selected semantics is conveyed to the screen reader.
|
||||
li.setAttribute('aria-selected', true);
|
||||
this.els.selected = li;
|
||||
}
|
||||
},
|
||||
|
||||
template: function(data) {
|
||||
return '<div class="inner">' +
|
||||
'<div class="settings_header">' +
|
||||
'<div class="settings-back-btn js-back" ' +
|
||||
'data-icon="left" role="button" data-l10n-id="back-button"></div>' +
|
||||
'<h2 aria-level="1" class="settings_title" data-l10n-id="' +
|
||||
data.header + '"></h2>' +
|
||||
'</div>' +
|
||||
'<div class="settings_items">' +
|
||||
'<ul role="listbox" class="inner js-list"></ul>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
84
gaia/apps/camera/js/views/setting.js
Executable file
84
gaia/apps/camera/js/views/setting.js
Executable file
|
|
@ -0,0 +1,84 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:setting');
|
||||
var bind = require('lib/bind');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
tag: 'li',
|
||||
name: 'setting',
|
||||
|
||||
initialize: function(options) {
|
||||
this.l10n = options.l10n || document.l10n;
|
||||
this.model = options.model;
|
||||
this.model.on('change', this.render);
|
||||
this.on('destroy', this.onDestroy);
|
||||
this.el.dataset.icon = this.model.get('icon');
|
||||
this.el.classList.add('test-' + this.model.get('title') + '-setting');
|
||||
bind(this.el, 'click', this.onClick);
|
||||
},
|
||||
|
||||
onClick: function() {
|
||||
this.emit('click', this);
|
||||
},
|
||||
|
||||
onDestroy: function() {
|
||||
this.model.off('change', this.render);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var data = this.model.get();
|
||||
data.selected = this.model.selected();
|
||||
data.value = data.selected && data.selected.title;
|
||||
this.el.setAttribute('role', 'option');
|
||||
|
||||
|
||||
var localizedValue;
|
||||
if (data.optionsLocalizable === false) {
|
||||
localizedValue = Promise.resolve(data.value);
|
||||
} else {
|
||||
localizedValue = this.l10n.formatValue(data.value);
|
||||
}
|
||||
|
||||
localizedValue.then(valueString => {
|
||||
this.l10n.setAttributes(
|
||||
this.el,
|
||||
'setting-option-' + data.title,
|
||||
{ value: valueString }
|
||||
);
|
||||
this.el.innerHTML = this.template({
|
||||
titleL10n: 'data-l10n-id="' + data.title + '"',
|
||||
valueL10n: data.optionsLocalizable !== false ? 'data-l10n-id="' +
|
||||
data.value + '"' : '',
|
||||
valueText: data.optionsLocalizable === false ? data.value : '',
|
||||
});
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered (item %s)', data.key);
|
||||
});
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
template: function(data) {
|
||||
return '<div class="setting_text">' +
|
||||
'<h4 class="setting_title" ' + data.titleL10n + '></h4>' +
|
||||
'<h5 class="setting_value" ' + data.valueL10n + '>' +
|
||||
data.valueText +
|
||||
'</h5>' +
|
||||
'</div>';
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
139
gaia/apps/camera/js/views/settings.js
Executable file
139
gaia/apps/camera/js/views/settings.js
Executable file
|
|
@ -0,0 +1,139 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:settings');
|
||||
var OptionsView = require('views/setting-options');
|
||||
var SettingView = require('views/setting');
|
||||
var View = require('view');
|
||||
var bind = require('lib/bind');
|
||||
|
||||
/**
|
||||
* Exports
|
||||
*/
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'settings',
|
||||
fadeTime: 150,
|
||||
|
||||
initialize: function(options) {
|
||||
this.OptionsView = options.OptionsView || OptionsView;
|
||||
this.items = options.items;
|
||||
this.children = [];
|
||||
this.on('destroy', this.onDestroy);
|
||||
bind(this.el, 'click', this.onClick);
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
onItemClick: function(view) {
|
||||
this.showSetting(view.model);
|
||||
},
|
||||
|
||||
showSetting: function(model) {
|
||||
this.optionsView = new this.OptionsView({ model: model })
|
||||
.render()
|
||||
.appendTo(this.els.pane2)
|
||||
.on('click:option', this.firer('click:option'))
|
||||
.on('click:back', this.goBack);
|
||||
|
||||
this.showPane(2);
|
||||
},
|
||||
|
||||
onDestroy: function() {
|
||||
this.children.forEach(this.destroyChild);
|
||||
this.destroyOptionsView();
|
||||
debug('destroyed');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
this.els.items = this.find('.js-items');
|
||||
this.els.pane2 = this.find('.js-pane-2');
|
||||
this.els.close = this.find('.js-close');
|
||||
this.items.forEach(this.addItem);
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.els.close, 'click', this.firer('click:close'));
|
||||
return this;
|
||||
},
|
||||
|
||||
goBack: function() {
|
||||
this.showPane(1);
|
||||
this.destroyOptionsView();
|
||||
},
|
||||
|
||||
destroyChild: function(view) {
|
||||
view.destroy();
|
||||
debug('destroyed child');
|
||||
},
|
||||
|
||||
destroyOptionsView: function() {
|
||||
if (this.optionsView) {
|
||||
this.optionsView.destroy();
|
||||
this.optionsView = null;
|
||||
debug('options view destroyed');
|
||||
}
|
||||
},
|
||||
|
||||
addItem: function(model) {
|
||||
var setting = new SettingView({ model: model })
|
||||
.render()
|
||||
.appendTo(this.els.items)
|
||||
.on('click', this.onItemClick);
|
||||
|
||||
this.children.push(setting);
|
||||
debug('add item key: %s', model.key);
|
||||
},
|
||||
|
||||
showPane: function(name) {
|
||||
this.el.setAttribute('show-pane', 'pane-' + name);
|
||||
},
|
||||
|
||||
fadeIn: function(done) {
|
||||
setTimeout(this.show);
|
||||
if (!done) { return; }
|
||||
setTimeout(done, this.fadeTime);
|
||||
},
|
||||
|
||||
fadeOut: function(done) {
|
||||
this.hide();
|
||||
if (!done) { return; }
|
||||
setTimeout(done, this.fadeTime);
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div class="pane pane-1">' +
|
||||
'<div class="inner">' +
|
||||
'<div class="settings_inner">' +
|
||||
'<div class="settings_header">' +
|
||||
'<h2 class="settings_title" data-l10n-id="options" ' +
|
||||
'aria-level="1"></h2>' +
|
||||
'</div>' +
|
||||
'<div class="settings_items">' +
|
||||
'<ul class="inner js-items" role="listbox"></ul>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="pane pane-2">' +
|
||||
'<div class="inner js-pane-2"></div>' +
|
||||
'</div>' +
|
||||
'<div role="button" class="settings_close js-close" data-icon="menu" ' +
|
||||
'data-l10n-id="close-menu-button"></div>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
312
gaia/apps/camera/js/views/viewfinder.js
Executable file
312
gaia/apps/camera/js/views/viewfinder.js
Executable file
|
|
@ -0,0 +1,312 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:viewfinder');
|
||||
var bind = require('lib/bind');
|
||||
var CameraUtils = require('lib/camera-utils');
|
||||
var View = require('view');
|
||||
|
||||
/**
|
||||
* Locals
|
||||
*/
|
||||
|
||||
var isZoomEnabled = false;
|
||||
var scaleSizeTo = {
|
||||
fill: CameraUtils.scaleSizeToFillViewport,
|
||||
fit: CameraUtils.scaleSizeToFitViewport
|
||||
};
|
||||
|
||||
var clamp = function(value, minimum, maximum) {
|
||||
return Math.min(Math.max(value, minimum), maximum);
|
||||
};
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'viewfinder',
|
||||
className: 'js-viewfinder',
|
||||
fadeTime: 360,
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
this.getSize();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
this.els.frame = this.find('.js-frame');
|
||||
this.els.video = this.find('.js-video');
|
||||
this.els.videoContainer = this.find('.js-video-container');
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.el, 'click', this.onClick);
|
||||
bind(this.el, 'animationend', this.onShutterEnd);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores the container size.
|
||||
*
|
||||
* We're using window dimensions
|
||||
* here to avoid causing costly
|
||||
* reflows.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getSize: function() {
|
||||
this.width = window.innerWidth;
|
||||
this.height = window.innerHeight;
|
||||
return {
|
||||
width: this.width,
|
||||
height: this.height
|
||||
};
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
this.emit('click', e);
|
||||
},
|
||||
|
||||
enableZoom: function(minimumZoom, maximumZoom) {
|
||||
if (minimumZoom) {
|
||||
this._minimumZoom = minimumZoom;
|
||||
}
|
||||
|
||||
if (maximumZoom) {
|
||||
this._maximumZoom = maximumZoom;
|
||||
}
|
||||
|
||||
isZoomEnabled = true;
|
||||
},
|
||||
|
||||
disableZoom: function() {
|
||||
this._minimumZoom = 1.0;
|
||||
this._maximumZoom = 1.0;
|
||||
|
||||
this.setZoom(1.0);
|
||||
|
||||
isZoomEnabled = false;
|
||||
},
|
||||
|
||||
_minimumZoom: 1.0,
|
||||
|
||||
setMinimumZoom: function(minimumZoom) {
|
||||
this._minimumZoom = minimumZoom;
|
||||
},
|
||||
|
||||
_maximumZoom: 1.0,
|
||||
|
||||
setMaximumZoom: function(maximumZoom) {
|
||||
this._maximumZoom = maximumZoom;
|
||||
},
|
||||
|
||||
_zoom: 1.0,
|
||||
|
||||
/**
|
||||
* Update the internal state of the view so that any future
|
||||
* pinch-to-zoom gestures can correctly adjust the current zoom
|
||||
* level in the event that the zoom level is changed outside of
|
||||
* the pinch-to-zoom gesture (e.g.: ZoomBar). This gets called
|
||||
* when the `Camera` emits a `zoomchanged` event.
|
||||
*/
|
||||
setZoom: function(zoom) {
|
||||
if (!isZoomEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._zoom = clamp(zoom, this._minimumZoom, this._maximumZoom);
|
||||
},
|
||||
|
||||
_useZoomPreviewAdjustment: false,
|
||||
|
||||
enableZoomPreviewAdjustment: function() {
|
||||
this._useZoomPreviewAdjustment = true;
|
||||
},
|
||||
|
||||
disableZoomPreviewAdjustment: function() {
|
||||
this._useZoomPreviewAdjustment = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adjust the scale of the <video/> tag to compensate for the inability
|
||||
* of the Camera API to zoom the preview stream beyond a certain point.
|
||||
* This gets called when the `Camera` emits a `zoomchanged` event and is
|
||||
* calculated by `Camera.prototype.getZoomPreviewAdjustment()`.
|
||||
*/
|
||||
setZoomPreviewAdjustment: function(zoomPreviewAdjustment) {
|
||||
if (this._useZoomPreviewAdjustment) {
|
||||
this.els.video.style.transform = 'scale(' + zoomPreviewAdjustment + ')';
|
||||
}
|
||||
},
|
||||
|
||||
stopStream: function() {
|
||||
this.els.video.mozSrcObject = null;
|
||||
},
|
||||
|
||||
fadeOut: function() {
|
||||
debug('fade-out');
|
||||
var self = this;
|
||||
this.hide();
|
||||
clearTimeout(this.fadeTimeout);
|
||||
this.fadeTimeout = setTimeout(function() {
|
||||
self.emit('fadedout');
|
||||
}, this.fadeTime);
|
||||
},
|
||||
|
||||
fadeIn: function(firstRun) {
|
||||
debug('fade-in');
|
||||
this.show();
|
||||
if (firstRun) {
|
||||
this.emit('fadedin');
|
||||
// Delay to prevent the duration from applying too early
|
||||
requestAnimationFrame(() => {
|
||||
this.el.style.transitionDuration = this.fadeTime + 'ms';
|
||||
});
|
||||
} else {
|
||||
this.fadeTimeout = setTimeout(this.firer('fadedin'), this.fadeTime);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers a quick shutter style animation.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
shutter: function() {
|
||||
this.el.classList.add('shutter');
|
||||
},
|
||||
|
||||
/**
|
||||
* Force a reflow before removing
|
||||
* the shutter class so that it
|
||||
* doesn't impact the animation.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
onShutterEnd: function() {
|
||||
this.reflow = this.el.offsetTop;
|
||||
this.el.classList.remove('shutter');
|
||||
},
|
||||
|
||||
/**
|
||||
* Sizes and positions the preview stream.
|
||||
*
|
||||
* @param {Object} preview
|
||||
* @param {Number} sensorAngle
|
||||
* @param {Boolean} mirrored
|
||||
*/
|
||||
updatePreview: function(preview, sensorAngle, mirrored) {
|
||||
if (!preview) { return; }
|
||||
var aspect;
|
||||
|
||||
// Invert dimensions if the camera's `sensorAngle` is
|
||||
// 0 or 180 degrees.
|
||||
if (sensorAngle % 180 === 0) {
|
||||
this.container = {
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
aspect: this.width / this.height
|
||||
};
|
||||
|
||||
aspect = preview.height / preview.width;
|
||||
} else {
|
||||
this.container = {
|
||||
width: this.height,
|
||||
height: this.width,
|
||||
aspect: this.height / this.width
|
||||
};
|
||||
|
||||
aspect = preview.width / preview.height;
|
||||
}
|
||||
|
||||
var shouldFill = aspect > this.container.aspect;
|
||||
var scaleType = this.scaleType || (shouldFill ? 'fill' : 'fit');
|
||||
|
||||
this.updatePreviewMetrics(preview, sensorAngle, mirrored, scaleType);
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates the correct sizing
|
||||
* depending on the chosen 'scaleType'.
|
||||
*
|
||||
* 'scale-type' attribute set as a styling hook.
|
||||
*
|
||||
* @param {Object} preview
|
||||
* @param {Number} sensorAngle
|
||||
* @param {Boolean} mirrored
|
||||
* @param {String} scaleType 'fill'|'fit'
|
||||
*/
|
||||
updatePreviewMetrics: function(preview, sensorAngle, mirrored, scaleType) {
|
||||
debug('update preview scaleType: %s', scaleType, preview);
|
||||
|
||||
// Calculate the correct scale to apply to the
|
||||
// preview to either 'fill' or 'fit' the viewfinder
|
||||
// container (always preserving the aspect ratio).
|
||||
var landscape = scaleSizeTo[scaleType](this.container, preview);
|
||||
var portrait = { width: landscape.height, height: landscape.width };
|
||||
|
||||
// Set the size of the frame to match 'portrait' dimensions
|
||||
this.els.frame.style.width = portrait.width + 'px';
|
||||
this.els.frame.style.height = portrait.height + 'px';
|
||||
|
||||
var transform = '';
|
||||
if (mirrored) {
|
||||
transform += 'scale(-1, 1) ';
|
||||
}
|
||||
|
||||
transform += 'rotate(' + sensorAngle + 'deg)';
|
||||
|
||||
// Set the size of the video container to match the
|
||||
// 'landscape' dimensions (CSS is used to rotate
|
||||
// the 'landscape' video stream to 'portrait')
|
||||
this.els.videoContainer.style.width = landscape.width + 'px';
|
||||
this.els.videoContainer.style.height = landscape.height + 'px';
|
||||
this.els.videoContainer.style.transform = transform;
|
||||
|
||||
// CSS aligns the contents slightly
|
||||
// differently depending on the scaleType
|
||||
this.set('scaleType', scaleType);
|
||||
|
||||
debug('updated preview size/position', landscape);
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div class="viewfinder-frame js-frame">' +
|
||||
'<div class="viewfinder-video-container js-video-container" ' +
|
||||
'aria-hidden="true">' +
|
||||
'<video class="viewfinder-video js-video"></video>' +
|
||||
'</div>' +
|
||||
'<div class="viewfinder-grid">' +
|
||||
'<div class="row"></div>' +
|
||||
'<div class="row middle"></div>' +
|
||||
'<div class="row"></div>' +
|
||||
'<div class="column left">' +
|
||||
'<div class="cell top"></div>' +
|
||||
'<div class="cell middle"></div>' +
|
||||
'<div class="cell bottom"></div>' +
|
||||
'</div>' +
|
||||
'<div class="column middle">' +
|
||||
'<div class="cell top"></div>' +
|
||||
'<div class="cell middle"></div>' +
|
||||
'<div class="cell bottom"></div>' +
|
||||
'</div>' +
|
||||
'<div class="column right">' +
|
||||
'<div class="cell top"></div>' +
|
||||
'<div class="cell middle"></div>' +
|
||||
'<div class="cell bottom"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
224
gaia/apps/camera/js/views/zoom-bar.js
Executable file
224
gaia/apps/camera/js/views/zoom-bar.js
Executable file
|
|
@ -0,0 +1,224 @@
|
|||
define(function(require, exports, module) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Dependencies
|
||||
*/
|
||||
|
||||
var debug = require('debug')('view:zoom-bar');
|
||||
var View = require('view');
|
||||
var bind = require('lib/bind');
|
||||
var orientation = require('lib/orientation');
|
||||
|
||||
/**
|
||||
* Locals
|
||||
*/
|
||||
|
||||
var lastTouch = null;
|
||||
|
||||
var clamp = function(value, minimum, maximum) {
|
||||
return Math.min(Math.max(value, minimum), maximum);
|
||||
};
|
||||
|
||||
module.exports = View.extend({
|
||||
name: 'zoom-bar',
|
||||
|
||||
initialize: function() {
|
||||
this.render();
|
||||
|
||||
this._orientation = orientation.get();
|
||||
|
||||
// Amount (%) to adjust the Zoom Bar by when tapping the min/max indicators
|
||||
this.zoomBarIndicatorInterval = 10;
|
||||
|
||||
// Amount of inactivity time (in milliseconds) to hide the Zoom Bar
|
||||
this.zoomBarInactivityTimeout = 3000;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.el.innerHTML = this.template();
|
||||
this.els.maxIndicator = this.find('.zoom-bar-max-indicator');
|
||||
this.els.minIndicator = this.find('.zoom-bar-min-indicator');
|
||||
this.els.inner = this.find('.zoom-bar-inner');
|
||||
this.els.track = this.find('.zoom-bar-track');
|
||||
this.els.scrubber = this.find('.zoom-bar-scrubber');
|
||||
|
||||
// Clean up
|
||||
delete this.template;
|
||||
|
||||
debug('rendered');
|
||||
return this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
bind(this.els.scrubber, 'touchstart', this.onTouchStart);
|
||||
bind(this.els.scrubber, 'touchmove', this.onTouchMove);
|
||||
bind(this.els.scrubber, 'touchend', this.onTouchEnd);
|
||||
bind(this.els.maxIndicator, 'click', this.onIncrement);
|
||||
bind(this.els.minIndicator, 'click', this.onDecrement);
|
||||
orientation.on('orientation', this.setOrientation);
|
||||
return this;
|
||||
},
|
||||
|
||||
template: function() {
|
||||
return '<div class="zoom-bar-max-indicator"></div>' +
|
||||
'<div class="zoom-bar-inner">' +
|
||||
'<div class="zoom-bar-track"></div>' +
|
||||
'<div class="zoom-bar-scrubber"></div>' +
|
||||
'</div>' +
|
||||
'<div class="zoom-bar-min-indicator"></div>';
|
||||
},
|
||||
|
||||
onTouchStart: function(evt) {
|
||||
lastTouch = evt.touches[0];
|
||||
this.resetInactivityTimeout();
|
||||
this.setScrubberActive(true);
|
||||
this._innerHeight = this.els.inner.offsetHeight;
|
||||
|
||||
evt.stopPropagation();
|
||||
},
|
||||
|
||||
onTouchMove: function(evt) {
|
||||
if (!lastTouch) {
|
||||
return;
|
||||
}
|
||||
|
||||
var touch = evt.touches[0];
|
||||
var deltaX = lastTouch.pageX - touch.pageX;
|
||||
var deltaY = lastTouch.pageY - touch.pageY;
|
||||
|
||||
var scale = 100 / this._innerHeight;
|
||||
|
||||
deltaX *= scale;
|
||||
deltaY *= scale;
|
||||
|
||||
switch (this._orientation) {
|
||||
case 0:
|
||||
this.setValue(this._value + deltaY, true);
|
||||
break;
|
||||
case 90:
|
||||
this.setValue(this._value + deltaX, true);
|
||||
break;
|
||||
case 180:
|
||||
this.setValue(this._value - deltaY, true);
|
||||
break;
|
||||
case 270:
|
||||
this.setValue(this._value - deltaX, true);
|
||||
break;
|
||||
}
|
||||
|
||||
lastTouch = touch;
|
||||
},
|
||||
|
||||
onTouchEnd: function(evt) {
|
||||
if (!lastTouch) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastTouch = null;
|
||||
this.resetInactivityTimeout();
|
||||
this.setScrubberActive(false);
|
||||
},
|
||||
|
||||
onIncrement: function(evt) {
|
||||
this.setValue(this._value + this.zoomBarIndicatorInterval, true);
|
||||
this.flashScrubberActive();
|
||||
evt.stopPropagation();
|
||||
},
|
||||
|
||||
onDecrement: function(evt) {
|
||||
this.setValue(this._value - this.zoomBarIndicatorInterval, true);
|
||||
this.flashScrubberActive();
|
||||
evt.stopPropagation();
|
||||
},
|
||||
|
||||
_orientation: 0,
|
||||
|
||||
setOrientation: function(orientation) {
|
||||
var el = this.el;
|
||||
el.classList.remove('zooming');
|
||||
|
||||
// Force ZoomBar to hide *immediately* on orientation change
|
||||
window.requestAnimationFrame(function() {
|
||||
el.style.transitionDuration = '0ms';
|
||||
window.requestAnimationFrame(function() {
|
||||
el.style.transitionDuration = '';
|
||||
});
|
||||
});
|
||||
|
||||
this._orientation = orientation;
|
||||
},
|
||||
|
||||
_value: 0,
|
||||
|
||||
setValue: function(value, emitChange) {
|
||||
this.resetInactivityTimeout();
|
||||
|
||||
var lastValue = this._value;
|
||||
this._value = clamp(value, 0, 100);
|
||||
if (this._value === lastValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._value === 0) {
|
||||
this.els.minIndicator.classList.add('active');
|
||||
} else {
|
||||
this.els.minIndicator.classList.remove('active');
|
||||
}
|
||||
|
||||
if (this._value === 100) {
|
||||
this.els.maxIndicator.classList.add('active');
|
||||
} else {
|
||||
this.els.maxIndicator.classList.remove('active');
|
||||
}
|
||||
|
||||
var self = this;
|
||||
window.requestAnimationFrame(function() {
|
||||
self.els.track.style.top = (100 - self._value) + '%';
|
||||
self.els.scrubber.style.bottom = self._value + '%';
|
||||
});
|
||||
|
||||
if (emitChange) {
|
||||
this.emit('change', this._value);
|
||||
}
|
||||
},
|
||||
|
||||
setScrubberActive: function(active) {
|
||||
window.clearTimeout(this._scrubberTimeout);
|
||||
|
||||
if (active) {
|
||||
this.els.scrubber.classList.add('active');
|
||||
} else {
|
||||
this.els.scrubber.classList.remove('active');
|
||||
}
|
||||
},
|
||||
|
||||
flashScrubberActive: function() {
|
||||
this.setScrubberActive(true);
|
||||
var self = this;
|
||||
this._scrubberTimeout = window.setTimeout(function() {
|
||||
self.setScrubberActive(false);
|
||||
}, 150);
|
||||
},
|
||||
|
||||
resetInactivityTimeout: function() {
|
||||
window.clearTimeout(this._inactivityTimeout);
|
||||
|
||||
this.show();
|
||||
|
||||
var self = this;
|
||||
this._inactivityTimeout = window.setTimeout(function() {
|
||||
self.hide();
|
||||
}, this.zoomBarInactivityTimeout);
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this.el.classList.add('zooming');
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
this.el.classList.remove('zooming');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
Loading…
Reference in a new issue