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

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

View 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>';
}
});
});

View 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';
}
});
});

View 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 = [];
}
});
});

View 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
View 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>';
}
});
});

View 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>';
}
});
});

View 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>'
});
});

View 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');
}
});
});

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

View 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>';
}
});
});

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

View 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>';
}
});
});

View 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>';
},
});
});

View 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>';
}
});
});

View 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>';
}
});
});

View 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');
}
});
});