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.
25
gaia/tv_apps/smart-home/bower.json
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "smart-home",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/mozilla-b2g/gaia/tree/master/tv_apps/smart-home",
|
||||
"description": "Smart Screen Home App",
|
||||
"main": "index.html",
|
||||
"license": "MPL",
|
||||
"private": true,
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests"
|
||||
],
|
||||
"dependencies": {
|
||||
"evt": "fxos-components/evt#~0.4.0",
|
||||
"smart-button": "fxos-components/smart-button#~0.0.2",
|
||||
"menu-group": "fxos-components/menu-group",
|
||||
"smart-icons": "smart-components/smart-icons#~0.0.2",
|
||||
"smart-bubbles": "smart-components/smart-bubbles",
|
||||
"smart-dialog": "smart-components/smart-dialog",
|
||||
"smart-modal-dialog": "smart-components/smart-modal-dialog"
|
||||
}
|
||||
}
|
||||
28
gaia/tv_apps/smart-home/bower_components/evt/.bower.json
vendored
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "evt",
|
||||
"main": "index.js",
|
||||
"version": "0.4.0",
|
||||
"homepage": "https://github.com/wilsonpage/evt",
|
||||
"authors": [
|
||||
"Wilson Page <wilsonpage@me.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"package.json",
|
||||
"test",
|
||||
"tests",
|
||||
"README.md"
|
||||
],
|
||||
"_release": "0.4.0",
|
||||
"_resolution": {
|
||||
"type": "version",
|
||||
"tag": "v0.4.0",
|
||||
"commit": "3593dd6b4dd19f683d00fd0eaf2581c11a128da6"
|
||||
},
|
||||
"_source": "git://github.com/gaia-components/evt.git",
|
||||
"_target": "~0.4.0",
|
||||
"_originalSource": "gaia-components/evt"
|
||||
}
|
||||
19
gaia/tv_apps/smart-home/bower_components/evt/bower.json
vendored
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "evt",
|
||||
"main": "index.js",
|
||||
"version": "0.4.0",
|
||||
"homepage": "https://github.com/wilsonpage/evt",
|
||||
"authors": [
|
||||
"Wilson Page <wilsonpage@me.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"package.json",
|
||||
"test",
|
||||
"tests",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
154
gaia/tv_apps/smart-home/bower_components/evt/index.js
vendored
Executable file
|
|
@ -0,0 +1,154 @@
|
|||
|
||||
/**
|
||||
* Evt
|
||||
*
|
||||
* A super lightweight
|
||||
* event emitter library.
|
||||
*
|
||||
* @version 0.3.3
|
||||
* @author Wilson Page <wilson.page@me.com>
|
||||
*/
|
||||
|
||||
;(function() {
|
||||
|
||||
/**
|
||||
* Locals
|
||||
*/
|
||||
|
||||
var proto = Events.prototype;
|
||||
var slice = [].slice;
|
||||
|
||||
/**
|
||||
* Creates a new event emitter
|
||||
* instance, or if passed an
|
||||
* object, mixes the event logic
|
||||
* into it.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @return {Object}
|
||||
*/
|
||||
function Events(obj) {
|
||||
if (!(this instanceof Events)) return new Events(obj);
|
||||
if (obj) return mixin(obj, proto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback
|
||||
* with an event name.
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {Function} cb
|
||||
* @return {Event}
|
||||
*/
|
||||
proto.on = function(name, cb) {
|
||||
this._cbs = this._cbs || {};
|
||||
(this._cbs[name] || (this._cbs[name] = [])).push(cb);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attach a callback that once
|
||||
* called, detaches itself.
|
||||
*
|
||||
* TODO: Implement `.off()` to work
|
||||
* with `once()` callbacks.
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {Function} cb
|
||||
* @public
|
||||
*/
|
||||
proto.once = function(name, cb) {
|
||||
this.on(name, one);
|
||||
function one() {
|
||||
cb.apply(this, arguments);
|
||||
this.off(name, one);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a single callback,
|
||||
* or all callbacks associated
|
||||
* with the passed event name.
|
||||
*
|
||||
* @param {String} name
|
||||
* @param {Function} cb
|
||||
* @return {Event}
|
||||
*/
|
||||
proto.off = function(name, cb) {
|
||||
this._cbs = this._cbs || {};
|
||||
|
||||
if (!name) { this._cbs = {}; return; }
|
||||
if (!cb) { return delete this._cbs[name]; }
|
||||
|
||||
var cbs = this._cbs[name] || [];
|
||||
var i;
|
||||
|
||||
while (cbs && ~(i = cbs.indexOf(cb))) { cbs.splice(i, 1); }
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fires an event, triggering
|
||||
* all callbacks registered on this
|
||||
* event name.
|
||||
*
|
||||
* @param {String} name
|
||||
* @return {Event}
|
||||
*/
|
||||
proto.fire = proto.emit = function(options) {
|
||||
var cbs = this._cbs = this._cbs || {};
|
||||
var name = options.name || options;
|
||||
var batch = (cbs[name] || []).concat(cbs['*'] || []);
|
||||
var ctx = options.ctx || this;
|
||||
|
||||
if (batch.length) {
|
||||
this._fireArgs = arguments;
|
||||
var args = slice.call(arguments, 1);
|
||||
while (batch.length) {
|
||||
batch.shift().apply(ctx, args);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
proto.firer = function(name) {
|
||||
var self = this;
|
||||
return function() {
|
||||
var args = slice.call(arguments);
|
||||
args.unshift(name);
|
||||
self.fire.apply(self, args);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Util
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mixes in the properties
|
||||
* of the second object into
|
||||
* the first.
|
||||
*
|
||||
* @param {Object} a
|
||||
* @param {Object} b
|
||||
* @return {Object}
|
||||
*/
|
||||
function mixin(a, b) {
|
||||
for (var key in b) a[key] = b[key];
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose 'Event' (UMD)
|
||||
*/
|
||||
|
||||
if (typeof exports === 'object') {
|
||||
module.exports = Events;
|
||||
} else if (typeof define === 'function' && define.amd) {
|
||||
define(function(){ return Events; });
|
||||
} else {
|
||||
window.evt = Events;
|
||||
}
|
||||
|
||||
})();
|
||||
29
gaia/tv_apps/smart-home/bower_components/menu-group/.bower.json
vendored
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "menu-group",
|
||||
"homepage": "https://github.com/gaia-components/menu-group",
|
||||
"main": "script.js",
|
||||
"license": "MPL",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests",
|
||||
"examples",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"devDependencies": {
|
||||
"evt": "gaia-components/evt#~0.4.0",
|
||||
"smart-button": "gaia-components/smart-button"
|
||||
},
|
||||
"_release": "088b750c3f",
|
||||
"_resolution": {
|
||||
"type": "branch",
|
||||
"branch": "master",
|
||||
"commit": "088b750c3fd32c898f2fc30125a41f59eaee1257"
|
||||
},
|
||||
"_source": "git://github.com/gaia-components/menu-group.git",
|
||||
"_target": "*",
|
||||
"_originalSource": "gaia-components/menu-group"
|
||||
}
|
||||
21
gaia/tv_apps/smart-home/bower_components/menu-group/bower.json
vendored
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "menu-group",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/gaia-components/menu-group",
|
||||
"main": "script.js",
|
||||
"license": "MPL",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests",
|
||||
"examples",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"devDependencies": {
|
||||
"evt": "gaia-components/evt#~0.4.0",
|
||||
"smart-button": "gaia-components/smart-button"
|
||||
}
|
||||
}
|
||||
128
gaia/tv_apps/smart-home/bower_components/menu-group/script.js
vendored
Executable file
|
|
@ -0,0 +1,128 @@
|
|||
'use strict';
|
||||
/* global evt */
|
||||
|
||||
window.MenuGroup = (function(win) {
|
||||
|
||||
// Extend from the HTMLElement prototype
|
||||
var proto = evt(Object.create(HTMLElement.prototype));
|
||||
|
||||
proto.createdCallback = function() {
|
||||
this.style.width = '0';
|
||||
this.classList.add('closed');
|
||||
this.addEventListener('transitionend', this);
|
||||
};
|
||||
|
||||
proto.calculateChildWidth = function() {
|
||||
var child = this.firstElementChild;
|
||||
var childWidth = 0;
|
||||
var style;
|
||||
while(child) {
|
||||
style = window.getComputedStyle(child);
|
||||
childWidth += child.offsetWidth + parseInt(style.marginLeft, 10) +
|
||||
parseInt(style.marginRight, 10);
|
||||
child = child.nextElementSibling;
|
||||
}
|
||||
return childWidth;
|
||||
};
|
||||
|
||||
proto.fireEvent = function(event, detail) {
|
||||
var evtObject = new CustomEvent(event, {
|
||||
bubbles: false,
|
||||
detail: detail || this
|
||||
});
|
||||
this.dispatchEvent(evtObject);
|
||||
this.fire(event, detail);
|
||||
};
|
||||
|
||||
proto.handleEvent = function(evt) {
|
||||
switch(evt.type) {
|
||||
// Like System app, the transition is our state machine.
|
||||
case 'transitionend':
|
||||
// We only process 'background-color' because all states have this
|
||||
// change.
|
||||
if ((evt.propertyName !== 'background-color' &&
|
||||
evt.propertyName !== 'width') ||
|
||||
evt.target !== this) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.classList.contains('enlarging')) {
|
||||
this.classList.remove('enlarging');
|
||||
this.classList.add('shrinking');
|
||||
// change to shrinking
|
||||
} else if (this.classList.contains('shrinking')) {
|
||||
// XXX: this is a workaround of CSS transform. We cannot have a
|
||||
// rotation right after resizing without any settimeout???
|
||||
setTimeout((function() {
|
||||
if (!this.classList.contains('shrinking')) {
|
||||
// If we don't have shrinking class here, that means this group
|
||||
// is changing to another state and we don't need to opening
|
||||
// state.
|
||||
return;
|
||||
}
|
||||
this.classList.remove('shrinking');
|
||||
this.classList.remove('closed');
|
||||
this.classList.add('opening');
|
||||
// change to opening
|
||||
this.style.width = this.calculateChildWidth() + 'px';
|
||||
}).bind(this));
|
||||
} else if (this.classList.contains('opening')) {
|
||||
this.classList.remove('opening');
|
||||
// final state: opened
|
||||
this.fireEvent('opened');
|
||||
} else if (this.classList.contains('closing')) {
|
||||
this.classList.remove('closing');
|
||||
this.classList.add('closed');
|
||||
// final state: closed
|
||||
this.fireEvent('closed');
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
proto.focus = proto.open = function() {
|
||||
this.fireEvent('will-open');
|
||||
// If we get focus when we closing the group, we need to cancel the closing
|
||||
// state.
|
||||
this.classList.remove('shrinking');
|
||||
this.classList.remove('opening');
|
||||
this.classList.remove('closing');
|
||||
this.classList.add('enlarging');
|
||||
};
|
||||
|
||||
proto.blur = proto.close = function() {
|
||||
this.fireEvent('will-close');
|
||||
// We may call close at mid state, we need to reset all of them and go to
|
||||
// closing state
|
||||
this.classList.remove('enlarging');
|
||||
this.classList.remove('shrinking');
|
||||
this.classList.remove('opening');
|
||||
this.classList.add('closing');
|
||||
this.style.width = '0';
|
||||
};
|
||||
|
||||
proto.changeIcon = function(icon) {
|
||||
if (!this.dataset.icon) {
|
||||
this.dataset.icon = icon;
|
||||
return;
|
||||
}
|
||||
var current = this.dataset.icon.split(' ')[0];
|
||||
if (current === icon) {
|
||||
return;
|
||||
}
|
||||
this.classList.add('switching-icon');
|
||||
this.dataset.icon = icon + ' ' + current;
|
||||
|
||||
// Let gecko to recalculate the value and remove it.
|
||||
// We shouldn't put any value at the setTimeout, but it doesn't work in some
|
||||
// device, like FirefoxNightly, b2g-desktop. The 100 ms works well in
|
||||
// these environments.
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
self.classList.remove('switching-icon');
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Register and return the constructor
|
||||
return document.registerElement('menu-group', { prototype: proto });
|
||||
})(window);
|
||||
136
gaia/tv_apps/smart-home/bower_components/menu-group/style.css
vendored
Executable file
|
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* Button state:
|
||||
* 1. normal (class name: [none])
|
||||
* 2. enlarging (class name: enlarging)
|
||||
* 3. shrinking (class name: shrinking)
|
||||
* 4. opening (class name: opening)
|
||||
* 5. closing (class name: closing)
|
||||
* 6. closed (class name: closed)
|
||||
**/
|
||||
|
||||
/**
|
||||
* focus: closed -> closed + enlarging -> closed + shrinking -> opening -> ''
|
||||
* close: '' -> closing -> closed;
|
||||
**/
|
||||
|
||||
menu-group {
|
||||
outline: 0;
|
||||
display: inline-block;
|
||||
height: 8rem;
|
||||
padding-left: 8rem;
|
||||
padding-right: 4rem;
|
||||
opacity: 1;
|
||||
/* we need to set border to none to remove the button border */
|
||||
border: none;
|
||||
/* the radius is still make button as round even if we don't have border */
|
||||
border-radius: 4rem;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
word-break: keep-all;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
|
||||
transition-duration: 0.42s;
|
||||
}
|
||||
|
||||
menu-group:before {
|
||||
content: " ";
|
||||
background-repeat: no-repeat;
|
||||
background-size: 5.4rem auto;
|
||||
background-position: 1.3rem center;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
|
||||
transition-duration: 0.42s;
|
||||
}
|
||||
|
||||
menu-group[rotate]:before {
|
||||
transform: rotate(360deg);
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
|
||||
transition-duration: 0.42s;
|
||||
}
|
||||
|
||||
menu-group[data-icon]:before {
|
||||
width: 5.4rem;
|
||||
left: 1.3rem;
|
||||
font-size: 5.4rem;
|
||||
line-height: 8rem;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
white-space: normal;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
menu-group[data-icon].switching-icon:before {
|
||||
font-size: 5.4rem;
|
||||
line-height: 8rem;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
/**
|
||||
* According to visual spec, we have falling down transition while icon
|
||||
* change.
|
||||
*
|
||||
* The trick is:
|
||||
* 1. to put the next icon at the top of current and slide icons down.
|
||||
* 2. to set transition duration to 0 here to elimiate the slide up
|
||||
* transition.
|
||||
**/
|
||||
top: -8rem;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
menu-group.closed {
|
||||
padding-right: 0;
|
||||
|
||||
transition-duration: 0.5s;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
menu-group.closed > * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
menu-group[rotate].closed:before {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
menu-group.opening {
|
||||
transition-duration: 0.72s;
|
||||
/* add the same padding-right of normal to make it run transition */
|
||||
padding-right: 4rem;
|
||||
}
|
||||
|
||||
menu-group.opening:before {
|
||||
transition-duration: 0.72s;
|
||||
}
|
||||
|
||||
menu-group.closing {
|
||||
transition-duration: 0.5s;
|
||||
padding-right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
menu-group[rotate].closing:before {
|
||||
transform: rotate(0deg);
|
||||
transition-duration: 0.5s;
|
||||
}
|
||||
|
||||
menu-group.enlarging {
|
||||
opacity: 1;
|
||||
background-color: #ffffff;
|
||||
transform: scale(1.2);
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
menu-group.shrinking {
|
||||
transition-duration: 0.23s;
|
||||
}
|
||||
30
gaia/tv_apps/smart-home/bower_components/smart-bubbles/.bower.json
vendored
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "smart-bubbles",
|
||||
"homepage": "https://github.com/smart-components/smart-bubbles",
|
||||
"main": "script.js",
|
||||
"license": "MPL",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests",
|
||||
"examples",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"gulpfile.js",
|
||||
"package.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"smart-button": "smart-components/smart-button"
|
||||
},
|
||||
"_release": "5c08ef5d84",
|
||||
"_resolution": {
|
||||
"type": "branch",
|
||||
"branch": "master",
|
||||
"commit": "5c08ef5d84b0bd1d8252600cfc744aa7869e9f92"
|
||||
},
|
||||
"_source": "git://github.com/smart-components/smart-bubbles.git",
|
||||
"_target": "*",
|
||||
"_originalSource": "smart-components/smart-bubbles"
|
||||
}
|
||||
22
gaia/tv_apps/smart-home/bower_components/smart-bubbles/bower.json
vendored
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "smart-bubbles",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/smart-components/smart-bubbles",
|
||||
"main": "script.js",
|
||||
"license": "MPL",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests",
|
||||
"examples",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"gulpfile.js",
|
||||
"package.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"smart-button": "smart-components/smart-button"
|
||||
}
|
||||
}
|
||||
83
gaia/tv_apps/smart-home/bower_components/smart-bubbles/script.js
vendored
Executable file
|
|
@ -0,0 +1,83 @@
|
|||
window.SmartBubbles = (function(win) {
|
||||
'use strict';
|
||||
|
||||
var DEFAULT_INTERVAL = 100;
|
||||
|
||||
// Extend from the HTMLElement prototype
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
proto.fireEvent = function(event, detail) {
|
||||
var evtObject = new CustomEvent(event, {
|
||||
bubbles: false,
|
||||
detail: detail || this
|
||||
});
|
||||
this.dispatchEvent(evtObject);
|
||||
};
|
||||
|
||||
proto.handleEvent = function(evt) {
|
||||
switch(evt.type) {
|
||||
case 'animationend':
|
||||
this.fireEvent('item-bubbled', ++this._finishedAnimationCount);
|
||||
if (this._targetAnimationCount === this._finishedAnimationCount) {
|
||||
this.playing = false;
|
||||
[].forEach.call(this._elements, function(elem) {
|
||||
elem.classList.remove('smart-bubble-playing');
|
||||
// XXX:Usually we use <smart-button> within container of
|
||||
// smart-bubbles. If we remove style of smart-bubble-playing,
|
||||
// Gecko would apply original style of smart-button instead and it
|
||||
// has transition property too. This results in another transition
|
||||
// happens on <smart-button> while we thought we'd just stop
|
||||
// animation. That is why we need to add 'smart-bubble-stopped' here
|
||||
// to prevent unexpected animation from happening.
|
||||
elem.classList.add('smart-bubble-stopped');
|
||||
// use getComputedStyle to force flushing re-style
|
||||
window.getComputedStyle(elem).animationName;
|
||||
elem.classList.remove('smart-bubble-stopped');
|
||||
window.getComputedStyle(elem).animationName;
|
||||
elem.style.animationDelay = '';
|
||||
}.bind(this));
|
||||
this._elements = null;
|
||||
this.fireEvent('all-items-bubbled');
|
||||
}
|
||||
evt.currentTarget.removeEventListener('animationend', this);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
proto.play = function(elements) {
|
||||
this.stopImmediately();
|
||||
this._elements = elements;
|
||||
this._finishedAnimationCount = 0;
|
||||
this._targetAnimationCount = 0;
|
||||
var interval = parseInt(this.getAttribute('interval'), 10) ||
|
||||
DEFAULT_INTERVAL;
|
||||
[].forEach.call(elements, function(elem) {
|
||||
this._targetAnimationCount++;
|
||||
var delay = this._targetAnimationCount * interval / 1000;
|
||||
elem.classList.add('smart-bubble-playing');
|
||||
elem.style.animationDelay = delay + 's';
|
||||
elem.addEventListener('animationend', this);
|
||||
}.bind(this));
|
||||
this.playing = true;
|
||||
};
|
||||
|
||||
proto.stopImmediately = function() {
|
||||
if (!this.playing || !this._elements) {
|
||||
return;
|
||||
}
|
||||
[].forEach.call(this._elements, function(elem) {
|
||||
elem.classList.remove('smart-bubble-playing');
|
||||
elem.classList.add('smart-bubble-stopped');
|
||||
elem.removeEventListener('animationend', this);
|
||||
// use getComputedStyle to force flushing re-style
|
||||
window.getComputedStyle(elem).animationName;
|
||||
elem.classList.remove('smart-bubble-stopped');
|
||||
}.bind(this));
|
||||
this._elements = null;
|
||||
this.playing = false;
|
||||
this.fireEvent('all-items-stopped');
|
||||
};
|
||||
|
||||
// Register and return the constructor
|
||||
return document.registerElement('smart-bubbles', { prototype: proto });
|
||||
})(window);
|
||||
1
gaia/tv_apps/smart-home/bower_components/smart-bubbles/script.min.js
vendored
Executable file
|
|
@ -0,0 +1 @@
|
|||
window.SmartBubbles=function(t){"use strict";var e=100,i=Object.create(HTMLElement.prototype);return i.fireEvent=function(t,e){var i=new CustomEvent(t,{bubbles:!1,detail:e||this});this.dispatchEvent(i)},i.handleEvent=function(t){switch(t.type){case"animationend":this.fireEvent("item-bubbled",++this._finishedAnimationCount),this._targetAnimationCount===this._finishedAnimationCount&&(this.playing=!1,[].forEach.call(this._elements,function(t){t.classList.remove("smart-bubble-playing"),t.classList.add("smart-bubble-stopped"),window.getComputedStyle(t).animationName,t.classList.remove("smart-bubble-stopped"),window.getComputedStyle(t).animationName,t.style.animationDelay=""}.bind(this)),this._elements=null,this.fireEvent("all-items-bubbled")),t.currentTarget.removeEventListener("animationend",this)}},i.play=function(t){this.stopImmediately(),this._elements=t,this._finishedAnimationCount=0,this._targetAnimationCount=0;var i=parseInt(this.getAttribute("interval"),10)||e;[].forEach.call(t,function(t){this._targetAnimationCount++;var e=this._targetAnimationCount*i/1e3;t.classList.add("smart-bubble-playing"),t.style.animationDelay=e+"s",t.addEventListener("animationend",this)}.bind(this)),this.playing=!0},i.stopImmediately=function(){this.playing&&this._elements&&([].forEach.call(this._elements,function(t){t.classList.remove("smart-bubble-playing"),t.classList.add("smart-bubble-stopped"),t.removeEventListener("animationend",this),window.getComputedStyle(t).animationName,t.classList.remove("smart-bubble-stopped")}.bind(this)),this._elements=null,this.playing=!1,this.fireEvent("all-items-stopped"))},document.registerElement("smart-bubbles",{prototype:i})}(window);
|
||||
83
gaia/tv_apps/smart-home/bower_components/smart-bubbles/src/script.js
vendored
Executable file
|
|
@ -0,0 +1,83 @@
|
|||
window.SmartBubbles = (function(win) {
|
||||
'use strict';
|
||||
|
||||
var DEFAULT_INTERVAL = 100;
|
||||
|
||||
// Extend from the HTMLElement prototype
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
proto.fireEvent = function(event, detail) {
|
||||
var evtObject = new CustomEvent(event, {
|
||||
bubbles: false,
|
||||
detail: detail || this
|
||||
});
|
||||
this.dispatchEvent(evtObject);
|
||||
};
|
||||
|
||||
proto.handleEvent = function(evt) {
|
||||
switch(evt.type) {
|
||||
case 'animationend':
|
||||
this.fireEvent('item-bubbled', ++this._finishedAnimationCount);
|
||||
if (this._targetAnimationCount === this._finishedAnimationCount) {
|
||||
this.playing = false;
|
||||
[].forEach.call(this._elements, function(elem) {
|
||||
elem.classList.remove('smart-bubble-playing');
|
||||
// XXX:Usually we use <smart-button> within container of
|
||||
// smart-bubbles. If we remove style of smart-bubble-playing,
|
||||
// Gecko would apply original style of smart-button instead and it
|
||||
// has transition property too. This results in another transition
|
||||
// happens on <smart-button> while we thought we'd just stop
|
||||
// animation. That is why we need to add 'smart-bubble-stopped' here
|
||||
// to prevent unexpected animation from happening.
|
||||
elem.classList.add('smart-bubble-stopped');
|
||||
// use getComputedStyle to force flushing re-style
|
||||
window.getComputedStyle(elem).animationName;
|
||||
elem.classList.remove('smart-bubble-stopped');
|
||||
window.getComputedStyle(elem).animationName;
|
||||
elem.style.animationDelay = '';
|
||||
}.bind(this));
|
||||
this._elements = null;
|
||||
this.fireEvent('all-items-bubbled');
|
||||
}
|
||||
evt.currentTarget.removeEventListener('animationend', this);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
proto.play = function(elements) {
|
||||
this.stopImmediately();
|
||||
this._elements = elements;
|
||||
this._finishedAnimationCount = 0;
|
||||
this._targetAnimationCount = 0;
|
||||
var interval = parseInt(this.getAttribute('interval'), 10) ||
|
||||
DEFAULT_INTERVAL;
|
||||
[].forEach.call(elements, function(elem) {
|
||||
this._targetAnimationCount++;
|
||||
var delay = this._targetAnimationCount * interval / 1000;
|
||||
elem.classList.add('smart-bubble-playing');
|
||||
elem.style.animationDelay = delay + 's';
|
||||
elem.addEventListener('animationend', this);
|
||||
}.bind(this));
|
||||
this.playing = true;
|
||||
};
|
||||
|
||||
proto.stopImmediately = function() {
|
||||
if (!this.playing || !this._elements) {
|
||||
return;
|
||||
}
|
||||
[].forEach.call(this._elements, function(elem) {
|
||||
elem.classList.remove('smart-bubble-playing');
|
||||
elem.classList.add('smart-bubble-stopped');
|
||||
elem.removeEventListener('animationend', this);
|
||||
// use getComputedStyle to force flushing re-style
|
||||
window.getComputedStyle(elem).animationName;
|
||||
elem.classList.remove('smart-bubble-stopped');
|
||||
}.bind(this));
|
||||
this._elements = null;
|
||||
this.playing = false;
|
||||
this.fireEvent('all-items-stopped');
|
||||
};
|
||||
|
||||
// Register and return the constructor
|
||||
return document.registerElement('smart-bubbles', { prototype: proto });
|
||||
})(window);
|
||||
26
gaia/tv_apps/smart-home/bower_components/smart-bubbles/src/style.css
vendored
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
*[smart-bubbles="true"] .smart-bubble-playing {
|
||||
transition-property: none;
|
||||
transform: none;
|
||||
animation: bubble-animation 0.5s cubic-bezier(0.25, 0, 0, 1) both;
|
||||
}
|
||||
|
||||
*[smart-bubbles="true"][smart-bubbles-direction="down"] .smart-bubble-playing {
|
||||
animation: sinking-animation 0.5s cubic-bezier(0.25, 0, 0, 1) both;
|
||||
}
|
||||
|
||||
*[smart-bubbles="true"] .smart-bubble-stopped {
|
||||
transition-property: none;
|
||||
transform: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes bubble-animation {
|
||||
0% {opacity: 0; transform: translateY(8rem);}
|
||||
50% {opacity: 1;}
|
||||
100% {transform: translateY(0);}
|
||||
}
|
||||
|
||||
@keyframes sinking-animation {
|
||||
0% {opacity: 1; transform: translateY(0);}
|
||||
100% {opacity: 0; transform: translateY(8rem);}
|
||||
}
|
||||
26
gaia/tv_apps/smart-home/bower_components/smart-bubbles/style.css
vendored
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
*[smart-bubbles="true"] .smart-bubble-playing {
|
||||
transition-property: none;
|
||||
transform: none;
|
||||
animation: bubble-animation 0.5s cubic-bezier(0.25, 0, 0, 1) both;
|
||||
}
|
||||
|
||||
*[smart-bubbles="true"][smart-bubbles-direction="down"] .smart-bubble-playing {
|
||||
animation: sinking-animation 0.5s cubic-bezier(0.25, 0, 0, 1) both;
|
||||
}
|
||||
|
||||
*[smart-bubbles="true"] .smart-bubble-stopped {
|
||||
transition-property: none;
|
||||
transform: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes bubble-animation {
|
||||
0% {opacity: 0; transform: translateY(8rem);}
|
||||
50% {opacity: 1;}
|
||||
100% {transform: translateY(0);}
|
||||
}
|
||||
|
||||
@keyframes sinking-animation {
|
||||
0% {opacity: 1; transform: translateY(0);}
|
||||
100% {opacity: 0; transform: translateY(8rem);}
|
||||
}
|
||||
26
gaia/tv_apps/smart-home/bower_components/smart-button/.bower.json
vendored
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "smart-button",
|
||||
"version": "0.0.2",
|
||||
"homepage": "https://github.com/gaia-components/smart-button",
|
||||
"main": "script.js",
|
||||
"license": "MPL",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests",
|
||||
"examples",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"_release": "0.0.2",
|
||||
"_resolution": {
|
||||
"type": "version",
|
||||
"tag": "v0.0.2",
|
||||
"commit": "04fdae935e2f2ba99d1c4f3e156d17f12eb2d142"
|
||||
},
|
||||
"_source": "git://github.com/gaia-components/smart-button.git",
|
||||
"_target": "~0.0.2",
|
||||
"_originalSource": "gaia-components/smart-button"
|
||||
}
|
||||
17
gaia/tv_apps/smart-home/bower_components/smart-button/bower.json
vendored
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "smart-button",
|
||||
"version": "0.0.2",
|
||||
"homepage": "https://github.com/gaia-components/smart-button",
|
||||
"main": "script.js",
|
||||
"license": "MPL",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests",
|
||||
"examples",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
62
gaia/tv_apps/smart-home/bower_components/smart-button/script.js
vendored
Executable file
|
|
@ -0,0 +1,62 @@
|
|||
'use strict';
|
||||
/* global KeyEvent */
|
||||
|
||||
window.SmartButton = (function(win) {
|
||||
// Extend from the HTMLButtonElement prototype
|
||||
var proto = Object.create(HTMLButtonElement.prototype);
|
||||
|
||||
proto.createdCallback = function() {
|
||||
this.addEventListener('mousedown', this);
|
||||
this.addEventListener('mouseup', this);
|
||||
this.addEventListener('touchstart', this);
|
||||
this.addEventListener('touchend', this);
|
||||
this.addEventListener('keydown', this);
|
||||
this.addEventListener('keyup', this);
|
||||
this.addEventListener('focus', this);
|
||||
this.addEventListener('blur', this);
|
||||
this.addEventListener('transitionend', this);
|
||||
this.tabIndex = 0;
|
||||
};
|
||||
|
||||
proto.handleEvent = function(evt) {
|
||||
switch(evt.type) {
|
||||
case 'mousedown':
|
||||
case 'touchstart':
|
||||
this.classList.add('pressed');
|
||||
break;
|
||||
case 'keydown':
|
||||
if (evt.keyCode === KeyEvent.DOM_VK_RETURN) {
|
||||
this.classList.add('pressed');
|
||||
}
|
||||
break;
|
||||
case 'mouseup':
|
||||
case 'touchend':
|
||||
this.classList.remove('pressed');
|
||||
this.classList.add('released');
|
||||
break;
|
||||
case 'keyup':
|
||||
if (evt.keyCode === KeyEvent.DOM_VK_RETURN) {
|
||||
this.classList.remove('pressed');
|
||||
this.classList.add('released');
|
||||
this.click();
|
||||
}
|
||||
break;
|
||||
case 'transitionend':
|
||||
if (this.classList.contains('released')) {
|
||||
this.classList.remove('released');
|
||||
}
|
||||
break;
|
||||
case 'focus':
|
||||
this.classList.add('focused');
|
||||
break;
|
||||
case 'blur':
|
||||
this.classList.remove('pressed');
|
||||
this.classList.remove('released');
|
||||
this.classList.remove('focused');
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Register and return the constructor
|
||||
return document.registerElement('smart-button', { prototype: proto });
|
||||
})(window);
|
||||
497
gaia/tv_apps/smart-home/bower_components/smart-button/style.css
vendored
Executable file
|
|
@ -0,0 +1,497 @@
|
|||
/**
|
||||
* Button state:
|
||||
* 1. normal
|
||||
* In this state, we don't add any class.
|
||||
* 2. focused (class name: focused)
|
||||
* The focused state may be triggered by keyboard or touch/mouse.
|
||||
* 3. pressed (class name: pressed)
|
||||
* This state is triggered by user pressed confirm button, mousedown,
|
||||
* and touchdown.
|
||||
* 4. released (class name: released)
|
||||
* This state is only for changing duration of transition. We enter this
|
||||
* state when user release the button. After transitionend, we goes back
|
||||
* to focused state.
|
||||
**/
|
||||
smart-button {
|
||||
font-family: "Fira Sans";
|
||||
display: inline-block;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
/* we need to set border to none to remove the button border */
|
||||
border: none;
|
||||
/* the radius is still make button as round even if we don't have border */
|
||||
border-radius: 50%;
|
||||
/* 50% is not a real circle because of rounding, we have to use a value larger
|
||||
than 50% */
|
||||
-moz-outline-radius: 60%;
|
||||
outline: transparent 0 solid;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 5.4rem auto;
|
||||
background-position: center center;
|
||||
font-style: italic;
|
||||
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
|
||||
transition-duration: 0.42s;
|
||||
}
|
||||
|
||||
smart-button.focused {
|
||||
outline: 0;
|
||||
background-color: #ffffff;
|
||||
transform: scale(1.2);
|
||||
transition-duration: 0.42s;
|
||||
}
|
||||
|
||||
smart-button.released {
|
||||
transition-duration: 0.16s;
|
||||
}
|
||||
|
||||
smart-button.toggled {
|
||||
background-color: #00caf2;
|
||||
}
|
||||
|
||||
smart-button.pressed {
|
||||
background-color: #00caf2;
|
||||
transform: scale(0.8);
|
||||
transition-duration: 0.06s;
|
||||
}
|
||||
|
||||
smart-button.disabled {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
smart-button[data-icon]:before {
|
||||
width: 5.4rem;
|
||||
left: 1.3rem;
|
||||
font-size: 5.4rem;
|
||||
line-height: 8rem;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
white-space: normal;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
smart-button[data-icon].focused:before {
|
||||
color: #2d2d2d;
|
||||
}
|
||||
|
||||
smart-button[data-icon].toggled:before {
|
||||
color: #00caf2;
|
||||
}
|
||||
|
||||
smart-button[data-icon].pressed:before {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
smart-button[data-icon].disabled:before {
|
||||
color: #ffffff;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple Text Button
|
||||
**/
|
||||
smart-button[type="simple-text"] {
|
||||
color: #dddddd;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
|
||||
width: auto;
|
||||
height: 8rem;
|
||||
|
||||
line-height: 8rem;
|
||||
font-size: 2.4rem;
|
||||
-moz-user-select: none;
|
||||
padding-left: 2.6rem;
|
||||
padding-right: 2.6rem;
|
||||
}
|
||||
|
||||
smart-button[type="simple-text"].focused {
|
||||
color: #ffffff;
|
||||
background: none;
|
||||
}
|
||||
|
||||
smart-button[type="simple-text"].toggled {
|
||||
color: #00caf2;
|
||||
}
|
||||
|
||||
smart-button[type="simple-text"].pressed {
|
||||
color: #00caf2;
|
||||
background: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* End of Simple Text Button
|
||||
**/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* underlined text button
|
||||
*/
|
||||
smart-button[type="underlined-text"] {
|
||||
position: relative;
|
||||
color: white;
|
||||
font-size: 2.4rem;
|
||||
padding-left: 3.8rem;
|
||||
padding-right: 3.8rem;
|
||||
border-radius: 4rem;
|
||||
width: auto;
|
||||
line-height: 8rem;
|
||||
margin-right: 1.4rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
smart-button[type="underlined-text"]:after {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
content: ' ';
|
||||
border-bottom: 0.6rem #61acd7 solid;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
smart-button[type="underlined-text"].focused {
|
||||
transform: scale(1.2);
|
||||
color: #2d2d2d;
|
||||
}
|
||||
|
||||
smart-button[type="underlined-text"].pressed {
|
||||
transform: scale(1);
|
||||
color: white;
|
||||
background-color: #00caf2;
|
||||
}
|
||||
|
||||
/**
|
||||
* End of underlined text button
|
||||
*/
|
||||
|
||||
/**
|
||||
* App Icons
|
||||
**/
|
||||
smart-button[type="app-button"] {
|
||||
/* To use 33.4rem instead of to use 33.6rem is to work around the anti-alias
|
||||
pixel. So, the circle is filled by the background almost totally. */
|
||||
width: 33.4rem;
|
||||
height: 33.4rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"].focused {
|
||||
transform: scale(1.4);
|
||||
/* To use outline will not change the size of button and make the whole button
|
||||
thrilling. */
|
||||
outline: 0.8rem solid #ffffff;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"].pressed {
|
||||
/* While scaling, we may have a gap between outline and icon which may be
|
||||
anti-alias pixel. So, we use scale(1.22) to prevent such kind of case */
|
||||
transform: scale(1.22);
|
||||
/* To use outline will not change the size of button and make the whole button
|
||||
thrilling. */
|
||||
outline: 1.0rem solid #ffffff;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"].fullsized {
|
||||
background-size: 33.6rem auto;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"].fitted {
|
||||
background-size: 22rem auto;
|
||||
}
|
||||
|
||||
/** we use :after as label */
|
||||
smart-button[type="app-button"]:after {
|
||||
content: attr(label);
|
||||
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: auto;
|
||||
right: auto;
|
||||
|
||||
width: 33.6rem;
|
||||
height: 8rem;
|
||||
|
||||
padding-top: 1rem;
|
||||
|
||||
line-height: 3rem;
|
||||
text-align: center;
|
||||
|
||||
font-size: 2.8rem;
|
||||
font-style: italic;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/**
|
||||
* text backgorund color
|
||||
**/
|
||||
smart-button[type="app-button"][app-type="deck"]:after {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"][app-type="tv"]:after {
|
||||
background-color: #d90c83;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"][app-type="app"]:after {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
smart-button[type="app-button"][app-type="device"]:after {
|
||||
background-color: #4a82ea;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"][app-type="folder"] {
|
||||
background-color: #595959;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"]:before {
|
||||
left: 0;
|
||||
line-height: 30rem;
|
||||
font-size: 16rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"][app-type="folder"]:before {
|
||||
color: #CCCCCC;
|
||||
}
|
||||
|
||||
/**
|
||||
* End of App Icons
|
||||
**/
|
||||
|
||||
/**
|
||||
* text and icon button
|
||||
*/
|
||||
smart-button[type="text-and-icon"] {
|
||||
position: relative;
|
||||
color: white;
|
||||
font-size: 2.4rem;
|
||||
padding-left: 4.6rem;
|
||||
padding-right: 4.6rem;
|
||||
border-radius: 4rem;
|
||||
width: auto;
|
||||
line-height: 8rem;
|
||||
margin-right: 1.4rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
smart-button[type="text-and-icon"][data-icon] {
|
||||
padding-left: 9.6rem;
|
||||
}
|
||||
|
||||
smart-button[type="text-and-icon"][data-icon]:before {
|
||||
left: 3.8rem;
|
||||
}
|
||||
|
||||
smart-button[type="text-and-icon"].focused {
|
||||
transform: scale(1.2);
|
||||
color: #2d2d2d;
|
||||
}
|
||||
|
||||
smart-button[type="text-and-icon"].pressed {
|
||||
transform: scale(1);
|
||||
color: white;
|
||||
background-color: #00caf2;
|
||||
}
|
||||
|
||||
/**
|
||||
* End of text and icon button
|
||||
*/
|
||||
|
||||
/**
|
||||
* banner button
|
||||
**/
|
||||
smart-button[type="banner-button"] {
|
||||
position: relative;
|
||||
|
||||
color: #ffffff;
|
||||
font-size: 2rem;
|
||||
|
||||
width: 36rem;
|
||||
height: 20rem;
|
||||
overflow: hidden;
|
||||
|
||||
border-radius: 0;
|
||||
border: 0 solid #ffffff;
|
||||
|
||||
background-color: #5f5f5f;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 120%;
|
||||
|
||||
box-sizing: border-box;
|
||||
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1.0);
|
||||
transition-duration: 0.1s;
|
||||
}
|
||||
|
||||
smart-button[type="banner-button"][label]:before {
|
||||
content: attr(label);
|
||||
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
height: 5rem;
|
||||
|
||||
line-height: 5rem;
|
||||
text-align: center;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
smart-button[type="banner-button"]:after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
|
||||
transition-duration: 0.06s;
|
||||
background-color: rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
smart-button[type="banner-button"].focused {
|
||||
transform: none;
|
||||
border-width: 1.2rem;
|
||||
background-size: 100%;
|
||||
transition-duration: 0.24s;
|
||||
}
|
||||
|
||||
smart-button[type="banner-button"].pressed {
|
||||
transform: scale(0.945, 0.9);
|
||||
}
|
||||
|
||||
smart-button[type="banner-button"].pressed:after {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* banner button
|
||||
**/
|
||||
|
||||
/**
|
||||
* circle text button
|
||||
*/
|
||||
smart-button[type="circle-text"] {
|
||||
width: 21rem;
|
||||
height: 21rem;
|
||||
color: white;
|
||||
background-color: #5f6368;
|
||||
line-height: 21rem;
|
||||
padding: 2.5rem 2rem 1.5rem 2rem;
|
||||
text-align: center;
|
||||
font-size: 3.2rem;
|
||||
font-weight: bolder;
|
||||
overflow: hidden;
|
||||
transition-property: transform, color, background-color;
|
||||
}
|
||||
|
||||
smart-button[type="circle-text"].focused {
|
||||
transform: scale(1.2);
|
||||
color: #2d2d2d;
|
||||
background-color: white;
|
||||
transition-property: transform, color, background-color;
|
||||
}
|
||||
|
||||
smart-button[type="circle-text"].pressed {
|
||||
transform: scale(1);
|
||||
color: white;
|
||||
background-color: #00caf2;
|
||||
transition-property: transform, color, background-color;
|
||||
}
|
||||
|
||||
smart-button[type="circle-text"].primary {
|
||||
background-color: #00caf2;
|
||||
transition-property: transform, color, background-color;
|
||||
}
|
||||
|
||||
smart-button[type="circle-text"].primary.focused {
|
||||
color: #ffffff;
|
||||
background-color: #00caf2;
|
||||
transition-property: transform, color, background-color;
|
||||
}
|
||||
|
||||
smart-button[type="circle-text"].primary.pressed {
|
||||
background-color: #0097b5;
|
||||
transition-property: transform, color, background-color;
|
||||
}
|
||||
|
||||
smart-button[type="circle-text"].danger {
|
||||
background-color: #e63d2f;
|
||||
transition-property: transform, color, background-color;
|
||||
}
|
||||
|
||||
smart-button[type="circle-text"].danger.focused {
|
||||
color: #ffffff;
|
||||
background-color: #e63d2f;
|
||||
transition-property: transform, color, background-color;
|
||||
}
|
||||
|
||||
smart-button[type="circle-text"].danger.pressed {
|
||||
background-color: #ac2e23;
|
||||
transition-property: transform, color, background-color;
|
||||
}
|
||||
|
||||
/**
|
||||
* End of circle text button
|
||||
*/
|
||||
|
||||
/**
|
||||
* rounded-corner text button
|
||||
*/
|
||||
smart-button[type="rounded-corner-text"] {
|
||||
-moz-user-select: none;
|
||||
width: auto;
|
||||
height: 8rem;
|
||||
border-radius: 4rem;
|
||||
background-color: #5f6368;
|
||||
padding: 0 3.8rem;
|
||||
line-height: 8rem;
|
||||
color: white;
|
||||
text-align: center;
|
||||
font-size: 2.4rem;
|
||||
overflow: hidden;
|
||||
transition-property: transform, color, background-color;
|
||||
}
|
||||
|
||||
smart-button[type="rounded-corner-text"].focused,
|
||||
smart-button[type="rounded-corner-text"]:hover {
|
||||
transform: scale(1.2);
|
||||
color: #2d2d2d;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
smart-button[type="rounded-corner-text"].pressed {
|
||||
transform: scale(1);
|
||||
/*color: white;*/
|
||||
background-color: #00caf2;
|
||||
}
|
||||
|
||||
smart-button[type="rounded-corner-text"].primary {
|
||||
background-color: #00caf2;
|
||||
}
|
||||
|
||||
smart-button[type="rounded-corner-text"].primary:hover,
|
||||
smart-button[type="rounded-corner-text"].primary.focused {
|
||||
color: #ffffff;
|
||||
background-color: #00caf2;
|
||||
}
|
||||
|
||||
smart-button[type="rounded-corner-text"].primary.pressed {
|
||||
background-color: #0097b5;
|
||||
}
|
||||
|
||||
/**
|
||||
* End of rounded-corner text button
|
||||
*/
|
||||
26
gaia/tv_apps/smart-home/bower_components/smart-dialog/.bower.json
vendored
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "smart-dialog",
|
||||
"homepage": "https://github.com/smart-components/smart-dialog",
|
||||
"main": "script.js",
|
||||
"license": "MPL",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests",
|
||||
"examples",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"_release": "8910d99b8f",
|
||||
"_resolution": {
|
||||
"type": "branch",
|
||||
"branch": "master",
|
||||
"commit": "8910d99b8fb7c624b32ac494fea63c0898e886df"
|
||||
},
|
||||
"_source": "git://github.com/smart-components/smart-dialog.git",
|
||||
"_target": "*",
|
||||
"_originalSource": "smart-components/smart-dialog",
|
||||
"_direct": true
|
||||
}
|
||||
17
gaia/tv_apps/smart-home/bower_components/smart-dialog/bower.json
vendored
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "smart-dialog",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/smart-components/smart-dialog",
|
||||
"main": "script.js",
|
||||
"license": "MPL",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests",
|
||||
"examples",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
123
gaia/tv_apps/smart-home/bower_components/smart-dialog/script.js
vendored
Executable file
|
|
@ -0,0 +1,123 @@
|
|||
/**
|
||||
* Smart-Dialog
|
||||
* - Event:
|
||||
* 1. will-open
|
||||
* 2. opened
|
||||
* 3. will-close
|
||||
* 4. closed
|
||||
* - API:
|
||||
* 1. open - open a smart-dialog: closed -> opening -> opened
|
||||
* 2. close - close a smart-dialog: opened -> closing -> closed
|
||||
* - Attribute:
|
||||
* 1. title - title of the smart-dialog
|
||||
* 2. esc-close - close dialog when ESC key is clicked (default true).
|
||||
* Default scale: 1920x1080 (2*sqrt(960*960 + 1080*1080) / 20 = 144.5)
|
||||
*/
|
||||
|
||||
window.SmartDialog = (function(win) {
|
||||
'use strict';
|
||||
|
||||
// The value of openDuration and closeDuration should be the same as
|
||||
// transitionDuration in opening and closing css class.
|
||||
const openDuration = 1000;
|
||||
const closeDuration = 650;
|
||||
|
||||
// Extend from the HTMLElement prototype
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
proto.createdCallback = function sd_createdCallback() {
|
||||
this.addEventListener('transitionend', this);
|
||||
document.addEventListener('keyup', this);
|
||||
this.transitionTimer = null;
|
||||
this.classList.add('closed');
|
||||
if (this.hasAttribute('background-color')) {
|
||||
this.style.backgroundColor = this.getAttribute('background-color');
|
||||
}
|
||||
};
|
||||
|
||||
proto._onOpened = function sd_onOpened() {
|
||||
this.classList.remove('opening');
|
||||
this.classList.add('opened');
|
||||
this.fireEvent('opened');
|
||||
};
|
||||
|
||||
proto._onClosed = function sd_onClosed() {
|
||||
this.classList.remove('closing');
|
||||
this.classList.add('closed');
|
||||
this.fireEvent('closed');
|
||||
};
|
||||
|
||||
proto.open = function sd_open() {
|
||||
if (this.classList.contains('opened') ||
|
||||
this.classList.contains('opening')) {
|
||||
return;
|
||||
}
|
||||
this.fireEvent('will-open');
|
||||
this.classList.remove('closing');
|
||||
this.classList.remove('closed');
|
||||
this.classList.add('opening');
|
||||
// handle the case when transitionend event is not fired
|
||||
clearTimeout(this.transitionTimer);
|
||||
this.transitionTimer = setTimeout(function() {
|
||||
this._onOpened();
|
||||
this.transitionTimer = null;
|
||||
}.bind(this), openDuration);
|
||||
};
|
||||
|
||||
proto.close = function sd_close() {
|
||||
if (this.classList.contains('closed') ||
|
||||
this.classList.contains('closing')) {
|
||||
return;
|
||||
}
|
||||
this.fireEvent('will-close');
|
||||
this.classList.remove('opening');
|
||||
this.classList.remove('opened');
|
||||
this.classList.add('closing');
|
||||
// handle the case when transitionend event is not fired
|
||||
clearTimeout(this.transitionTimer);
|
||||
this.transitionTimer = setTimeout(function() {
|
||||
this._onClosed();
|
||||
this.transitionTimer = null;
|
||||
}.bind(this), closeDuration);
|
||||
};
|
||||
|
||||
proto.handleEvent = function sd_handleEvent(evt) {
|
||||
|
||||
switch(evt.type) {
|
||||
case 'transitionend':
|
||||
if (evt.target === this && this.transitionTimer) {
|
||||
if (this.classList.contains('opening')) {
|
||||
this._onOpened();
|
||||
} else if (this.classList.contains('closing')) {
|
||||
this._onClosed();
|
||||
}
|
||||
clearTimeout(this.transitionTimer);
|
||||
this.transitionTimer = null;
|
||||
}
|
||||
break;
|
||||
case 'keyup':
|
||||
// close dialog when Back is triggered
|
||||
if (this.getAttribute('esc-close') != 'false' &&
|
||||
(evt.keyCode === window.KeyEvent.DOM_VK_BACK_SPACE ||
|
||||
evt.key === 'Backspace') &&
|
||||
this.classList.contains('opened')) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.close();
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
proto.fireEvent = function sd_fireEvent(event, detail) {
|
||||
var evtObject = new CustomEvent(event, {
|
||||
bubbles: false,
|
||||
detail: detail || this
|
||||
});
|
||||
this.dispatchEvent(evtObject);
|
||||
};
|
||||
|
||||
// Register and return the constructor
|
||||
return document.registerElement('smart-dialog', { prototype: proto });
|
||||
})(window);
|
||||
103
gaia/tv_apps/smart-home/bower_components/smart-dialog/style.css
vendored
Executable file
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* Smart-Dialog
|
||||
* - State:
|
||||
* 1. closed
|
||||
* 2. opening
|
||||
* 3. opened - content visible
|
||||
* 4. closing
|
||||
* Default scale: 1920x1080 (2*sqrt(960*960 + 1080*1080) / 20 = 144.5)
|
||||
*/
|
||||
|
||||
smart-dialog {
|
||||
position: fixed;
|
||||
z-index: 2147483647;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
smart-dialog:before {
|
||||
content: attr(title);
|
||||
display: block;
|
||||
margin-top: 5rem;
|
||||
margin-left: 7.6rem;
|
||||
font-family: "Fira Sans";
|
||||
font-size: 5.4rem;
|
||||
font-style: italic;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
smart-dialog:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: fixed;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
top: calc(100% - 1rem);
|
||||
left: calc(50% - 1rem);
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
transition: none;
|
||||
transform: scale(20);
|
||||
}
|
||||
|
||||
smart-dialog.closed > *,
|
||||
smart-dialog.closing > *,
|
||||
smart-dialog.opening > *,
|
||||
smart-dialog.closed:before,
|
||||
smart-dialog.closing:before,
|
||||
smart-dialog.opening:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
smart-dialog.closed {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
smart-dialog.opened {
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
smart-dialog.opened:after {
|
||||
visibility: hidden;
|
||||
border-radius: 50%;
|
||||
transform: scale(144.5);
|
||||
}
|
||||
|
||||
smart-dialog.opening:after {
|
||||
border-radius: 50%;
|
||||
transform: scale(144.5);
|
||||
/* transitionDuration should be the same as openDuration variable in script */
|
||||
transition: transform 1s cubic-bezier(0, 0, 0 ,1);
|
||||
}
|
||||
|
||||
smart-dialog.closing:after {
|
||||
border-radius: 50%;
|
||||
transform: scale(20);
|
||||
/* transitionDuration should be the same as closeDuration variable in script */
|
||||
transition: transform 0.65s cubic-bezier(0.6, 0, 1 ,1);
|
||||
}
|
||||
|
||||
/* 1280x800 for flatfish */
|
||||
@media (width: 1280px) and (height: 800px) {
|
||||
smart-dialog:after,
|
||||
smart-dialog.closing:after {
|
||||
transform: scale(7.5);
|
||||
}
|
||||
|
||||
smart-dialog.opening:after {
|
||||
transform: scale(102.5);
|
||||
}
|
||||
}
|
||||
|
||||
/* 800x1280 for flatfish vertical */
|
||||
@media (width: 800px) and (height: 1280px) {
|
||||
smart-dialog:after,
|
||||
smart-dialog.closing:after {
|
||||
transform: scale(8.3);
|
||||
}
|
||||
|
||||
smart-dialog.opening:after {
|
||||
transform: scale(134);
|
||||
}
|
||||
}
|
||||
30
gaia/tv_apps/smart-home/bower_components/smart-icons/.bower.json
vendored
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "smart-icons",
|
||||
"version": "0.0.2",
|
||||
"homepage": "https://github.com/smart-components/smart-icons",
|
||||
"description": "Icon font for smart-screen",
|
||||
"license": "MPL",
|
||||
"private": true,
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"templates",
|
||||
"images",
|
||||
"test",
|
||||
"tests",
|
||||
"index.html",
|
||||
"Gruntfile.js",
|
||||
"README.md",
|
||||
"package.json"
|
||||
],
|
||||
"_release": "0.0.2",
|
||||
"_resolution": {
|
||||
"type": "version",
|
||||
"tag": "v0.0.2",
|
||||
"commit": "f3c42aa51a2765fbae61630bc7a3d39d8ead4737"
|
||||
},
|
||||
"_source": "git://github.com/smart-components/smart-icons.git",
|
||||
"_target": "~0.0.2",
|
||||
"_originalSource": "smart-components/smart-icons"
|
||||
}
|
||||
21
gaia/tv_apps/smart-home/bower_components/smart-icons/bower.json
vendored
Executable file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "smart-icons",
|
||||
"version": "0.0.2",
|
||||
"homepage": "https://github.com/smart-components/smart-icons",
|
||||
"description": "Icon font for smart-screen",
|
||||
"license": "MPL",
|
||||
"private": true,
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"templates",
|
||||
"images",
|
||||
"test",
|
||||
"tests",
|
||||
"index.html",
|
||||
"Gruntfile.js",
|
||||
"README.md",
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
BIN
gaia/tv_apps/smart-home/bower_components/smart-icons/fonts/smart-icons.ttf
vendored
Executable file
25
gaia/tv_apps/smart-home/bower_components/smart-icons/smart-icons-embedded.css
vendored
Executable file
25
gaia/tv_apps/smart-home/bower_components/smart-icons/smart-icons.css
vendored
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
/* Generated by grunt-webfont */
|
||||
/* Based on https://github.com/endtwist/fontcustom/blob/master/lib/fontcustom/templates/fontcustom.css */
|
||||
|
||||
|
||||
@font-face {
|
||||
font-family: "smart-icons";
|
||||
src: url("fonts/smart-icons.ttf") format("truetype");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
|
||||
[data-icon]:before,
|
||||
.ligature-icons {
|
||||
font-family: "smart-icons";
|
||||
content: attr(data-icon);
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
text-decoration: inherit;
|
||||
text-transform: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-size: 30px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
31
gaia/tv_apps/smart-home/bower_components/smart-modal-dialog/.bower.json
vendored
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "smart-modal-dialog",
|
||||
"homepage": "https://github.com/smart-components/smart-modal-dialog",
|
||||
"main": "script.js",
|
||||
"license": "MPL",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests",
|
||||
"examples",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"dependencies": {
|
||||
"smart-dialog": "smart-components/smart-dialog",
|
||||
"smart-bubbles": "smart-components/smart-bubbles",
|
||||
"smart-button": "smart-components/smart-button"
|
||||
},
|
||||
"_release": "1e5b8b14db",
|
||||
"_resolution": {
|
||||
"type": "branch",
|
||||
"branch": "master",
|
||||
"commit": "1e5b8b14db99c0d5ae6cefd0b8b5ac9a81942321"
|
||||
},
|
||||
"_source": "git://github.com/smart-components/smart-modal-dialog.git",
|
||||
"_target": "*",
|
||||
"_originalSource": "smart-components/smart-modal-dialog",
|
||||
"_direct": true
|
||||
}
|
||||
362
gaia/tv_apps/smart-home/bower_components/smart-modal-dialog/LICENSE
vendored
Executable file
|
|
@ -0,0 +1,362 @@
|
|||
Mozilla Public License, version 2.0
|
||||
|
||||
1. Definitions
|
||||
|
||||
1.1. "Contributor"
|
||||
|
||||
means each individual or legal entity that creates, contributes to the
|
||||
creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
|
||||
means the combination of the Contributions of others (if any) used by a
|
||||
Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
|
||||
means Source Code Form to which the initial Contributor has attached the
|
||||
notice in Exhibit A, the Executable Form of such Source Code Form, and
|
||||
Modifications of such Source Code Form, in each case including portions
|
||||
thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
a. that the initial Contributor has attached the notice described in
|
||||
Exhibit B to the Covered Software; or
|
||||
|
||||
b. that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the terms of
|
||||
a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
|
||||
means a work that combines Covered Software with other material, in a
|
||||
separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
|
||||
means having the right to grant, to the maximum extent possible, whether
|
||||
at the time of the initial grant or subsequently, any and all of the
|
||||
rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
|
||||
means any of the following:
|
||||
|
||||
a. any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered Software; or
|
||||
|
||||
b. any new file in Source Code Form that contains any Covered Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the License,
|
||||
by the making, using, selling, offering for sale, having made, import,
|
||||
or transfer of either its Contributions or its Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
|
||||
means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||
General Public License, Version 2.1, the GNU Affero General Public
|
||||
License, Version 3.0, or any later versions of those licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that controls, is
|
||||
controlled by, or is under common control with You. For purposes of this
|
||||
definition, "control" means (a) the power, direct or indirect, to cause
|
||||
the direction or management of such entity, whether by contract or
|
||||
otherwise, or (b) ownership of more than fifty percent (50%) of the
|
||||
outstanding shares or beneficial ownership of such entity.
|
||||
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
a. under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
b. under Patent Claims of such Contributor to make, use, sell, offer for
|
||||
sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
a. for any code that a Contributor has removed from Covered Software; or
|
||||
|
||||
b. for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
c. under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights to
|
||||
grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
|
||||
Section 2.1.
|
||||
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
a. such Covered Software must also be made available in Source Code Form,
|
||||
as described in Section 3.1, and You must inform recipients of the
|
||||
Executable Form how they can obtain a copy of such Source Code Form by
|
||||
reasonable means in a timely manner, at a charge no more than the cost
|
||||
of distribution to the recipient; and
|
||||
|
||||
b. You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter the
|
||||
recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty, or
|
||||
limitations of liability) contained within the Source Code Form of the
|
||||
Covered Software, except that You may alter any license notices to the
|
||||
extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this License
|
||||
with respect to some or all of the Covered Software due to statute,
|
||||
judicial order, or regulation then You must: (a) comply with the terms of
|
||||
this License to the maximum extent possible; and (b) describe the
|
||||
limitations and the code they affect. Such description must be placed in a
|
||||
text file included with all distributions of the Covered Software under
|
||||
this License. Except to the extent prohibited by statute or regulation,
|
||||
such description must be sufficiently detailed for a recipient of ordinary
|
||||
skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically if You
|
||||
fail to comply with any of its terms. However, if You become compliant,
|
||||
then the rights granted under this License from a particular Contributor
|
||||
are reinstated (a) provisionally, unless and until such Contributor
|
||||
explicitly and finally terminates Your grants, and (b) on an ongoing
|
||||
basis, if such Contributor fails to notify You of the non-compliance by
|
||||
some reasonable means prior to 60 days after You have come back into
|
||||
compliance. Moreover, Your grants from a particular Contributor are
|
||||
reinstated on an ongoing basis if such Contributor notifies You of the
|
||||
non-compliance by some reasonable means, this is the first time You have
|
||||
received notice of non-compliance with this License from such
|
||||
Contributor, and You become compliant prior to 30 days after Your receipt
|
||||
of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
|
||||
license agreements (excluding distributors and resellers) which have been
|
||||
validly granted by You or Your distributors under this License prior to
|
||||
termination shall survive termination.
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
Covered Software is provided under this License on an "as is" basis,
|
||||
without warranty of any kind, either expressed, implied, or statutory,
|
||||
including, without limitation, warranties that the Covered Software is free
|
||||
of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
The entire risk as to the quality and performance of the Covered Software
|
||||
is with You. Should any Covered Software prove defective in any respect,
|
||||
You (not any Contributor) assume the cost of any necessary servicing,
|
||||
repair, or correction. This disclaimer of warranty constitutes an essential
|
||||
part of this License. No use of any Covered Software is authorized under
|
||||
this License except under this disclaimer.
|
||||
|
||||
7. Limitation of Liability
|
||||
|
||||
Under no circumstances and under no legal theory, whether tort (including
|
||||
negligence), contract, or otherwise, shall any Contributor, or anyone who
|
||||
distributes Covered Software as permitted above, be liable to You for any
|
||||
direct, indirect, special, incidental, or consequential damages of any
|
||||
character including, without limitation, damages for lost profits, loss of
|
||||
goodwill, work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses, even if such party shall have been
|
||||
informed of the possibility of such damages. This limitation of liability
|
||||
shall not apply to liability for death or personal injury resulting from
|
||||
such party's negligence to the extent applicable law prohibits such
|
||||
limitation. Some jurisdictions do not allow the exclusion or limitation of
|
||||
incidental or consequential damages, so this exclusion and limitation may
|
||||
not apply to You.
|
||||
|
||||
8. Litigation
|
||||
|
||||
Any litigation relating to this License may be brought only in the courts
|
||||
of a jurisdiction where the defendant maintains its principal place of
|
||||
business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions. Nothing
|
||||
in this Section shall prevent a party's ability to bring cross-claims or
|
||||
counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides that
|
||||
the language of a contract shall be construed against the drafter shall not
|
||||
be used to construe this License against a Contributor.
|
||||
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses If You choose to distribute Source Code Form that is
|
||||
Incompatible With Secondary Licenses under the terms of this version of
|
||||
the License, the notice described in Exhibit B of this License must be
|
||||
attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the
|
||||
terms of the Mozilla Public License, v.
|
||||
2.0. If a copy of the MPL was not
|
||||
distributed with this file, You can
|
||||
obtain one at
|
||||
http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular file,
|
||||
then You may include the notice in a location (such as a LICENSE file in a
|
||||
relevant directory) where a recipient would be likely to look for such a
|
||||
notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
This Source Code Form is "Incompatible
|
||||
With Secondary Licenses", as defined by
|
||||
the Mozilla Public License, v. 2.0.
|
||||
22
gaia/tv_apps/smart-home/bower_components/smart-modal-dialog/bower.json
vendored
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "smart-modal-dialog",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/smart-components/smart-modal-dialog",
|
||||
"main": "script.js",
|
||||
"license": "MPL",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests",
|
||||
"examples",
|
||||
"LICENSE",
|
||||
"README.md"
|
||||
],
|
||||
"dependencies": {
|
||||
"smart-dialog": "smart-components/smart-dialog",
|
||||
"smart-bubbles": "smart-components/smart-bubbles",
|
||||
"smart-button": "smart-components/smart-button"
|
||||
}
|
||||
}
|
||||
194
gaia/tv_apps/smart-home/bower_components/smart-modal-dialog/script.js
vendored
Executable file
|
|
@ -0,0 +1,194 @@
|
|||
/**
|
||||
* SmartModalDialog creates a dialog with a group of buttons as select
|
||||
* options. It also includes open/close animation and bubbling animation.
|
||||
* The template is shown below:
|
||||
* <smart-dialog class="modal-dialog">
|
||||
* <div class="outer-container">
|
||||
* <div class="container">
|
||||
* <div class="modal-dialog-message">
|
||||
* </div>
|
||||
* <div class="modal-dialog-button-group">
|
||||
* <smart-button></smart-button>
|
||||
* ...
|
||||
* <smart-button></smart-button>
|
||||
* </div>
|
||||
* </div>
|
||||
* </div>
|
||||
* </smart-dialog>
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
function SmartModalDialog(container) {
|
||||
|
||||
// Determine whether this dialog is opened or not
|
||||
this.isOpened = false;
|
||||
this._clickedIndex = null;
|
||||
|
||||
this.smartBubble = document.createElement('smart-bubbles');
|
||||
|
||||
this.element = document.createElement('smart-dialog');
|
||||
this.element.classList.add('modal-dialog');
|
||||
// make dialog focusable in order to catch focus from mouse click or touch
|
||||
// event.
|
||||
this.element.setAttribute('tabIndex', '-1');
|
||||
|
||||
// in order to make vertical align, we need additional container
|
||||
this.outerContainer = document.createElement('div');
|
||||
this.outerContainer.classList.add('outer-container');
|
||||
|
||||
this.container = document.createElement('div');
|
||||
this.container.classList.add('container');
|
||||
|
||||
this.messageElement = document.createElement('div');
|
||||
this.messageElement.classList.add('modal-dialog-message');
|
||||
|
||||
this.buttonGroup = document.createElement('div');
|
||||
this.buttonGroup.classList.add('modal-dialog-button-group');
|
||||
this.buttonGroup.setAttribute('smart-bubbles', 'true');
|
||||
|
||||
this.container.appendChild(this.messageElement);
|
||||
this.container.appendChild(this.buttonGroup);
|
||||
|
||||
this.outerContainer.appendChild(this.container);
|
||||
this.element.appendChild(this.outerContainer);
|
||||
|
||||
this.container = container || document.body;
|
||||
this.container.appendChild(this.element);
|
||||
|
||||
this.element.addEventListener('opened', this);
|
||||
this.element.addEventListener('closed', this);
|
||||
this.smartBubble.addEventListener('all-items-bubbled', this);
|
||||
}
|
||||
|
||||
var proto = Object.create(SmartDialog);
|
||||
|
||||
proto.open = function(options) {
|
||||
this._focusedIndex = -1;
|
||||
this.isOpened = true;
|
||||
this.message = options.message || {};
|
||||
|
||||
// onCancel is triggered when ESC key is pressed.
|
||||
this.onCancel = options.onCancel || function() {};
|
||||
this.buttonElements = [];
|
||||
this.buttonSettings = options.buttonSettings || [];
|
||||
this.messageElement.textContent = this.message.textRaw || '';
|
||||
this.messageElement.setAttribute('data-l10n-id', this.message.textL10nId);
|
||||
this.buttonGroup.innerHTML = '';
|
||||
|
||||
this.messageElement.classList.remove('hidden');
|
||||
if (!this.message.textRaw && !this.message.textL10nId) {
|
||||
this.messageElement.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Set up every button
|
||||
this.buttonSettings.forEach(function(buttonSetting, index) {
|
||||
var button = document.createElement('smart-button');
|
||||
button.setAttribute('type', 'circle-text');
|
||||
button.setAttribute('data-l10n-id', buttonSetting.textL10nId);
|
||||
button.textContent = buttonSetting.textRaw || 'OK';
|
||||
button.classList.add(buttonSetting.class || 'confirm');
|
||||
button.addEventListener('click', function() {
|
||||
// Click action will be handled in closed event
|
||||
this._clickedIndex = index;
|
||||
this.element.close();
|
||||
}.bind(this));
|
||||
this.buttonElements.push(button);
|
||||
this.buttonGroup.appendChild(button);
|
||||
this._focusedIndex =
|
||||
(buttonSetting.defaultFocus) ? index : this._focusedIndex;
|
||||
}.bind(this));
|
||||
|
||||
this.element.classList.add('visible');
|
||||
this.element.open();
|
||||
};
|
||||
|
||||
proto.remove = function() {
|
||||
this.container.removeChild(this.element);
|
||||
};
|
||||
|
||||
proto.startKeyNavigation = function() {
|
||||
this.element.addEventListener('keydown', this);
|
||||
if (this.buttonElements.length > 0) {
|
||||
if (this._focusedIndex === -1) {
|
||||
this._focusedIndex = 0;
|
||||
}
|
||||
this.focus();
|
||||
}
|
||||
};
|
||||
|
||||
proto.focus = function() {
|
||||
var elem = this.buttonElements[this._focusedIndex];
|
||||
if (elem.focus && (typeof elem.focus) === 'function') {
|
||||
elem.focus();
|
||||
}
|
||||
};
|
||||
|
||||
proto.blur = function() {
|
||||
var elem = this.buttonElements[this._focusedIndex];
|
||||
if (elem.blur && (typeof elem.blur === 'function')) {
|
||||
elem.blur();
|
||||
}
|
||||
};
|
||||
|
||||
proto.stopKeyNavigation = function() {
|
||||
this.element.removeEventListener('keydown', this);
|
||||
};
|
||||
|
||||
proto.movePrevious = function() {
|
||||
if (this._focusedIndex < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._focusedIndex--;
|
||||
this.focus();
|
||||
};
|
||||
|
||||
proto.moveNext = function() {
|
||||
if (this._focusedIndex > this.buttonElements.length - 2) {
|
||||
return;
|
||||
}
|
||||
this._focusedIndex++;
|
||||
this.focus();
|
||||
};
|
||||
|
||||
proto.handleEvent = function(e) {
|
||||
if (e.keyCode === KeyEvent.DOM_VK_LEFT) {
|
||||
this.movePrevious();
|
||||
} else if (e.keyCode === KeyEvent.DOM_VK_RIGHT) {
|
||||
this.moveNext();
|
||||
}
|
||||
|
||||
switch(e.target) {
|
||||
case this.element:
|
||||
if (e.type === 'opened') {
|
||||
// Play bubble animation when the smart-dialog is opened
|
||||
this.smartBubble.play(this.buttonElements);
|
||||
} else if (e.type === 'closed') {
|
||||
// Do click actions when the smart-dialog is closed
|
||||
this.element.classList.remove('visible');
|
||||
if ((this._clickedIndex || this._clickedIndex === 0) &&
|
||||
this.buttonSettings[this._clickedIndex].onClick) {
|
||||
this.buttonSettings[this._clickedIndex].onClick();
|
||||
this._clickedIndex = null;
|
||||
} else if (this.onCancel) {
|
||||
// ESC key enter
|
||||
this.onCancel();
|
||||
}
|
||||
this.isOpened = false;
|
||||
this.stopKeyNavigation();
|
||||
}
|
||||
break;
|
||||
case this.smartBubble:
|
||||
this.startKeyNavigation();
|
||||
this.focus();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
SmartModalDialog.prototype = proto;
|
||||
exports.SmartModalDialog = SmartModalDialog;
|
||||
|
||||
})(window);
|
||||
|
||||
47
gaia/tv_apps/smart-home/bower_components/smart-modal-dialog/style.css
vendored
Executable file
|
|
@ -0,0 +1,47 @@
|
|||
.modal-dialog > .outer-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.modal-dialog > .outer-container:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
width: 0.1rem;
|
||||
margin-left: -0.1rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.modal-dialog .container {
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.modal-dialog .modal-dialog-message {
|
||||
font-size: 3.8rem;
|
||||
font-style: italic;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
font-family: Fira Sans;
|
||||
font-weight: 100;
|
||||
margin-bottom: 13rem;
|
||||
}
|
||||
|
||||
.modal-dialog .modal-dialog-message.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-dialog .modal-dialog-button-group {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-dialog .modal-dialog-button-group > * {
|
||||
margin-right: 4.4rem;
|
||||
}
|
||||
|
||||
.modal-dialog .modal-dialog-button-group > *:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
199
gaia/tv_apps/smart-home/index.html
Executable file
|
|
@ -0,0 +1,199 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<meta charset="utf-8">
|
||||
<title>Home</title>
|
||||
|
||||
<!-- building blocks -->
|
||||
<link rel="stylesheet" type="text/css" href="bower_components/smart-icons/smart-icons.css">
|
||||
<link rel="stylesheet" type="text/css" href="bower_components/smart-button/style.css">
|
||||
<link rel="stylesheet" type="text/css" href="bower_components/smart-dialog/style.css">
|
||||
<link rel="stylesheet" type="text/css" href="bower_components/smart-bubbles/style.css">
|
||||
<link rel="stylesheet" type="text/css" href="bower_components/smart-modal-dialog/style.css">
|
||||
<link rel="stylesheet" type="text/css" href="bower_components/menu-group/style.css">
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/smart-screen/animations.css">
|
||||
|
||||
<!-- home -->
|
||||
<link rel="stylesheet" type="text/css" href="style/header.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/home.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/deck.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/edit.css">
|
||||
<link rel="stylesheet" type="text/css" href="shared/style/smart-screen/navigation.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/info-bar.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/search-bar.css">
|
||||
<link rel="stylesheet" type="text/css" href="style/fte.css">
|
||||
|
||||
<!-- default cards for first time launch -->
|
||||
<link rel="resource" type="application/json" href="shared/resources/default-cards.json">
|
||||
|
||||
<!-- For test purpose on phone only -->
|
||||
<script defer src="shared/js/gesture_detector.js"></script>
|
||||
<script defer src="shared/js/smart-screen/init_gesture.js"></script>
|
||||
|
||||
<!-- Shared libraries -->
|
||||
<script defer src="shared/js/async_semaphore.js"></script>
|
||||
<script defer src="shared/js/uuid.js"></script>
|
||||
<script defer src="shared/js/iac_handler.js"></script>
|
||||
|
||||
<!-- Helper libraries -->
|
||||
<script defer src="bower_components/evt/index.js"></script>
|
||||
<script defer src="shared/js/smart-screen/shared_utils.js"></script>
|
||||
<script defer src="shared/js/smart-screen/clock.js"></script>
|
||||
<script defer src="shared/js/smart-screen/piped_promise.js"></script>
|
||||
<script defer src="shared/js/smart-screen/card_store.js"></script>
|
||||
<script defer src="shared/js/smart-screen/cards/card.js"></script>
|
||||
<script defer src="shared/js/smart-screen/cards/application.js"></script>
|
||||
<script defer src="shared/js/smart-screen/cards/deck.js"></script>
|
||||
<script defer src="shared/js/smart-screen/cards/folder.js"></script>
|
||||
<script defer src="shared/js/smart-screen/cards/app_bookmark.js"></script>
|
||||
<script defer src="shared/js/smart-screen/card_manager.js"></script>
|
||||
<script defer src="shared/js/smart-screen/animations.js"></script>
|
||||
<script defer src="shared/js/smart-screen/simple_key_navigation.js"></script>
|
||||
<script defer src="shared/js/smart-screen/f_t_e_wizard.js"></script>
|
||||
|
||||
<!-- building blocks -->
|
||||
<script defer src="bower_components/smart-button/script.js"></script>
|
||||
<script defer src="bower_components/smart-dialog/script.js"></script>
|
||||
<script defer src="bower_components/smart-bubbles/script.js"></script>
|
||||
<script defer src="bower_components/smart-modal-dialog/script.js"></script>
|
||||
<!-- menu group depends on evt, it should be added after evt.js -->
|
||||
<script defer src="bower_components/menu-group/script.js"></script>
|
||||
|
||||
<!-- Shared TV library for keyboard-based navigating -->
|
||||
<script defer src="shared/js/smart-screen/scrollable.js"></script>
|
||||
<script defer src="shared/js/smart-screen/spatial_navigator.js"></script>
|
||||
<script defer src="shared/js/smart-screen/key_navigation_adapter.js"></script>
|
||||
<script defer src="shared/js/smart-screen/connection_manager.js"></script>
|
||||
<!-- Specific code -->
|
||||
<script defer src="js/utils.js"></script>
|
||||
<script defer src="js/card_filter.js"></script>
|
||||
<script defer src="js/filter_manager.js"></script>
|
||||
<script defer src="js/message_handler.js"></script>
|
||||
<script defer src="js/edit.js"></script>
|
||||
<script defer src="js/search_bar.js"></script>
|
||||
|
||||
<script defer src="js/home.js"></script>
|
||||
<script defer src="js/bootstrap.js"></script>
|
||||
|
||||
<!-- Shared L10n libraries -->
|
||||
<script src="shared/js/l10n.js"></script>
|
||||
<script src="shared/js/l10n_date.js"></script>
|
||||
|
||||
<!-- L10n properties -->
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="locales/home.{locale}.properties"/>
|
||||
<link rel="localization" href="shared/locales/date/date.{locale}.properties"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<article tabIndex="-1" role="region" class="skin-dark" id="main-section">
|
||||
<section id="background" class="background"></section>
|
||||
<nav id="nav-regular">
|
||||
<section id="header-buttons" class="left-top-panel">
|
||||
<smart-button data-icon="search" id="search-button" class="search-button"></smart-button>
|
||||
<smart-button type="text-and-icon" id="edit-button" class="edit-button">
|
||||
<span data-l10n-id="edit">edit</span>
|
||||
</smart-button>
|
||||
<smart-button type="text-and-icon" id="settings-button" class="settings-button">
|
||||
<span data-l10n-id="settings">settings</span>
|
||||
</smart-button>
|
||||
</section>
|
||||
<section id="information-bar" class="information-bar">
|
||||
<div id="clock-info" class="clock-info"><span id="time" class="time"></span></div>
|
||||
<div id="weather-info" class="weather-info">
|
||||
<!-- use span here because we will change to icon font -->
|
||||
<span id="weather-image" class="weather-image"></span>
|
||||
<span id="temperature" class="temperature">
|
||||
<!-- do not add \n here because it adds a space between temperature-number, degree-marker and degree-system -->
|
||||
<label class="temperature-number">72</label><label class="degree-marker">°</label><label class="degree-system">F</label>
|
||||
</span>
|
||||
<span id="weather-text" class="weather-text">Mostly Cloudy</span>
|
||||
</div>
|
||||
</section>
|
||||
</nav>
|
||||
<nav id="nav-edit-title">
|
||||
<header data-l10n-id="edit-mode-title">
|
||||
Edit mode
|
||||
</header>
|
||||
</nav>
|
||||
<nav id="nav-edit">
|
||||
<section id="edit-menu">
|
||||
<smart-button type="underlined-text" id="done-button">
|
||||
<span data-l10n-id="done">done</span>
|
||||
</smart-button>
|
||||
<smart-button type="text-and-icon" id="add-new-folder-button" data-icon="add">
|
||||
<span data-l10n-id="add-new-folder">add new folder</span>
|
||||
</smart-button>
|
||||
</section>
|
||||
</nav>
|
||||
|
||||
<smart-bubbles id="bubbles" interval="100"></smart-bubbles>
|
||||
<section id="card-viewer">
|
||||
<div id="card-list-frame">
|
||||
<div id="card-list" class="hidden" data-default-item="0" smart-bubbles="true">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="folder-viewer">
|
||||
<div id="folder-list-frame">
|
||||
<div id="folder-list" data-default-item="0">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="filter-tab-container" class="filter-tab-container">
|
||||
<menu-group data-icon="filter" id="filter-tab-group" class="filter-tab-group">
|
||||
<smart-button type="simple-text" id="filter-all-button" data-icon-type="filter">
|
||||
<span data-l10n-id="all"></span>
|
||||
</smart-button>
|
||||
<smart-button type="simple-text" id="filter-tv-button" data-icon-type="tv">
|
||||
<span data-l10n-id="tv"></span>
|
||||
</smart-button>
|
||||
<smart-button type="simple-text" id="filter-app-button" data-icon-type="application">
|
||||
<span data-l10n-id="app"></span>
|
||||
</smart-button>
|
||||
<smart-button type="simple-text" id="filter-device-button" data-icon-type="device">
|
||||
<span data-l10n-id="device"></span>
|
||||
</smart-button>
|
||||
<smart-button type="simple-text" id="filter-website-button" data-icon-type="web">
|
||||
<span data-l10n-id="website"></span>
|
||||
</smart-button>
|
||||
</menu-group>
|
||||
</section>
|
||||
<section id="search-bar" class="search-bar hidden">
|
||||
<smart-button data-icon="mic" class="search-mic-button fade-out"></smart-button>
|
||||
<div data-icon="search" class="search-input">
|
||||
<span data-l10n-id="search-default-text">search</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="fte">
|
||||
<section class="fte-page">
|
||||
<div class="fte-content-image">
|
||||
<div id="fte-tvdeck-detail" data-l10n-id="fte-tvdeck-detail"></div>
|
||||
<div id="fte-appdeck-detail" data-l10n-id="fte-appdeck-detail"></div>
|
||||
<div id="fte-devicedeck-detail" data-l10n-id="fte-devicedeck-detail"></div>
|
||||
<div class="fte-indicator-p1"></div>
|
||||
</div>
|
||||
<section class="fte-panel">
|
||||
<smart-button data-behavior="next" type="rounded-corner-text" class="fte-button primary" data-l10n-id="next"></smart-button>
|
||||
</section>
|
||||
</section>
|
||||
<section class="fte-page">
|
||||
<div class="fte-content-image">
|
||||
<div id="fte-search-detail" data-l10n-id="fte-search-detail"></div>
|
||||
<div id="fte-settings-detail" data-l10n-id="fte-settings-detail"></div>
|
||||
<div id="fte-addfolder-detail" data-l10n-id="fte-addfolder-detail"></div>
|
||||
<div id="fte-filter-detail" data-l10n-id="fte-filter-detail"></div>
|
||||
<div class="fte-indicator-p2"></div>
|
||||
</div>
|
||||
<section class="fte-panel">
|
||||
<smart-button data-behavior="finish" type="rounded-corner-text" class="fte-button primary" data-l10n-id="ok"></smart-button>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</article>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
4
gaia/tv_apps/smart-home/js/bootstrap.js
vendored
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
'use strict';
|
||||
/* global Home */
|
||||
window.home = new Home();
|
||||
window.home.init();
|
||||
82
gaia/tv_apps/smart-home/js/card_filter.js
Executable file
|
|
@ -0,0 +1,82 @@
|
|||
/* global evt */
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* This class controls UI and user interaction of filter, with the help from
|
||||
* [MenuGroup](http://bit.ly/1JbbnZ8).
|
||||
*
|
||||
* @class CardFilter
|
||||
* @requires {@link http://bit.ly/1JbbnZ8|MenuGroup}
|
||||
* @fires CardFilter#opened
|
||||
* @fires CardFilter#filterchanged
|
||||
*/
|
||||
function CardFilter() {}
|
||||
|
||||
var proto = CardFilter.prototype = new evt();
|
||||
|
||||
Object.defineProperty(proto, 'filter', {
|
||||
get: function cf_getFilter() {
|
||||
return this._selectedFilter;
|
||||
},
|
||||
set: function cf_setFilter(icon) {
|
||||
if (this.menuGroup && this._buttons[icon]) {
|
||||
if (this._buttons[this._selectedFilter]) {
|
||||
this._buttons[this._selectedFilter].classList.remove('toggled');
|
||||
}
|
||||
this._selectedFilter = icon;
|
||||
this.menuGroup.changeIcon(icon);
|
||||
this._buttons[icon].classList.add('toggled');
|
||||
/**
|
||||
* This event fires whenever filter changes.
|
||||
* @event CardFilter#filterchanged
|
||||
*/
|
||||
this.emit('filterchanged', icon);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
proto.start = function cf_start(menuGroup) {
|
||||
this.menuGroup = menuGroup;
|
||||
this.menuGroup.addEventListener('opened', function() {
|
||||
/**
|
||||
* This event fires whenever [MenuGroup](http://bit.ly/1JbbnZ8) is opened.
|
||||
* @event CardFilter#opened
|
||||
*/
|
||||
this.fire('opened');
|
||||
}.bind(this));
|
||||
var buttons = this.menuGroup.querySelectorAll(
|
||||
'smart-button[data-icon-type]');
|
||||
this._buttons = {};
|
||||
for (var i = 0; i < buttons.length; i++) {
|
||||
this._buttons[buttons[i].dataset.iconType] = buttons[i];
|
||||
buttons[i].addEventListener('click', this);
|
||||
}
|
||||
};
|
||||
|
||||
proto.stop = function cf_stop() {
|
||||
var that = this;
|
||||
var buttonKeys = Object.keys(this._buttons);
|
||||
buttonKeys.forEach(function(key) {
|
||||
that._buttons[key].removeEventListener('click', that);
|
||||
});
|
||||
};
|
||||
|
||||
proto.handleEvent = function cf_handleEvent(evt) {
|
||||
if (!evt.target.dataset.iconType) {
|
||||
return;
|
||||
}
|
||||
this.filter = evt.target.dataset.iconType;
|
||||
};
|
||||
|
||||
proto.hide = function cf_hide() {
|
||||
this.menuGroup.classList.add('hidden');
|
||||
};
|
||||
|
||||
proto.show = function cf_show() {
|
||||
this.menuGroup.classList.remove('hidden');
|
||||
};
|
||||
|
||||
exports.CardFilter = CardFilter;
|
||||
|
||||
})(window);
|
||||
551
gaia/tv_apps/smart-home/js/edit.js
Executable file
|
|
@ -0,0 +1,551 @@
|
|||
/* global evt, Folder, SmartModalDialog, Utils */
|
||||
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
const EDIT_MODE_SCALE = 0.57;
|
||||
const CARD_TRANSFORM_LATENCY = 500;
|
||||
|
||||
/**
|
||||
* Edit is the controller class when smart-home is in edit mode.
|
||||
*
|
||||
* @class Edit
|
||||
*/
|
||||
function Edit() {}
|
||||
|
||||
Edit.prototype = evt({
|
||||
mainSection: document.getElementById('main-section'),
|
||||
doneButton: document.getElementById('done-button'),
|
||||
searchButton: document.getElementById('search-button'),
|
||||
addNewFolderButton: document.getElementById('add-new-folder-button'),
|
||||
editButton: document.getElementById('edit-button'),
|
||||
settingsButton: document.getElementById('settings-button'),
|
||||
|
||||
regularNavElements: undefined,
|
||||
editNavElements: undefined,
|
||||
currentNode: undefined,
|
||||
|
||||
init: function(spatialNavigator, cardManager,
|
||||
cardScrollable, folderScrollable) {
|
||||
var that = this;
|
||||
this.modalDialog = new SmartModalDialog();
|
||||
|
||||
this.spatialNavigator = spatialNavigator;
|
||||
this.cardManager = cardManager;
|
||||
this.cardScrollable = cardScrollable;
|
||||
this.folderScrollable = folderScrollable;
|
||||
|
||||
this.regularNavElements =
|
||||
[this.searchButton, this.settingsButton, this.editButton];
|
||||
this.editNavElements = [this.doneButton, this.addNewFolderButton];
|
||||
|
||||
this.cardManager.on('card-swapped', this.onCardSwapped.bind(this));
|
||||
this.cardManager.getCardList().then(function(cardList) {
|
||||
cardList.forEach(function(card) {
|
||||
if (card instanceof Folder) {
|
||||
card.on('card-swapped', that.onCardSwapped.bind(that));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.cardScrollable.on('listTransformEnd',
|
||||
this.handleListTransformEnd.bind(this));
|
||||
this.cardScrollable.on('nodeTransformEnd',
|
||||
this.onCardMoveAnimationEnd.bind(this));
|
||||
this.cardScrollable.on('hovering-node-removed',
|
||||
this.onHoveringNodeRemoved.bind(this));
|
||||
this.cardScrollable.on('node-inserted-over-folder',
|
||||
this.onNodeInsertedOverFolder.bind(this));
|
||||
|
||||
this.spatialNavigator.on('focus', this.handleFocus.bind(this));
|
||||
this.spatialNavigator.on('unfocus', this.handleUnfocus.bind(this));
|
||||
|
||||
// A workaround for checking if the folder list is navigable
|
||||
this.isFolderReady = false;
|
||||
// The hovering card
|
||||
this._hoveringCard = null;
|
||||
},
|
||||
/**
|
||||
* State of home app looks like this:
|
||||
* (normal state)<-->Edit mode<-->Arrange mode
|
||||
* (Only adjacant states are switchable)
|
||||
*/
|
||||
toggleEditMode: function() {
|
||||
if (this.mainSection.dataset.mode === 'edit') {
|
||||
this.mainSection.dataset.mode = '';
|
||||
this.spatialNavigator.multiAdd(this.regularNavElements);
|
||||
this.spatialNavigator.multiRemove(this.editNavElements);
|
||||
this.cardScrollable.setScale();
|
||||
this.folderScrollable.setScale();
|
||||
this.cardScrollable.listElem.classList.add('exiting-edit-mode');
|
||||
this.cardManager.writeCardlistInCardStore({cleanEmptyFolder: true})
|
||||
.then(function() {
|
||||
// Since writeCardlistInCardStore triggers card-removed event that
|
||||
// causes re-focus on other elements in card list, we need to wait
|
||||
// until those actions to be done before focusing editButton.
|
||||
this.spatialNavigator.focus(this.editButton);
|
||||
this.currentNode.classList.remove('focused');
|
||||
}.bind(this));
|
||||
this._concealPanel(this.currentScrollable, this.currentNode);
|
||||
this.fire('exit-edit-mode');
|
||||
} else {
|
||||
this.mainSection.dataset.mode = 'edit';
|
||||
this.spatialNavigator.multiRemove(this.regularNavElements);
|
||||
this.spatialNavigator.multiAdd(this.editNavElements);
|
||||
|
||||
this._setCurrentScrollable(this.cardScrollable);
|
||||
this.currentNode =
|
||||
this.cardScrollable.getNodeFromItem(this.cardScrollable.currentItem);
|
||||
this._revealPanel(this.currentScrollable, this.currentNode);
|
||||
|
||||
this.spatialNavigator.focus(this.cardScrollable);
|
||||
this.cardScrollable.setScale(EDIT_MODE_SCALE);
|
||||
this.folderScrollable.setScale(EDIT_MODE_SCALE);
|
||||
this.fire('enter-edit-mode');
|
||||
}
|
||||
},
|
||||
|
||||
toggleArrangeMode: function() {
|
||||
if (this.mainSection.dataset.mode === 'edit') {
|
||||
this.mainSection.dataset.mode = 'arrange';
|
||||
this._concealPanel(this.currentScrollable, this.currentNode);
|
||||
this.currentScrollable.focus(this.currentScrollable.currentItem);
|
||||
this._setHintArrow();
|
||||
if (this.currentScrollable === this.folderScrollable) {
|
||||
this.cardScrollable.setColspanOnFocus(1);
|
||||
}
|
||||
this.fire('arrange');
|
||||
} else if (this.mainSection.dataset.mode == 'arrange') {
|
||||
this.mainSection.dataset.mode = 'edit';
|
||||
|
||||
this.currentNode = this.currentScrollable.getNodeFromItem(
|
||||
this.currentScrollable.currentItem);
|
||||
this._revealPanel(this.currentScrollable, this.currentNode);
|
||||
this._clearHintArrow();
|
||||
this.cardScrollable.setColspanOnFocus(0);
|
||||
this.folderScrollable.realignToReferenceElement();
|
||||
}
|
||||
},
|
||||
|
||||
_moveQueue: {
|
||||
queue: [],
|
||||
isEmpty: function() {
|
||||
return (this.queue.length === 0);
|
||||
},
|
||||
push: function(obj) {
|
||||
this.queue.push(obj);
|
||||
},
|
||||
shift: function() {
|
||||
return this.queue.shift();
|
||||
},
|
||||
getLength: function() {
|
||||
return this.queue.length;
|
||||
},
|
||||
clearQueue: function() {
|
||||
this.queue.length = 0;
|
||||
}
|
||||
},
|
||||
|
||||
_moveTimer: undefined,
|
||||
|
||||
_produceMove: function(key) {
|
||||
this._moveQueue.push(key);
|
||||
if (!this._moveTimer) {
|
||||
this._consumeMove();
|
||||
}
|
||||
},
|
||||
|
||||
_consumeMove: function() {
|
||||
var focus = this.spatialNavigator.getFocusedElement();
|
||||
|
||||
if (!this._moveQueue.isEmpty() && focus.CLASS_NAME === 'XScrollable') {
|
||||
var key = this._moveQueue.shift();
|
||||
var targetItem = focus.getTargetItem(key);
|
||||
|
||||
switch (key) {
|
||||
case 'left':
|
||||
case 'right':
|
||||
if (!focus.isHovering && targetItem &&
|
||||
focus.currentItem.getAttribute('app-type') !== 'deck' &&
|
||||
focus.currentItem.getAttribute('app-type') !== 'folder' &&
|
||||
targetItem.getAttribute('app-type') === 'folder') {
|
||||
// hover on folder
|
||||
this.hoverCard(focus, focus.currentItem, targetItem);
|
||||
this._moveTimer =
|
||||
window.setTimeout(this.onCardMoveAnimationEnd.bind(this),
|
||||
CARD_TRANSFORM_LATENCY, targetItem);
|
||||
} else if (focus.isHovering && targetItem !== focus.hoveredItem) {
|
||||
this.unhoverCard(focus, true);
|
||||
this._moveTimer =
|
||||
window.setTimeout(this.onCardMoveAnimationEnd.bind(this),
|
||||
CARD_TRANSFORM_LATENCY, targetItem);
|
||||
} else if (targetItem) {
|
||||
if (focus.isHovering) {
|
||||
this.unhoverCard(focus, false);
|
||||
}
|
||||
var focusedCard = this.cardManager.findCardFromCardList(
|
||||
{cardId: focus.currentItem.dataset.cardId});
|
||||
var nonFocusedCard = this.cardManager.findCardFromCardList(
|
||||
{cardId: targetItem.dataset.cardId});
|
||||
if (this.currentScrollable === this.folderScrollable) {
|
||||
var containingFolder = this.cardManager.findContainingFolder(
|
||||
{cardId: focus.currentItem.dataset.cardId});
|
||||
containingFolder.swapCard(focusedCard, nonFocusedCard);
|
||||
} else {
|
||||
this.cardManager.swapCard(focusedCard, nonFocusedCard);
|
||||
}
|
||||
|
||||
this._moveTimer =
|
||||
window.setTimeout(this.onCardMoveAnimationEnd.bind(this),
|
||||
CARD_TRANSFORM_LATENCY, targetItem);
|
||||
} else {
|
||||
// if queue is not empty but we are failed to get targetItem
|
||||
// that means we reach the boundary of cards. Thus we should
|
||||
// consume next action in queue
|
||||
this._consumeMove();
|
||||
}
|
||||
|
||||
break;
|
||||
case 'down':
|
||||
if (focus.isHovering && this.isFolderReady) {
|
||||
this.moveToFolder();
|
||||
this._moveTimer =
|
||||
window.setTimeout(this.onCardMoveAnimationEnd.bind(this),
|
||||
CARD_TRANSFORM_LATENCY);
|
||||
}
|
||||
this._moveQueue.clearQueue();
|
||||
break;
|
||||
case 'up':
|
||||
if (this.currentScrollable === this.folderScrollable) {
|
||||
this.moveToCardList(focus);
|
||||
this._moveTimer =
|
||||
window.setTimeout(this.onCardMoveAnimationEnd.bind(this),
|
||||
CARD_TRANSFORM_LATENCY);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onCardMoveAnimationEnd: function(elem) {
|
||||
if (this._moveTimer) {
|
||||
window.clearTimeout(this._moveTimer);
|
||||
this._moveTimer = undefined;
|
||||
}
|
||||
if (elem &&
|
||||
!elem.classList.contains('focused') &&
|
||||
!this._moveQueue.isEmpty()) {
|
||||
this._consumeMove();
|
||||
}
|
||||
},
|
||||
|
||||
onCardSwapped: function(card1, card2, idx1, idx2) {
|
||||
this.currentScrollable.swap(idx1, idx2);
|
||||
this._setHintArrow();
|
||||
},
|
||||
|
||||
onMove: function(key) {
|
||||
if (this.modalDialog.isOpened) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.mode !== 'arrange') {
|
||||
return false;
|
||||
}
|
||||
this._produceMove(key);
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
_setHintArrow: function(scrollable) {
|
||||
var index = parseInt(this.currentNode.dataset.idx, 10);
|
||||
if (scrollable && scrollable.isHovering) {
|
||||
this.currentNode.classList.toggle('left_arrow', true);
|
||||
this.currentNode.classList.toggle('right_arrow', true);
|
||||
this.currentNode.classList.toggle('down_arrow', true);
|
||||
this.currentNode.classList.remove('up_arrow');
|
||||
} else {
|
||||
this.currentNode.classList.toggle('left_arrow', index > 0);
|
||||
this.currentNode.classList.toggle('right_arrow',
|
||||
index < this.currentScrollable.length - 1);
|
||||
this.currentNode.classList.remove('down_arrow');
|
||||
this.currentNode.classList.toggle('up_arrow',
|
||||
this.currentScrollable === this.folderScrollable);
|
||||
}
|
||||
},
|
||||
|
||||
_setCurrentScrollable: function(scrollable) {
|
||||
this.currentScrollable = scrollable;
|
||||
document.getElementById('card-viewer').classList.toggle(
|
||||
'current-scrollable', scrollable === this.cardScrollable);
|
||||
document.getElementById('folder-viewer').classList.toggle(
|
||||
'current-scrollable', scrollable === this.folderScrollable);
|
||||
},
|
||||
|
||||
_clearHintArrow: function() {
|
||||
this.currentNode.classList.remove('left_arrow');
|
||||
this.currentNode.classList.remove('right_arrow');
|
||||
},
|
||||
|
||||
addNewFolder: function() {
|
||||
var folder = this.cardManager.insertNewFolder({id: 'new-folder'},
|
||||
this.cardScrollable.currentIndex);
|
||||
folder.on('card-swapped', this.onCardSwapped.bind(this));
|
||||
this.spatialNavigator.focus(this.cardScrollable);
|
||||
},
|
||||
|
||||
moveToFolder: function() {
|
||||
// Remove the card from the main list
|
||||
this.cardManager.removeCard(this._hoveringCard);
|
||||
},
|
||||
|
||||
/**
|
||||
* Runs after moveToFolder call, when the hovering card is successfully
|
||||
* removed by cardManager and the node is removed from cardScrollable.
|
||||
*/
|
||||
onHoveringNodeRemoved: function(cardItem, folderItem) {
|
||||
var card = this._hoveringCard;
|
||||
var folder = this.cardManager.findCardFromCardList(
|
||||
{cardId: folderItem.dataset.cardId});
|
||||
// Add the card into the folder
|
||||
folder.addCard(card, this.folderScrollable.isReversed ? null : 0);
|
||||
this._hoveringCard = null;
|
||||
this._setCurrentScrollable(this.folderScrollable);
|
||||
this.folderScrollable.realignToReferenceElement();
|
||||
this.currentNode = this.folderScrollable.getNodeFromItem(
|
||||
this.folderScrollable.currentItem);
|
||||
if (!this.spatialNavigator.focus(this.folderScrollable)) {
|
||||
// When a card is added to a new empty folder,
|
||||
// add the folderScrollable to this.spatialNavigator.
|
||||
this.spatialNavigator.add(this.folderScrollable);
|
||||
this.spatialNavigator.focus(this.folderScrollable);
|
||||
}
|
||||
this._setHintArrow();
|
||||
},
|
||||
|
||||
moveToCardList: function(focus) {
|
||||
var folder = this.cardManager.findContainingFolder(
|
||||
{cardId: focus.currentItem.dataset.cardId});
|
||||
var card = this.cardManager.findCardFromCardList(
|
||||
{cardId: focus.currentItem.dataset.cardId});
|
||||
var folderItem = this.cardScrollable.currentItem;
|
||||
var idx = this.cardScrollable.getNodeFromItem(folderItem).dataset.idx;
|
||||
|
||||
this._hoveringCard = card;
|
||||
|
||||
folder.removeCard(card);
|
||||
|
||||
// insert card
|
||||
this.cardManager.insertCard({
|
||||
card: card,
|
||||
index: parseInt(idx, 10),
|
||||
overFolder: true
|
||||
});
|
||||
},
|
||||
|
||||
onNodeInsertedOverFolder: function() {
|
||||
this.spatialNavigator.focus(this.cardScrollable);
|
||||
this._setCurrentScrollable(this.cardScrollable);
|
||||
this.currentNode = this.cardScrollable.getNodeFromItem(
|
||||
this.cardScrollable.currentItem);
|
||||
this._setHintArrow(this.currentScrollable);
|
||||
this.folderScrollable.realignToReferenceElement();
|
||||
},
|
||||
|
||||
onEnter: function() {
|
||||
if (this.mode !== 'edit' && this.mode !== 'arrange') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.modalDialog.isOpened) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var focus = this.spatialNavigator.getFocusedElement();
|
||||
if (focus === this.doneButton) {
|
||||
this.toggleEditMode();
|
||||
} else if (focus === this.addNewFolderButton) {
|
||||
this.addNewFolder();
|
||||
} else if (focus.CLASS_NAME === 'XScrollable') {
|
||||
var currentItem = focus.currentItem;
|
||||
|
||||
if (currentItem.classList.contains('rename-btn')) {
|
||||
this.renameCard(focus, focus.getNodeFromItem(currentItem));
|
||||
} else if (currentItem.classList.contains('delete-btn')) {
|
||||
this.openDeleteCardDialog(focus, focus.getNodeFromItem(currentItem));
|
||||
} else if (currentItem.classList.contains('hover')) {
|
||||
// When a card is hovering over a folder, do nothing here.
|
||||
} else {
|
||||
// Current focus is on a card
|
||||
this.toggleArrangeMode();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
renameCard: function(scrollable, nodeElem) {
|
||||
var card = this.cardManager.findCardFromCardList({
|
||||
cardId: scrollable.getItemFromNode(nodeElem).dataset.cardId
|
||||
});
|
||||
var _ = navigator.mozL10n.get;
|
||||
|
||||
var lang = document.documentElement.lang;
|
||||
var oldName = this.cardManager.resolveCardName(card, lang);
|
||||
oldName = oldName.raw ? oldName.raw : _(oldName.id);
|
||||
|
||||
var result = prompt(_('enter-new-name'), oldName);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
card.name = {raw: result};
|
||||
this.cardManager.updateCard(card);
|
||||
},
|
||||
|
||||
openDeleteCardDialog: function(scrollable, nodeElem) {
|
||||
Utils.holdFocusForAnimation();
|
||||
this.modalDialog.open({
|
||||
message: {
|
||||
textL10nId: 'delete-card-alert'
|
||||
},
|
||||
buttonSettings: [
|
||||
{
|
||||
textL10nId: 'cancel',
|
||||
defaultFocus: true,
|
||||
onClick: this.spatialNavigator.focus.bind(this.spatialNavigator)
|
||||
},
|
||||
{
|
||||
textL10nId: 'delete',
|
||||
class: 'danger',
|
||||
onClick: this.deleteCard.bind(this, scrollable, nodeElem)
|
||||
}
|
||||
],
|
||||
onCancel: this.spatialNavigator.focus.bind(this.spatialNavigator)
|
||||
});
|
||||
},
|
||||
|
||||
deleteCard: function(scrollable, nodeElem) {
|
||||
var itemElem = scrollable.getItemFromNode(nodeElem);
|
||||
this._concealPanel(scrollable, nodeElem);
|
||||
nodeElem.classList.add('delete');
|
||||
|
||||
if (scrollable === this.folderScrollable) {
|
||||
var folder = this.cardManager.findContainingFolder(
|
||||
{cardId: itemElem.dataset.cardId});
|
||||
var card = this.cardManager.findCardFromCardList(
|
||||
{cardId: itemElem.dataset.cardId});
|
||||
}
|
||||
|
||||
var deletedItem = scrollable.getItemFromNode(nodeElem);
|
||||
deletedItem.addEventListener('transitionend', function(evt) {
|
||||
if (evt.propertyName === 'transform') {
|
||||
scrollable.spatialNavigator
|
||||
.focus(scrollable.getItemFromNode(nodeElem));
|
||||
if (scrollable === this.cardScrollable) {
|
||||
this.cardManager.removeCard(parseInt(nodeElem.dataset.idx, 10));
|
||||
} else {
|
||||
folder.removeCard(card);
|
||||
|
||||
if (folder.isEmpty()) {
|
||||
this.spatialNavigator.focus(this.cardScrollable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
hoverCard: function(scrollable, focusedItem, targetItem) {
|
||||
var card = this.cardManager.findCardFromCardList(
|
||||
{cardId: focusedItem.dataset.cardId});
|
||||
this._hoveringCard = card;
|
||||
scrollable.hover(focusedItem, targetItem);
|
||||
// set up/bottom arrow
|
||||
this._setHintArrow(scrollable);
|
||||
},
|
||||
|
||||
unhoverCard: function(scrollable, shouldResetCardPositions) {
|
||||
scrollable.unhover(shouldResetCardPositions);
|
||||
this._hoveringCard = null;
|
||||
// clear up/bottom arrow
|
||||
if (shouldResetCardPositions) {
|
||||
this._setHintArrow();
|
||||
}
|
||||
},
|
||||
|
||||
_getPanel: function(nodeElem) {
|
||||
var cardPanel = nodeElem.getElementsByClassName('card-panel')[0];
|
||||
return {
|
||||
panel: cardPanel,
|
||||
renameBtn: cardPanel.getElementsByClassName('rename-btn')[0],
|
||||
deleteBtn: cardPanel.getElementsByClassName('delete-btn')[0]
|
||||
};
|
||||
},
|
||||
|
||||
_revealPanel: function(scrollable, nodeElem) {
|
||||
var panel = this._getPanel(nodeElem);
|
||||
if(scrollable.getItemFromNode(nodeElem).getAttribute('app-type') ===
|
||||
'deck') {
|
||||
// Decks can't be renamed or deleted.
|
||||
return;
|
||||
}
|
||||
scrollable.spatialNavigator.add(panel.renameBtn);
|
||||
scrollable.spatialNavigator.add(panel.deleteBtn);
|
||||
},
|
||||
|
||||
_concealPanel: function(scrollable, nodeElem) {
|
||||
var panel = this._getPanel(nodeElem);
|
||||
scrollable.spatialNavigator.remove(panel.renameBtn);
|
||||
scrollable.spatialNavigator.remove(panel.deleteBtn);
|
||||
},
|
||||
|
||||
handleFocus: function(elem) {
|
||||
if (this.mode === 'edit' && elem.CLASS_NAME === 'XScrollable') {
|
||||
this.handleCardFocus(
|
||||
elem, elem.currentItem, elem.getNodeFromItem(elem.currentItem));
|
||||
}
|
||||
},
|
||||
|
||||
handleUnfocus: function(elem) {
|
||||
if (this.mode === 'edit' && elem === this.currentScrollable) {
|
||||
this._concealPanel(this.currentScrollable, this.currentNode);
|
||||
// Make sure we leave focus on item itself rather than any panel buttons
|
||||
this.currentScrollable.silentFocus(
|
||||
this.currentScrollable.getItemFromNode(this.currentNode));
|
||||
}
|
||||
},
|
||||
|
||||
handleCardFocus: function(scrollable, itemElem, nodeElem) {
|
||||
if (this.mode !== 'edit' ||
|
||||
// When focus goes outside the scrollable and the last selected item
|
||||
// is a panel button, we still focus back to the card corresponding to
|
||||
// that button to keep track of the last focused card. But in this
|
||||
// case we don't need any further action on UI view.
|
||||
this.spatialNavigator.getFocusedElement() !== scrollable) {
|
||||
return;
|
||||
}
|
||||
// If users are moving from card to panel button, they're under the same
|
||||
// node element. We need to "unfocus" the node only when user really move
|
||||
// to another card. So here we call handleCardUnfocus manually.
|
||||
if (this.currentNode != nodeElem) {
|
||||
this._concealPanel(this.currentScrollable, this.currentNode);
|
||||
|
||||
this.currentNode = nodeElem;
|
||||
this._setCurrentScrollable(scrollable);
|
||||
}
|
||||
this._revealPanel(scrollable, nodeElem);
|
||||
itemElem.focus();
|
||||
nodeElem.classList.add('focused');
|
||||
},
|
||||
|
||||
handleListTransformEnd: function() {
|
||||
if (this.mode === '') {
|
||||
this.cardScrollable.listElem.classList.remove('exiting-edit-mode');
|
||||
}
|
||||
},
|
||||
|
||||
get mode() {
|
||||
return this.mainSection.dataset.mode;
|
||||
}
|
||||
});
|
||||
|
||||
exports.Edit = Edit;
|
||||
}(window));
|
||||
310
gaia/tv_apps/smart-home/js/filter_manager.js
Executable file
|
|
@ -0,0 +1,310 @@
|
|||
/* global CardFilter, Folder */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* FilterManager is the controller class of filter. It relies on
|
||||
* {@link CardFilter} to do UI works of filter itself. However it also needs
|
||||
* to co-operate with {@link http://bit.ly/1JcrKZO|SmartBubbles},
|
||||
* {@link Edit}, and {@link Home} classes to do the actual *filtering* task
|
||||
* in smart-home.
|
||||
*
|
||||
* The working flow when user change filter is like:
|
||||
*
|
||||
* 1. FilterManager receives `filterchanged` from {@link CardFilter}
|
||||
* 2. Play bubbling down animation, see
|
||||
* {@link FilterManager#onFilterChanged|onFilterChanged()}.
|
||||
* 3. When animation ends, change card icon in smart-home, with help from
|
||||
* {@link Home} and XScrollable. See
|
||||
* {@link FilterManager#onAnimationEnd|onAnimationEnd()}.
|
||||
* 4. Play bubbling up animation, see
|
||||
* {@link FilterManager#_performBubbleUp|_performBubbleUp()}.
|
||||
*
|
||||
* @class FilterManager
|
||||
* @requires {@link Home}
|
||||
* @requires XScrollable
|
||||
* @requires {@link Edit}
|
||||
* @requires {@link CardFilter}
|
||||
* @requires {@link http://bit.ly/1JcrKZO|SmartBubbles}
|
||||
*/
|
||||
var FilterManager = function() {};
|
||||
|
||||
/**
|
||||
* @namespace FilterManager.FILTERS
|
||||
*/
|
||||
FilterManager.FILTERS = Object.freeze({
|
||||
/**
|
||||
* @readonly
|
||||
* @memberof FilterManager.FILTERS
|
||||
* @property {String} name - value is `all`
|
||||
* @property {String} iconName - value is `filter`
|
||||
*/
|
||||
'ALL': {
|
||||
name: 'all',
|
||||
iconName: 'filter'
|
||||
},
|
||||
/**
|
||||
* @readonly
|
||||
* @memberof FilterManager.FILTERS
|
||||
* @property {String} name - value is `tv`
|
||||
* @property {String} iconName - value is `tv`
|
||||
*/
|
||||
'TV': {
|
||||
name: 'tv',
|
||||
iconName: 'tv'
|
||||
},
|
||||
/**
|
||||
* @readonly
|
||||
* @memberof FilterManager.FILTERS
|
||||
* @property {String} name - value is `device`
|
||||
* @property {String} iconName - value is `device`
|
||||
*/
|
||||
'DEVICE': {
|
||||
name: 'device',
|
||||
iconName: 'device'
|
||||
},
|
||||
/**
|
||||
* @readonly
|
||||
* @memberof FilterManager.FILTERS
|
||||
* @property {String} name - value is `application`
|
||||
* @property {String} iconName - value is `application`
|
||||
*/
|
||||
'APPLICATION': {
|
||||
name: 'application',
|
||||
iconName: 'application'
|
||||
},
|
||||
/**
|
||||
* @readonly
|
||||
* @memberof FilterManager.FILTERS
|
||||
* @property {String} name - value is `website`
|
||||
* @property {String} iconName - value is `website`
|
||||
*/
|
||||
'WEBSITE': {
|
||||
name: 'website',
|
||||
iconName: 'web'
|
||||
}
|
||||
});
|
||||
|
||||
FilterManager.prototype = {
|
||||
/**
|
||||
* Initialize FilterManager
|
||||
*
|
||||
* @public
|
||||
* @method FilterManager#init
|
||||
* @param {Object} options
|
||||
* @param {Object} options.cardListElem - `HTMLElement` of `card-list`
|
||||
* @param {Object} options.cardScrollable - instance of XScrollable
|
||||
* @param {Object} options.home - instance of {@link Home}
|
||||
* @param {Object} options.cardManager - instance of CardManager
|
||||
*/
|
||||
init: function(options) {
|
||||
this._cardListElem = options.cardListElem;
|
||||
this._cardScrollable = options.cardScrollable;
|
||||
this._home = options.home;
|
||||
this._edit = this._home.edit;
|
||||
this._cardManager = options.cardManager;
|
||||
|
||||
this._cardFilter = new CardFilter();
|
||||
this._cardFilter.start(document.getElementById('filter-tab-group'));
|
||||
this._cardFilter.filter = FilterManager.FILTERS.ALL.iconName;
|
||||
this._filterChangedHandler = this.onFilterChanged.bind(this);
|
||||
this._cardFilter.on('filterchanged', this._filterChangedHandler);
|
||||
this._cardFilter.on('opened', function() {
|
||||
this._home.cleanFolderScrollable(true);
|
||||
}.bind(this));
|
||||
|
||||
this._hideFilter = this.hide.bind(this);
|
||||
this._showFilter = this.show.bind(this);
|
||||
this._edit.on('enter-edit-mode', this._hideFilter);
|
||||
this._edit.on('exit-edit-mode', this._showFilter);
|
||||
|
||||
this._smartBubblesElem = document.getElementById('bubbles');
|
||||
|
||||
this._animationEndHandler = this.onAnimationEnd.bind(this);
|
||||
this._smartBubblesElem.addEventListener('all-items-bubbled',
|
||||
this._animationEndHandler);
|
||||
},
|
||||
|
||||
/**
|
||||
* Do the opposite of initalization
|
||||
*
|
||||
* @public
|
||||
* @method FilterManager#uninit
|
||||
*/
|
||||
uninit: function() {
|
||||
this._cardFilter.off('filterchanged', this._filterChangedHandler);
|
||||
this._edit.off('enter-edit-mode', this._hideFilter);
|
||||
this._edit.off('exit-edit-mode', this._showFilter);
|
||||
this._smartBubblesElem.removeEventListener('all-items-bubbled',
|
||||
this._animationEndHandler);
|
||||
},
|
||||
|
||||
_filteredCardList: undefined,
|
||||
|
||||
_isFirstFrame: false,
|
||||
|
||||
_isFilterChanging: false,
|
||||
|
||||
_isBubbleSinking: function fm_isBubbleSinking() {
|
||||
return (
|
||||
this._cardListElem.getAttribute('smart-bubbles-direction') === 'down');
|
||||
},
|
||||
|
||||
/**
|
||||
* Perform bubbling up animation. We must hide the first frame of animation
|
||||
* because there are sliding animation on XScrollable. We don't want user to
|
||||
* see this according to UX spec.
|
||||
*
|
||||
* @private
|
||||
* @method FilterManager#_performBubbleUp
|
||||
*/
|
||||
_performBubbleUp: function fm_performBubbleUp() {
|
||||
if (this._isFirstFrame) {
|
||||
this._isFirstFrame = false;
|
||||
this._cardListElem.style.transition = 'none';
|
||||
this._cardScrollable.resetScroll();
|
||||
window.requestAnimationFrame(this._performBubbleUp.bind(this));
|
||||
} else {
|
||||
this._cardListElem.style.opacity = 1;
|
||||
this._cardListElem.style.transition = '';
|
||||
this._cardListElem.removeAttribute('smart-bubbles-direction');
|
||||
this._smartBubblesElem.play(
|
||||
document.querySelectorAll('#card-list > .card > .app-button'));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get constant object of {@link FilterManager.FILTERS} of the `iconName`
|
||||
*
|
||||
* @public
|
||||
* @method FilterManager#getFilterByIconName
|
||||
* @param {String} iconName -
|
||||
*/
|
||||
getFilterByIconName: function fm_getFilterByIconName(iconName) {
|
||||
var keys = Object.keys(FilterManager.FILTERS);
|
||||
var filter;
|
||||
keys.some(function(key) {
|
||||
var cursor = FilterManager.FILTERS[key];
|
||||
if (iconName === cursor.iconName) {
|
||||
filter = cursor;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return filter;
|
||||
},
|
||||
|
||||
/**
|
||||
* EventHandler of `all-items-bubbled` event.
|
||||
* [SmartBubbles](http://bit.ly/1JcrKZO) fires `all-items-bubbled` when it
|
||||
* is done with bubbling down animation. We start to alter the card content
|
||||
* of XScrollable only after bubbling down animation ends. And after we are
|
||||
* done with altering content, we'll have to perform bubbling up animation
|
||||
* by calling {@link FilterManager#_performBubbleUp}.
|
||||
*
|
||||
* @public
|
||||
* @method FilterManager#onAnimationEnd
|
||||
*/
|
||||
onAnimationEnd: function fm_onAnimationEnd() {
|
||||
if (!this._isFilterChanging) {
|
||||
return;
|
||||
}
|
||||
|
||||
var that = this;
|
||||
var filter = this.getFilterByIconName(this._cardFilter.filter);
|
||||
var gotCardLists = function(results) {
|
||||
// results[0] is the main card list which has been filtered.
|
||||
// results[1] is the main card list.
|
||||
var filteredList = results[0];
|
||||
var cardList = results[1];
|
||||
|
||||
// Get filtered card list for each folder.
|
||||
cardList.filter(elem => elem instanceof Folder).forEach(elem => {
|
||||
filteredList = filteredList.concat(
|
||||
elem.getFilteredCardList(filter.name));
|
||||
});
|
||||
|
||||
filteredList.forEach(function(card) {
|
||||
that._cardScrollable.addNode(that._home.createCardNode(card));
|
||||
});
|
||||
|
||||
that._filteredCardList =
|
||||
(filter.name !== 'all') ? filteredList : undefined;
|
||||
that._cardListElem.style.opacity = 0;
|
||||
window.requestAnimationFrame(that._performBubbleUp.bind(that));
|
||||
};
|
||||
|
||||
if (this._isBubbleSinking()) {
|
||||
this._cardScrollable.clean();
|
||||
Promise.all([
|
||||
this._cardManager.getFilteredCardList(filter.name),
|
||||
this._cardManager.getCardList()
|
||||
]).then(gotCardLists);
|
||||
} else {
|
||||
this._isFilterChanging = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* EventHandler of `filterchanged` event. {@link CardFilter} fires
|
||||
* `filterchanged` event whenever user select a different filter. This is
|
||||
* becasue we need to play bubbling down animation whenever filter is
|
||||
* changed.
|
||||
*
|
||||
* @public
|
||||
* @method FilterManager#onFilterChanged
|
||||
*/
|
||||
onFilterChanged: function fm_onFilterChange(iconName) {
|
||||
this._isFirstFrame = true;
|
||||
|
||||
if (this._isFilterChanging) {
|
||||
this._smartBubblesElem.stopImmediately();
|
||||
}
|
||||
this._isFilterChanging = true;
|
||||
this._cardListElem.setAttribute('smart-bubbles-direction', 'down');
|
||||
|
||||
// Notice that methods like document.querySelectorAll or
|
||||
// document.getElementsByClassName returns elements based on the
|
||||
// sequence of time that element added to DOM structure. But we should
|
||||
// play animation in the left-to-right order. Card scrollable keeps the
|
||||
// order of all items, we just use them directly to play animation.
|
||||
// See http://bugzil.la/1169538
|
||||
this._smartBubblesElem.play(this._cardScrollable.allItems);
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide filter. This should be called when we are in edit mode.
|
||||
*
|
||||
* @public
|
||||
* @method FilterManager#hide
|
||||
*/
|
||||
hide: function fm_hide() {
|
||||
this._cardFilter.hide();
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide filter. This should be called when we are exit from edit mode.
|
||||
*
|
||||
* @public
|
||||
* @method FilterManager#show
|
||||
*/
|
||||
show: function fm_show() {
|
||||
this._cardFilter.show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset filter to {@link FilterManager.FILTERS.ALL}. We should always reset
|
||||
* filter when smart-home is back to foreground.
|
||||
*
|
||||
* @public
|
||||
* @method FilterManager#resetFilter
|
||||
*/
|
||||
resetFilter: function fm_resetFilter() {
|
||||
if (this._cardFilter.filter !== FilterManager.FILTERS.ALL.iconName) {
|
||||
this._cardFilter.filter = FilterManager.FILTERS.ALL.iconName;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.FilterManager = FilterManager;
|
||||
}(window));
|
||||
831
gaia/tv_apps/smart-home/js/home.js
Executable file
|
|
@ -0,0 +1,831 @@
|
|||
'use strict';
|
||||
/* global Application, FilterManager, CardManager, Clock, Deck, Edit, Folder,
|
||||
KeyNavigationAdapter, MessageHandler, MozActivity, SearchBar,
|
||||
SharedUtils, SpatialNavigator, URL, XScrollable, Animations,
|
||||
Utils, FTEWizard, AppBookmark */
|
||||
/* jshint nonew: false */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const FULLSIZED_ICON = 336 * (window.devicePixelRatio || 1);
|
||||
const DEFAULT_ICON = 'url("/style/images/appic_developer.png")';
|
||||
const DEFAULT_BGCOLOR = 'rgba(0, 0, 0, 0.5)';
|
||||
const DEFAULT_BGCOLOR_ARRAY = [0, 0, 0, 0.5];
|
||||
const CARDLIST_LEFT_MARGIN = 8.4;
|
||||
|
||||
/**
|
||||
* Main class of smart-home
|
||||
* @class Home
|
||||
*/
|
||||
function Home() {}
|
||||
|
||||
Home.prototype = {
|
||||
navigableIds:
|
||||
['search-button', 'search-input', 'edit-button', 'settings-button',
|
||||
'filter-tab-group'],
|
||||
|
||||
topElementIds: ['search-button', 'search-input', 'edit-button',
|
||||
'settings-button'],
|
||||
|
||||
bottomElementIds: ['filter-tab-group', 'filter-all-button',
|
||||
'filter-tv-button', 'filter-device-button', 'filter-app-button',
|
||||
'filter-website-button'],
|
||||
|
||||
isNavigable: true,
|
||||
navigableClasses: ['filter-tab', 'command-button'],
|
||||
navigableScrollable: [],
|
||||
cardScrollable: undefined,
|
||||
folderScrollable: undefined,
|
||||
_focus: undefined,
|
||||
_focusScrollable: undefined,
|
||||
_folderCard: undefined,
|
||||
|
||||
filterElementIds: ['filter-all-button', 'filter-tv-button',
|
||||
'filter-device-button', 'filter-app-button', 'filter-website-button'],
|
||||
|
||||
filterManager: undefined,
|
||||
cardListElem: document.getElementById('card-list'),
|
||||
folderListElem: document.getElementById('folder-list'),
|
||||
cardManager: undefined,
|
||||
editButton: document.getElementById('edit-button'),
|
||||
settingsButton: document.getElementById('settings-button'),
|
||||
searchButton: document.getElementById('search-button'),
|
||||
timeElem: document.getElementById('time'),
|
||||
fteElem: document.getElementById('fte'),
|
||||
|
||||
|
||||
init: function() {
|
||||
var that = this;
|
||||
|
||||
this.initClock();
|
||||
|
||||
this.cardManager = new CardManager();
|
||||
this.cardManager.init();
|
||||
|
||||
this.searchBar = new SearchBar();
|
||||
this.searchBar.init(document.getElementById('search-bar'));
|
||||
this.searchBar.on('shown', this.onSearchBarShown.bind(this));
|
||||
this.searchBar.on('hidden', this.onSearchBarHidden.bind(this));
|
||||
|
||||
this.cardManager.getCardList().then(function(cardList) {
|
||||
that.messageHandler = new MessageHandler();
|
||||
that.messageHandler.init(that);
|
||||
|
||||
that._createCardList(cardList);
|
||||
that.cardScrollable = new XScrollable({
|
||||
frameElem: 'card-list-frame',
|
||||
listElem: 'card-list',
|
||||
itemClassName: 'app-button',
|
||||
leftMargin: CARDLIST_LEFT_MARGIN});
|
||||
|
||||
that.folderScrollable = new XScrollable({
|
||||
frameElem: 'folder-list-frame',
|
||||
listElem: 'folder-list',
|
||||
itemClassName: 'app-button',
|
||||
leftMargin: CARDLIST_LEFT_MARGIN,
|
||||
scale: 0.68,
|
||||
referenceElement: that.cardScrollable});
|
||||
|
||||
that.navigableScrollable = [that.cardScrollable, that.folderScrollable];
|
||||
var collection = that._getNavigateElements();
|
||||
|
||||
that.spatialNavigator = new SpatialNavigator(collection);
|
||||
that.spatialNavigator.straightOnly = true;
|
||||
|
||||
that.keyNavigatorAdapter = new KeyNavigationAdapter();
|
||||
that.keyNavigatorAdapter.init();
|
||||
that.keyNavigatorAdapter.on('move', that.onMove.bind(that));
|
||||
// All behaviors which no need to have multple events while holding
|
||||
// the key should use keyup.
|
||||
that.keyNavigatorAdapter.on('enter-keyup', that.onEnter.bind(that));
|
||||
|
||||
that.cardListElem.addEventListener('transitionend',
|
||||
that.determineFolderExpand.bind(that));
|
||||
|
||||
that.cardManager.on('card-inserted',
|
||||
that.onCardInserted.bind(that, that.cardScrollable));
|
||||
that.cardManager.on('card-removed',
|
||||
that.onCardRemoved.bind(that, that.cardScrollable));
|
||||
that.cardManager.on('card-updated', that.onCardUpdated.bind(that));
|
||||
|
||||
that.spatialNavigator.on('focus', that.handleFocus.bind(that));
|
||||
that.spatialNavigator.on('unfocus', that.handleUnfocus.bind(that));
|
||||
var handleCardFocusBound = that.handleCardFocus.bind(that);
|
||||
var handleCardUnfocusBound = that.handleCardUnfocus.bind(that);
|
||||
var handleCardUnhoverBound = that.handleCardUnhover.bind(that);
|
||||
|
||||
that.navigableScrollable.forEach(function(scrollable) {
|
||||
scrollable.on('focus', handleCardFocusBound);
|
||||
scrollable.on('unfocus', handleCardUnfocusBound);
|
||||
scrollable.on('unhover', handleCardUnhoverBound);
|
||||
if (scrollable.isEmpty()) {
|
||||
that.spatialNavigator.remove(scrollable);
|
||||
}
|
||||
});
|
||||
|
||||
that.edit = new Edit();
|
||||
that.edit.init(that.spatialNavigator, that.cardManager,
|
||||
that.cardScrollable, that.folderScrollable);
|
||||
that.edit.on('arrange', that.onArrangeMode.bind(that));
|
||||
|
||||
that.filterManager = new FilterManager();
|
||||
that.filterManager.init({
|
||||
cardListElem: that.cardListElem,
|
||||
cardScrollable: that.cardScrollable,
|
||||
home: that,
|
||||
cardManager: that.cardManager,
|
||||
});
|
||||
|
||||
// In some case, we can do action at keydown which is translated as
|
||||
// onEnter in home.js. But in button click case, we need to listen
|
||||
// keyup. So, instead keydown/keyup, we just use click event to handle
|
||||
// it. The click event is translated at smart-button when use press
|
||||
// enter on smart-button.
|
||||
that.searchButton.addEventListener('click', function() {
|
||||
that.searchBar.show();
|
||||
// hide the searchButton because searchBar has an element whose
|
||||
// appearance is the same as it.
|
||||
that.searchButton.classList.add('hidden');
|
||||
}.bind(that));
|
||||
|
||||
// handle animation
|
||||
that.endBubble = null;
|
||||
document.addEventListener(
|
||||
'visibilitychange', that.onVisibilityChange.bind(that));
|
||||
// if this init function is executed after the document is set to
|
||||
// visible, the visibilitychange event may not be triggered.
|
||||
if (document.visibilityState === 'visible') {
|
||||
that.onVisibilityChange();
|
||||
}
|
||||
|
||||
cardList.forEach(function(card) {
|
||||
if (card instanceof Folder) {
|
||||
card.on('card-inserted',
|
||||
that.onCardInserted.bind(that, that.folderScrollable));
|
||||
card.on('card-removed',
|
||||
that.onCardRemoved.bind(that, that.folderScrollable));
|
||||
}
|
||||
});
|
||||
that._fteWizard = new FTEWizard('homeFTE');
|
||||
that._fteWizard.init({
|
||||
container: that.fteElem,
|
||||
onfinish: () => {
|
||||
that.spatialNavigator.focus(that.cardScrollable);
|
||||
that.isNavigable = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onVisibilityChange: function() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
Utils.holdFocusForAnimation();
|
||||
this.cardListElem.classList.remove('hidden');
|
||||
var that = this;
|
||||
Promise.all([new Promise(function(resolve) {
|
||||
that.skipBubble = Animations.doBubbleAnimation(
|
||||
that.cardListElem, '.app-button', 100, resolve);
|
||||
|
||||
}), new Promise(function(resolve) {
|
||||
if (that._folderCard) {
|
||||
that.skipFolderBubble = Animations.doBubbleAnimation(
|
||||
that.folderListElem, '.app-button', 100, resolve);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
|
||||
})]).then(function() {
|
||||
// Catch focus back unless there is a pin activity since callback of
|
||||
// pinning would catch the focus for us.
|
||||
if (that._fteWizard.running) {
|
||||
that._fteWizard.focus();
|
||||
} else if (!that.messageHandler.resumeActivity()) {
|
||||
that.spatialNavigator.focus();
|
||||
}
|
||||
|
||||
that.isNavigable = true;
|
||||
that.skipBubble = null;
|
||||
that.skipFolderBubble = null;
|
||||
});
|
||||
} else {
|
||||
this.cardListElem.classList.add('hidden');
|
||||
this.messageHandler.stopActivity();
|
||||
this.isNavigable = false;
|
||||
// An user may close home app when bubbling or sliding animations are
|
||||
// still playing, and then open home app again right away. In this case,
|
||||
// the user will see the last unfinished animations. In order to solve
|
||||
// this, we have to force disable all the animations and trigger their
|
||||
// callbacks when home app is in hidden state.
|
||||
if (this.skipBubble) {
|
||||
this.skipBubble();
|
||||
}
|
||||
if (this.skipFolderBubble) {
|
||||
this.skipFolderBubble();
|
||||
}
|
||||
if (this.cardScrollable.isSliding) {
|
||||
this.cardScrollable.endSlide();
|
||||
}
|
||||
this.filterManager.resetFilter();
|
||||
}
|
||||
},
|
||||
|
||||
initClock: function() {
|
||||
var that = this;
|
||||
navigator.mozL10n.ready(function() {
|
||||
that.clock = new Clock();
|
||||
that.clock.start(that.updateClock.bind(that));
|
||||
// Listen to 'moztimechange'
|
||||
window.addEventListener('moztimechange',
|
||||
that.restartClock.bind(that));
|
||||
// Listen to 'timeformatchange'
|
||||
window.addEventListener('timeformatchange',
|
||||
that.restartClock.bind(that));
|
||||
});
|
||||
},
|
||||
|
||||
_localizeCardName: function(elem, card) {
|
||||
if (!elem || !card) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We should use user given name first, otherwise we use localized
|
||||
// application/deck name.
|
||||
var lang = document.documentElement.lang;
|
||||
var name = this.cardManager.resolveCardName(card, lang);
|
||||
if (name && name.raw) {
|
||||
elem.removeAttribute('data-l10n-id');
|
||||
elem.textContent = name.raw;
|
||||
} else if (name && name.id) {
|
||||
SharedUtils.localizeElement(elem, name);
|
||||
}
|
||||
},
|
||||
|
||||
onCardInserted: function(scrollable, card, idx, overFolder) {
|
||||
if (card instanceof Folder) {
|
||||
card.on('card-inserted',
|
||||
this.onCardInserted.bind(this, this.folderScrollable));
|
||||
card.on('card-removed',
|
||||
this.onCardRemoved.bind(this, this.folderScrollable));
|
||||
}
|
||||
|
||||
var newCardElem = this.createCardNode(card);
|
||||
var newCardButtonElem = newCardElem.firstElementChild;
|
||||
// Initial transition for new card
|
||||
newCardButtonElem.classList.add('new-card');
|
||||
newCardButtonElem.classList.add('new-card-transition');
|
||||
newCardButtonElem.addEventListener('transitionend', function onPinned() {
|
||||
newCardButtonElem.classList.remove('new-card-transition');
|
||||
newCardButtonElem.classList.remove('last-card');
|
||||
newCardButtonElem.removeEventListener('transitionend', onPinned);
|
||||
});
|
||||
// insert new card into cardScrollable
|
||||
this.isNavigable = false;
|
||||
scrollable.on('slideEnd', function() {
|
||||
newCardButtonElem.classList.remove('new-card');
|
||||
if (scrollable.nodes.indexOf(newCardElem) ===
|
||||
scrollable.nodes.length - 1) {
|
||||
newCardButtonElem.classList.add('last-card');
|
||||
}
|
||||
this.isNavigable = true;
|
||||
}.bind(this));
|
||||
if (!overFolder) {
|
||||
scrollable.insertNodeBefore(newCardElem, idx);
|
||||
} else {
|
||||
scrollable.insertNodeOver(newCardElem, idx);
|
||||
}
|
||||
},
|
||||
|
||||
onCardUpdated: function(card, idx) {
|
||||
var that = this;
|
||||
var cardButton = this.cardScrollable.getItemFromNode(
|
||||
this.cardScrollable.getNode(idx));
|
||||
var spans =
|
||||
SharedUtils.nodeListToArray(cardButton.getElementsByTagName('span'));
|
||||
spans.forEach(function(span) {
|
||||
if (span.classList.contains('name')) {
|
||||
that._localizeCardName(span, card);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onCardRemoved: function(scrollable, indices) {
|
||||
indices.forEach(function(idx) {
|
||||
var elm = scrollable.getNode(idx);
|
||||
if (elm && elm.dataset.revokableURL) {
|
||||
URL.revokeObjectURL(elm.dataset.revokableURL);
|
||||
}
|
||||
}, this);
|
||||
scrollable.removeNodes(indices);
|
||||
},
|
||||
|
||||
_setCardIcon: function (cardButton, card, blob, bgColor) {
|
||||
try {
|
||||
var bgUrl = URL.createObjectURL(blob);
|
||||
if (bgColor) {
|
||||
cardButton.style.backgroundColor = bgColor;
|
||||
cardButton.classList.add('fitted');
|
||||
card.backgroundType = 'fitted';
|
||||
} else {
|
||||
cardButton.classList.add('fullsized');
|
||||
card.backgroundType = 'fullsized';
|
||||
}
|
||||
cardButton.dataset.revokableURL = bgUrl;
|
||||
cardButton.style.backgroundImage = 'url("' + bgUrl + '")';
|
||||
} catch (e) {
|
||||
// If the blob is broken, we may get an exception while creating object
|
||||
// URL.
|
||||
cardButton.style.backgroundImage = DEFAULT_ICON;
|
||||
cardButton.style.backgroundColor = DEFAULT_BGCOLOR;
|
||||
}
|
||||
},
|
||||
|
||||
_createWave: function(cardButton, card) {
|
||||
|
||||
// deck's icon using gaia font
|
||||
var deckIcon = document.createElement('span');
|
||||
deckIcon.className = 'icon';
|
||||
deckIcon.dataset.icon = card.deckClass;
|
||||
|
||||
// front wave of a deck
|
||||
var waveFront = document.createElement('div');
|
||||
waveFront.className = 'deck-wave';
|
||||
waveFront.classList.add('wave-front');
|
||||
waveFront.classList.add(card.deckClass + '-wave-front');
|
||||
waveFront.classList.add('wave-paused');
|
||||
|
||||
// back wave of a deck
|
||||
var waveBack = document.createElement('div');
|
||||
waveBack.className = 'deck-wave';
|
||||
waveBack.classList.add('wave-back');
|
||||
waveBack.classList.add(card.deckClass + '-wave-back');
|
||||
|
||||
cardButton.appendChild(waveBack);
|
||||
cardButton.appendChild(deckIcon);
|
||||
cardButton.appendChild(waveFront);
|
||||
cardButton.classList.add('deck-' + card.deckClass);
|
||||
},
|
||||
|
||||
_fillCardIcon: function(cardButton, card) {
|
||||
var manifestURL = card.nativeApp && card.nativeApp.manifestURL;
|
||||
var that = this;
|
||||
// We have thumbnail which is created by pin
|
||||
if (card.thumbnail) {
|
||||
this._setCardIcon(cardButton, card, card.thumbnail,
|
||||
card.backgroundColor);
|
||||
// TODO add backgroundColor??? How to do it???
|
||||
} else if (!card.cachedIconBlob) {
|
||||
// We don't have cachedIconBlob, just get icon from app
|
||||
this.cardManager.getIconBlob({
|
||||
manifestURL: manifestURL,
|
||||
entryPoint: card.entryPoint,
|
||||
// XXX: preferredSize should be determined by
|
||||
// real offsetWidth of cardThumbnailElem instead of hard-coded value
|
||||
preferredSize: FULLSIZED_ICON
|
||||
}).then(function(iconData) {
|
||||
var blob = iconData[0];
|
||||
var size = iconData[1];
|
||||
if (size >= FULLSIZED_ICON) {
|
||||
that._setCardIcon(cardButton, card, blob, null);
|
||||
} else {
|
||||
that._getIconColor(blob, function(color, err) {
|
||||
if (err) {
|
||||
that._setCardIcon(cardButton, card, blob, DEFAULT_BGCOLOR);
|
||||
} else {
|
||||
that._setCardIcon(cardButton, card, blob, 'rgba(' + color[0] +
|
||||
', ' + color[1] + ', ' + color[2] + ', ' + color[3] + ')');
|
||||
}
|
||||
});
|
||||
}
|
||||
card.cachedIconBlob = blob;
|
||||
});
|
||||
} else if (card.cachedIconBlob) {
|
||||
// We already have cacedIconBlob which is created by previous step.
|
||||
this._setCardIcon(cardButton, card, card.cachedIconBlob,
|
||||
card.backgroundColor);
|
||||
}
|
||||
},
|
||||
|
||||
_getIconColor: function(blob, callback) {
|
||||
var dy = 0;
|
||||
function checkColor(color, err) {
|
||||
if (err) {
|
||||
callback(null, err);
|
||||
} else if (color[3] < 255 && dy < 0.5) {
|
||||
dy += 0.25;
|
||||
SharedUtils.readColorCode(blob, 0.5, dy, checkColor);
|
||||
} else {
|
||||
callback(color[3] < 255 ? DEFAULT_BGCOLOR_ARRAY : color, err);
|
||||
}
|
||||
}
|
||||
|
||||
SharedUtils.readColorCode(blob, 0.5, 0, checkColor);
|
||||
},
|
||||
|
||||
onArrangeMode: function() {
|
||||
if (this._focusScrollable !== this.folderScrollable) {
|
||||
this.cleanFolderScrollable();
|
||||
}
|
||||
},
|
||||
|
||||
createCardNode: function(card) {
|
||||
// card element would be created like this:
|
||||
// <div class="card">
|
||||
// <smart-button>/* Card button */</smart-button>
|
||||
// <section class="card-panel">
|
||||
// <smart-button>/* Rename button */</smart-button>
|
||||
// <smart-button>/* Delete button */</smart-button>
|
||||
// </section>
|
||||
// </div>
|
||||
// and return DOM element
|
||||
var cardNode = document.createElement('div');
|
||||
cardNode.classList.add('card');
|
||||
|
||||
var cardButton = document.createElement('smart-button');
|
||||
cardButton.setAttribute('type', 'app-button');
|
||||
cardButton.className = 'app-button';
|
||||
cardButton.dataset.cardId = card.cardId;
|
||||
|
||||
var cardPanel = document.createElement('section');
|
||||
cardPanel.className = 'card-panel';
|
||||
|
||||
var renameButton = document.createElement('smart-button');
|
||||
renameButton.dataset.icon = 'rename';
|
||||
renameButton.classList.add('rename-btn');
|
||||
|
||||
var deleteButton = document.createElement('smart-button');
|
||||
deleteButton.dataset.icon = 'delete';
|
||||
deleteButton.classList.add('delete-btn');
|
||||
|
||||
cardPanel.appendChild(renameButton);
|
||||
cardPanel.appendChild(deleteButton);
|
||||
|
||||
cardNode.appendChild(cardButton);
|
||||
cardNode.appendChild(cardPanel);
|
||||
|
||||
// XXX: will support Folder and other type of Card in the future
|
||||
// for now, we only create card element for Application and Deck
|
||||
if (card instanceof Application) {
|
||||
if (card.group === 'tv') {
|
||||
cardButton.classList.add('tv-channel');
|
||||
cardButton.dataset.icon = 'tv';
|
||||
cardButton.setAttribute('app-type', 'tv');
|
||||
} else {
|
||||
cardButton.setAttribute('app-type', 'app');
|
||||
this._fillCardIcon(cardButton, card);
|
||||
}
|
||||
} else if (card instanceof Deck) {
|
||||
cardButton.setAttribute('app-type', 'deck');
|
||||
if (card.group === 'website') {
|
||||
this._fillCardIcon(cardButton, card);
|
||||
} else {
|
||||
this._createWave(cardButton, card);
|
||||
}
|
||||
} else if (card instanceof AppBookmark) {
|
||||
cardButton.setAttribute('app-type', 'appbookmark');
|
||||
this._fillCardIcon(cardButton, card);
|
||||
} else if (card instanceof Folder) {
|
||||
cardButton.setAttribute('app-type', 'folder');
|
||||
cardButton.dataset.icon = 'folder';
|
||||
}
|
||||
|
||||
// For smart-button, we put card name in pseudo-element :after. However,
|
||||
// we need to localize card name and l10n library do not support
|
||||
// localizing element with children elements.
|
||||
// Instead of using :after, we create a 'span' element under smart-button
|
||||
// and put card name in it.
|
||||
var nameSpan = document.createElement('span');
|
||||
nameSpan.classList.add('name');
|
||||
this._localizeCardName(nameSpan, card);
|
||||
cardButton.appendChild(nameSpan);
|
||||
|
||||
return cardNode;
|
||||
},
|
||||
|
||||
_createCardList: function(cardList) {
|
||||
cardList.forEach(function(card) {
|
||||
this.cardListElem.appendChild(this.createCardNode(card));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
onMove: function(key) {
|
||||
if (!this.isNavigable || this.edit.onMove(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var focus = this.spatialNavigator.getFocusedElement();
|
||||
|
||||
if (!(focus.CLASS_NAME == 'XScrollable' && focus.move(key))) {
|
||||
this.spatialNavigator.move(key);
|
||||
}
|
||||
},
|
||||
|
||||
onEnter: function() {
|
||||
if (!this.isNavigable || this.edit.onEnter()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var focusElem = this.focusElem;
|
||||
|
||||
if (focusElem === this.settingsButton) {
|
||||
this.openSettings();
|
||||
} else if (focusElem === this.editButton) {
|
||||
this.cleanFolderScrollable();
|
||||
this.edit.toggleEditMode();
|
||||
// XXX: Reset card filter when entering edit mode
|
||||
this.filterManager.resetFilter();
|
||||
} else if (focusElem &&
|
||||
this.filterElementIds.indexOf(focusElem.id) > -1) {
|
||||
this.cleanFolderScrollable();
|
||||
} else {
|
||||
// Current focus is on a card
|
||||
var cardId = focusElem.dataset.cardId;
|
||||
var card;
|
||||
if (this.focusScrollable === this.folderScrollable) {
|
||||
card = this._folderCard.findCard({cardId: cardId});
|
||||
} else {
|
||||
card = this.cardManager.findCardFromCardList({cardId: cardId});
|
||||
}
|
||||
|
||||
if (card) {
|
||||
card.launch();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onSearchBarShown: function() {
|
||||
var hideSearchBar = function() {
|
||||
document.removeEventListener('visibilitychange', hideSearchBar);
|
||||
this.searchBar.hide();
|
||||
}.bind(this);
|
||||
document.addEventListener('visibilitychange', hideSearchBar);
|
||||
|
||||
var activity = new MozActivity({
|
||||
name: 'search',
|
||||
data: { keyword: '' }
|
||||
});
|
||||
|
||||
activity.onerror = hideSearchBar;
|
||||
},
|
||||
|
||||
onSearchBarHidden: function() {
|
||||
this.searchButton.classList.remove('hidden');
|
||||
},
|
||||
|
||||
_getNavigateElements: function() {
|
||||
var elements = [];
|
||||
this.navigableIds.forEach(function(id) {
|
||||
var elem = document.getElementById(id);
|
||||
if (elem) {
|
||||
elements.push(elem);
|
||||
}
|
||||
});
|
||||
this.navigableClasses.forEach(function(className) {
|
||||
var elems = document.getElementsByClassName(className);
|
||||
if (elems.length) {
|
||||
// Change HTMLCollection to array before concatenating
|
||||
elements = elements.concat(Array.prototype.slice.call(elems));
|
||||
}
|
||||
});
|
||||
elements = elements.concat(this.navigableScrollable);
|
||||
return elements;
|
||||
},
|
||||
|
||||
handleFocus: function(elem) {
|
||||
if (elem.CLASS_NAME == 'XScrollable') {
|
||||
this._focusScrollable = elem;
|
||||
elem.focus();
|
||||
this.checkFocusedGroup();
|
||||
} else if (elem.nodeName) {
|
||||
if (this._focus) {
|
||||
this._focus.blur();
|
||||
}
|
||||
|
||||
switch(elem.nodeName.toLowerCase()) {
|
||||
case 'menu-group':
|
||||
this.handleFocusMenuGroup(elem);
|
||||
break;
|
||||
default:
|
||||
elem.focus();
|
||||
this._focus = elem;
|
||||
this._focusScrollable = undefined;
|
||||
this.checkFocusedGroup(elem);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this._focusScrollable = undefined;
|
||||
}
|
||||
},
|
||||
|
||||
handleUnfocus: function(elem, nodeElem) {
|
||||
if(elem.CLASS_NAME == 'XScrollable') {
|
||||
this.handleCardUnfocus(
|
||||
elem, elem.currentItem, elem.getNodeFromItem(elem.currentItem));
|
||||
}
|
||||
},
|
||||
|
||||
checkFocusedGroup: function(elem) {
|
||||
if (!this._focusedGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
// close the focused group when we move focus out of this group.
|
||||
if (!elem || !this._focusedGroup.contains(elem)) {
|
||||
this._focusedGroup.close();
|
||||
this._focusedGroup = null;
|
||||
}
|
||||
},
|
||||
|
||||
handleFocusMenuGroup: function(menuGroup) {
|
||||
var self = this;
|
||||
menuGroup.once('opened', function() {
|
||||
self.spatialNavigator.remove(menuGroup);
|
||||
var childElement = menuGroup.firstElementChild;
|
||||
var firstFocusable = null;
|
||||
while(childElement) {
|
||||
switch(childElement.nodeName.toLowerCase()) {
|
||||
case 'style':
|
||||
case 'script':
|
||||
break;
|
||||
default:
|
||||
firstFocusable = firstFocusable || childElement;
|
||||
self.spatialNavigator.add(childElement);
|
||||
}
|
||||
childElement = childElement.nextElementSibling;
|
||||
}
|
||||
if (firstFocusable) {
|
||||
self.spatialNavigator.focus(firstFocusable);
|
||||
}
|
||||
});
|
||||
menuGroup.once('will-close', function() {
|
||||
// Clear all opened event listener because we won't have it if opened is
|
||||
// not fired and the group is trying to close it self.
|
||||
menuGroup.off('opened');
|
||||
self.spatialNavigator.add(menuGroup);
|
||||
var childElement = menuGroup.firstElementChild;
|
||||
while(childElement) {
|
||||
switch(childElement.nodeName.toLowerCase()) {
|
||||
case 'style':
|
||||
case 'script':
|
||||
break;
|
||||
default:
|
||||
self.spatialNavigator.remove(childElement);
|
||||
}
|
||||
childElement = childElement.nextElementSibling;
|
||||
}
|
||||
});
|
||||
this.checkFocusedGroup(menuGroup);
|
||||
this._focusedGroup = menuGroup;
|
||||
menuGroup.open();
|
||||
},
|
||||
|
||||
handleCardFocus: function(scrollable, itemElem, nodeElem) {
|
||||
this._focus = itemElem;
|
||||
|
||||
if (this.edit.mode === 'edit') {
|
||||
this.edit.handleCardFocus(scrollable, itemElem, nodeElem);
|
||||
}
|
||||
itemElem.focus();
|
||||
nodeElem.classList.add('focused');
|
||||
if(scrollable === this.cardScrollable && this._folderCard &&
|
||||
itemElem.dataset.cardId !== this._folderCard.cardId &&
|
||||
!this.cardScrollable.isHovering) {
|
||||
this.cleanFolderScrollable();
|
||||
}
|
||||
},
|
||||
|
||||
cleanFolderScrollable: function(doNotChangeFocus) {
|
||||
if (this._focusScrollable === this.folderScrollable &&
|
||||
!doNotChangeFocus) {
|
||||
this.spatialNavigator.focus(this.cardScrollable);
|
||||
}
|
||||
this.spatialNavigator.remove(this.folderScrollable);
|
||||
this.folderScrollable.clean();
|
||||
this._folderCard = undefined;
|
||||
this.edit.isFolderReady = false;
|
||||
this.cardScrollable.setColspanOnFocus(0);
|
||||
},
|
||||
|
||||
handleCardUnfocus: function(scrollable, itemElem, nodeElem) {
|
||||
// Fix null error when the last card in a folder is removed.
|
||||
if (nodeElem) {
|
||||
nodeElem.classList.remove('focused');
|
||||
}
|
||||
},
|
||||
|
||||
handleCardUnhover: function(scrollable, itemElem, nodeElem) {
|
||||
this.cleanFolderScrollable();
|
||||
},
|
||||
|
||||
determineFolderExpand: function(evt) {
|
||||
// Folder expansion is performed on only when user moves cursor onto a
|
||||
// folder or hover a folder in edit mode and it finished its focus
|
||||
// transition.
|
||||
if (this.focusScrollable === this.cardScrollable &&
|
||||
evt.originalTarget.classList.contains('app-button') &&
|
||||
(!this._folderCard ||
|
||||
this._folderCard.cardId !== evt.originalTarget.dataset.cardId) &&
|
||||
(evt.originalTarget.classList.contains('focused') &&
|
||||
// Listen to 'outline-width' rather than 'transform' here since it
|
||||
// also applies to edit mode when user moves from panel button back
|
||||
// to card.
|
||||
evt.propertyName === 'outline-width' &&
|
||||
document.getElementById('main-section').dataset.mode !== 'arrange' ||
|
||||
// Folder needs to be expanded when hovered as well.
|
||||
evt.originalTarget.classList.contains('hovered'))) {
|
||||
this.buildFolderList(evt.originalTarget);
|
||||
}
|
||||
},
|
||||
|
||||
buildFolderList: function(target) {
|
||||
var cardId = target.dataset.cardId;
|
||||
var card = this.cardManager.findCardFromCardList({cardId: cardId});
|
||||
if (!(card instanceof Folder)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._folderCard = card;
|
||||
var folderList = this._folderCard.getCardList();
|
||||
|
||||
if (folderList.length === 0) {
|
||||
this.edit.isFolderReady = true;
|
||||
// Needs to disable the folder-list animation right away to
|
||||
// prevent it from affecting the new-card into folder animation.
|
||||
this.folderListElem.style.transition = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build folder list
|
||||
folderList.forEach(function(card) {
|
||||
this.folderScrollable.addNode(this.createCardNode(card));
|
||||
}, this);
|
||||
|
||||
var isFirstFrame = true;
|
||||
var initFolderAnimation = function() {
|
||||
if (isFirstFrame) {
|
||||
isFirstFrame = false;
|
||||
// At first frame, we call setReferenceElement to move folder list
|
||||
// right under folder card. Transition should be replaced by 'none'
|
||||
// since we don't need to show this process as animation to user.
|
||||
this.folderListElem.style.transition = 'none';
|
||||
this.folderScrollable.realignToReferenceElement();
|
||||
this.skipFolderBubble = Animations.doBubbleAnimation(
|
||||
this.folderListElem, '.app-button', 100, function() {
|
||||
this.spatialNavigator.add(this.folderScrollable);
|
||||
this.edit.isFolderReady = true;
|
||||
this.skipFolderBubble = undefined;
|
||||
}.bind(this));
|
||||
|
||||
window.requestAnimationFrame(initFolderAnimation);
|
||||
} else {
|
||||
// 2nd frame, recover original transition.
|
||||
this.folderListElem.style.transition = '';
|
||||
}
|
||||
}.bind(this);
|
||||
window.requestAnimationFrame(initFolderAnimation);
|
||||
},
|
||||
|
||||
openSettings: function() {
|
||||
/* jshint nonew: false */
|
||||
new MozActivity({
|
||||
name: 'configure',
|
||||
data: {}
|
||||
});
|
||||
},
|
||||
|
||||
updateClock: function() {
|
||||
var now = new Date();
|
||||
var _ = navigator.mozL10n.get;
|
||||
var use12Hour = window.navigator.mozHour12;
|
||||
|
||||
var f = new navigator.mozL10n.DateTimeFormat();
|
||||
|
||||
var timeFormat = use12Hour ? _('shortTimeFormat12') :
|
||||
_('shortTimeFormat24');
|
||||
// remove AM/PM and we use our owned style to show it.
|
||||
timeFormat = timeFormat.replace('%p', '').trim();
|
||||
var formatted = f.localeFormat(now, timeFormat);
|
||||
|
||||
this.timeElem.innerHTML = formatted;
|
||||
this.timeElem.dataset.ampm = use12Hour ? f.localeFormat(now, '%p') : '';
|
||||
},
|
||||
|
||||
restartClock: function() {
|
||||
navigator.mozL10n.ready((function() {
|
||||
// restart clcok
|
||||
this.clock.stop();
|
||||
this.clock.start(this.updateClock.bind(this));
|
||||
}).bind(this));
|
||||
},
|
||||
|
||||
get focusElem() {
|
||||
return this._focus;
|
||||
},
|
||||
|
||||
get focusScrollable() {
|
||||
return this._focusScrollable;
|
||||
}
|
||||
};
|
||||
|
||||
exports.Home = Home;
|
||||
}(window));
|
||||
|
||||
134
gaia/tv_apps/smart-home/js/message_handler.js
Executable file
|
|
@ -0,0 +1,134 @@
|
|||
'use strict';
|
||||
|
||||
/* global ConnectionManager */
|
||||
|
||||
(function(exports) {
|
||||
|
||||
/**
|
||||
* MessageHandler handles all incoming messages which includes both
|
||||
* [IAC](http://mzl.la/1TKR6zw) and [MozActivity](http://mzl.la/1Pu8ecc).
|
||||
* This class relies on {@link ConnectionManager} to monitor incoming `unpin`
|
||||
* message on [IAC](http://mzl.la/1TKR6zw).
|
||||
*
|
||||
* @class MessageHandler
|
||||
* @requires {@link ConnectionManager}
|
||||
* @requires {@link Home}
|
||||
*/
|
||||
var MessageHandler = function() {
|
||||
this.isStop = false;
|
||||
this.activityQueue = [];
|
||||
};
|
||||
|
||||
MessageHandler.prototype = {
|
||||
_connectionManager: undefined,
|
||||
|
||||
_home: undefined,
|
||||
|
||||
/**
|
||||
* Initialize MessageHandler
|
||||
* @public
|
||||
* @method MessageHandler#init
|
||||
*/
|
||||
init: function cm_init(home) {
|
||||
this._home = home;
|
||||
navigator.mozSetMessageHandler(
|
||||
'activity', this.handleActivity.bind(this));
|
||||
this._connectionManager = new ConnectionManager();
|
||||
this._connectionManager.init(['iac-appdeck-channel']);
|
||||
this._connectionManager.on('unpin', this.unpin.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Pin card of `cardEntry`
|
||||
* @public
|
||||
* @method MessageHandler#pin
|
||||
* @param {Object} cardEntry - cardEntry representing the card
|
||||
* @param {String} cardEntry.type - valid values are 'Application', 'Deck',
|
||||
* 'AppBookmark', and 'Folder'
|
||||
* @param {String} cardEntry.group - valud values are 'tv', 'application',
|
||||
* 'device', and 'dashboard'
|
||||
* @param {String} cardEntry.manifestURL - manifestURL, if the card
|
||||
* represents an app or a deck
|
||||
* @param {String} cardEntry.launchURL - optional
|
||||
* @param {String} cardEntry.url - URL, if the card represents a bookmark
|
||||
* @param {String} cardEntry.name.raw - optional display name
|
||||
* @param {Blob} cardEntry.thumnail - see [blob](http://mzl.la/1BRI7IV)
|
||||
*/
|
||||
pin: function mh_pin(cardEntry) {
|
||||
this._home.cardManager.insertCard({
|
||||
cardEntry: cardEntry
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Unpin card
|
||||
* @public
|
||||
* @method MessageHandler#unpin
|
||||
* @param {Object} data
|
||||
* @param {String} data.manifestURL - manifestURL if unpinning an app
|
||||
* @param {String} data.launchURL - optional launchURL if unpinning an
|
||||
* app
|
||||
* @param {String} data.url - link URL when unpinning an bookmark
|
||||
*/
|
||||
unpin: function mh_unpin(data) {
|
||||
var home = this._home;
|
||||
// XXX: this is extra step, we should remove this step once we were
|
||||
// done fixing and refactoring cardManager.removeCard.
|
||||
// The reason we need this step here is because cardManager.removeCard
|
||||
// only accept card instance as sole parameter.
|
||||
var card = home.cardManager.findCardFromCardList(data);
|
||||
home.cardManager.removeCard(card);
|
||||
},
|
||||
|
||||
handleActivity: function mh_handleActivity(activity) {
|
||||
if(this.isStop) {
|
||||
this.activityQueue.push(activity);
|
||||
return;
|
||||
}
|
||||
this._digestActivity(activity);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop from processing activity in activityQueue. The reason we need this
|
||||
* is that we don't need to do pinning when smart-home is not visible.
|
||||
*
|
||||
* @public
|
||||
* @method MessageHandler#stopActivity
|
||||
*/
|
||||
stopActivity: function mh_stopActivity() {
|
||||
this.isStop = true;
|
||||
},
|
||||
|
||||
_digestActivity: function(activity) {
|
||||
var name = activity.source.name;
|
||||
switch(name) {
|
||||
case 'pin':
|
||||
this.pin(activity.source.data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Resume processing activity in activityQueue. We could call this method to
|
||||
* continue processing pinning when smart-home is visible.
|
||||
*
|
||||
* @public
|
||||
* @method MessageHandler#resumeActivity
|
||||
*/
|
||||
resumeActivity: function mh_resumeActivity() {
|
||||
this.isStop = false;
|
||||
if (this.activityQueue.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var activity;
|
||||
while(this.activityQueue.length > 0) {
|
||||
activity = this.activityQueue.pop();
|
||||
this._digestActivity(activity);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
exports.MessageHandler = MessageHandler;
|
||||
})(window);
|
||||
69
gaia/tv_apps/smart-home/js/search_bar.js
Executable file
|
|
@ -0,0 +1,69 @@
|
|||
/* global evt */
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
function SearchBar() {}
|
||||
|
||||
SearchBar.STATES = Object.freeze({
|
||||
'HIDDEN': 'hidden',
|
||||
'SHOWING': 'showing',
|
||||
'SHOWN': 'shown'
|
||||
});
|
||||
|
||||
var proto = SearchBar.prototype = new evt();
|
||||
|
||||
Object.defineProperty(proto, 'state', {
|
||||
get: function sb_getState() {
|
||||
return this._state;
|
||||
},
|
||||
set: function sb_setState(state) {
|
||||
this._state = state;
|
||||
this.fire(state);
|
||||
}
|
||||
});
|
||||
|
||||
proto.init = function sb_init(ui) {
|
||||
this.searchUI = ui;
|
||||
this.searchUI.addEventListener('transitionend', this);
|
||||
|
||||
this.voiceButton = this.searchUI.querySelector('.search-mic-button');
|
||||
this.voiceButton.addEventListener('transitionend', this);
|
||||
|
||||
this.state = this.searchUI.classList.contains('hidden') ?
|
||||
SearchBar.STATES.HIDDEN : SearchBar.STATES.SHOWN;
|
||||
};
|
||||
|
||||
proto.uninit = function sb_uninit() {
|
||||
this.searchUI.removeEventListener('transitionend', this);
|
||||
this.voiceButton.removeEventListener('transitionend', this);
|
||||
};
|
||||
|
||||
proto.show = function sb_show() {
|
||||
this.searchUI.classList.remove('hidden');
|
||||
this.state = SearchBar.STATES.SHOWING;
|
||||
};
|
||||
|
||||
proto.hide = function sb_hide() {
|
||||
this.searchUI.classList.add('hidden');
|
||||
this.searchUI.classList.remove('opened');
|
||||
this.state = SearchBar.STATES.HIDDEN;
|
||||
};
|
||||
|
||||
proto.handleEvent = function sb_handleEvent(evt) {
|
||||
// We have two steps transition:
|
||||
// 1. background-color transition on searchUI
|
||||
// 2. opacity on voiceButton
|
||||
if (evt.target === this.searchUI &&
|
||||
evt.propertyName === 'background-color' &&
|
||||
this.state === SearchBar.STATES.SHOWING) {
|
||||
this.searchUI.classList.add('opened');
|
||||
} else if (evt.target === this.voiceButton &&
|
||||
evt.propertyName === 'opacity' &&
|
||||
this.state === SearchBar.STATES.SHOWING) {
|
||||
this.state = SearchBar.STATES.SHOWN;
|
||||
}
|
||||
};
|
||||
|
||||
exports.SearchBar = SearchBar;
|
||||
|
||||
})(window);
|
||||
13
gaia/tv_apps/smart-home/js/utils.js
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
exports.Utils = {
|
||||
holdFocusForAnimation: function utils_holdFocusForAnimation() {
|
||||
// we have to move focus to another element to prevent user press enter
|
||||
// to trigger unexpected behavior. We cannot use blur() to do it because
|
||||
// it makes smart-home without focus. In this case, other apps can
|
||||
// request focus from smart-home which makes focus lost, bug 1161940.
|
||||
document.getElementById('main-section').focus();
|
||||
}
|
||||
};
|
||||
})(window);
|
||||
18
gaia/tv_apps/smart-home/jsdoc.json
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "smart-home",
|
||||
"tags": {
|
||||
"dictionaries": ["jsdoc"]
|
||||
},
|
||||
"source": {
|
||||
"include": ["tv_apps/smart-home/js/**/*.js"]
|
||||
},
|
||||
"plugins": [
|
||||
"plugins/markdown",
|
||||
"plugins/summarize"
|
||||
],
|
||||
"opts": {
|
||||
"private": true,
|
||||
"recurse": true,
|
||||
"destination": "docs/smart-home"
|
||||
}
|
||||
}
|
||||
37
gaia/tv_apps/smart-home/locales/home.en-US.properties
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
#Filter tags
|
||||
all=All
|
||||
tv=Live TV
|
||||
device=Device
|
||||
app=Application
|
||||
website=Website
|
||||
|
||||
#Buttons
|
||||
edit=Edit
|
||||
settings=Settings
|
||||
done=done
|
||||
add-new-folder=Add new folder
|
||||
edit-mode-title=Edit Mode
|
||||
new-folder=New folder
|
||||
delete=Delete
|
||||
cancel=Cancel
|
||||
ok=OK
|
||||
next=Next
|
||||
|
||||
#search bar
|
||||
search-default-text=Search
|
||||
|
||||
enter-new-name=Please enter new name to the card.
|
||||
delete-card-alert=Are you sure you want to delete this card?
|
||||
channel-name=CH {{ number }}
|
||||
|
||||
#fte page 1
|
||||
fte-tvdeck-detail=Watch TV channels
|
||||
fte-appdeck-detail=Enjoy your applications
|
||||
fte-devicedeck-detail=Find devices connected to TV
|
||||
fte-browser-detail=Discover applications
|
||||
|
||||
#fte page 2
|
||||
fte-search-detail=Search
|
||||
fte-settings-detail=Settings
|
||||
fte-addfolder-detail=Add a folder
|
||||
fte-filter-detail=Filter
|
||||
57
gaia/tv_apps/smart-home/manifest.webapp
Executable file
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"name": "Home",
|
||||
"description": "Smart Screen Home App",
|
||||
"launch_path": "/index.html",
|
||||
"type": "certified",
|
||||
"role": "homescreen",
|
||||
"developer": {
|
||||
"name": "The Gaia Team",
|
||||
"url": "https://github.com/mozilla-b2g/gaia"
|
||||
},
|
||||
"locales": {
|
||||
"en-US": {
|
||||
"name": "Home",
|
||||
"description": "Smart Screen Home App"
|
||||
}
|
||||
},
|
||||
"activities": {
|
||||
"pin": {
|
||||
"filters": {
|
||||
"type": {
|
||||
"required": true
|
||||
},
|
||||
"manifestURL": { "pattern": "(https?|app):.{1,16384}" },
|
||||
"url": { "pattern": "https?:.{1,16384}" }
|
||||
}
|
||||
},
|
||||
"unpin": {
|
||||
"filters": {
|
||||
"manifestURL": { "pattern": "(https?|app):.{1,16384}" },
|
||||
"url": { "pattern":"https?:.{1,16384}" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"connections": {
|
||||
"appdeck-channel": {
|
||||
"description": "communications for app-deck to add/remove application cards",
|
||||
"rules": {}
|
||||
}
|
||||
},
|
||||
"default_locale": "en-US",
|
||||
"icons": {
|
||||
"128": "/style/icons/Blank.png"
|
||||
},
|
||||
"orientation": "landscape",
|
||||
"permissions": {
|
||||
"webapps-manage": {},
|
||||
"open-remote-window":{},
|
||||
"systemXHR":{},
|
||||
"moz-extremely-unstable-and-will-change-webcomponents":{}
|
||||
},
|
||||
"datastores-owned": {
|
||||
"home_cards": {
|
||||
"access": "readwrite",
|
||||
"description": "Stores all type of cards/decks/apps"
|
||||
}
|
||||
}
|
||||
}
|
||||
161
gaia/tv_apps/smart-home/style/deck.css
Executable file
|
|
@ -0,0 +1,161 @@
|
|||
/**
|
||||
* Deck smart-button
|
||||
*/
|
||||
|
||||
smart-button .icon[data-icon] {
|
||||
position: relative;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
line-height: 28rem;
|
||||
}
|
||||
|
||||
smart-button .icon[data-icon]:before {
|
||||
font-size: 30rem;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"].deck-tv {
|
||||
background-color: #D70270;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"].deck-application {
|
||||
background-color: #07CAB0;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"].deck-device {
|
||||
background-color: #0074D4;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"].deck-dashboard {
|
||||
background-color: #FF7E00;
|
||||
}
|
||||
|
||||
.card smart-button {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card smart-button[type="app-button"]::after {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deck wave effect
|
||||
*/
|
||||
|
||||
.deck-wave {
|
||||
position: absolute;
|
||||
width: 134.4rem;
|
||||
height: 67.2rem;
|
||||
transform: scale(1.2);
|
||||
animation: defaultWaveAnimation 5.0s linear 0s infinite normal;
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
.card.focused .deck-wave {
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
#main-section[data-mode="edit"] .deck-wave,
|
||||
#main-section[data-mode="arrange"] .deck-wave {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
.wave-front {
|
||||
animation-duration: 3.05s
|
||||
}
|
||||
|
||||
.wave-back {
|
||||
animation-duration: 3.0s;
|
||||
}
|
||||
|
||||
.tv-wave-front {
|
||||
bottom: -16rem;
|
||||
animation-name: tvWaveFrontAnimation;
|
||||
background-image: url('images/wave_pink_01.png');
|
||||
}
|
||||
|
||||
.tv-wave-back {
|
||||
bottom: -14rem;
|
||||
animation-name: tvWaveBackAnimation;
|
||||
background-image: url('images/wave_pink_02.png');
|
||||
}
|
||||
|
||||
.application-wave-front {
|
||||
bottom: -16rem;
|
||||
animation-name: applicationWaveFrontAnimation;
|
||||
background-image: url('images/wave_green_01.png');
|
||||
}
|
||||
|
||||
.application-wave-back {
|
||||
bottom: -14rem;
|
||||
animation-name: applicationWaveBackAnimation;
|
||||
background-image: url('images/wave_green_02.png');
|
||||
}
|
||||
|
||||
.device-wave-front {
|
||||
bottom: -16rem;
|
||||
animation-name: deviceWaveFrontAnimation;
|
||||
background-image: url('images/wave_blue_01.png');
|
||||
}
|
||||
|
||||
.device-wave-back {
|
||||
bottom: -13rem;
|
||||
animation-name: deviceWaveBackAnimation;
|
||||
background-image: url('images/wave_blue_02.png');
|
||||
}
|
||||
|
||||
.dashboard-wave-front {
|
||||
bottom: -16rem;
|
||||
animation-name: dashboardWaveFrontAnimation;
|
||||
background-image: url('images/wave_orange_01.png');
|
||||
}
|
||||
|
||||
.dashboard-wave-back {
|
||||
bottom: -13rem;
|
||||
animation-name: dashboardWaveBackAnimation;
|
||||
background-image: url('images/wave_orange_02.png');
|
||||
}
|
||||
|
||||
@keyframes defaultWaveAnimation {
|
||||
from {transform: translateX(0);}
|
||||
to {transform: translateX(-67.2rem);}
|
||||
}
|
||||
|
||||
@keyframes tvWaveFrontAnimation {
|
||||
from {transform: translateX(0);}
|
||||
to {transform: translateX(-67.2rem);}
|
||||
}
|
||||
|
||||
@keyframes tvWaveBackAnimation {
|
||||
from {transform: translateX(-3rem);}
|
||||
to {transform: translateX(-70.2rem);}
|
||||
}
|
||||
|
||||
@keyframes applicationWaveFrontAnimation {
|
||||
from {transform: translateX(-16rem);}
|
||||
to {transform: translateX(-83.2rem);}
|
||||
}
|
||||
|
||||
@keyframes applicationWaveBackAnimation {
|
||||
from {transform: translateX(-23rem);}
|
||||
to {transform: translateX(-90.2rem);}
|
||||
}
|
||||
|
||||
@keyframes deviceWaveFrontAnimation {
|
||||
from {transform: translateX(-22rem);}
|
||||
to {transform: translateX(-89.2rem);}
|
||||
}
|
||||
|
||||
@keyframes deviceWaveBackAnimation {
|
||||
from {transform: translateX(-25rem);}
|
||||
to {transform: translateX(-92.2rem);}
|
||||
}
|
||||
|
||||
@keyframes dashboardWaveFrontAnimation {
|
||||
from {transform: translateX(-35rem);}
|
||||
to {transform: translateX(-102.2rem);}
|
||||
}
|
||||
|
||||
@keyframes dashboardWaveBackAnimation {
|
||||
from {transform: translateX(-32rem);}
|
||||
to {transform: translateX(-99.2rem);}
|
||||
}
|
||||
147
gaia/tv_apps/smart-home/style/edit.css
Executable file
|
|
@ -0,0 +1,147 @@
|
|||
#main-section[data-mode="edit"] #nav-regular,
|
||||
#main-section[data-mode="arrange"] #nav-regular,
|
||||
#main-section:not([data-mode="edit"]) #nav-edit-title,
|
||||
#main-section:not([data-mode="edit"]) #nav-edit {
|
||||
transform: translateY(-16rem);
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] #card-viewer.current-scrollable {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#main-section[data-mode="edit"] #folder-list-frame,
|
||||
#main-section[data-mode="arrange"] #folder-list-frame {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .app-button.focused {
|
||||
transform: scale(1.72);
|
||||
}
|
||||
|
||||
#main-section[data-mode="edit"] .card.focused > .card-panel {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#main-section[data-mode="edit"] .card.delete > .card-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#main-section[data-mode="edit"] .card.focused > smart-button {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card.hovered > smart-button {
|
||||
transform: scale(1.75);
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card.focused.hover > smart-button {
|
||||
transform: scale(1.2);
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
#main-section[data-mode="edit"] .card.delete > smart-button {
|
||||
transform: scale(1.4) translateY(20rem);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#main-section[data-mode="edit"] .card.focused > .card-panel smart-button {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#main-section[data-mode="edit"] .card.focused > .card-panel smart-button.focused {
|
||||
/* Override the transform of smart-button.focused, otherwise, the smart-button
|
||||
may drops down when focused */
|
||||
transform: scale(1.2) translateY(0);
|
||||
}
|
||||
|
||||
#main-section[data-mode="edit"] .card > .card-panel smart-button:nth-child(2) {
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
/* Delete button may have 0.1s delay when focused */
|
||||
#main-section[data-mode="edit"] .card.focused > smart-button:not(.focused) + .card-panel smart-button {
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
#main-section[data-mode="edit"] .card.focused > smart-button[app-type='deck'] ~ .card-panel,
|
||||
#main-section[data-mode="edit"] .card.focused > smart-button.animation-bubble-start ~ .card-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card.focused:after {
|
||||
position: absolute;
|
||||
left: -25.1rem;
|
||||
right: -25.1rem;
|
||||
top: -25.1rem;
|
||||
bottom: -25.1rem;
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
#card-list.exiting-edit-mode {
|
||||
transition: transform 0.8s cubic-bezier(.6, 0, 0, 1);
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card:not(.focused) {
|
||||
filter: url(filter.svg#brightness50percent);
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card.focused.left_arrow:after {
|
||||
background: url(images/animated_arrows_left.png) no-repeat left center / 13rem;
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card.focused.right_arrow:after {
|
||||
background: url(images/animated_arrows_right.png) no-repeat right center / 13rem;
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card.focused.up_arrow:after {
|
||||
background: url(images/animated_arrows_up.png) no-repeat top center / 13rem;
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card.focused.left_arrow.down_arrow:after {
|
||||
background: url(images/animated_arrows_left.png) no-repeat left center / 13rem,
|
||||
url(images/animated_arrows_down.png) no-repeat bottom center / 13rem;
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card.focused.right_arrow.down_arrow:after {
|
||||
background: url(images/animated_arrows_right.png) no-repeat right center / 13rem,
|
||||
url(images/animated_arrows_down.png) no-repeat bottom center / 13rem;
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card.focused.left_arrow.up_arrow:after {
|
||||
background: url(images/animated_arrows_left.png) no-repeat left center / 13rem,
|
||||
url(images/animated_arrows_up.png) no-repeat top center / 13rem;
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card.focused.right_arrow.up_arrow:after {
|
||||
background: url(images/animated_arrows_right.png) no-repeat right center / 13rem,
|
||||
url(images/animated_arrows_up.png) no-repeat top center / 13rem;
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card.focused.left_arrow.right_arrow:after {
|
||||
background: url(images/animated_arrows_left.png) no-repeat left center / 13rem,
|
||||
url(images/animated_arrows_right.png) no-repeat right center / 13rem;
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card.focused.left_arrow.right_arrow.down_arrow:after {
|
||||
background: url(images/animated_arrows_left.png) no-repeat left center / 13rem,
|
||||
url(images/animated_arrows_right.png) no-repeat right center / 13rem,
|
||||
url(images/animated_arrows_down.png) no-repeat bottom center / 13rem;
|
||||
}
|
||||
|
||||
#main-section[data-mode="arrange"] .card.focused.left_arrow.right_arrow.up_arrow:after {
|
||||
background: url(images/animated_arrows_left.png) no-repeat left center / 13rem,
|
||||
url(images/animated_arrows_right.png) no-repeat right center / 13rem,
|
||||
url(images/animated_arrows_up.png) no-repeat top center / 13rem;
|
||||
}
|
||||
|
||||
#main-section[data-mode="edit"] .background,
|
||||
#main-section[data-mode="arrange"] .background {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
#main-section[data-mode="edit"] #card-list,
|
||||
#main-section[data-mode="arrange"] #card-list {
|
||||
transition: transform 0.66s cubic-bezier(.25, 0, 0, 1);
|
||||
}
|
||||
9
gaia/tv_apps/smart-home/style/filter.svg
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
|
||||
<filter id="brightness50percent">
|
||||
<feComponentTransfer>
|
||||
<feFuncR type="linear" slope="0.5"/>
|
||||
<feFuncG type="linear" slope="0.5"/>
|
||||
<feFuncB type="linear" slope="0.5"/>
|
||||
</feComponentTransfer>
|
||||
</filter>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 299 B |
124
gaia/tv_apps/smart-home/style/fte.css
Executable file
|
|
@ -0,0 +1,124 @@
|
|||
#fte {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: RGBA(0, 0, 0, 0.3);
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s linear;
|
||||
}
|
||||
|
||||
#fte.finish {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fte-page {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url(images/fte_bg.png) 100%;
|
||||
font-size: 3.6rem;
|
||||
color: white;
|
||||
font-style: italic;
|
||||
opacity: 1;
|
||||
transition: opacity 0.5s linear;
|
||||
}
|
||||
|
||||
.fte-page.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition-delay: 0.5s;
|
||||
}
|
||||
|
||||
.fte-content-image {
|
||||
position: absolute;
|
||||
left: calc(50vw - 74.9rem);
|
||||
top: calc(50vh - 42.1rem);
|
||||
width: 149.8rem;
|
||||
height: 84.2rem;
|
||||
background: url(images/fte_content_image.png) 100%;
|
||||
overflow: visible;
|
||||
box-shadow: 0.5rem 1rem 1rem RGBA(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.fte-indicator-p1 {
|
||||
position: absolute;
|
||||
left: -21.1rem;
|
||||
top: -11.9rem;
|
||||
width: 192rem;
|
||||
height: 108rem;
|
||||
background: url(images/fte_indicator_p1.png) 100%;
|
||||
}
|
||||
|
||||
.fte-indicator-p2 {
|
||||
position: absolute;
|
||||
left: -21.1rem;
|
||||
top: -11.9rem;
|
||||
width: 192rem;
|
||||
height: 108rem;
|
||||
background: url(images/fte_indicator_p2.png) 100%;
|
||||
}
|
||||
|
||||
.fte-panel {
|
||||
position: absolute;
|
||||
bottom: calc(50vh - 42.1rem - 4rem);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fte-panel > .fte-button {
|
||||
width: 30.4rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#fte-tvdeck-detail {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fte-content-image > div {
|
||||
position: absolute;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
#fte-tvdeck-detail {
|
||||
left: 27.7rem;
|
||||
top: 15.9rem;
|
||||
}
|
||||
|
||||
#fte-appdeck-detail {
|
||||
right: 51.9rem;
|
||||
bottom: 16.5rem;
|
||||
}
|
||||
|
||||
#fte-devicedeck-detail {
|
||||
left: 84.9rem;
|
||||
top: 15.9rem;
|
||||
}
|
||||
|
||||
#fte-browser-detail {
|
||||
left: 105.9rem;
|
||||
bottom: 16.5rem;
|
||||
}
|
||||
|
||||
#fte-search-detail {
|
||||
left: -10.6rem;
|
||||
top: -7.2rem;
|
||||
}
|
||||
|
||||
#fte-settings-detail {
|
||||
left: 8.7rem;
|
||||
top: -7.2rem;
|
||||
}
|
||||
|
||||
#fte-addfolder-detail {
|
||||
left: 30.2rem;
|
||||
top: -7.2rem;
|
||||
}
|
||||
|
||||
#fte-filter-detail {
|
||||
left: -10.6rem;
|
||||
bottom: 16.5rem;
|
||||
}
|
||||
13
gaia/tv_apps/smart-home/style/header.css
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
.left-top-panel {
|
||||
left: 6rem;
|
||||
top: 4rem;
|
||||
position: absolute;
|
||||
}
|
||||
#header-buttons > smart-button {
|
||||
margin-right: 1.8rem;
|
||||
}
|
||||
smart-button[type="text-and-icon"] {
|
||||
min-width: 15rem;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
314
gaia/tv_apps/smart-home/style/home.css
Executable file
|
|
@ -0,0 +1,314 @@
|
|||
html {
|
||||
font-size: 10px;
|
||||
/* We have Fira Sans as default font in real device. But it isn't default in
|
||||
* browser. That's the main reason this line is here.*/
|
||||
font-family: "Fira Sans";
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
#main-section {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
a:hover, a:active, a:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
/* Card list */
|
||||
#card-viewer {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#card-list-frame {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 33.6rem;
|
||||
top: 32.41%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#card-list {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.4s cubic-bezier(0.25, 0, 0, 1);
|
||||
transform-origin: 0 50%;
|
||||
}
|
||||
|
||||
#card-list.hidden {
|
||||
/* Using display none here would prevent animation, and using visibility
|
||||
hidden causes some bugs in rendering */
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#card-list.no-transition,
|
||||
#folder-list.no-transition {
|
||||
transition-property: none;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
transition: transform 0.4s cubic-bezier(0.25, 0, 0, 1), filter 0.3s ease;
|
||||
}
|
||||
|
||||
.card smart-button[type="app-button"].tv-channel {
|
||||
background-color: #272727;
|
||||
}
|
||||
|
||||
.card smart-button[type="app-button"].tv-channel:before {
|
||||
font-size: 30rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card smart-button[type="app-button"].new-card {
|
||||
opacity: 0;
|
||||
transform: scale(0.5, 0.5);
|
||||
}
|
||||
|
||||
.card smart-button[type="app-button"].new-card-transition {
|
||||
transition: transform 0.4s cubic-bezier(0.25, 0, 0, 1) 0.2s,
|
||||
opacity 0.4s cubic-bezier(0.25, 0, 0, 1) 0.2s;
|
||||
}
|
||||
|
||||
.card smart-button[type="app-button"].new-card-transition.last-card {
|
||||
transition-delay: 0s;
|
||||
}
|
||||
|
||||
#folder-list .card smart-button[type="app-button"].new-card {
|
||||
opacity: 1;
|
||||
transform: translateY(-44rem);
|
||||
}
|
||||
|
||||
#folder-list .card smart-button[type="app-button"].new-card-transition {
|
||||
transition: transform 0.4s cubic-bezier(0.25, 0, 0, 1),
|
||||
opacity 0.4s cubic-bezier(0.25, 0, 0, 1);
|
||||
}
|
||||
|
||||
.card-panel {
|
||||
position: absolute;
|
||||
top: -23.14rem;
|
||||
width: 100%;
|
||||
height: 14.926rem;
|
||||
text-align: center;
|
||||
/* Use visibility hidden instead of display none here because display none
|
||||
may prevent transition happens */
|
||||
visibility: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.card-panel smart-button {
|
||||
position: relative;
|
||||
margin-left: 1.86em;
|
||||
width: 14.926rem;
|
||||
height: 14.926rem;
|
||||
transform: translateY(12rem);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.card-panel smart-button:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.card-panel smart-button[data-icon]::before {
|
||||
width: 10.074rem;
|
||||
left: 2.43rem;
|
||||
font-size: 10.07rem;
|
||||
line-height: 15.47rem;
|
||||
|
||||
}
|
||||
|
||||
.card smart-button.app-button {
|
||||
/* smart-button should be in front of card-panel */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card.focused {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* folder list
|
||||
*/
|
||||
|
||||
#folder-viewer {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#folder-list-frame {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 33.6rem;
|
||||
top: calc(32.41% + 32.3rem);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#folder-list {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.4s cubic-bezier(0.25, 0, 0, 1);
|
||||
transform-origin: 0 50%;
|
||||
}
|
||||
|
||||
/**
|
||||
* filters
|
||||
**/
|
||||
.filter-tab-container {
|
||||
position: absolute;
|
||||
left: 6rem;
|
||||
bottom: 5.4rem;
|
||||
}
|
||||
|
||||
.filter-tab-group.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* background section
|
||||
**/
|
||||
.background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: rgba(0, 0, 0, 0.65);
|
||||
transition: background-color 0.66s linear;
|
||||
}
|
||||
|
||||
.background:before {
|
||||
content: "";
|
||||
height: 29rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
.background:after {
|
||||
content: "";
|
||||
height: 29rem;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
#edit-menu {
|
||||
position: absolute;
|
||||
top: 4rem;
|
||||
right: 6rem;
|
||||
}
|
||||
|
||||
/**
|
||||
* nav bars
|
||||
*/
|
||||
nav {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
transition: transform 0.4s cubic-bezier(0.165, 0.840, 0.440, 1.000);
|
||||
}
|
||||
|
||||
nav header {
|
||||
padding-left: 7rem;
|
||||
padding-top: 5rem;
|
||||
|
||||
font-size: 5.4rem;
|
||||
font-style: italic;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#main-section[data-mode="edit"] #nav-regular,
|
||||
#main-section[data-mode="arrange"] #nav-regular,
|
||||
#main-section:not([data-mode="edit"]) #nav-edit-title,
|
||||
#main-section:not([data-mode="edit"]) #nav-edit {
|
||||
transform: translateY(-16rem);
|
||||
}
|
||||
|
||||
smart-button.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* For smart-button, we put card name in pseudo-element :after. However, we need
|
||||
* to localize card name and l10n library do not support localizing element with
|
||||
* children elements.
|
||||
* Instead of using :after, we create a 'span' element under smart-button and
|
||||
* put card name in it.
|
||||
*/
|
||||
smart-button[type="app-button"]:after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"] > span.name {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
width: 33.6rem;
|
||||
height: 8rem;
|
||||
|
||||
padding-top: 1rem;
|
||||
|
||||
line-height: 3rem;
|
||||
text-align: center;
|
||||
|
||||
font-size: 2.8rem;
|
||||
font-style: italic;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"][app-type="deck"] > span.name {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"][app-type="tv"] > span.name {
|
||||
background-color: #d90c83;
|
||||
}
|
||||
|
||||
smart-button[type="app-button"][app-type="app"] > span.name,
|
||||
smart-button[type="app-button"][app-type="appbookmark"] > span.name {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
smart-button[type="app-button"][app-type="device"] > span.name {
|
||||
background-color: #4a82ea;
|
||||
}
|
||||
|
||||
BIN
gaia/tv_apps/smart-home/style/icons/Blank.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
gaia/tv_apps/smart-home/style/images/animated_arrows_down.png
Executable file
|
After Width: | Height: | Size: 66 KiB |
BIN
gaia/tv_apps/smart-home/style/images/animated_arrows_left.png
Executable file
|
After Width: | Height: | Size: 143 KiB |
BIN
gaia/tv_apps/smart-home/style/images/animated_arrows_right.png
Executable file
|
After Width: | Height: | Size: 142 KiB |
BIN
gaia/tv_apps/smart-home/style/images/animated_arrows_up.png
Executable file
|
After Width: | Height: | Size: 67 KiB |
BIN
gaia/tv_apps/smart-home/style/images/appic_application.png
Executable file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
gaia/tv_apps/smart-home/style/images/appic_application@2x.png
Executable file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
gaia/tv_apps/smart-home/style/images/appic_dashboard.png
Executable file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
gaia/tv_apps/smart-home/style/images/appic_dashboard@2x.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
gaia/tv_apps/smart-home/style/images/appic_developer.png
Executable file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
gaia/tv_apps/smart-home/style/images/appic_developer@2x.png
Executable file
|
After Width: | Height: | Size: 19 KiB |
BIN
gaia/tv_apps/smart-home/style/images/appic_device.png
Executable file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
gaia/tv_apps/smart-home/style/images/appic_device@2x.png
Executable file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
gaia/tv_apps/smart-home/style/images/appic_tv.png
Executable file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
gaia/tv_apps/smart-home/style/images/appic_tv@2x.png
Executable file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
gaia/tv_apps/smart-home/style/images/fte_bg.png
Executable file
|
After Width: | Height: | Size: 2 MiB |
BIN
gaia/tv_apps/smart-home/style/images/fte_content_image.png
Executable file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
gaia/tv_apps/smart-home/style/images/fte_indicator_p1.png
Executable file
|
After Width: | Height: | Size: 632 KiB |
BIN
gaia/tv_apps/smart-home/style/images/fte_indicator_p2.png
Executable file
|
After Width: | Height: | Size: 633 KiB |
BIN
gaia/tv_apps/smart-home/style/images/ic_setting.png
Executable file
|
After Width: | Height: | Size: 958 B |
BIN
gaia/tv_apps/smart-home/style/images/ic_setting@2x.png
Executable file
|
After Width: | Height: | Size: 2 KiB |
BIN
gaia/tv_apps/smart-home/style/images/img_weather-cloudy.png
Executable file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
gaia/tv_apps/smart-home/style/images/img_weather-cloudy@2x.png
Executable file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
gaia/tv_apps/smart-home/style/images/img_weather_mostly_cloudy.png
Executable file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
gaia/tv_apps/smart-home/style/images/img_weather_mostly_cloudy@2x.png
Executable file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
gaia/tv_apps/smart-home/style/images/wave_blue_01.png
Executable file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
gaia/tv_apps/smart-home/style/images/wave_blue_02.png
Executable file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
gaia/tv_apps/smart-home/style/images/wave_green_01.png
Executable file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
gaia/tv_apps/smart-home/style/images/wave_green_02.png
Executable file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
gaia/tv_apps/smart-home/style/images/wave_orange_01.png
Executable file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
gaia/tv_apps/smart-home/style/images/wave_orange_02.png
Executable file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
gaia/tv_apps/smart-home/style/images/wave_pink_01.png
Executable file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
gaia/tv_apps/smart-home/style/images/wave_pink_02.png
Executable file
|
After Width: | Height: | Size: 8.7 KiB |
97
gaia/tv_apps/smart-home/style/info-bar.css
Executable file
|
|
@ -0,0 +1,97 @@
|
|||
|
||||
.information-bar {
|
||||
position: absolute;
|
||||
right: 2.4rem;
|
||||
top: 3rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.weather-info {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 25rem;
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
.weather-image {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
background-image: url(images/img_weather_mostly_cloudy.png);
|
||||
background-size: 10rem auto;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.temperature {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
|
||||
bottom: 3rem;
|
||||
left: 10rem; /* sit at the right of weather-image */
|
||||
height: 5.6rem;
|
||||
line-height: 5.6rem;
|
||||
|
||||
font-size: 6rem;
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.degree-marker {
|
||||
font-size: 2.4rem;
|
||||
line-height: 2.4rem;
|
||||
font-weight: 100;
|
||||
position: relative;
|
||||
top: -2.4rem;
|
||||
}
|
||||
|
||||
.weather-text {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
|
||||
bottom: 0;
|
||||
left: 10rem; /* sit at the right of weather-image */
|
||||
|
||||
font-size: 1.8rem;
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.clock-info {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
/* right align to weather-info(25rem) + 1rem spacing */
|
||||
right: 26.2rem;
|
||||
width: 26rem;
|
||||
height: 10rem;
|
||||
|
||||
border-right: solid 0.2rem #ffffff;
|
||||
}
|
||||
|
||||
.time:before {
|
||||
content: attr(data-ampm);
|
||||
position: absolute;
|
||||
height: 1.8rem;
|
||||
line-height: 1.8rem;
|
||||
|
||||
font-size: 1.8rem;
|
||||
bottom: 1.8rem;
|
||||
left: -2.8rem;
|
||||
}
|
||||
|
||||
.time {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 1.2rem;
|
||||
height: 10rem;
|
||||
line-height: 10rem;
|
||||
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
|
||||
font-weight: 100;
|
||||
font-size: 10.6rem;
|
||||
|
||||
}
|
||||
123
gaia/tv_apps/smart-home/style/search-bar.css
Executable file
|
|
@ -0,0 +1,123 @@
|
|||
.search-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
transition-property: background-color;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
|
||||
transition-duration: 0.92s;
|
||||
}
|
||||
|
||||
.search-bar.hidden {
|
||||
opacity: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
transition-property: none;
|
||||
}
|
||||
|
||||
smart-button.search-mic-button {
|
||||
opacity: 1;
|
||||
background-color: #5d6368;
|
||||
position: absolute;
|
||||
left: 28.6rem;
|
||||
top: 4rem;
|
||||
}
|
||||
|
||||
.search-bar.hidden .search-input {
|
||||
left: 6rem;
|
||||
right: auto;
|
||||
top: 4rem;
|
||||
width: 8rem;
|
||||
border-radius: 4rem;
|
||||
padding-left: 0;
|
||||
transition-property: none;
|
||||
}
|
||||
|
||||
.search-input span {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.search-bar.opened .search-input span {
|
||||
opacity: 1;
|
||||
transition-property: opacity;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
|
||||
transition-duration: 0.92s;
|
||||
}
|
||||
|
||||
/* icon parts */
|
||||
.search-bar.hidden [data-icon].search-input:before {
|
||||
left: 1.3rem;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
[data-icon].search-input:before {
|
||||
width: 5.4rem;
|
||||
right: 1.2rem;
|
||||
font-size: 5.4rem;
|
||||
line-height: 8rem;
|
||||
text-align: center;
|
||||
color: #2d2d2d;
|
||||
white-space: normal;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
/* 38.4rem = 28.6rem (left) + 8rem (voice button) + 1.8rem (margin) */
|
||||
left: 38.4rem;
|
||||
/* 77rem = left: 38.4rem + right 38.6rem */
|
||||
width: calc(100% - 77rem);
|
||||
top: 4rem;
|
||||
height: 8rem;
|
||||
position: absolute;
|
||||
|
||||
display: inline-block;
|
||||
background-color: #ffffff;
|
||||
|
||||
color: #808080;
|
||||
|
||||
font-size: 3.2rem;
|
||||
line-height: 8rem;
|
||||
|
||||
overflow: hidden;
|
||||
padding-left: 4rem;
|
||||
border-radius: 4rem;
|
||||
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
|
||||
transition-duration: 0.92s;
|
||||
}
|
||||
|
||||
/* blue underline */
|
||||
.search-input:after {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
content: ' ';
|
||||
border-bottom: 0.6rem #61acd7 solid;
|
||||
box-sizing: border-box;
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
|
||||
transition-duration: 0.92s;
|
||||
}
|
||||
|
||||
.search-bar.hidden .search-input:after {
|
||||
opacity: 0;
|
||||
transition-property: none;
|
||||
}
|
||||
|
||||
.search-bar.opened .search-mic-button {
|
||||
transition-property: opacity;
|
||||
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
|
||||
transition-duration: 0.66s;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.search-bar .search-mic-button {
|
||||
transition-property: none;
|
||||
opacity: 0;
|
||||
}
|
||||
14
gaia/tv_apps/smart-home/test/marionette/.jshintrc
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "../../../../.jshintrc",
|
||||
"node": true,
|
||||
"predef": [
|
||||
"suite",
|
||||
"test",
|
||||
"setup",
|
||||
"teardown",
|
||||
"suiteSetup",
|
||||
"suiteTeardown",
|
||||
"marionette",
|
||||
"marionetteScriptFinished"
|
||||
]
|
||||
}
|
||||
124
gaia/tv_apps/smart-home/test/marionette/smart_home_test.js
Executable file
|
|
@ -0,0 +1,124 @@
|
|||
'use strict';
|
||||
|
||||
var APP_URL = 'app://smart-home.gaiamobile.org';
|
||||
|
||||
var Keys = {
|
||||
'up': '\ue013',
|
||||
'right': '\ue014',
|
||||
'down': '\ue015',
|
||||
'left': '\ue012',
|
||||
'enter': '\ue006',
|
||||
'esc': '\ue00c'
|
||||
};
|
||||
|
||||
var assert = require('chai').assert;
|
||||
|
||||
var containsClass = function(elem, className) {
|
||||
if (elem) {
|
||||
return elem.getAttribute('class').indexOf(className) > -1;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Bug 1207453 - Skip the test due to unknown test enviroment issue for now.
|
||||
// We should investigate the issue and re-enable the test later.
|
||||
marionette.skip('Smart Home', function() {
|
||||
|
||||
var opts = {
|
||||
hostOptions: {
|
||||
screen: {
|
||||
width: 1920,
|
||||
height: 1080
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var client = marionette.client({
|
||||
profile: opts,
|
||||
// XXX: Set this to true once Accessibility is implemented in TV
|
||||
desiredCapabilities: { raisesAccessibilityExceptions: false }
|
||||
});
|
||||
|
||||
setup(function() {
|
||||
// Launch test app
|
||||
client.apps.launch(APP_URL);
|
||||
client.apps.switchToApp(APP_URL);
|
||||
});
|
||||
|
||||
suite('> Card Filter', function() {
|
||||
test('Hide card filter when enter edit mode and show card filter when' +
|
||||
' exit edit mode ', { devices: ['tv'] }, function () {
|
||||
|
||||
var editButton = client.findElement('#edit-button');
|
||||
var element = client.findElement('#main-section');
|
||||
var searchButton = client.findElement('#search-button');
|
||||
var deckTvCard = client.findElement('#card-list > .card');
|
||||
var deckTvButton = client.findElement('.app-button.deck-tv');
|
||||
|
||||
client.waitFor(function() {
|
||||
return containsClass(deckTvCard, 'focused');
|
||||
});
|
||||
client.waitFor(function() {
|
||||
return containsClass(deckTvButton, 'focused');
|
||||
});
|
||||
|
||||
element.sendKeys(Keys.up);
|
||||
client.waitFor(function() {
|
||||
return containsClass(searchButton, 'focused');
|
||||
});
|
||||
|
||||
element.sendKeys(Keys.right);
|
||||
client.waitFor(function() {
|
||||
return containsClass(editButton, 'focused');
|
||||
});
|
||||
|
||||
element.sendKeys(Keys.enter);
|
||||
client.waitFor(function() {
|
||||
return element.getAttribute('data-mode') === 'edit';
|
||||
});
|
||||
var cardFilter = client.findElement('#filter-tab-group');
|
||||
assert.isTrue(containsClass(cardFilter, 'hidden'));
|
||||
|
||||
element.sendKeys(Keys.up);
|
||||
element.sendKeys(Keys.enter);
|
||||
assert.isFalse(containsClass(cardFilter, 'hidden'));
|
||||
});
|
||||
|
||||
test('Collapse folder when filter is focused',
|
||||
{ devices: ['tv'] }, function() {
|
||||
|
||||
// add new folder
|
||||
client.executeScript(function() {
|
||||
var home = window.wrappedJSObject.home;
|
||||
var newFolder = home.cardManager.insertNewFolder({id: 'new-folder'},
|
||||
home.cardManager._cardList.length);
|
||||
var card = home.cardManager._deserializeCardEntry({group: 'application',
|
||||
manifestURL: 'app://tv-epg.gaiamobile.org/manifest.webapp',
|
||||
type: 'Application'
|
||||
});
|
||||
newFolder.addCard(card);
|
||||
});
|
||||
|
||||
var element = client.findElement('#main-section');
|
||||
var newFolderCard =
|
||||
client.findElement('#card-list div.card:last-child');
|
||||
client.waitFor(function() {
|
||||
return containsClass(newFolderCard, 'focused');
|
||||
});
|
||||
|
||||
// move focus to card filter
|
||||
element.sendKeys(Keys.down);
|
||||
element.sendKeys(Keys.down);
|
||||
|
||||
// wait until animation of card filter ends
|
||||
var cardFilter = client.findElement('#filter-tab-group');
|
||||
client.waitFor(function() {
|
||||
return !containsClass(cardFilter, 'closed');
|
||||
});
|
||||
|
||||
// wait for folder to be collapsed
|
||||
client.helper.waitForElementToDisappear('#folder-list .card');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
18
gaia/tv_apps/smart-home/test/unit/.jshintrc
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "../../../../.jshintrc",
|
||||
"predef": [
|
||||
"assert",
|
||||
"suite",
|
||||
"test",
|
||||
"setup",
|
||||
"teardown",
|
||||
"suiteSetup",
|
||||
"suiteTeardown",
|
||||
"require",
|
||||
"requireApp",
|
||||
"sinon",
|
||||
"mocha",
|
||||
"loadBodyHTML"
|
||||
]
|
||||
}
|
||||
|
||||
186
gaia/tv_apps/smart-home/test/unit/card_filter_test.js
Executable file
|
|
@ -0,0 +1,186 @@
|
|||
/* global MockSmartButton, MockMenuGroup, CardFilter */
|
||||
'use strict';
|
||||
require('/shared/test/unit/mocks/mock_event_target.js');
|
||||
require('/bower_components/evt/index.js');
|
||||
requireApp('smart-home/test/unit/mock_smart_button.js');
|
||||
requireApp('smart-home/test/unit/mock_menu_group.js');
|
||||
requireApp('smart-home/js/card_filter.js');
|
||||
|
||||
suite('smart-home/CardFilter', function() {
|
||||
var cardFilter;
|
||||
var mockMenuGroup;
|
||||
var mockSmartButtons;
|
||||
|
||||
var generateSmartButtons = function() {
|
||||
return [
|
||||
new MockSmartButton('filter'),
|
||||
new MockSmartButton('tv'),
|
||||
new MockSmartButton('dashbaord'),
|
||||
new MockSmartButton('device'),
|
||||
new MockSmartButton('application')
|
||||
];
|
||||
};
|
||||
|
||||
suite('> start', function() {
|
||||
setup(function() {
|
||||
mockSmartButtons = generateSmartButtons();
|
||||
mockSmartButtons.forEach(function(button) {
|
||||
sinon.spy(button, 'addEventListener');
|
||||
});
|
||||
|
||||
mockMenuGroup = new MockMenuGroup();
|
||||
sinon.stub(mockMenuGroup, 'querySelectorAll', function() {
|
||||
return mockSmartButtons;
|
||||
});
|
||||
sinon.spy(mockMenuGroup, 'addEventListener');
|
||||
cardFilter = new CardFilter();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
mockMenuGroup = undefined;
|
||||
cardFilter = undefined;
|
||||
mockSmartButtons = undefined;
|
||||
});
|
||||
|
||||
test('start() should register "click" event listeners on smart-button',
|
||||
function() {
|
||||
cardFilter.start(mockMenuGroup);
|
||||
assert.isTrue(mockMenuGroup.querySelectorAll.calledOnce);
|
||||
mockSmartButtons.forEach(function(button) {
|
||||
assert.isTrue(button.addEventListener.calledOnce);
|
||||
});
|
||||
});
|
||||
|
||||
test('start() should listen to "opened" event on menu group', function() {
|
||||
cardFilter.start(mockMenuGroup);
|
||||
assert.isTrue(mockMenuGroup.addEventListener.calledOnce);
|
||||
var call = mockMenuGroup.addEventListener.firstCall;
|
||||
assert.equal(call.args[0], 'opened');
|
||||
assert.isTrue(typeof call.args[1] === 'function');
|
||||
});
|
||||
});
|
||||
|
||||
suite('> stop', function() {
|
||||
setup(function() {
|
||||
mockSmartButtons = generateSmartButtons();
|
||||
mockSmartButtons.forEach(function(button) {
|
||||
sinon.spy(button, 'removeEventListener');
|
||||
});
|
||||
|
||||
mockMenuGroup = new MockMenuGroup();
|
||||
sinon.stub(mockMenuGroup, 'querySelectorAll', function() {
|
||||
return mockSmartButtons;
|
||||
});
|
||||
cardFilter = new CardFilter();
|
||||
cardFilter.start(mockMenuGroup);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
mockMenuGroup = undefined;
|
||||
cardFilter = undefined;
|
||||
mockSmartButtons = undefined;
|
||||
});
|
||||
|
||||
test('stop() should remove "click" event listeners on smart-button',
|
||||
function() {
|
||||
cardFilter.stop();
|
||||
mockSmartButtons.forEach(function(button) {
|
||||
assert.isTrue(button.removeEventListener.calledOnce);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('> handleEvent', function() {
|
||||
setup(function() {
|
||||
mockSmartButtons = generateSmartButtons();
|
||||
|
||||
mockMenuGroup = new MockMenuGroup();
|
||||
sinon.stub(mockMenuGroup, 'querySelectorAll', function() {
|
||||
return mockSmartButtons;
|
||||
});
|
||||
cardFilter = new CardFilter();
|
||||
sinon.spy(cardFilter, 'handleEvent');
|
||||
cardFilter.start(mockMenuGroup);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
mockMenuGroup = undefined;
|
||||
cardFilter = undefined;
|
||||
mockSmartButtons = undefined;
|
||||
});
|
||||
|
||||
test('handleEvent() should be called with event argument', function() {
|
||||
mockSmartButtons[0].dispatchEvent({
|
||||
type: 'click'
|
||||
});
|
||||
assert.isTrue(cardFilter.handleEvent.calledOnce);
|
||||
assert.equal(cardFilter.handleEvent.getCall(0).args[0].type, 'click');
|
||||
});
|
||||
});
|
||||
|
||||
suite('> filter', function() {
|
||||
setup(function() {
|
||||
mockSmartButtons = generateSmartButtons();
|
||||
|
||||
mockMenuGroup = new MockMenuGroup();
|
||||
sinon.stub(mockMenuGroup, 'querySelectorAll', function() {
|
||||
return mockSmartButtons;
|
||||
});
|
||||
cardFilter = new CardFilter();
|
||||
cardFilter.filter = 'filter';
|
||||
sinon.spy(cardFilter, 'emit');
|
||||
cardFilter.start(mockMenuGroup);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
mockMenuGroup = undefined;
|
||||
cardFilter = undefined;
|
||||
mockSmartButtons = undefined;
|
||||
});
|
||||
|
||||
test('should fire filterchanged event when filter is changed', function() {
|
||||
cardFilter.filter = 'tv';
|
||||
assert.isTrue(cardFilter.emit.calledOnce);
|
||||
assert.equal(cardFilter.emit.getCall(0).args[0], 'filterchanged');
|
||||
assert.equal(cardFilter.emit.getCall(0).args[1], 'tv');
|
||||
});
|
||||
});
|
||||
|
||||
suite('> show and hide', function() {
|
||||
setup(function() {
|
||||
mockSmartButtons = generateSmartButtons();
|
||||
|
||||
mockMenuGroup = new MockMenuGroup();
|
||||
sinon.stub(mockMenuGroup, 'querySelectorAll', function() {
|
||||
return mockSmartButtons;
|
||||
});
|
||||
sinon.spy(mockMenuGroup.classList, 'add');
|
||||
sinon.spy(mockMenuGroup.classList, 'remove');
|
||||
cardFilter = new CardFilter();
|
||||
cardFilter.filter = 'filter';
|
||||
cardFilter.start(mockMenuGroup);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
mockMenuGroup.classList.add.restore();
|
||||
mockMenuGroup.classList.remove.restore();
|
||||
mockMenuGroup = undefined;
|
||||
cardFilter = undefined;
|
||||
mockSmartButtons = undefined;
|
||||
});
|
||||
|
||||
test('should add "hidden" class to menu group when calling hide()',
|
||||
function() {
|
||||
cardFilter.hide();
|
||||
assert.isTrue(mockMenuGroup.classList.add.calledWithExactly('hidden'));
|
||||
});
|
||||
|
||||
test('should remove "hidden" class from menu group when calling show()',
|
||||
function() {
|
||||
cardFilter.show();
|
||||
assert.isTrue(
|
||||
mockMenuGroup.classList.remove.calledWithExactly('hidden'));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
623
gaia/tv_apps/smart-home/test/unit/card_manager_test.js
Executable file
|
|
@ -0,0 +1,623 @@
|
|||
'use strict';
|
||||
/* global Application, CardManager, Deck, Folder, AsyncSemaphore,
|
||||
MockCardStore, MockPipedPromise, MockXMLHttpRequest */
|
||||
|
||||
require('/bower_components/evt/index.js');
|
||||
require('/shared/js/async_semaphore.js');
|
||||
require('/shared/js/uuid.js');
|
||||
require('/shared/js/smart-screen/shared_utils.js');
|
||||
require('/shared/test/unit/mocks/mock_piped_promise.js');
|
||||
require('/shared/test/unit/mocks/mock_card_store.js');
|
||||
require('/shared/test/unit/mocks/mock_xml_http_request.js');
|
||||
require('/shared/js/smart-screen/cards/card.js');
|
||||
require('/shared/js/smart-screen/cards/deck.js');
|
||||
require('/shared/js/smart-screen/cards/folder.js');
|
||||
require('/shared/js/smart-screen/cards/application.js');
|
||||
|
||||
suite('smart-home/CardManager', function() {
|
||||
var realPipedPromise;
|
||||
var decks;
|
||||
var apps;
|
||||
var folders;
|
||||
|
||||
suiteSetup(function(done) {
|
||||
realPipedPromise = window.PipedPromise;
|
||||
// real PipedPromise will make test failed because it waits for
|
||||
// last promise to be resolved. But we need to call done in the last
|
||||
// promise. So we use a MockPipedPromise (which is just a genuine native
|
||||
// Promise) instead.
|
||||
window.PipedPromise = MockPipedPromise;
|
||||
|
||||
decks = {
|
||||
dashboard: new Deck({
|
||||
name: {
|
||||
raw: 'Dashboard'
|
||||
},
|
||||
group: 'dashboard',
|
||||
nativeApp: {
|
||||
name: 'Dashboard',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://dashboard.gaiamobile.org/manifest.webapp'
|
||||
}
|
||||
}),
|
||||
tv: new Deck({
|
||||
name: {
|
||||
raw: 'TV'
|
||||
},
|
||||
group: 'tv',
|
||||
nativeApp: {
|
||||
name: 'TV',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://tv-deck.gaiamobile.org/manifest.webapp'
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
apps = {
|
||||
music: new Application({
|
||||
name: {
|
||||
raw: 'Music'
|
||||
},
|
||||
group: 'application',
|
||||
nativeApp: {
|
||||
name: 'Music',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://music.gaiamobile.org/manifest.webapp'
|
||||
}
|
||||
}),
|
||||
video: new Application({
|
||||
name: {
|
||||
raw: 'Video'
|
||||
},
|
||||
group: 'application',
|
||||
nativeApp: {
|
||||
name: 'Video',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://video.gaiamobile.org/manifest.webapp'
|
||||
}
|
||||
}),
|
||||
gallery: new Application({
|
||||
name: {
|
||||
raw: 'Gallery'
|
||||
},
|
||||
group: 'application',
|
||||
nativeApp: {
|
||||
name: 'Gallery',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://gallery.gaiamobile.org/manifest.webapp'
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
folders = {
|
||||
emptyOne: new Folder({
|
||||
name: {
|
||||
raw: 'New Empty Folder'
|
||||
}
|
||||
}),
|
||||
emptyTwo: new Folder({
|
||||
name: {
|
||||
raw: 'New Empty Folder 2'
|
||||
}
|
||||
}),
|
||||
nonEmpty: new Folder({
|
||||
name: {
|
||||
raw: 'Non Empty'
|
||||
},
|
||||
_cardsInFolder: [apps.video, apps.gallery]
|
||||
})
|
||||
};
|
||||
|
||||
require('/shared/js/smart-screen/card_manager.js', function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
decks = undefined;
|
||||
apps = undefined;
|
||||
folders = undefined;
|
||||
window.PipedPromise = realPipedPromise;
|
||||
});
|
||||
|
||||
function prepareCardManagerForTesting() {
|
||||
var result = new CardManager();
|
||||
// We bypass init() because it involves navigator.mozApps.mgmt
|
||||
// Instead we need to prepare necessary properties of cardManager
|
||||
result._asyncSemaphore = new AsyncSemaphore();
|
||||
result._cardStore = MockCardStore;
|
||||
|
||||
result._cardList = [
|
||||
decks.dashboard,
|
||||
apps.music,
|
||||
decks.tv,
|
||||
folders.nonEmpty
|
||||
];
|
||||
|
||||
result.installedApps = {
|
||||
'app://dashboard.gaiamobile.org/manifest.webapp': {
|
||||
name: 'Dashboard',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://dashboard.gaiamobile.org/manifest.webapp'
|
||||
},
|
||||
'app://music.gaiamobile.org/manifest.webapp': {
|
||||
name: 'Music',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://music.gaiamobile.org/manifest.webapp'
|
||||
},
|
||||
'app://tv-deck.gaiamobile.org/manifest.webapp': {
|
||||
name: 'TV',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://tv-deck.gaiamobile.org/manifest.webapp'
|
||||
},
|
||||
'app://video.gaiamobile.org/manifest.webapp': {
|
||||
name: 'Video',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://video.gaiamobile.org/manifest.webapp'
|
||||
},
|
||||
'app://gallery.gaiamobile.org/manifest.webapp': {
|
||||
name: 'Gallery',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://gallery.gaiamobile.org/manifest.webapp'
|
||||
},
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
suite('_bestMatchingIcon()', function() {
|
||||
var cardManager;
|
||||
var app;
|
||||
var manifestWithIconsInOrder = {
|
||||
icons: {
|
||||
'32': '/style/icons/32.png',
|
||||
'64': '/style/icons/64.png',
|
||||
'128': '/style/icons/128.png',
|
||||
'256': '/style/icons/256.png'
|
||||
}
|
||||
};
|
||||
var manifestWithIconsInRandom = {
|
||||
icons: {
|
||||
'32': '/style/icons/32.png',
|
||||
'256': '/style/icons/256.png',
|
||||
'128': '/style/icons/128.png',
|
||||
'64': '/style/icons/64.png'
|
||||
}
|
||||
};
|
||||
|
||||
setup(function() {
|
||||
cardManager = new CardManager();
|
||||
app = {
|
||||
origin: 'app://stub.gaiamobile.org/'
|
||||
};
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
cardManager = undefined;
|
||||
app = undefined;
|
||||
});
|
||||
|
||||
test('call without preferredSize', function() {
|
||||
var actual = cardManager._bestMatchingIcon(app, manifestWithIconsInOrder);
|
||||
assert.equal(actual, 'app://stub.gaiamobile.org/style/icons/256.png');
|
||||
});
|
||||
|
||||
test('call with random order of icon size and ' +
|
||||
'without preferredSize ', function() {
|
||||
var actual =
|
||||
cardManager._bestMatchingIcon(app, manifestWithIconsInRandom);
|
||||
assert.equal(actual, 'app://stub.gaiamobile.org/style/icons/256.png');
|
||||
});
|
||||
|
||||
test('call with preferredSize', function() {
|
||||
var actual =
|
||||
cardManager._bestMatchingIcon(app, manifestWithIconsInOrder, 100);
|
||||
assert.equal(actual, 'app://stub.gaiamobile.org/style/icons/128.png');
|
||||
});
|
||||
|
||||
test('call with random order of icon size and ' +
|
||||
'preferredSize ', function() {
|
||||
var actual =
|
||||
cardManager._bestMatchingIcon(app, manifestWithIconsInRandom, 100);
|
||||
assert.equal(actual, 'app://stub.gaiamobile.org/style/icons/128.png');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('_reloadCardList()', function() {
|
||||
var cardManager;
|
||||
var realXMLHttpRequest;
|
||||
|
||||
setup(function() {
|
||||
cardManager = new CardManager();
|
||||
// We bypass init() because it involves navigator.mozApps.mgmt
|
||||
// Instead we need to prepare necessary properties of cardManager
|
||||
cardManager._cardList = [];
|
||||
cardManager._cardStore = MockCardStore;
|
||||
cardManager._asyncSemaphore = new AsyncSemaphore();
|
||||
realXMLHttpRequest = window.XMLHttpRequest;
|
||||
window.XMLHttpRequest = MockXMLHttpRequest;
|
||||
this.sinon.spy(cardManager, 'writeCardlistInCardStore');
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
MockCardStore.mClearData();
|
||||
window.XMLHttpRequest = realXMLHttpRequest;
|
||||
cardManager = undefined;
|
||||
});
|
||||
|
||||
test('should load cardList from datastore if possible', function(done) {
|
||||
MockCardStore.mPrepareData('cardList', [{
|
||||
'name': 'Television',
|
||||
'type': 'Deck'
|
||||
}]);
|
||||
|
||||
cardManager._reloadCardList().then(function() {
|
||||
assert.isTrue(cardManager._cardList.length > 0);
|
||||
assert.equal(cardManager._cardList[0].name, 'Television');
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('should load cardList from file if nothing in datastore',
|
||||
function(done) {
|
||||
cardManager._reloadCardList().then(function() {
|
||||
assert.isTrue(cardManager._cardList.length > 0);
|
||||
assert.equal(cardManager._cardList[0].name, 'Devices');
|
||||
assert.isTrue(cardManager.writeCardlistInCardStore.calledOnce);
|
||||
}, function(reason) {
|
||||
assert.fail('should not reject promise due to ' + reason);
|
||||
}).then(done, done);
|
||||
|
||||
// XXX: We should remove window.setTimeout in test but since we are
|
||||
// guarding this test with done(), let's just put it as is until we
|
||||
// have better solution
|
||||
window.setTimeout(function() {
|
||||
MockXMLHttpRequest.triggerReadyStateChange({
|
||||
status: 200,
|
||||
response: {
|
||||
'card_list': [{
|
||||
'name': 'Devices',
|
||||
'type': 'Deck'
|
||||
}]
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('findCardFromCardList()', function() {
|
||||
var cardManager;
|
||||
var dashboardCardId;
|
||||
var folderCardId;
|
||||
|
||||
setup(function() {
|
||||
cardManager = prepareCardManagerForTesting();
|
||||
dashboardCardId = cardManager._cardList[0].cardId;
|
||||
folderCardId = cardManager._cardList[3].cardId;
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
MockCardStore.mClearData();
|
||||
cardManager = undefined;
|
||||
dashboardCardId = undefined;
|
||||
folderCardId = undefined;
|
||||
});
|
||||
|
||||
test('should find card based on cardId', function() {
|
||||
var card = cardManager.findCardFromCardList({
|
||||
cardId: dashboardCardId
|
||||
});
|
||||
assert.equal(card.name.raw, 'Dashboard');
|
||||
});
|
||||
|
||||
test('should find folder based on cardId', function() {
|
||||
var card = cardManager.findCardFromCardList({
|
||||
cardId: folderCardId
|
||||
});
|
||||
assert.isTrue(card instanceof Folder);
|
||||
assert.equal(card.name.raw, 'Non Empty');
|
||||
});
|
||||
|
||||
test('should return undefined by querying non-existing launchURL',
|
||||
function() {
|
||||
var card = cardManager.findCardFromCardList({
|
||||
manifestURL: 'app://tv-deck.gaiamobile.org/manifest.webapp',
|
||||
launchURL: 'app://tv-deck.gaiamobile.org/#42'
|
||||
});
|
||||
|
||||
assert.isUndefined(card);
|
||||
});
|
||||
|
||||
test('should return undefined if not found', function() {
|
||||
var card = cardManager.findCardFromCardList({
|
||||
cardId: 'incorrect-card-id'
|
||||
});
|
||||
assert.isUndefined(card);
|
||||
});
|
||||
|
||||
test('should find card inside of folder', function() {
|
||||
var card = cardManager.findCardFromCardList({
|
||||
manifestURL: 'app://video.gaiamobile.org/manifest.webapp'
|
||||
});
|
||||
assert.equal(card.name.raw, 'Video');
|
||||
});
|
||||
|
||||
test('should be able to find folder by querying via cardEntry', function() {
|
||||
var folderEntry = folders.nonEmpty.serialize();
|
||||
var card = cardManager.findCardFromCardList({
|
||||
cardEntry: folderEntry
|
||||
});
|
||||
assert.equal(card, folders.nonEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getFilteredCardList()', function() {
|
||||
var cardManager;
|
||||
|
||||
setup(function() {
|
||||
cardManager = prepareCardManagerForTesting();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
MockCardStore.mClearData();
|
||||
cardManager = undefined;
|
||||
});
|
||||
|
||||
test('should get only apps if filter="application"', function(done) {
|
||||
var filter = 'application';
|
||||
cardManager.getFilteredCardList(filter).then(function(cards) {
|
||||
assert.isTrue(cards.length === 1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('should get all cards if filter="all"', function(done) {
|
||||
var filter = 'all';
|
||||
cardManager.getFilteredCardList(filter).then(function(cards) {
|
||||
assert.isTrue(cards.length === cardManager._cardList.length);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('should get empty array if filter is undefined', function(done) {
|
||||
cardManager.getFilteredCardList().then(function(cards) {
|
||||
assert.isTrue(cards.length === 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('insertNewFolder()', function() {
|
||||
var cardManager;
|
||||
var dashboardCardId;
|
||||
|
||||
setup(function() {
|
||||
cardManager = new CardManager();
|
||||
// We bypass init() because it involves navigator.mozApps.mgmt
|
||||
// Instead we need to prepare necessary properties of cardManager
|
||||
cardManager._asyncSemaphore = new AsyncSemaphore();
|
||||
cardManager._cardStore = MockCardStore;
|
||||
cardManager._cardList = [decks.dashboard];
|
||||
cardManager.installedApps = {
|
||||
'app://dashboard.gaiamobile.org/manifest.webapp': {
|
||||
name: 'Dashboard',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://dashboard.gaiamobile.org/manifest.webapp'
|
||||
}
|
||||
};
|
||||
dashboardCardId = cardManager._cardList[0].cardId;
|
||||
|
||||
this.sinon.spy(cardManager, 'writeFolderInCardStore');
|
||||
this.sinon.spy(cardManager, 'writeCardlistInCardStore');
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
MockCardStore.mClearData();
|
||||
cardManager = undefined;
|
||||
});
|
||||
|
||||
test('should return new folder instance whenever a new folder is inserted',
|
||||
function() {
|
||||
var newFolder = cardManager.insertNewFolder('an empty folder');
|
||||
|
||||
assert.ok(newFolder.folderId);
|
||||
assert.equal(newFolder.state, Folder.STATES.DETACHED);
|
||||
});
|
||||
|
||||
test('should write to data store when folder has content', function() {
|
||||
var newFolder = cardManager.insertNewFolder('a test folder');
|
||||
assert.isFalse(cardManager.writeCardlistInCardStore.calledOnce);
|
||||
newFolder.addCard(new Application({
|
||||
name: 'Music',
|
||||
group: 'application'
|
||||
}));
|
||||
|
||||
assert.isTrue(cardManager.writeCardlistInCardStore.calledOnce);
|
||||
});
|
||||
|
||||
test('should insert at correct location if index is specified', function() {
|
||||
var targetLocation = 0;
|
||||
var newFolder =
|
||||
cardManager.insertNewFolder('a test folder', targetLocation);
|
||||
assert.isFalse(cardManager.writeCardlistInCardStore.calledOnce);
|
||||
newFolder.addCard(apps.music);
|
||||
|
||||
assert.isTrue(cardManager.writeCardlistInCardStore.calledOnce);
|
||||
assert.equal(cardManager._cardList[targetLocation], newFolder);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('insertCard()', function() {
|
||||
var cardManager;
|
||||
|
||||
setup(function() {
|
||||
cardManager = prepareCardManagerForTesting();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
MockCardStore.mClearData();
|
||||
cardManager = undefined;
|
||||
});
|
||||
|
||||
test('should not be able to insert the same card twice', function() {
|
||||
var expectedCardListLength = cardManager._cardList.length;
|
||||
cardManager.insertCard({
|
||||
cardEntry: {
|
||||
'type': 'Application',
|
||||
'group': 'app',
|
||||
'manifestURL': 'app://music.gaiamobile.org/manifest.webapp'
|
||||
}
|
||||
});
|
||||
assert.equal(cardManager._cardList.length, expectedCardListLength);
|
||||
});
|
||||
|
||||
test('should be able to insert same app twice with different launchURL',
|
||||
function() {
|
||||
var expectedCardListLength = cardManager._cardList.length + 1;
|
||||
cardManager.insertCard({
|
||||
cardEntry: {
|
||||
'type': 'Application',
|
||||
'group': 'tv',
|
||||
'manifestURL': 'app://tv-deck.gaiamobile.org/manifest.webapp',
|
||||
'launchURL': 'app://tv-deck.gaiamobile.org/#42'
|
||||
}
|
||||
});
|
||||
assert.equal(cardManager._cardList.length, expectedCardListLength);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('removeCard()', function() {
|
||||
var cardManager;
|
||||
setup(function() {
|
||||
cardManager = prepareCardManagerForTesting();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
MockCardStore.mClearData();
|
||||
cardManager = undefined;
|
||||
});
|
||||
|
||||
test('should be able to remove card with index specified', function() {
|
||||
assert.ok(
|
||||
cardManager.findCardFromCardList({
|
||||
manifestURL: 'app://music.gaiamobile.org/manifest.webapp'})
|
||||
);
|
||||
|
||||
cardManager.removeCard(1); // remove card of 'Music' app
|
||||
|
||||
assert.isUndefined(
|
||||
cardManager.findCardFromCardList({
|
||||
manifestURL: 'app://music.gaiamobile.org/manifest.webapp'})
|
||||
);
|
||||
});
|
||||
|
||||
test('should be able to remove card within folder', function(done) {
|
||||
assert.ok(
|
||||
cardManager.findCardFromCardList({
|
||||
manifestURL: 'app://video.gaiamobile.org/manifest.webapp'})
|
||||
);
|
||||
|
||||
cardManager.getCardList().then(function(cardList) {
|
||||
cardManager.removeCard(cardList[3].getCardList()[0]);
|
||||
|
||||
assert.isUndefined(
|
||||
cardManager.findCardFromCardList({
|
||||
manifestURL: 'app://video.gaiamobile.org/manifest.webapp'})
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('writeCardlistInCardStore()', function() {
|
||||
var cardManager;
|
||||
|
||||
setup(function() {
|
||||
cardManager = new CardManager();
|
||||
// We bypass init() because it involves navigator.mozApps.mgmt
|
||||
// Instead we need to prepare necessary properties of cardManager
|
||||
cardManager._asyncSemaphore = new AsyncSemaphore();
|
||||
cardManager._cardStore = MockCardStore;
|
||||
|
||||
cardManager._cardList = [
|
||||
decks.dashbaord,
|
||||
folders.emptyOne,
|
||||
folders.emptyTwo,
|
||||
apps.music
|
||||
];
|
||||
|
||||
cardManager.installedApps = {
|
||||
'app://dashboard.gaiamobile.org/manifest.webapp': {
|
||||
name: 'Dashboard',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://dashboard.gaiamobile.org/manifest.webapp'
|
||||
},
|
||||
'app://music.gaiamobile.org/manifest.webapp': {
|
||||
name: 'Music',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://music.gaiamobile.org/manifest.webapp'
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
MockCardStore.mClearData();
|
||||
cardManager = undefined;
|
||||
});
|
||||
|
||||
test('write with empty folder should eliminate it from card list',
|
||||
function(done) {
|
||||
var expectedLength = cardManager._cardList.length - 2;
|
||||
|
||||
cardManager.writeCardlistInCardStore({
|
||||
cleanEmptyFolder: true
|
||||
}).then(function() {
|
||||
assert.equal(cardManager._cardList.length, expectedLength);
|
||||
assert.isTrue(cardManager._cardList.indexOf(folders.emptyOne) < 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('swapCard()', function() {
|
||||
var cardManager;
|
||||
|
||||
setup(function() {
|
||||
cardManager = prepareCardManagerForTesting();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
MockCardStore.mClearData();
|
||||
cardManager = undefined;
|
||||
});
|
||||
|
||||
test('swap adjacent cards', function() {
|
||||
var card1 = cardManager._cardList[0];
|
||||
var card2 = cardManager._cardList[1];
|
||||
cardManager.swapCard(card1, card2);
|
||||
assert.equal(cardManager._cardList[0], card2);
|
||||
assert.equal(cardManager._cardList[1], card1);
|
||||
});
|
||||
|
||||
test('swap non-adjacent cards', function() {
|
||||
var card1 = cardManager._cardList[0];
|
||||
var card2 = cardManager._cardList[2];
|
||||
cardManager.swapCard(card1, card2);
|
||||
assert.equal(cardManager._cardList[0], card2);
|
||||
assert.equal(cardManager._cardList[2], card1);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
158
gaia/tv_apps/smart-home/test/unit/filter_manager_test.js
Executable file
|
|
@ -0,0 +1,158 @@
|
|||
/* global MockCardFilter, FilterManager, MockXScrollable, MockCardManager,
|
||||
MockHome */
|
||||
|
||||
'use strict';
|
||||
require('/bower_components/evt/index.js');
|
||||
requireApp('smart-home/test/unit/mock_card_filter.js');
|
||||
requireApp('smart-home/test/unit/mock_x_scrollable.js');
|
||||
requireApp('smart-home/test/unit/mock_card_manager.js');
|
||||
requireApp('smart-home/test/unit/mock_home.js');
|
||||
requireApp('smart-home/js/filter_manager.js');
|
||||
|
||||
suite('smart-gome/FilterManager', function() {
|
||||
|
||||
var setupDOM = function() {
|
||||
var filterTabGroup = document.createElement('menu');
|
||||
filterTabGroup.id = 'filter-tab-group';
|
||||
filterTabGroup.dataset.iconType = 'filter';
|
||||
filterTabGroup.classList.add('filter-tab-group');
|
||||
|
||||
var bubbles = document.createElement('div');
|
||||
bubbles.id = 'bubbles';
|
||||
|
||||
var cardListElem = document.createElement('div');
|
||||
cardListElem.id = 'card-list';
|
||||
cardListElem.setAttribute('smart-bubbles', 'true');
|
||||
cardListElem.dataset.defaultItem = '0';
|
||||
|
||||
document.body.appendChild(filterTabGroup);
|
||||
document.body.appendChild(bubbles);
|
||||
document.body.appendChild(cardListElem);
|
||||
};
|
||||
|
||||
var teardownDOM = function() {
|
||||
document.body.removeChild(document.getElementById('filter-tab-group'));
|
||||
document.body.removeChild(document.getElementById('bubbles'));
|
||||
document.body.removeChild(document.getElementById('card-list'));
|
||||
};
|
||||
|
||||
suite('> filterchanged event', function() {
|
||||
var realCardFilter;
|
||||
var filterManager;
|
||||
|
||||
setup(function() {
|
||||
realCardFilter = window.CardFilter;
|
||||
window.CardFilter = MockCardFilter;
|
||||
setupDOM();
|
||||
|
||||
filterManager = new FilterManager();
|
||||
filterManager.init({
|
||||
cardListElem: document.getElementById('card-list'),
|
||||
cardScrollable: new MockXScrollable(),
|
||||
home: new MockHome(),
|
||||
cardManager: new MockCardManager()
|
||||
});
|
||||
|
||||
filterManager._smartBubblesElem.play = function() {};
|
||||
filterManager._smartBubblesElem.stopImmediately = function() {};
|
||||
sinon.spy(filterManager._smartBubblesElem, 'play');
|
||||
sinon.spy(filterManager._smartBubblesElem, 'stopImmediately');
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
window.cardFilter = realCardFilter;
|
||||
filterManager._smartBubblesElem.play.restore();
|
||||
filterManager._smartBubblesElem.stopImmediately.restore();
|
||||
filterManager = undefined;
|
||||
teardownDOM();
|
||||
});
|
||||
|
||||
test('start playing animation when filterchanged is fired', function() {
|
||||
var cardListElem = document.getElementById('card-list');
|
||||
filterManager._cardFilter.mFireEvent('filterchanged', 'tv');
|
||||
|
||||
assert.equal(
|
||||
cardListElem.getAttribute('smart-bubbles-direction'), 'down');
|
||||
assert.isTrue(filterManager._smartBubblesElem.play.calledOnce);
|
||||
});
|
||||
|
||||
test('stop playing animation when filterchanged is fired before previous ' +
|
||||
' animation ends', function() {
|
||||
|
||||
filterManager._isFilterChanging = true;
|
||||
filterManager._cardFilter.mFireEvent('filterchanged', 'tv');
|
||||
|
||||
assert.isTrue(filterManager._smartBubblesElem.stopImmediately.calledOnce);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('> all-items-bubbled event', function() {
|
||||
var realCardFilter;
|
||||
var filterManager;
|
||||
var mockXScrollable;
|
||||
var mockCardManager;
|
||||
var mockHome;
|
||||
var cardListElem;
|
||||
|
||||
setup(function() {
|
||||
realCardFilter = window.CardFilter;
|
||||
window.CardFilter = MockCardFilter;
|
||||
setupDOM();
|
||||
|
||||
cardListElem = document.getElementById('card-list');
|
||||
mockXScrollable = new MockXScrollable();
|
||||
mockCardManager = new MockCardManager();
|
||||
filterManager = new FilterManager();
|
||||
mockHome = new MockHome();
|
||||
filterManager.init({
|
||||
cardListElem: cardListElem,
|
||||
cardScrollable: mockXScrollable,
|
||||
home: mockHome,
|
||||
cardManager: mockCardManager
|
||||
});
|
||||
|
||||
filterManager._smartBubblesElem.play = function() {};
|
||||
filterManager._smartBubblesElem.stopImmediately = function() {};
|
||||
sinon.spy(filterManager, 'getFilterByIconName');
|
||||
sinon.spy(mockXScrollable, 'clean');
|
||||
sinon.spy(mockCardManager, 'getFilteredCardList');
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
window.cardFilter = realCardFilter;
|
||||
filterManager.getFilterByIconName.restore();
|
||||
filterManager = undefined;
|
||||
mockXScrollable.clean.restore();
|
||||
mockXScrollable = undefined;
|
||||
mockCardManager.getFilteredCardList.restore();
|
||||
mockCardManager = undefined;
|
||||
mockHome = undefined;
|
||||
teardownDOM();
|
||||
});
|
||||
|
||||
test('clean scrollable and get filtered cards when bubbling down ' +
|
||||
'animation ends', function() {
|
||||
// pretend that we already went through 'onFilterChanged'
|
||||
filterManager._isFilterChanging = true;
|
||||
cardListElem.setAttribute('smart-bubbles-direction', 'down');
|
||||
filterManager._smartBubblesElem.dispatchEvent(
|
||||
new CustomEvent('all-items-bubbled'));
|
||||
|
||||
assert.isTrue(mockXScrollable.clean.calledOnce);
|
||||
assert.isTrue(mockCardManager.getFilteredCardList.calledOnce);
|
||||
});
|
||||
|
||||
test('reset _isFilterChanging to false when bubbling up animation ends',
|
||||
function() {
|
||||
// pretend that we already went through 'onFilterChanged'
|
||||
filterManager._isFilterChanging = true;
|
||||
cardListElem.removeAttribute('smart-bubbles-direction');
|
||||
filterManager._smartBubblesElem.dispatchEvent(
|
||||
new CustomEvent('all-items-bubbled'));
|
||||
|
||||
assert.isFalse(mockXScrollable.clean.calledOnce);
|
||||
assert.isFalse(mockCardManager.getFilteredCardList.calledOnce);
|
||||
});
|
||||
});
|
||||
});
|
||||
144
gaia/tv_apps/smart-home/test/unit/folder_test.js
Executable file
|
|
@ -0,0 +1,144 @@
|
|||
'use strict';
|
||||
|
||||
/* global Deck, Folder, Application */
|
||||
|
||||
require('/bower_components/evt/index.js');
|
||||
require('/shared/js/uuid.js');
|
||||
require('/shared/js/smart-screen/cards/card.js');
|
||||
require('/shared/js/smart-screen/cards/application.js');
|
||||
require('/shared/js/smart-screen/cards/deck.js');
|
||||
require('/shared/js/smart-screen/cards/folder.js');
|
||||
|
||||
suite('Folder', function() {
|
||||
var folder;
|
||||
var stubFolder;
|
||||
var nonEmptyFolder;
|
||||
var apps;
|
||||
var deck;
|
||||
|
||||
var eventSpy;
|
||||
|
||||
setup(function() {
|
||||
apps = {
|
||||
music: new Application({
|
||||
name: 'Music',
|
||||
nativeApp: {
|
||||
name: 'Music',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://music.gaiamobile.org/manifest.webapp'
|
||||
}
|
||||
}),
|
||||
video: new Application({
|
||||
name: 'Video',
|
||||
nativeApp: {
|
||||
name: 'Video',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://video.gaiamobile.org/manifest.webapp'
|
||||
}
|
||||
}),
|
||||
gallery: new Application({
|
||||
name: 'Gallery',
|
||||
nativeApp: {
|
||||
name: 'Gallery',
|
||||
removable: false,
|
||||
manifest: {},
|
||||
manifestURL: 'app://gallery.gaiamobile.org/manifest.webapp'
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
folder = new Folder({
|
||||
name: 'folder under test'
|
||||
});
|
||||
|
||||
deck = new Deck({
|
||||
nativeApp: {},
|
||||
name: 'a deck'
|
||||
});
|
||||
|
||||
stubFolder = new Folder({
|
||||
name: 'folder as stub'
|
||||
});
|
||||
|
||||
nonEmptyFolder = new Folder({
|
||||
name: 'non empty',
|
||||
_cardsInFolder: [apps.music, apps.video, apps.gallery]
|
||||
});
|
||||
|
||||
eventSpy = this.sinon.spy();
|
||||
nonEmptyFolder.on('folder-changed', eventSpy);
|
||||
nonEmptyFolder.on('card-inserted', eventSpy);
|
||||
nonEmptyFolder.on('card-removed', eventSpy);
|
||||
nonEmptyFolder.on('card-updated', eventSpy);
|
||||
nonEmptyFolder.on('card-swapped', eventSpy);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
folder = undefined;
|
||||
deck = undefined;
|
||||
stubFolder = undefined;
|
||||
nonEmptyFolder = undefined;
|
||||
apps = undefined;
|
||||
eventSpy = undefined;
|
||||
});
|
||||
|
||||
var assertFolderIsEmpty = function(target) {
|
||||
assert.isTrue(target._cardsInFolder.length === 0);
|
||||
};
|
||||
|
||||
var assertFolderIsDirty = function(target) {
|
||||
assert.equal(target.state, Folder.STATES.DIRTY);
|
||||
};
|
||||
|
||||
test('should be able to add deck into folder', function() {
|
||||
assertFolderIsEmpty(folder);
|
||||
folder.addCard(deck);
|
||||
assert.equal(folder.getCardList()[0], deck);
|
||||
assertFolderIsDirty(folder);
|
||||
});
|
||||
|
||||
test('should not be able to add folder into folder', function() {
|
||||
assertFolderIsEmpty(stubFolder);
|
||||
folder.addCard(stubFolder);
|
||||
assertFolderIsEmpty(stubFolder);
|
||||
assert.equal(stubFolder.state, Folder.STATES.NORMAL);
|
||||
});
|
||||
|
||||
test('should be able to remove deck from folder', function() {
|
||||
folder.getCardList()[0] = deck;
|
||||
|
||||
folder.removeCard(deck);
|
||||
assertFolderIsEmpty(folder);
|
||||
assertFolderIsDirty(folder);
|
||||
});
|
||||
|
||||
test('should be able to update card in folder', function() {
|
||||
var index = 0;
|
||||
var cardToBeUpdated = nonEmptyFolder.getCardList()[index];
|
||||
cardToBeUpdated.name = 'new gallery';
|
||||
|
||||
assert.isFalse(eventSpy.called);
|
||||
nonEmptyFolder.updateCard(cardToBeUpdated, index);
|
||||
assert.equal(
|
||||
nonEmptyFolder.getCardList()[0].cardId, cardToBeUpdated.cardId);
|
||||
assert.equal(nonEmptyFolder.getCardList()[0].name, cardToBeUpdated.name);
|
||||
assert.isTrue(eventSpy.calledWithExactly(nonEmptyFolder));
|
||||
assert.isTrue(eventSpy.calledWithExactly(cardToBeUpdated, index));
|
||||
assertFolderIsDirty(nonEmptyFolder);
|
||||
});
|
||||
|
||||
test('should be able to swap cards in folder', function() {
|
||||
var card1 = nonEmptyFolder.getCardList()[0];
|
||||
var card2 = nonEmptyFolder.getCardList()[2];
|
||||
|
||||
assert.isFalse(eventSpy.called);
|
||||
nonEmptyFolder.swapCard(card1, card2);
|
||||
assert.equal(nonEmptyFolder.getCardList()[0], card2);
|
||||
assert.equal(nonEmptyFolder.getCardList()[2], card1);
|
||||
assert.isTrue(eventSpy.calledWithExactly(nonEmptyFolder));
|
||||
assert.isTrue(eventSpy.calledWithExactly(card2, card1, 0, 2));
|
||||
assertFolderIsDirty(nonEmptyFolder);
|
||||
});
|
||||
});
|
||||
359
gaia/tv_apps/smart-home/test/unit/home_test.js
Executable file
|
|
@ -0,0 +1,359 @@
|
|||
'use strict';
|
||||
/* global Folder, navigator, MockL10n, document, MocksHelper,
|
||||
XScrollable, MozActivity, SpatialNavigator, CardManager, MessageHandler,
|
||||
Home, SearchBar, FilterManager, Edit, Utils, Application, MockMozActivity,
|
||||
FTEWizard */
|
||||
|
||||
require('/bower_components/evt/index.js');
|
||||
require('/shared/js/uuid.js');
|
||||
require('/shared/js/smart-screen/cards/card.js');
|
||||
require('/shared/js/smart-screen/cards/application.js');
|
||||
require('/shared/js/smart-screen/cards/deck.js');
|
||||
require('/shared/js/smart-screen/cards/folder.js');
|
||||
require('/shared/js/smart-screen/cards/app_bookmark.js');
|
||||
require('/shared/js/smart-screen/clock.js');
|
||||
require('/shared/test/unit/mocks/mock_event_target.js');
|
||||
require('mock_card_manager.js');
|
||||
require('mock_search_bar.js');
|
||||
require('mock_menu_group.js');
|
||||
require('mock_message_handler.js');
|
||||
require('mock_x_scrollable.js');
|
||||
require('mock_card_filter.js');
|
||||
require('mock_edit.js');
|
||||
require('mock_animations.js');
|
||||
require('mock_utils.js');
|
||||
require('mock_filter_manager.js');
|
||||
require('/shared/test/unit/mocks/smart-screen/mock_spatial_navigator.js');
|
||||
require('/shared/test/unit/mocks/smart-screen/mock_key_navigation_adapter.js');
|
||||
require('/shared/test/unit/mocks/smart-screen/mock_f_t_e_wizard.js');
|
||||
require('/shared/test/unit/mocks/mock_l10n.js');
|
||||
require('/shared/test/unit/mocks/mocks_helper.js');
|
||||
require('/shared/test/unit/mocks/mock_moz_activity.js');
|
||||
require('/tv_apps/smart-home/js/home.js');
|
||||
|
||||
var mocksHelperForHomeTest = new MocksHelper([
|
||||
'CardManager',
|
||||
'CardFilter',
|
||||
'SearchBar',
|
||||
'MessageHandler',
|
||||
'XScrollable',
|
||||
'SpatialNavigator',
|
||||
'KeyNavigationAdapter',
|
||||
'Animations',
|
||||
'Edit',
|
||||
'MozActivity',
|
||||
'Utils',
|
||||
'FilterManager',
|
||||
'FTEWizard'
|
||||
]).init();
|
||||
|
||||
suite('home', function() {
|
||||
var realL10n = navigator.mozL10n;
|
||||
var realMozActivity = window.MozActivity;
|
||||
var searchButton, settingsButton, editButton, timeElem;
|
||||
var filterTabGroup, filterAllButton;
|
||||
var subject;
|
||||
var fakeTimer;
|
||||
|
||||
mocksHelperForHomeTest.attachTestHelpers();
|
||||
|
||||
suiteSetup(function() {
|
||||
realL10n = navigator.mozL10n;
|
||||
navigator.mozL10n = MockL10n;
|
||||
|
||||
searchButton = document.createElement('button');
|
||||
searchButton.id = 'search-button';
|
||||
document.body.appendChild(searchButton);
|
||||
|
||||
settingsButton = document.createElement('button');
|
||||
settingsButton.id = 'settings-button';
|
||||
document.body.appendChild(settingsButton);
|
||||
|
||||
editButton = document.createElement('button');
|
||||
editButton.id = 'edit-button';
|
||||
document.body.appendChild(editButton);
|
||||
|
||||
timeElem = document.createElement('div');
|
||||
timeElem.id = 'time';
|
||||
document.body.appendChild(timeElem);
|
||||
|
||||
filterTabGroup = document.createElement('menu-group');
|
||||
filterTabGroup.once = function() {};
|
||||
filterTabGroup.open = function() {};
|
||||
filterTabGroup.close = function() {};
|
||||
filterTabGroup.id = 'filter-tab-group';
|
||||
filterAllButton = document.createElement('smart-button');
|
||||
filterAllButton.id = 'filter-all-button';
|
||||
filterTabGroup.appendChild(filterAllButton);
|
||||
document.body.appendChild(filterTabGroup);
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
navigator.mozL10n = realL10n;
|
||||
document.body.removeChild(searchButton);
|
||||
document.body.removeChild(settingsButton);
|
||||
document.body.removeChild(editButton);
|
||||
document.body.removeChild(timeElem);
|
||||
document.body.removeChild(filterTabGroup);
|
||||
|
||||
});
|
||||
|
||||
setup(function() {
|
||||
subject = new Home();
|
||||
subject.cardScrollable = new XScrollable();
|
||||
subject.folderScrollable = new XScrollable();
|
||||
subject.messageHandler = new MessageHandler();
|
||||
subject.spatialNavigator = new SpatialNavigator();
|
||||
subject.cardManager = new CardManager();
|
||||
subject.searchBar = new SearchBar();
|
||||
subject.filterManager = new FilterManager();
|
||||
subject._fteWizard = new FTEWizard();
|
||||
subject.edit = new Edit();
|
||||
subject.cardListElem = document.createElement('div');
|
||||
subject.spatialNavigator.m_focusedElement = document.createElement('div');
|
||||
subject.isNavigable = true;
|
||||
subject.settingsButton = settingsButton;
|
||||
subject.editButton = editButton;
|
||||
subject.searchButton = searchButton;
|
||||
|
||||
fakeTimer = this.sinon.useFakeTimers();
|
||||
window.MozActivity = MockMozActivity;
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
window.MozActivity = realMozActivity;
|
||||
});
|
||||
|
||||
suite('visibilityChange > ', function() {
|
||||
suite('visible > ', function() {
|
||||
setup(function() {
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
value: 'visible',
|
||||
writeable: true,
|
||||
configurable: true
|
||||
});
|
||||
});
|
||||
|
||||
test('should set focus back on current focused element',
|
||||
function(done) {
|
||||
subject.spatialNavigator.focus = done;
|
||||
subject.onVisibilityChange();
|
||||
assert.isTrue(Utils.holdFocusForAnimation.calledOnce);
|
||||
});
|
||||
|
||||
test('should start activity', function(done) {
|
||||
subject.messageHandler.resumeActivity = done;
|
||||
subject.onVisibilityChange();
|
||||
});
|
||||
});
|
||||
|
||||
suite('invisible > ', function() {
|
||||
setup(function() {
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
value: 'hidden',
|
||||
writeable: true,
|
||||
configurable: true
|
||||
});
|
||||
});
|
||||
|
||||
test('should stop activity when invisible', function() {
|
||||
var spy = this.sinon.spy(subject.messageHandler, 'stopActivity');
|
||||
subject.onVisibilityChange();
|
||||
assert.isTrue(spy.calledOnce);
|
||||
});
|
||||
|
||||
test('should reset filter', function() {
|
||||
var spy = this.sinon.spy(subject.filterManager, 'resetFilter');
|
||||
subject.onVisibilityChange();
|
||||
assert.isTrue(spy.calledOnce);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('initClock >', function() {
|
||||
test('should set time after time elapsed', function() {
|
||||
subject.initClock();
|
||||
var stub = this.sinon.stub(subject, 'updateClock');
|
||||
fakeTimer.tick(1000);
|
||||
assert.isTrue(stub.calledOnce);
|
||||
});
|
||||
});
|
||||
|
||||
suite('onCardInserted >', function() {
|
||||
test('should insert a card into scrollable', function() {
|
||||
var card1 = new Folder({});
|
||||
var card2 = new Folder({});
|
||||
var stub = this.sinon.stub();
|
||||
var testCase1 = stub.withArgs(sinon.match.truthy, 0);
|
||||
var testCase2 = stub.withArgs(sinon.match.truthy, 1);
|
||||
subject.cardScrollable.insertNodeBefore = stub;
|
||||
|
||||
subject.onCardInserted(subject.cardScrollable, card1, 0);
|
||||
assert.isTrue(testCase1.calledOnce);
|
||||
assert.isFalse(testCase2.calledOnce);
|
||||
subject.onCardInserted(subject.cardScrollable, card2, 1);
|
||||
assert.isTrue(testCase2.calledOnce);
|
||||
});
|
||||
});
|
||||
|
||||
suite('onCardRemoved >', function() {
|
||||
test('should remove specified cards from scrollable', function() {
|
||||
var stub = this.sinon.stub();
|
||||
subject.cardScrollable.removeNodes = stub;
|
||||
subject.onCardRemoved(subject.cardScrollable, [0, 1]);
|
||||
assert.isTrue(stub.withArgs([0, 1]).calledOnce);
|
||||
});
|
||||
});
|
||||
|
||||
suite('onMove >', function() {
|
||||
test('should ask spatial navigator for next focus on non-scrollable target',
|
||||
function() {
|
||||
subject.spatialNavigator.m_focusedElement = document.createElement('div');
|
||||
var stub = this.sinon.stub();
|
||||
subject.spatialNavigator.move = stub;
|
||||
subject.onMove('left');
|
||||
assert.isTrue(stub.calledWith('left'));
|
||||
});
|
||||
|
||||
test('should ask scrollable to move its focus on scrollable target',
|
||||
function() {
|
||||
var scrollable = new XScrollable();
|
||||
scrollable.move = this.sinon.stub();
|
||||
scrollable.move.returns(true);
|
||||
subject.spatialNavigator.m_focusedElement = scrollable;
|
||||
subject.onMove('right');
|
||||
assert.isTrue(scrollable.move.calledWith('right'));
|
||||
});
|
||||
|
||||
test('should ask scrollable to move its focus on scrollable target, but ' +
|
||||
'fall-back to spatial navigator when scrollable didn\'t handle it',
|
||||
function() {
|
||||
var scrollable = new XScrollable();
|
||||
scrollable.move = this.sinon.stub();
|
||||
scrollable.move.returns(false);
|
||||
subject.spatialNavigator.m_focusedElement = scrollable;
|
||||
var spatialNavigatorMoveStub = this.sinon.stub();
|
||||
subject.spatialNavigator.move = spatialNavigatorMoveStub;
|
||||
subject.onMove('up');
|
||||
assert.isTrue(scrollable.move.calledWith('up'));
|
||||
assert.isTrue(spatialNavigatorMoveStub.calledWith('up'));
|
||||
});
|
||||
|
||||
// TODO: add case
|
||||
// 'should bypass logic of onMove if edit.onMove returns true'
|
||||
});
|
||||
|
||||
suite('onEnter >', function() {
|
||||
test('should open settings on settings button', function() {
|
||||
subject.handleFocus(settingsButton);
|
||||
|
||||
assert.equal(MozActivity.calls.length, 0);
|
||||
subject.onEnter();
|
||||
assert.equal(MozActivity.calls[0].name, 'configure');
|
||||
});
|
||||
|
||||
test('should toggle edit mode on edit button', function() {
|
||||
subject.handleFocus(editButton);
|
||||
|
||||
subject.edit.toggleEditMode = this.sinon.stub();
|
||||
subject.onEnter();
|
||||
assert.isTrue(subject.edit.toggleEditMode.calledOnce);
|
||||
});
|
||||
|
||||
test('should launch application on card ', function() {
|
||||
var testCard = document.createElement('div');
|
||||
testCard.dataset.cardId = '123';
|
||||
|
||||
var testApp = new Application({});
|
||||
testApp.launch = this.sinon.stub();
|
||||
subject.cardManager.m_card['123'] = testApp;
|
||||
|
||||
subject.handleFocus(testCard);
|
||||
subject.onEnter();
|
||||
assert.isTrue(testApp.launch.calledOnce);
|
||||
});
|
||||
|
||||
// TODO: add case
|
||||
// 'should bypass logic of onEnter if edit.onEnter returns true'
|
||||
});
|
||||
|
||||
suite('onSearchBarShown >', function() {
|
||||
setup(function() {
|
||||
this.sinon.stub(document, 'addEventListener')
|
||||
.withArgs('visibilitychange').callsArg(1);
|
||||
subject.searchBar.hide = this.sinon.stub();
|
||||
});
|
||||
|
||||
test('should fire search activity', function() {
|
||||
assert.equal(MozActivity.calls.length, 0);
|
||||
subject.onSearchBarShown();
|
||||
assert.equal(MozActivity.calls[0].name, 'search');
|
||||
});
|
||||
|
||||
test('should hide search bar after visibility has changed', function(done) {
|
||||
subject.searchBar.hide = function() {
|
||||
assert.isTrue(true);
|
||||
done();
|
||||
};
|
||||
subject.onSearchBarShown();
|
||||
});
|
||||
});
|
||||
|
||||
suite('onSearchBarHidden >', function() {
|
||||
test('should show search button again', function() {
|
||||
searchButton.classList.add('hidden');
|
||||
subject.onSearchBarHidden();
|
||||
assert.isFalse(searchButton.classList.contains('hidden'));
|
||||
});
|
||||
});
|
||||
|
||||
suite('handleFocus >', function() {
|
||||
test('should switch focus to scrollable target', function() {
|
||||
var spy = this.sinon.spy(subject.cardScrollable, 'focus');
|
||||
subject.handleFocus(subject.cardScrollable);
|
||||
assert.isTrue(spy.calledOnce);
|
||||
});
|
||||
|
||||
test('should switch focus to next focus for normal target', function() {
|
||||
var spy = this.sinon.spy(editButton, 'focus');
|
||||
subject.handleFocus(subject.editButton);
|
||||
assert.isTrue(spy.calledOnce);
|
||||
});
|
||||
|
||||
test('should open menu for menu group target', function() {
|
||||
var stub = this.sinon.stub(subject, 'handleFocusMenuGroup');
|
||||
subject.handleFocus(filterTabGroup);
|
||||
assert.isTrue(stub.calledOnce);
|
||||
});
|
||||
|
||||
test('should call checkFocusedGroup for non-menu elements', function() {
|
||||
var stub = this.sinon.stub(subject, 'checkFocusedGroup');
|
||||
subject.handleFocus(settingsButton);
|
||||
assert.isTrue(stub.withArgs(settingsButton).calledOnce);
|
||||
});
|
||||
});
|
||||
|
||||
suite('checkFocusedGroup >', function() {
|
||||
test('should close group menu when blur', function() {
|
||||
subject.handleFocusMenuGroup(filterTabGroup);
|
||||
var stub = this.sinon.stub(filterTabGroup, 'close');
|
||||
subject.checkFocusedGroup();
|
||||
assert.isTrue(stub.calledOnce);
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Test items:
|
||||
// handleFocusMenuGroup >
|
||||
// should switch navigable target to child elements of menu on open
|
||||
// should switch navigable target back to menu iteslf on close
|
||||
// handleScrollableItemFocus >
|
||||
// should focus specified item of scrollable
|
||||
// handleScrollableItemUnfocus >
|
||||
// should blur specified item of scrollable
|
||||
// openSettings >
|
||||
// should launch configure activity
|
||||
// updateClock >
|
||||
// should update clock display
|
||||
// restartClock >
|
||||
// should stop and start clock display again
|
||||
});
|
||||
13
gaia/tv_apps/smart-home/test/unit/mock_animations.js
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
/* globals evt */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var MockAnimations = evt({
|
||||
init: function() {},
|
||||
doBubbleAnimation: function(parentElem, childClass, length, cb) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
exports.MockAnimations = MockAnimations;
|
||||
})(window);
|
||||
48
gaia/tv_apps/smart-home/test/unit/mock_card_filter.js
Executable file
|
|
@ -0,0 +1,48 @@
|
|||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var MockCardFilter = function() {
|
||||
this.mEventHandlers = {};
|
||||
};
|
||||
|
||||
MockCardFilter.FILTERS = Object.freeze({
|
||||
'ALL': 'filter',
|
||||
'TV': 'tv',
|
||||
'DASHBOARD': 'dashboard',
|
||||
'DEVICE': 'device',
|
||||
'APPLICATION': 'application'
|
||||
});
|
||||
|
||||
MockCardFilter.prototype = {
|
||||
start: function() {},
|
||||
stop: function() {},
|
||||
on: function(eventName, callback) {
|
||||
if (typeof callback === 'function') {
|
||||
if (!this.mEventHandlers[eventName]) {
|
||||
this.mEventHandlers[eventName] = [];
|
||||
}
|
||||
this.mEventHandlers[eventName].push(callback);
|
||||
}
|
||||
|
||||
},
|
||||
off: function(eventName, callback) {
|
||||
var handlers = this.mEventHandlers[eventName];
|
||||
if (typeof callback === 'function' && handlers) {
|
||||
var index = handlers.indexOf(callback);
|
||||
handlers.splice(index, 1);
|
||||
}
|
||||
},
|
||||
mFireEvent: function(eventName, detail) {
|
||||
var handlers = this.mEventHandlers[eventName];
|
||||
if (handlers) {
|
||||
handlers.forEach(function(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
callback(detail);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.MockCardFilter = MockCardFilter;
|
||||
}(window));
|
||||
40
gaia/tv_apps/smart-home/test/unit/mock_card_manager.js
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
/* global Promise */
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var MockCardManager = function() {};
|
||||
|
||||
MockCardManager.prototype = {
|
||||
_cardList: [],
|
||||
m_card: {},
|
||||
|
||||
init: function() {
|
||||
return null;
|
||||
},
|
||||
|
||||
mPrepareFilteredCardList: function(filteredCardList) {
|
||||
this.mFilteredCardList = filteredCardList;
|
||||
},
|
||||
|
||||
getFilteredCardList: function(name) {
|
||||
var that = this;
|
||||
return new Promise(function(resolve, reject) {
|
||||
resolve(that.mFilteredCardList);
|
||||
});
|
||||
},
|
||||
|
||||
getCardList: function getCardList() {
|
||||
return Promise.resolve(this._cardList);
|
||||
},
|
||||
|
||||
findCardFromCardList: function(param) {
|
||||
return this.m_card[param.cardId];
|
||||
},
|
||||
|
||||
resolveCardName: function() {
|
||||
return 'cardname';
|
||||
}
|
||||
};
|
||||
|
||||
exports.MockCardManager = MockCardManager;
|
||||
}(window));
|
||||
16
gaia/tv_apps/smart-home/test/unit/mock_edit.js
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
/* globals evt */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var MockEdit = function() {
|
||||
};
|
||||
|
||||
MockEdit.prototype = evt({
|
||||
init: function() {},
|
||||
onMove: function() { return false; },
|
||||
onEnter: function() {return false; }
|
||||
});
|
||||
|
||||
exports.MockEdit = MockEdit;
|
||||
})(window);
|
||||
15
gaia/tv_apps/smart-home/test/unit/mock_filter_manager.js
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var MockFilterManager = function() {};
|
||||
|
||||
MockFilterManager.prototype = {
|
||||
init: function() {
|
||||
return null;
|
||||
},
|
||||
|
||||
resetFilter: function() {}
|
||||
};
|
||||
|
||||
exports.MockFilterManager = MockFilterManager;
|
||||
}(window));
|
||||
15
gaia/tv_apps/smart-home/test/unit/mock_home.js
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var MockHome = function() {};
|
||||
|
||||
MockHome.prototype = {
|
||||
edit: {
|
||||
on: function() {},
|
||||
off: function() {}
|
||||
},
|
||||
createCardNode: function() {}
|
||||
};
|
||||
|
||||
exports.MockHome = MockHome;
|
||||
}(window));
|
||||