Publish Vulpes Preview 15 sources for desktop, Android and Tundra

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

View file

@ -0,0 +1,26 @@
{
"name": "fling-player",
"version": "0.0.0",
"homepage": "https://github.com/mozilla-b2g/gaia/tree/master/tv_apps/fling-player",
"authors": [
"Tzu-Lin Huang <tzhuang@mozilla.com>"
],
"description": "Smart Screen Fling Player",
"main": "index.html",
"license": "MPL",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"gaia-loading": "fxos-components/gaia-loading",
"gaia-theme": "fxos-components/gaia-theme#~0.4.5",
"evt": "fxos-components/evt#~0.4.0",
"smart-button": "fxos-components/smart-button",
"smart-icons": "smart-components/smart-icons"
}
}

View 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"
}

View 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"
]
}

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

View file

@ -0,0 +1,26 @@
{
"name": "gaia-component",
"version": "0.3.4",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "gaia-component.js",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"homepage": "https://github.com/gaia-components/gaia-component",
"_release": "0.3.4",
"_resolution": {
"type": "version",
"tag": "v0.3.4",
"commit": "84f95014d48d88fa8df570dc4ede50dc5be318b6"
},
"_source": "git://github.com/gaia-components/gaia-component.git",
"_target": "~0.3.0",
"_originalSource": "gaia-components/gaia-component"
}

View file

@ -0,0 +1,16 @@
{
"name": "gaia-component",
"version": "0.3.4",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "gaia-component.js",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
]
}

View file

@ -0,0 +1,379 @@
;(function(define){define(function(require,exports,module){
'use strict';
/**
* Locals
*/
var textContent = Object.getOwnPropertyDescriptor(Node.prototype, 'textContent');
var innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
var removeAttribute = Element.prototype.removeAttribute;
var setAttribute = Element.prototype.setAttribute;
var noop = function() {};
/**
* Register a new component.
*
* @param {String} name
* @param {Object} props
* @return {constructor}
* @public
*/
exports.register = function(name, props) {
var baseProto = getBaseProto(props.extends);
// Clean up
delete props.extends;
// Pull out CSS that needs to be in the light-dom
if (props.template) {
var output = processCss(props.template, name);
props.template = document.createElement('template');
props.template.innerHTML = output.template;
props.lightCss = output.lightCss;
props.globalCss = props.globalCss || '';
props.globalCss += output.globalCss;
}
// Inject global CSS into the document,
// and delete as no longer needed
injectGlobalCss(props.globalCss);
delete props.globalCss;
// Merge base getter/setter attributes with the user's,
// then define the property descriptors on the prototype.
var descriptors = Object.assign(props.attrs || {}, base.descriptors);
// Store the orginal descriptors somewhere
// a little more private and delete the original
props._attrs = props.attrs;
delete props.attrs;
// Create the prototype, extended from base and
// define the descriptors directly on the prototype
var proto = createProto(baseProto, props);
Object.defineProperties(proto, descriptors);
// Register the custom-element and return the constructor
return document.registerElement(name, { prototype: proto });
};
var base = {
properties: {
GaiaComponent: true,
attributeChanged: noop,
attached: noop,
detached: noop,
created: noop,
createdCallback: function() {
if (this.rtl) { addDirObserver(); }
injectLightCss(this);
this.created();
},
/**
* It is very common to want to keep object
* properties in-sync with attributes,
* for example:
*
* el.value = 'foo';
* el.setAttribute('value', 'foo');
*
* So we support an object on the prototype
* named 'attrs' to provide a consistent
* way for component authors to define
* these properties. When an attribute
* changes we keep the attr[name]
* up-to-date.
*
* @param {String} name
* @param {String||null} from
* @param {String||null} to
*/
attributeChangedCallback: function(name, from, to) {
var prop = toCamelCase(name);
if (this._attrs && this._attrs[prop]) { this[prop] = to; }
this.attributeChanged(name, from, to);
},
attachedCallback: function() { this.attached(); },
detachedCallback: function() { this.detached(); },
/**
* A convenient method for setting up
* a shadow-root using the defined template.
*
* @return {ShadowRoot}
*/
setupShadowRoot: function() {
if (!this.template) { return; }
var node = document.importNode(this.template.content, true);
this.createShadowRoot().appendChild(node);
return this.shadowRoot;
},
/**
* Sets an attribute internally
* and externally. This is so that
* we can style internal shadow-dom
* content.
*
* @param {String} name
* @param {String} value
*/
setAttr: function(name, value) {
var internal = this.shadowRoot.firstElementChild;
setAttribute.call(internal, name, value);
setAttribute.call(this, name, value);
},
/**
* Removes an attribute internally
* and externally. This is so that
* we can style internal shadow-dom
* content.
*
* @param {String} name
* @param {String} value
*/
removeAttr: function(name) {
var internal = this.shadowRoot.firstElementChild;
removeAttribute.call(internal, name);
removeAttribute.call(this, name);
}
},
descriptors: {
textContent: {
set: function(value) {
textContent.set.call(this, value);
if (this.lightStyle) { this.appendChild(this.lightStyle); }
},
get: textContent.get
},
innerHTML: {
set: function(value) {
innerHTML.set.call(this, value);
if (this.lightStyle) { this.appendChild(this.lightStyle); }
},
get: innerHTML.get
}
}
};
/**
* The default base prototype to use
* when `extends` is undefined.
*
* @type {Object}
*/
var defaultPrototype = createProto(HTMLElement.prototype, base.properties);
/**
* Returns a suitable prototype based
* on the object passed.
*
* @param {HTMLElementPrototype|undefined} proto
* @return {HTMLElementPrototype}
* @private
*/
function getBaseProto(proto) {
if (!proto) { return defaultPrototype; }
proto = proto.prototype || proto;
return !proto.GaiaComponent
? createProto(proto, base.properties)
: proto;
}
/**
* Extends the given proto and mixes
* in the given properties.
*
* @param {Object} proto
* @param {Object} props
* @return {Object}
*/
function createProto(proto, props) {
return Object.assign(Object.create(proto), props);
}
/**
* Detects presence of shadow-dom
* CSS selectors.
*
* @return {Boolean}
*/
var hasShadowCSS = (function() {
var div = document.createElement('div');
try { div.querySelector(':host'); div.querySelector('::content'); return true; }
catch (e) { return false; }
})();
/**
* Regexs used to extract shadow-css
*
* @type {Object}
*/
var regex = {
shadowCss: /(?:\:host|\:\:content)[^{]*\{[^}]*\}/g,
':host': /(?:\:host)/g,
':host()': /\:host\((.+)\)(?: \:\:content)?/g,
':host-context': /\:host-context\((.+)\)([^{,]+)?/g,
'::content': /(?:\:\:content)/g
};
/**
* Extracts the :host and ::content rules
* from the shadow-dom CSS and rewrites
* them to work from the <style scoped>
* injected at the root of the component.
*
* @return {String}
*/
function processCss(template, name) {
var globalCss = '';
var lightCss = '';
if (!hasShadowCSS) {
template = template.replace(regex.shadowCss, function(match) {
var hostContext = regex[':host-context'].exec(match);
if (hostContext) {
globalCss += match
.replace(regex['::content'], '')
.replace(regex[':host-context'], '$1 ' + name + '$2')
.replace(/ +/g, ' '); // excess whitespace
} else {
lightCss += match
.replace(regex[':host()'], name + '$1')
.replace(regex[':host'], name)
.replace(regex['::content'], name);
}
return '';
});
}
return {
template: template,
lightCss: lightCss,
globalCss: globalCss
};
}
/**
* Some CSS rules, such as @keyframes
* and @font-face don't work inside
* scoped or shadow <style>. So we
* have to put them into 'global'
* <style> in the head of the
* document.
*
* @param {String} css
*/
function injectGlobalCss(css) {
if (!css) return;
var style = document.createElement('style');
style.innerHTML = css.trim();
headReady().then(() => {
document.head.appendChild(style)
});
}
/**
* Resolves a promise once document.head is ready.
*
* @private
*/
function headReady() {
return new Promise(resolve => {
if (document.head) { return resolve(); }
window.addEventListener('load', function fn() {
window.removeEventListener('load', fn);
resolve();
});
});
}
/**
* The Gecko platform doesn't yet have
* `::content` or `:host`, selectors,
* without these we are unable to style
* user-content in the light-dom from
* within our shadow-dom style-sheet.
*
* To workaround this, we clone the <style>
* node into the root of the component,
* so our selectors are able to target
* light-dom content.
*
* @private
*/
function injectLightCss(el) {
if (hasShadowCSS) { return; }
el.lightStyle = document.createElement('style');
el.lightStyle.setAttribute('scoped', '');
el.lightStyle.innerHTML = el.lightCss;
el.appendChild(el.lightStyle);
}
/**
* Convert hyphen separated
* string to camel-case.
*
* Example:
*
* toCamelCase('foo-bar'); //=> 'fooBar'
*
* @param {Sring} string
* @return {String}
*/
function toCamelCase(string) {
return string.replace(/-(.)/g, function replacer(string, p1) {
return p1.toUpperCase();
});
}
/**
* Observer (singleton)
*
* @type {MutationObserver|undefined}
*/
var dirObserver;
/**
* Observes the document `dir` (direction)
* attribute and dispatches a global event
* when it changes.
*
* Components can listen to this event and
* make internal changes if need be.
*
* @private
*/
function addDirObserver() {
if (dirObserver) { return; }
dirObserver = new MutationObserver(onChanged);
dirObserver.observe(document.documentElement, {
attributeFilter: ['dir'],
attributes: true
});
function onChanged(mutations) {
document.dispatchEvent(new Event('dirchanged'));
}
}
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-component',this));

View file

@ -0,0 +1,30 @@
{
"name": "gaia-component",
"homepage": "https://github.com/gaia-components/gaia-component",
"license": "ISC",
"main": "gaia-component.js",
"directories": {
"example": "examples",
"test": "test"
},
"devDependencies": {
"bower": "^1.3.5",
"browserify": "^5.11.2",
"karma": "^0.12.16",
"karma-firefox-launcher": "^0.1.3",
"karma-mocha": "^0.1.3",
"karma-sinon-chai": "^0.1.6",
"mozilla-download": "^0.4.3"
},
"scripts": {
"test": "./node_modules/karma/bin/karma start test/karma.conf.js --single-run",
"test-dev": "./node_modules/karma/bin/karma start test/karma.conf.js"
},
"repository": {
"type": "git",
"url": "git://github.com/gaia-components/gaia-component.git"
},
"bugs": {
"url": "https://github.com/gaia-components/gaia-component/issues"
}
}

View file

@ -0,0 +1,30 @@
{
"name": "gaia-loading",
"homepage": "https://github.com/gaia-components/gaia-loading",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "gaia-loading.js",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"README.md"
],
"dependencies": {
"gaia-component": "gaia-components/gaia-component#~0.3.0"
},
"_release": "84a8803886",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "84a880388686523e68172651fce2d570d4499daf"
},
"_source": "git://github.com/gaia-components/gaia-loading.git",
"_target": "*",
"_originalSource": "gaia-components/gaia-loading"
}

View file

@ -0,0 +1,22 @@
{
"name": "gaia-loading",
"version": "0.0.0",
"homepage": "https://github.com/gaia-components/gaia-loading",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"main": "gaia-loading.js",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"README.md"
],
"dependencies": {
"gaia-component": "gaia-components/gaia-component#~0.3.0"
}
}

View file

@ -0,0 +1,82 @@
;(function(define){define(function(require,exports,module){
/*jshint esnext:true*/
'use strict';
/**
* Dependencies
*/
var component = require('gaia-component');
/**
* Exports
*/
module.exports = component.register('gaia-loading', {
created: function() {
this.setupShadowRoot();
},
template: `
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
<style>
/** Host
---------------------------------------------------------*/
:host {
position: relative;
display: block;
width: 40px;
height: 40px;
margin: var(--base-l, 24px) auto;
}
/** Circle
---------------------------------------------------------*/
.circle {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
animation-name: gaia-loading-animation;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0,.64,.46,.46);
animation-duration: 2000ms;
background:
var(--highlight-color);
}
.circle-2 {
animation-delay: 395ms;
}
</style>`,
globalCss: `
@keyframes gaia-loading-animation {
0% {
transform: scale(0);
opacity: 0.9;
}
40% {
transform: scale(1.4);
opacity: 0;
}
100% {
transform: scale(2);
opacity: 0;
}
}`
});
});})(typeof define=='function'&&define.amd?define
:(function(n,w){'use strict';return typeof module=='object'?function(c){
c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){
return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-loading',this));

View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>&lt;gaia-loading&gt;</title>
<meta name="description" content="gaia-loading example" />
<link rel="stylesheet" type="text/css" href="bower_components/gaia-theme/gaia-base.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-theme/gaia-theme.css">
<link rel="stylesheet" type="text/css" href="bower_components/gaia-fonts/style.css">
<script src="bower_components/gaia-theme/lib/gaia-theme-selector.js"></script>
<script src="gaia-loading.js"></script>
<style type="text/css">
body {
padding: 16px;
padding-top: 48px;
}
</style>
</head>
<body role="application">
<gaia-theme-selector></gaia-theme-selector>
<h1 class="large l-align-center l-mb-mega">&lt;gaia-loading&gt;</h1>
<section class="l-align-center">
<gaia-loading></gaia-loading>
</section>
</body>
</html>

View file

@ -0,0 +1,35 @@
{
"name": "gaia-theme",
"version": "0.4.5",
"homepage": "https://github.com/gaia-components/gaia-theme",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"test",
"tests",
"index.html",
"README.md"
],
"devDependencies": {
"gaia-icons": "gaia-components/gaia-icons",
"gaia-header": "gaia-components/gaia-header",
"gaia-button": "gaia-components/gaia-button",
"gaia-switch": "gaia-components/gaia-switch",
"gaia-checkbox": "gaia-components/gaia-checkbox"
},
"_release": "0.4.5",
"_resolution": {
"type": "version",
"tag": "v0.4.5",
"commit": "21def88c4aede2d634889302cd00962e057282af"
},
"_source": "git://github.com/gaia-components/gaia-theme.git",
"_target": "~0.4.5",
"_originalSource": "gaia-components/gaia-theme"
}

View file

@ -0,0 +1,4 @@
{
"strict": false,
"node": true
}

View file

@ -0,0 +1,26 @@
{
"name": "gaia-theme",
"version": "0.4.5",
"homepage": "https://github.com/gaia-components/gaia-theme",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"!.jshintrc",
"node_modules",
"bower_components",
"test",
"tests",
"index.html",
"README.md"
],
"devDependencies": {
"gaia-icons": "gaia-components/gaia-icons",
"gaia-header": "gaia-components/gaia-header",
"gaia-button": "gaia-components/gaia-button",
"gaia-switch": "gaia-components/gaia-switch",
"gaia-checkbox": "gaia-components/gaia-checkbox"
}
}

View file

@ -0,0 +1,234 @@
/*
Gaia theme variables
*/
:root {
/** Grey Colors
---------------------------------------------------------*/
--color-alpha: #333333;
--color-beta: #ffffff;
--color-gamma: #4d4d4d;
--color-delta: #5f5f5f;
--color-epsilon: #858585;
--color-zeta: #a6a6a6;
--color-eta: #c7c7c7;
--color-theta: #e7e7e7;
--color-iota: #f4f4f4;
/** Brand Colors
---------------------------------------------------------*/
--color-darkblue: #00539f;
--color-blue: #00caf2;
--color-turquoise: #27c8c2;
--color-darkorange: #e66000;
--color-orange: #ff9500;
--color-yellow: #ffcb00;
--color-violet: #c40c84;
--color-warning: #fbbd3c;
--color-destructive: #e2443a;
--color-preffered: #00ba91;
/** Background
---------------------------------------------------------*/
--background: var(--color-beta);
--background-plus: var(--color-theta);
--background-minus: var(--color-zeta);
/** Borders
---------------------------------------------------------*/
--border-color: var(--color-theta);
/** Text Color
---------------------------------------------------------*/
--text-color: var(--color-gamma);
--text-color-minus: var(--color-eta);
/** Button
---------------------------------------------------------*/
--button-background: var(--color-iota);
/** Links
---------------------------------------------------------*/
--link-color: var(--highlight-color);
/** Inputs
---------------------------------------------------------*/
--input-background: var(--color-beta);
--input-color: var(--color-alpha);
--input-clear-background: #909ca7;
/** Buttons
---------------------------------------------------------*/
--button-box-shadow:
inset 0 -1px 0 #d8d8d8,
inset 0 1px 0 #eeeeee;
--button-box-shadow-active: var(--button-box-shadow);
/** Header
---------------------------------------------------------*/
--header-disabled-button-color: rgba(255,255,255,0.3);
}
.theme-productivity {
/** Brand Color
---------------------------------------------------------*/
--highlight-color: var(--color-orange);
/** Header
---------------------------------------------------------*/
--header-background: var(--highlight-color);
--header-color: var(--color-beta);
}
.theme-communications {
/** Brand Color
---------------------------------------------------------*/
--highlight-color: var(--color-turquoise);
/** Links
---------------------------------------------------------*/
--link-color: #177874;
/** Header
---------------------------------------------------------*/
--header-background: var(--highlight-color);
--header-color: var(--color-beta);
--header-icon-color: var(--color-beta);
--header-button-color: var(--link-color);
}
.theme-settings {
/** Background
---------------------------------------------------------*/
--background: var(--color-iota);
--background-plus: var(--color-beta);
--background-minus: var(--color-epsilon);
--background-minus-minus: var(--color-zeta);
/** Highlight
---------------------------------------------------------*/
--highlight-color: var(--color-blue);
/** Borders
---------------------------------------------------------*/
--border-color: var(--color-theta);
/** Titles
---------------------------------------------------------*/
--title-color: var(--background-minus);
/** Buttons
---------------------------------------------------------*/
--button-background: var(--background-plus);
/** Switch
---------------------------------------------------------*/
--switch-background: var(--background-minus-minus);
/** Checkbox
---------------------------------------------------------*/
--checkbox-border-color: var(--background-minus-minus);
/** Header
---------------------------------------------------------*/
--header-color: var(--title-color);
--header-background: var(--background);
--header-button-color: var(--highlight-color);
--header-disabled-button-color: var(--color-theta);
--header-action-button-color: var(--title-color);
}
.theme-media {
/** Highlight Color
---------------------------------------------------------*/
--highlight-color: var(--color-blue);
/** Background
---------------------------------------------------------*/
--background: var(--color-alpha);
--background-plus: var(--color-gamma);
--background-minus: #2B2B2B;
--background-minus-minus: #1a1a1a;
/** Text Color
---------------------------------------------------------*/
--text-color: var(--color-beta);
/** Button
---------------------------------------------------------*/
--button-background: var(--background-plus);
/** Header
---------------------------------------------------------*/
--header-background: var(--background);
--header-icon-color: var(--text-color);
--header-button-color: var(--highlight-color);
/** Borders
---------------------------------------------------------*/
--border-color: var(--color-gamma);
/** Inputs
---------------------------------------------------------*/
--input-background: var(--background-plus);
/** Text Input
---------------------------------------------------------*/
--text-input-background: var(--background-minus);
/** Switch
---------------------------------------------------------*/
--switch-head-border-color: var(--background-minus-minus);
--switch-background: var(--background-minus-minus);
/** Checkbox
---------------------------------------------------------*/
--checkbox-border-color: var(--background-minus-minus);
/** Buttons
---------------------------------------------------------*/
--button-box-shadow: none;
--button-box-shadow-active: none;
}

View file

@ -0,0 +1,111 @@
(function(define){'use strict';define(function(require,exports,module){
/*jshint esnext:true*/
/**
* Extend from the `HTMLElement` prototype
*
* @type {Object}
*/
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
this.createShadowRoot().innerHTML = template;
this.els = {
themes: this.shadowRoot.querySelector('.themes'),
rtl: this.shadowRoot.querySelector('.toggle-rtl')
};
this.els.rtl.addEventListener('click', this.onRtlClick.bind(this));
this.els.themes.addEventListener('click', this.onThemeClick.bind(this));
this.styleHack();
this.set(localStorage.theme || 'settings');
};
proto.styleHack = function() {
var style = this.shadowRoot.querySelector('style');
style.setAttribute('scoped', '');
this.appendChild(style.cloneNode(true));
};
proto.onThemeClick = function(e) {
var theme = e.target.value;
if (theme) { this.set(theme); }
};
proto.onRtlClick = function() {
document.dir = this.els.rtl.checked ? 'rtl' : 'ltr';
};
proto.set = function(theme) {
var radio = this.shadowRoot.querySelector('[value="' + theme + '"]');
document.body.classList.remove('theme-' + this.theme);
document.body.classList.add('theme-' + theme);
radio.checked = true;
this.theme = theme;
localStorage.theme = theme;
};
var template = `
<style>
* { margin: 0; padding: 0; }
gaia-theme-selector {
position: fixed;
top: 0; left: 0;
z-index: 101;
width: 100%;
height: 30px;
background: rgba(255,255,255,0.8);
color: #000;
font-size: 9px;
text-align: center;
box-shadow: 0 1px 1px rgba(0,0,0,0.1);
direction: ltr;
}
form {
display: flex;
height: 100%;
align-items: center;
justify-content: center;
}
form label {
-moz-margin-start: 9px;
}
form input {
-moz-margin-end: 6px;
vertical-align: middle;
}
.rtl {
position: absolute;
top: 0;
left: 0;
height: 100%;
display: flex;
align-items: center;
}
</style>
<form>
<label class="rtl"><input type="checkbox" class="toggle-rtl" />RTL</label>
<span class="themes">
<label><input type="radio" name="theme" value="productivity"/>Prod</label>
<label><input type="radio" name="theme" value="communications"/>Comms</label>
<label><input type="radio" name="theme" value="media"/>Media</label>
<label><input type="radio" name="theme" value="settings"/>Settings</label>
</span>
</form>`;
document.registerElement('gaia-theme-selector', { prototype: proto });
});})((function(n,w){'use strict';return typeof define=='function'&&define.amd?
define:typeof module=='object'?function(c){c(require,exports,module);}:
function(c){var m={exports:{}},r=function(n){return w[n];};
w[n]=c(r,m.exports,m)||m.exports;};})('gaia-theme-selector',this));

View file

@ -0,0 +1,25 @@
{
"name": "smart-button",
"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": "a13686c562",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "a13686c5620b996e254764eb9b6993c9555da862"
},
"_source": "git://github.com/gaia-components/smart-button.git",
"_target": "*",
"_originalSource": "gaia-components/smart-button"
}

View file

@ -0,0 +1,17 @@
{
"name": "smart-button",
"version": "0.0.0",
"homepage": "https://github.com/gaia-components/smart-button",
"main": "script.js",
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"LICENSE",
"README.md"
]
}

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

View file

@ -0,0 +1,444 @@
/**
* 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: 9.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]: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
*/

View file

@ -0,0 +1,30 @@
{
"name": "smart-icons",
"version": "0.0.3",
"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.3",
"_resolution": {
"type": "version",
"tag": "v0.0.3",
"commit": "0c335ac23f484069b4fafa7b7e5afd9f3c08d66e"
},
"_source": "git://github.com/smart-components/smart-icons.git",
"_target": "*",
"_originalSource": "smart-components/smart-icons"
}

View file

@ -0,0 +1,21 @@
{
"name": "smart-icons",
"version": "0.0.3",
"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"
]
}

File diff suppressed because one or more lines are too long

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

View file

@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<title>Fling Player</title>
<!-- stylesheets of 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="style/fling_player.css">
<!-- 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="bower_components/evt/index.js"></script>
<!-- building block -->
<script defer src="bower_components/gaia-component/gaia-component.js"></script>
<script defer src="bower_components/gaia-loading/gaia-loading.js"></script>
<script defer src="bower_components/smart-button/script.js"></script>
<!-- Helper libraries -->
<script defer src="shared/js/smart-screen/simple_key_navigation.js"></script>
<script defer src="shared/js/smart-screen/key_navigation_adapter.js"></script>
<!-- main javascript code -->
<script defer src="js/master_debug.js"></script>
<script defer src="js/casting_message.js"></script>
<script defer src="js/video_player.js"></script>
<script defer src="js/connector.js"></script>
<script defer src="js/fling_player.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/fling-player.{locale}.properties"/>
</head>
<body class="theme-media">
<article class="skin-dark">
<section role="region" id="video-player-section">
<video id="player" preload="metadata"></video>
<nav id="video-control-panel" class="control-panel fade-out">
<div id="video-control-buttons">
<smart-button id="backward-button" data-icon="fling-player-rewind"></smart-button>
<smart-button id="play-button" data-icon="fling-player-play"></smart-button>
<smart-button id="forward-button" data-icon="fling-player-forward"></smart-button>
</div>
<div id="video-time-progress">
<div id="elapsed-time" class="time-text">00:00</div>
<div id="time-progress">
<div id="background-time-bar" class="time-bar"></div>
<div id="buffered-time-bar" class="time-bar"></div>
<div id="elapsed-time-bar" class="time-bar"></div>
</div>
<div id="duration-time" class="time-text">00:00</div>
</div>
</nav>
</section>
<section role="region" id="loading-section">
<gaia-loading></gaia-loading>
</section>
<section id="initial-message-section" class="fade-out" data-l10n-id="playing-video-sent-from-device"></section>
</article>
</body>
</html>

View file

@ -0,0 +1,220 @@
/* global mDBG */
(function(exports) {
'use strict';
/**
* This object parses and process the message casted
* between receiver and controller.
*
* The message casted is in JSON and shall carry properties below:
* - seq: Number. The sequence of message sent. Increased by 1 when sending
* one new message. The message sequences from controller and receiver
* shall be independent to each other.
* - type: String. Indicate the message type.
* - other extra properties depending on the type
*
* For example, the message of loading request from a controller may be like:
* {
* 'seq': 3,
* 'type': 'load',
* 'url': 'http://www.example.com/foo.ogg'
* }
*/
var castingMessage = {};
castingMessage.type = Object.freeze({
/**
* Represent the device info.
* The extra message properties:
* - displayName: String. The device display name
*/
'device-info': {
name: 'device-info',
sanitizeMsg: function (msg) {
if (msg.displayName !== undefined &&
typeof msg.displayName != 'string'
) {
throw new Error('Ilegal display name = ' + msg.displayName +
' in casting message of type = ' + this.name);
}
return msg;
}
},
/**
* Represent acknowledgement of message receiving.
* The extra message properties:
* - error: String. Present if there is error
*/
ack: {
name: 'ack',
sanitizeMsg: function (msg) {
if (msg.error !== null &&
msg.error !== undefined &&
typeof msg.error != 'string'
) {
throw new Error('Ilegal error = ' + msg.error +
' in casting message of type = ' + this.name);
}
return msg;
}
},
/**
* Represent video playing status. The extra message properties:
* - time: Number. The current video time
* - status: String. Describe status, could be
* - buffering: Video buffering
* - loaded: The media's metadata has finished loading;
* all attributes now contain as much useful information
* as they're going to.
* - buffered: Video buffered and ready to play
* - playing: Video is playing
* - seeked: Video seeking is done
* - stopped: Video is not playing (maybe ended or paused).
* - error: Error on playing.
* When error occurs, one more extra message properties:
* - error: String. The error code.
*/
status: {
name: 'status',
sanitizeMsg: function (msg) {
if (typeof msg.time != 'number' || (msg.time >= 0) === false) {
throw new Error('Ilegal time = ' + msg.time +
' in casting message of type = ' + this.name);
}
var validStatus = [
'buffering', 'loaded', 'buffered',
'playing', 'seeked', 'stopped', 'error'
];
if (validStatus.indexOf(msg.status) < 0) {
throw new Error('Ilegal status = ' + msg.status +
' in casting message of type = ' + this.name);
}
if (msg.status == 'error' && typeof msg.error != 'string') {
throw new Error('Ilegal error = ' + msg.error +
' in casting message of type = ' + this.name);
}
return msg;
}
},
/**
* Request to load and play video. The extra message properties:
* - url : String. The video url.
*/
load: {
name: 'load',
sanitizeMsg: function (msg) {
if (typeof msg.url != 'string' || !msg.url) {
throw new Error('Ilegal url = ' + msg.url +
' in casting message of type = ' + this.name);
}
return msg;
}
},
/**
* Request to play video
*/
play: {
name: 'play'
},
/**
* Request to pause video
*/
pause: {
name: 'pause'
},
/**
* Request to seek on video. The extra message properties:
* - time: Number. The time to seek.
*/
seek: {
name: 'seek',
sanitizeMsg: function (msg) {
if (typeof msg.time != 'number' || (msg.time >= 0) === false) {
throw new Error('Ilegal time in casting message of type = ' +
this.name, msg.time);
}
return msg;
}
}
});
/**
* Validate and make sure the message format, datatype.
*
* @param {Object} content the message content to sanitize
* @return {Object} Sanitized message
*/
castingMessage.sanitizeMsg = function (content) {
mDBG.log('castingMessage#sanitizeMsg');
mDBG.log('Sanitize message content:', content);
if (this.type[content.type]) {
if (typeof content.seq == 'number' && content.seq >= 0) {
if (typeof this.type[content.type].sanitizeMsg == 'function') {
content = this.type[content.type].sanitizeMsg(content);
}
} else {
throw new Error('Missing sequence of casting message : ' +
JSON.stringify(content));
}
} else {
throw new Error('Unknown type of casting message : ' + content.type);
}
return content;
};
/**
* @param {String} txt the message txt sent from controller
* @return {Array.<Object>} the parsed messages
*/
castingMessage.parse = function (txt) {
mDBG.log('castingMessage#parse');
mDBG.log('Parsing : ', txt);
var data = '[' + txt.replace(/}{/g, '},{') + ']';
mDBG.log('Transformed : ', data);
var msgs = JSON.parse(data);
mDBG.log('JSON Parsed : ', msgs);
var results = [];
msgs.forEach((m) => {
try {
results.push(this.sanitizeMsg(m));
} catch (e) {
mDBG.error(e);
}
});
mDBG.log('Sanitized : ', results);
return results;
};
/**
* @param {Object} content the message content being sent.
* @return {String} the messsage txt to sent
*/
castingMessage.stringify = function (content) {
return JSON.stringify(this.sanitizeMsg(content));
};
exports.castingMessage = castingMessage;
})(window);

View file

@ -0,0 +1,216 @@
/* global evt, castingMessage, mDBG */
(function(exports) {
'use strict';
/**
* This class handles the connection to controller via the presentation API
* The events of controller's message are:
* - loadRequest: Triggered when controller asks to load video.
* Will be passed in one object with url property storing the
* video url.
* - playRequest: Triggered when controller asks to play video.
* - pauseRequest: Triggered when controller asks to pause video.
* - seekRequest: Triggered when controller asks to seek video.
* Will be passed in one object with time property storing the
* seeking time.
*
* @param {object} presentation The navigator's presentation object
* @constructor
*/
function Connector(presentation) {
this._presentation = presentation;
this._msgSeq = 0; // This sequence of message sent to controller
this._lastSeq = -1; // The sequence of the last message received
this._isInit = false;
this._isInitConnection = false;
this._controllingDeviceInfo = null;
}
var proto = evt(Connector.prototype);
proto.init = function () {
if (this._isInit) {
return;
}
mDBG.log('Connector#init');
if (!this._presentation) {
throw new Error('Init connection without the presentation object.');
}
this._isInit = true;
this._presentation.receiver.getConnection().then(
this._initConnection.bind(this)
);
this._presentation.receiver.onconnectionavailable = (e) => {
this._presentation.receiver.getConnection().then(
this._initConnection.bind(this)
);
};
};
proto._initConnection = function (connection) {
if (this._isInitConnection) {
return;
}
mDBG.log('Connector#_initConnection');
if (!this._presentation) {
throw new Error('Init connection without the presentation object.');
}
this._isInitConnection = true;
mDBG.log('this._connection = ', connection);
this._connection = connection;
this._connection.onmessage = this.onConnectionMessage.bind(this);
this._connection.onstatechange = this.onConnectionStateChange.bind(this);
};
proto.sendMsg = function (msg) {
mDBG.log('Connector#sendMsg');
mDBG.log('msg = ', msg);
try {
var txt = castingMessage.stringify(msg);
this._connection.send(txt);
} catch (e) {
mDBG.error(e);
}
};
proto.getControllingDeviceInfo = function () {
return new Promise((resolve, reject) => {
if (this._controllingDeviceInfo) {
resolve(this._controllingDeviceInfo);
} else {
var handle = () => {
this.off('ControllingDeviceInfoUpdated', handle);
resolve(this._controllingDeviceInfo);
};
this.on('ControllingDeviceInfoUpdated', handle);
}
});
};
proto.replyACK = function (msg, error) {
mDBG.log('Connector#replyACK');
var reply = {
'type': 'ack',
'seq': msg.seq
};
if (error) {
reply.error = '' + error;
}
this.sendMsg(reply);
};
proto.reportStatus = function (status, data) {
mDBG.log('Connector#reportStatus');
var msg = {
'type': 'status',
'seq': this._msgSeq++,
'status': status,
'time': data.time
};
if (data.error) {
msg.error = '' + data.error;
}
if (data.detail) { // TODO: Discuss should we need this ?
msg.detail = data.detail;
}
this.sendMsg(msg);
};
proto.handleRemoteMessage = function (msg) {
mDBG.log('Connector#handleRemoteMessage');
mDBG.log('msg = ', msg);
var err;
try {
// We don't process the out of dated message.
if (this._lastSeq >= msg.seq) {
throw new Error('Receive outdated message with ' +
'msg sequence = ' + msg.seq);
}
this._lastSeq = msg.seq;
switch(msg.type) {
case 'load':
if (typeof msg.url != 'string' && !msg.url) {
throw new Error('Controller dose not provide the url to load.');
}
this.fire('loadRequest', { url : msg.url });
break;
case 'play':
this.fire('playRequest');
break;
case 'pause':
this.fire('pauseRequest');
break;
case 'seek':
var time = +msg.time;
if (time <= 0) {
throw new Error('Controller asks to seek on invalid time = ' +
time);
}
this.fire('seekRequest', { time : time });
break;
case 'device-info':
this._controllingDeviceInfo = {
displayName: msg.displayName
};
this.fire('ControllingDeviceInfoUpdated');
break;
}
} catch (e) {
err = e;
mDBG.error(e);
}
this.replyACK(msg, err);
};
proto.onConnectionMessage = function (e) {
mDBG.log('Connector#onConnectionMessage');
var messages = castingMessage.parse(e.data);
mDBG.log('messages = ', messages);
messages.sort((a, b) => { // Make sure message sequence
return a.seq - b.seq;
});
messages.forEach(message => this.handleRemoteMessage(message));
};
proto.onConnectionStateChange = function () {
mDBG.log('Connector#onConnectionStateChange');
mDBG.log('State = ', this._connection.state);
// TODO: How to do when presentation session is closed or terminated
};
exports.Connector = Connector;
})(window);

View file

@ -0,0 +1,600 @@
/* global VideoPlayer, Connector, mDBG, KeyNavigationAdapter,
SimpleKeyNavigation
*/
(function(exports) {
'use strict';
// Helping variables, methods
var uiID = {
player : 'player',
loadingUI : 'loading-section',
initialMsgSection : 'initial-message-section',
controlPanel : 'video-control-panel',
backwardButton : 'backward-button',
playButton : 'play-button',
forwardButton : 'forward-button',
bufferedTimeBar : 'buffered-time-bar',
elapsedTimeBar : 'elapsed-time-bar',
elapsedTime : 'elapsed-time',
durationTime : 'duration-time'
};
function $(id) {
return document.getElementById(id);
}
// Helping variables, methods end
/**
* FlingPlayer could be controlled by 2 sources.
* The 1st source is remote control device, like Fennec,
* which sends command through the Presentation API.
* The 2nd source is TV remote controller which sends command through
* UI navigation by pressing TV remote controller's key
*/
function FlingPlayer(videoPlayer, connector) {
this._player = videoPlayer;
this._connector = connector;
}
var proto = FlingPlayer.prototype;
proto.MAX_DISPLAYED_VIDEO_TIME_SEC = 3600 * 99 + 60 * 59 + 59;
proto.CONTROL_PANEL_HIDE_DELAY_MS = 3000;
proto.SEEK_ON_KEY_PRESS_INTERVAL_MS = 150;
proto.SEEK_ON_LONG_KEY_PRESS_MS = 3000;
proto.SEEK_ON_EXTRA_LONG_KEY_PRESS_MS = 6000;
proto.SEEK_ON_KEY_PRESS_NORMAL_STEP_SEC = 5;
proto.SEEK_ON_KEY_PRESS_LARGE_STEP_SEC = 10;
proto.SEEK_ON_KEY_PRESS_EXTRA_LARGE_STEP_SEC = 30;
proto.INITIAL_MSG_MIN_DISPLAY_TIME_MS = 2000;
proto.REFOCUS_PLAY_BUTTON_AFTER_HIDDING_SEC = 60000;
proto.init = function () {
this._autoPlayOnSeeked = true;
this._seekOnKeyPressTimer = null;
this._seekOnKeyPressDirection = null; // 'backward' or 'forward'
this._seekOnKeyPressStartTime = null; // in ms
this._hideControlsTimer = null;
this._initialMsgStartTime = null; // in ms
this._loadingUI = $(uiID.loadingUI);
this._initialMsgSection = $(uiID.initialMsgSection);
this._controlPanel = $(uiID.controlPanel);
this._backwardButton = $(uiID.backwardButton);
this._playButton = $(uiID.playButton);
this._forwardButton = $(uiID.forwardButton);
this._bufferedTimeBar = $(uiID.bufferedTimeBar);
this._elapsedTimeBar = $(uiID.elapsedTimeBar);
this._elapsedTime = $(uiID.elapsedTime);
this._durationTime = $(uiID.durationTime);
this._keyNav = new SimpleKeyNavigation();
this._keyNav.start(
[this._backwardButton, this._playButton, this._forwardButton],
SimpleKeyNavigation.DIRECTION.HORIZONTAL
);
this._keyNav.focusOn(this._playButton);
this._keyNav.pause();
this._keyNavAdapter = new KeyNavigationAdapter();
this._keyNavAdapter.init();
this._keyNavAdapter.on('enter', this.onKeyEnterDown.bind(this));
this._keyNavAdapter.on('enter-keyup', this.onKeyEnterUp.bind(this));
this._keyNavAdapter.on('esc-keyup', this.onBackKeyUp.bind(this));
this._keyNavAdapter.on('move-keyup',
this.onDemandingControlPanel.bind(this)
);
this._keyNavAdapter.on('enter-keyup',
this.onDemandingControlPanel.bind(this)
);
document.addEventListener('visibilitychange', () => {
// We don't need to restore the video while visibilityState goes back
// because system app will kill the original one and relaunch a new one.
if (document.visibilityState === 'hidden') {
this._player.release();
}
});
this._initPlayer();
this._initSession();
};
proto._initSession = function () {
this._connector.init();
this._connector.on('loadRequest', this.onLoadRequest.bind(this));
this._connector.on('playRequest', this.onPlayRequest.bind(this));
this._connector.on('seekRequest', this.onSeekRequest.bind(this));
this._connector.on('pauseRequest', this.onPauseRequest.bind(this));
};
proto._initPlayer = function () {
var video = this._player.getVideo();
this._player.init();
video.addEventListener('loadedmetadata', this);
video.addEventListener('durationchange', this);
video.addEventListener('timeupdate', this);
video.addEventListener('waiting', this);
video.addEventListener('playing', this);
video.addEventListener('seeked', this);
video.addEventListener('pause', this);
video.addEventListener('ended', this);
video.addEventListener('error', this);
};
// UI handling
proto.resetUI = function () {
this.moveTimeBar('elapsed', 0);
this.moveTimeBar('buffered', 0);
this.writeTimeInfo('elapsed', 0);
this.writeTimeInfo('duration', 0);
this.setPlayButtonState('paused');
this._keyNav.focusOn(this._playButton);
};
proto.showLoading = function (loading) {
this._loadingUI.hidden = !loading;
};
/**
* @param {boolean} on True to toggle on and false to toggle off
*/
proto.toggleInitialMessage = function (on, opt = {}) {
if (on) {
this._initialMsgStartTime = (new Date()).getTime();
this._initialMsgSection.classList.remove('fade-out');
} else {
if (opt.delayHiding) {
// Make sure that the initial message has beed displayed
// for at least the min duration
var displayDuration = (new Date()).getTime() -
this._initialMsgStartTime;
var gap = this.INITIAL_MSG_MIN_DISPLAY_TIME_MS - displayDuration;
if (gap < 0) {
gap = 0;
}
setTimeout(() => this.toggleInitialMessage(false), gap);
} else {
this._initialMsgSection.classList.add('fade-out');
}
}
};
/**
* @param {String} state 'playing' or 'paused'
*/
proto.setPlayButtonState = function (state) {
switch (state) {
case 'playing':
this._playButton.setAttribute('data-icon', 'fling-player-pause');
break;
case 'paused':
this._playButton.setAttribute('data-icon', 'fling-player-play');
break;
}
};
proto.isControlPanelHiding = function () {
return this._controlPanel.classList.contains('fade-out');
};
/**
* @param {Boolean} autoHide? Auto hide the controls later. Default to false
*/
proto.showControlPanel = function (autoHide) {
if (this._hideControlsTimer) {
clearTimeout(this._hideControlsTimer);
this._hideControlsTimer = null;
}
this._keyNav.resume();
this._controlPanel.classList.remove('fade-out');
if (autoHide === true) {
this.hideControlPanel();
}
};
/**
* @param {Boolean} immediate? Hide immediately or later. Default to false.
*/
proto.hideControlPanel = function (immediate) {
if (this._hideControlsTimer) {
clearTimeout(this._hideControlsTimer);
this._hideControlsTimer = null;
}
if (immediate === true) {
if (!this.isControlPanelHiding()) {
this._keyNav.pause();
this._controlPanel.classList.add('fade-out');
setTimeout(() => {
if (this.isControlPanelHiding()) {
this._keyNav.focusOn(this._playButton);
}
}, this.REFOCUS_PLAY_BUTTON_AFTER_HIDDING_SEC);
}
} else {
this._hideControlsTimer = setTimeout(() => {
this.hideControlPanel(true);
}, this.CONTROL_PANEL_HIDE_DELAY_MS);
}
};
/**
* @param {string} type 'buffered' or 'elapsed'
* @param {number} sec the sec in video you want to move to
*/
proto.moveTimeBar = function (type, sec) {
mDBG.log('FlingPlayer#moveTimeBar');
mDBG.log('Move type ', type);
mDBG.log('Move to ', sec);
var timeBar = this[`_${type}TimeBar`];
var duration = this._player.getRoundedDuration();
sec = Math.round(sec);
if (!timeBar ||
typeof sec != 'number' ||
isNaN(sec) ||
sec < 0 ||
sec > duration
) {
mDBG.warn('Not moving due to corrupt type/sec', type, sec);
return;
}
requestAnimationFrame(() => {
mDBG.log('Move to sec / duration = %d / %d', sec, duration);
timeBar.style.width = (100 * sec / duration) + '%';
});
};
/**
* @param {string} type 'elapsed' or 'duration'
* @param {number} sec
*/
proto.writeTimeInfo = function (type, sec) {
var timeInfo = this[`_${type}Time`];
var duration = Math.min(this._player.getRoundedDuration(),
this.MAX_DISPLAYED_VIDEO_TIME_SEC);
sec = Math.min(Math.round(sec), this.MAX_DISPLAYED_VIDEO_TIME_SEC);
if (!timeInfo ||
(sec >= 0) === false ||
(sec <= duration) === false
) {
return;
}
var t = this._player.parseTime(sec);
t.hh = (t.hh <= 0) ? '' :
(t.hh < 10) ? '0' + t.hh + ':' : t.hh + ':';
t.mm = (t.mm < 10) ? '0' + t.mm + ':' : t.mm + ':';
t.ss = (t.ss < 10) ? '0' + t.ss : t.ss;
timeInfo.textContent = t.hh + t.mm + t.ss;
};
// UI handling end
// Video handling
proto.play = function () {
this._player.play();
this.setPlayButtonState('playing');
};
proto.pause = function () {
this._player.pause();
this.setPlayButtonState('paused');
};
proto._updateControlPanel = function () {
var buf = this._player.getVideo().buffered;
var current = this._player.getRoundedCurrentTime();
this.writeTimeInfo('elapsed', current);
this.moveTimeBar('elapsed', current);
for (var bufEnd, bufStart, i = 0; i < buf.length; ++i ) {
bufEnd = buf.end(i);
bufStart = buf.start(i);
if (bufStart <= current && current <= bufEnd) {
this.moveTimeBar('buffered', bufEnd);
break;
}
}
};
proto._startSeekOnKeyPress = function (dir) {
if (this._seekOnKeyPressStartTime == null) { // Do not double start
this._seekOnKeyPressStartTime = (new Date()).getTime();
this._seekOnKeyPressDirection = dir;
this._seekOnKeyPress();
}
};
proto._stopSeekOnKeyPress = function () {
clearTimeout(this._seekOnKeyPressTimer);
this._seekOnKeyPressDirection = null;
this._seekOnKeyPressStartTime = null;
this._seekOnKeyPressTimer = null;
};
/**
* This is to handle this case that user seeks on video by long pressing key.
* The seeking policy would go based on duration of pressing
*/
proto._seekOnKeyPress = function () {
if (this._seekOnKeyPressStartTime != null) {
var time = this._player.getRoundedCurrentTime();
var factor = (this._seekOnKeyPressDirection == 'backward') ? -1 : 1;
var seekDuration = (new Date()).getTime() - this._seekOnKeyPressStartTime;
var seekStep;
if (seekDuration <= this.SEEK_ON_LONG_KEY_PRESS_MS) {
seekStep = this.SEEK_ON_KEY_PRESS_NORMAL_STEP_SEC;
} else if (seekDuration > this.SEEK_ON_LONG_KEY_PRESS_MS &&
seekDuration <= this.SEEK_ON_EXTRA_LONG_KEY_PRESS_MS) {
seekStep = this.SEEK_ON_KEY_PRESS_LARGE_STEP_SEC;
} else {
seekStep = this.SEEK_ON_KEY_PRESS_EXTRA_LARGE_STEP_SEC;
}
time += factor * seekStep;
time = Math.min(Math.max(0, time), this._player.getRoundedDuration());
this.seek(time);
this._seekOnKeyPressTimer = setTimeout(
this._seekOnKeyPress.bind(this),
this.SEEK_ON_KEY_PRESS_INTERVAL_MS
);
}
};
proto.seek = function (sec, opt) {
if (!(opt instanceof Object)) {
opt = {
autoPlayOnSeeked: true,
autoHideControlPanel: true
};
}
this._player.seek(sec);
this.moveTimeBar('elapsed', sec);
this.writeTimeInfo('elapsed', sec);
this._autoPlayOnSeeked = !!opt.autoPlayOnSeeked;
this.showControlPanel(!!opt.autoHideControlPanel);
};
// Video handling end
// Event handling
proto.handleEvent = function handleVideoEvent(e) {
mDBG.log('FlingPlayer#handleEvent: e.type = ' + e.type);
var data = { 'time': this._player.getRoundedCurrentTime() };
switch (e.type) {
case 'loadedmetadata':
this.writeTimeInfo('elapsed', this._player.getRoundedCurrentTime());
this.writeTimeInfo('duration', this._player.getRoundedDuration());
this._connector.reportStatus('loaded', data);
break;
// XXX: Bug 1238862.
// On real TV, decoder would provide NaN duration at the loadedmetadata
// event, so duration of 00:00 would be displayed.
// However, valid duration would be available at the durationchange
// event afterwards.
// So we listen to the durationchange and display valid duration info
// to workaround this NaN duration issue,
case 'durationchange':
this.writeTimeInfo('duration', this._player.getRoundedDuration());
break;
case 'timeupdate':
this._updateControlPanel();
break;
case 'waiting':
this.showLoading(true);
this._connector.reportStatus('buffering', data);
break;
case 'playing':
this.showLoading(false);
this.hideControlPanel();
this.toggleInitialMessage(false, { delayHiding: true });
this._connector.reportStatus('buffered', data);
this._connector.reportStatus('playing', data);
break;
case 'seeked':
if (this._autoPlayOnSeeked) {
this.play();
}
this._connector.reportStatus('seeked', data);
break;
case 'pause':
this.showControlPanel();
this._connector.reportStatus('stopped', data);
break;
case 'ended':
// Stop updating the control panel and prevent from showing loading UI.
// Restore these actions once back to the very 1st frame.
var handle = () => {
this._player.getVideo().addEventListener('waiting', this);
this._player.getVideo().addEventListener('timeupdate', this);
this._player.getVideo().removeEventListener('seeked', handle);
};
this._player.getVideo().addEventListener('seeked', handle);
this._player.getVideo().removeEventListener('waiting', this);
this._player.getVideo().removeEventListener('timeupdate', this);
// Go back to the very 1st frame on ended and reset the control panel
this.resetUI();
this.writeTimeInfo('duration', this._player.getRoundedDuration());
this.seek(0, {
autoPlayOnSeeked: false,
autoHideControlPanel: false
});
this._connector.reportStatus('stopped', data);
break;
case 'error':
this.showLoading(false);
data.error = e.target.error.code;
this._connector.reportStatus('error', data);
break;
}
};
proto.onLoadRequest = function (e) {
this.resetUI();
this.showLoading(true);
this._player.load(e.url);
this.play();
this._connector.getControllingDeviceInfo().then(info => {
var msg = this._initialMsgSection.textContent;
var name = 'device';
if (info.displayName && typeof info.displayName == 'string') {
name = info.displayName;
}
this._initialMsgSection.textContent =
msg.replace('{{ device }}', name);
this.toggleInitialMessage(true);
if (this._player.isPlaying()) {
this.toggleInitialMessage(false, { delayHiding: true });
}
});
};
proto.onPlayRequest = function () {
this.play();
};
proto.onPauseRequest = function () {
this.pause();
};
proto.onSeekRequest = function (e) {
this.seek(e.time);
};
proto.onKeyEnterDown = function () {
mDBG.log('FlingPlayer#onKeyEnterDown');
if (this.isControlPanelHiding()) {
mDBG.log('The control panel is hiding so no action is taken.');
return;
}
var focused = this._keyNav.getFocusedElement();
if (focused) {
mDBG.log('control focused = ', focused);
switch (focused.id) {
case uiID.backwardButton:
case uiID.forwardButton:
if (focused === this._backwardButton) {
this._startSeekOnKeyPress('backward');
} else {
this._startSeekOnKeyPress('forward');
}
break;
}
}
};
proto.onKeyEnterUp = function () {
mDBG.log('FlingPlayer#onKeyEnterUp');
if (this.isControlPanelHiding()) {
mDBG.log('The control panel is hiding so no action is taken.');
return;
}
var focused = this._keyNav.getFocusedElement();
if (focused) {
mDBG.log('control focused = ', focused);
switch (focused.id) {
case uiID.playButton:
if (this._player.isPlaying()) {
this.pause();
} else {
this.play();
}
break;
case uiID.backwardButton:
case uiID.forwardButton:
this._stopSeekOnKeyPress();
break;
}
}
};
proto.onBackKeyUp = function () {
navigator.mozL10n.formatValue('want-to-end')
.then((txt) => {
var end = window.confirm(txt);
if (end) {
this._connector.reportStatus('stopped', {
'time': this._player.getRoundedCurrentTime() });
window.close();
}
});
};
proto.onDemandingControlPanel = function () {
mDBG.log('FlingPlayer#onDemandingControlPanel');
this.showControlPanel(true);
};
// Event handling end
window.addEventListener('load', function() {
window.fp = new FlingPlayer(
new VideoPlayer($(uiID.player)),
new Connector(navigator.presentation)
);
window.fp.init();
if (document.visibilityState === 'hidden') {
navigator.mozApps.getSelf().onsuccess = function(evt) {
var app = evt.target.result;
if (app) {
app.launch();
}
};
}
});
exports.FlingPlayer = FlingPlayer;
})(window);

View file

@ -0,0 +1,65 @@
(function(exports) {
'use strict';
var proto;
/**
* This is the local debug mode controller.
* @class
* @constructor
*/
function MasterDebug(dbg) {
/**
* the local debug mode flag
* @type {boolean}
*/
this._flag_dbg = false;
this.setDBG(dbg);
}
proto = MasterDebug.prototype;
proto.isDBG = function () {
return this._flag_dbg;
};
proto.setDBG = function (dbg) {
this._flag_dbg = !!dbg;
};
proto.log = function (var_args) {
if (this.isDBG()) {
return console.log.apply(console, Array.prototype.slice.call(arguments));
}
};
proto.warn = function (var_args) {
if (this.isDBG()) {
return console.warn.apply(console, Array.prototype.slice.call(arguments));
}
};
proto.error = function (var_args) {
if (this.isDBG()) {
return console.error.apply(
console,
Array.prototype.slice.call(arguments)
);
}
};
/**
* Run provided testing behavior in the debug mode
* @param {function} behavior a callback executing testing behavior
*/
proto.test = function (behavior) {
if (this.isDBG() && typeof behavior === 'function') {
behavior();
}
};
window.mDBG = new MasterDebug(false);
})(window);

View file

@ -0,0 +1,125 @@
(function(exports) {
'use strict';
/**
* This class is a thin wrapper of HTMLVideoElement
* @param {HTMLVideoElement}
*/
function VideoPlayer(video) {
this._video = video;
}
var proto = VideoPlayer.prototype;
proto.init = function vp_init() {
this._video.mozAudioChannelType = 'content';
this._isLoaded = false;
this._video.addEventListener('loadedmetadata', this);
this._video.addEventListener('playing', this);
};
proto.getVideo = function () {
return this._video;
};
/**
* @return {Number} The rounded video duration in sec.
* Please read the video's duration property if
* require accurate to the decimal place.
*/
proto.getRoundedDuration = function () {
var s = Math.round(this._video.duration);
return isNaN(s) ? 0 : s;
};
/**
* @return {Number} The rounded current video time in sec.
* Please read the video's currentTime property if
* require accurate to the decimal place.
*/
proto.getRoundedCurrentTime = function () {
var s = Math.round(this._video.currentTime);
return isNaN(s) ? 0 : s;
};
proto.show = function () {
this._video.hidden = false;
};
proto.hide = function () {
this._video.hidden = true;
};
proto.load = function (url) {
this._video.src = url;
this._video.load();
};
proto.release = function () {
this._video.pause();
this._video.removeAttribute('src');
this._video.load();
};
proto.play = function () {
this._video.play();
};
proto.pause = function () {
this._video.pause();
};
proto.seek = function (t) {
if (!this._isLoaded) {
return;
}
this._video.currentTime = t;
};
proto.isPlaying = function () {
return !this._video.paused && !this._video.ended;
};
/**
* @param {Number} sec The time length in sec
* @return {Object} One object carrys the pared time value:
* - hh : hour
* - mm : minute
* - ss : sec
*/
proto.parseTime = function (sec) {
var t = {},
s = Math.round(sec);
if ((s > 0) === false) {
s = 0;
}
t.ss = s % 3600 % 60;
s -= t.ss;
t.mm = (s % 3600) / 60;
s -= t.mm * 60;
t.hh = s / 3600;
return t;
};
proto.handleEvent = function (evt) {
switch(evt.type) {
case 'loadedmetadata':
this._isLoaded = true;
break;
case 'playing':
this.show();
break;
}
};
exports.VideoPlayer = VideoPlayer;
})(window);

View file

@ -0,0 +1,2 @@
playing-video-sent-from-device=Playing video sent from {{ device }}
want-to-end=Do you want to end video playback?

View file

@ -0,0 +1,27 @@
{
"name": "Fling Player",
"description": "Smart Screen Fling Player",
"launch_path": "/index.html",
"type": "certified",
"role": "system",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"locales": {
"en-US": {
"name": "Fling Player",
"description": "Smart Screen Fling Player"
}
},
"default_locale": "en-US",
"icons": {
"90": "/style/icons/appic_mwc2015_90.png",
"135": "/style/icons/appic_mwc2015_135.png"
},
"permissions": {
"presentation":{},
"audio-channel-content":{},
"moz-extremely-unstable-and-will-change-webcomponents":{}
}
}

View file

@ -0,0 +1,186 @@
/**
External
*/
@import url("../bower_components/gaia-theme/gaia-theme.css");
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: black;
}
a {
outline: 0;
}
a:hover, a:active, a:focus {
outline: 0;
}
article, section, video {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#loading-section {
text-align: center;
top: calc(50% - 20rem);
left: calc(50% - 15rem);
height: 30rem;
width: 30rem;
}
#loading-section span{
color: #fff;
line-height: 5rem;
font-size: 2.4rem;
font-style: italic;
display: inline-block;
}
#initial-message-section {
width: auto;
height: 11rem;
line-height: 11rem;
border-radius: 5.5rem;
padding: 0 6rem;
top: auto;
bottom: 30rem;
left: 50%;
margin-right: -50%;
transform: translate(-50%, 0);
color: #fff;
font-size: 5rem;
text-align: center;
background-color: rgba(0, 0, 0, 0.5);
opacity: 1;
transition: opacity 1s ease;
}
#initial-message-section.fade-out {
opacity: 0;
}
.control-panel {
position: absolute;
left: 19.5rem;
right: 19.5rem;
bottom: 4rem;
opacity: 1;
transition: all 1s ease;
}
.control-panel.fade-out {
opacity: 0;
}
#video-time-progress {
height: 8rem;
border-radius: 4rem;
left: 34.8rem;
right: 0;
top: 0;
position: absolute;
display: flex;
flex-direction: row;
background-color: rgba(0, 0, 0, 0.5);
}
#time-progress {
height: 8rem;
left: 0;
right: 0;
flex-grow: 1;
}
.time-bar {
width: 100%;
height: 0.6rem;
background: #6d7177;
top: 4rem;
position: relative;
transform: translate(0, -50%);
}
#elapsed-time-bar {
width: 0;
top: 2.8rem;
background: #71c9f0;
}
#buffered-time-bar {
width: 0;
top: 3.4rem;
background: #fff;
}
.time-text {
font-size: 3.2rem;
height: 8rem;
line-height: 8rem;
font-style : italic;
color : #ffffff;
}
#elapsed-time {
margin-left: 4.2rem;
margin-right: 3rem;
}
#duration-time {
margin-left: 3rem;
margin-right: 4.2rem;
}
#video-control-buttons {
width: 31.8rem;
height: 8rem;
margin-right: 0;
display: inline-block;
position: relative;
}
#video-control-buttons > smart-button {
font-size: 5rem;
color: #fff;
position: absolute;
}
#play-button {
width: 9.8rem;
height: 9.8rem;
margin-top: -0.9rem;
left: 11rem;
}
#forward-button {
left: 23.8rem;
}
#video-control-buttons > smart-button[data-icon]::before {
top: -0.1rem;
}
#play-button[data-icon]::before,
#play-button.focused[data-icon]::before {
left: 2.2rem;
line-height: 9.8rem;
transform: scale(1.33);
}
#play-button.focused[data-icon]::before {
transform: scale(1.6);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View file

@ -0,0 +1,14 @@
{
"extends": "../../../../.jshintrc",
"node": true,
"predef": [
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"marionette",
"marionetteScriptFinished"
]
}

View file

@ -0,0 +1,18 @@
{
"extends": "../../../../.jshintrc",
"predef": [
"assert",
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"require",
"requireApp",
"sinon",
"mocha",
"loadBodyHTML"
]
}

View file

@ -0,0 +1,135 @@
/* global castingMessage */
'use strict';
requireApp('fling-player/js/master_debug.js');
requireApp('fling-player/js/casting_message.js');
suite('fling-player/castingMessage', function() {
var validMsg;
setup(function () {
validMsg = {
ack : {
type : 'ack',
seq : 1
},
statusBuffering : {
type : 'status',
seq : 1,
time : 168,
status : 'buffering'
},
statusLoaded : {
type : 'status',
seq : 1,
time : 168,
status : 'loaded'
},
statusBuffered : {
type : 'status',
seq : 1,
time : 168,
status : 'buffered'
},
statusPlaying : {
type : 'status',
seq : 1,
time : 168,
status : 'playing'
},
statusSeeked : {
type : 'status',
seq : 1,
time : 168,
status : 'seeked'
},
statusStopped : {
type : 'status',
seq : 1,
time : 168,
status : 'stopped'
},
statusError : {
type : 'status',
seq : 1,
time : 168,
status : 'error',
error : '404'
},
load : {
type : 'load',
seq : 1,
url : 'http://foo.com/bar.ogg'
},
play : {
type : 'play',
seq : 1
},
pause : {
type : 'pause',
seq : 1
},
seek : {
type : 'seek',
seq : 1,
time : 168
}
};
});
suite('Sanitize messages', function () {
var result;
test('should sanitize valid message', function () {
for (var m in validMsg) {
result = castingMessage.sanitizeMsg(validMsg[m]);
assert.isObject(result, `sanitizing ${m} message fails`);
}
});
test('should sanitize invalid message', function () {
// TODO
});
});
suite('Parse message', function () {
var txt, result;
test('should parse one message', function () {
for (var m in validMsg) {
txt = JSON.stringify(validMsg[m]);
result = castingMessage.parse(txt);
assert.isTrue(result instanceof Array);
assert.equal(result.length, 1, 'should only be one message');
assert.equal(JSON.stringify(result), `[${txt}]`);
}
});
test('should parse multiple messages', function () {
var msgs = [validMsg.play, validMsg.load];
txt = JSON.stringify(msgs[0]) + JSON.stringify(msgs[1]);
result = castingMessage.parse(txt);
assert.isTrue(result instanceof Array);
assert.equal(result.length, 2, 'should be two messages');
assert.equal(JSON.stringify(result), JSON.stringify(msgs));
});
});
suite('Stringify message', function () {
var result;
test('should stringify valid message', function () {
for (var m in validMsg) {
result = castingMessage.stringify(validMsg[m]);
assert.equal(result, JSON.stringify(validMsg[m]));
}
});
});
});

View file

@ -0,0 +1,116 @@
/* global mDBG */
'use strict';
requireApp('fling-player/js/master_debug.js');
suite('fling-player/MasterDebug', function() {
var exp, mockWindow;
setup(function () {
mockWindow = {};
mockWindow.log = mockWindow.warn = mockWindow.error = function () {};
});
teardown(function () {
exp = undefined;
});
test('should turn off debug mode before push to moz upstream', function () {
assert.isFalse(mDBG.isDBG(),
'Please turn off debug mode before push to moz upstream'
);
});
test('should set debug flag', function () {
exp = true;
mDBG.setDBG(exp);
assert.equal(mDBG.isDBG(), exp);
exp = false;
mDBG.setDBG(exp);
assert.equal(mDBG.isDBG(), exp);
});
suite('behavior in the debug mode', function () {
var a = 'a';
var b = 'b';
setup(function () {
exp = sinon.mock(console);
mDBG.setDBG(true);
});
test('should log in the debug mode', function () {
exp.expects('log').once().withExactArgs(a, b);
mDBG.log(a, b);
exp.verify();
});
test('should warn in the debug mode', function () {
exp.expects('warn').once().withExactArgs(a, b);
mDBG.warn(a, b);
exp.verify();
});
test('should error in the debug mode', function () {
exp.expects('error').once().withExactArgs(a, b);
mDBG.error(a, b);
exp.verify();
});
test('should test behavior in the debug mode', function () {
var obj = {
testSomething : function () {}
};
exp = sinon.mock(obj);
exp.expects('testSomething').once();
mDBG.test(obj.testSomething.bind(obj));
exp.verify();
});
});
suite('behavior not in the debug mode', function () {
setup(function () {
exp = sinon.mock(console);
mDBG.setDBG(false);
});
test('should not log not in the debug mode', function () {
exp.expects('log').never();
mDBG.log('test');
exp.verify();
});
test('should not warn not in the debug mode', function () {
exp.expects('warn').never();
mDBG.warn('test');
exp.verify();
});
test('should not error not in the debug mode', function () {
exp.expects('error').never();
mDBG.error('test');
exp.verify();
});
test('should not test behavior not in the debug mode', function () {
var obj = {
testSomething : function () {}
};
exp = sinon.mock(obj);
exp.expects('testSomething').never();
mDBG.test(obj.testSomething.bind(obj));
exp.verify();
});
});
});

View file

@ -0,0 +1,80 @@
/**
* This mock is for the case that just want a simple video element for testing.
* Also, see Bug 1199985, because a real HTMLVideoElement cannot play video
* on TV gecko, we can use it for a temp subtitude for test case that needs
* a HTMLVideoElement.
*/
(function(exports) {
'use strict';
function MockVideoElement(config = {}) {
var self = this;
this.src = config.src || '';
this.ended = config.ended || false;
this.paused = config.paused || true;
this.hidden = config.hidden || false;
this.duration = config.duration || 0;
this.currentTime = config.currentTime || 0;
this.buffered = {
length : 1,
start : function () {
return 0;
},
end : function () {
return Math.max(self.currentTime + 10, self.duration);
}
};
this._events = {};
}
var proto = MockVideoElement.prototype;
proto.fireEvent = function (e) {
var listeners = this._events[e.type];
if (listeners) {
listeners.forEach((listener) => {
if (typeof listener == 'function') {
listener.call(this, e);
} else {
listener.handleEvent(e);
}
});
}
};
proto.addEventListener = function (type, listener) {
var listeners = this._events[type];
if (!listeners) {
this._events[type] = listeners = [];
}
listeners.push(listener);
};
proto.removeAttribute = function (attr) {
this[attr] = '';
};
proto.load = function () {
this.paused = false;
this.currentTime = 0;
this.fireEvent(new Event('loadedmetadata'));
};
proto.play = function () {
this.paused = false;
this.fireEvent(new Event('playing'));
};
proto.pause = function () {
this.paused = true;
this.fireEvent(new Event('pause'));
};
exports.MockVideoElement = MockVideoElement;
})(window);

View file

@ -0,0 +1,218 @@
/* global VideoPlayer, MockVideoElement */
'use strict';
requireApp('fling-player/test/unit/mock_video_element.js');
requireApp('fling-player/js/video_player.js');
suite('fling-player/VideoPlayer', function() {
var player, video;
var videoURL =
'app://fling-player.gaiamobile.org/test_media/Movies/elephants-dream.webm';
setup(function () {
video = new MockVideoElement();
player = new VideoPlayer(video);
player.init();
});
teardown(function () {
player = video = undefined;
});
test('should return video element', function () {
assert.equal(player.getVideo(), video);
});
test('should return video duration in rounded sec', function () {
video.duration = NaN;
player = new VideoPlayer(video);
player.init();
assert.equal(player.getRoundedDuration(), 0);
video.duration = 100;
player = new VideoPlayer(video);
player.init();
assert.equal(player.getRoundedDuration(), Math.round(video.duration));
video.duration = 100.01;
player = new VideoPlayer(video);
player.init();
assert.equal(player.getRoundedDuration(), Math.round(video.duration));
video.duration = 100.55;
player = new VideoPlayer(video);
player.init();
assert.equal(player.getRoundedDuration(), Math.round(video.duration));
});
test('should return current video time in rounded sec', function () {
video.currentTime = NaN;
player = new VideoPlayer(video);
player.init();
assert.equal(player.getRoundedCurrentTime(), 0);
video.currentTime = 100;
player = new VideoPlayer(video);
player.init();
assert.equal(player.getRoundedCurrentTime(), Math.round(video.currentTime));
video.currentTime = 100.01;
player = new VideoPlayer(video);
player.init();
assert.equal(player.getRoundedCurrentTime(), Math.round(video.currentTime));
video.currentTime = 100.55;
player = new VideoPlayer(video);
player.init();
assert.equal(player.getRoundedCurrentTime(), Math.round(video.currentTime));
});
test('should show video element', function () {
video.hidden = true;
player.show();
assert.isFalse(video.hidden);
});
test('should hide video element', function () {
video.hidden = false;
player.hide();
assert.isTrue(video.hidden);
});
test('should load video src url', function () {
player.load(videoURL);
assert.equal(video.src, videoURL);
});
test('should release video src url', function () {
video.src = videoURL;
player.release();
assert.equal(video.src, '');
});
test('should play', function () {
var exp = sinon.mock(video);
exp.expects('play').once();
player.play();
exp.verify();
});
test('should pause', function () {
var exp = sinon.mock(video);
exp.expects('pause').once();
player.pause();
exp.verify();
});
test('should seek with loading metadata', function () {
var exp = video.currentTime + 10;
video.src = videoURL;
video.load();
player.seek(exp);
assert.equal(video.currentTime, exp);
});
test('should not seek without loading metadata', function () {
var origin = video.currentTime;
player.seek(origin + 10);
assert.equal(video.currentTime, origin);
});
suite('Event handling', function () {
var exp;
setup(function () {
exp = sinon.mock(player);
});
test('should handle the loadedmetadata event', function () {
var t = video.currentTime + 10;
exp.expects('seek').once().withExactArgs(t);
player.handleEvent({ type : 'loadedmetadata' });
player.seek(t);
exp.verify();
});
test('should handle the playing event', function () {
exp.expects('show').once();
player.handleEvent({ type : 'playing' });
exp.verify();
});
});
suite('Time parsing', function () {
var act, exp = {};
var toSec = (t) => {
return t.hh * 3600 + t.mm * 60 + t.ss;
};
test('should parse hh well', function () {
exp.hh = 99;
exp.mm = 0;
exp.ss = 0;
act = player.parseTime(toSec(exp));
assert.equal(act.hh, exp.hh);
assert.equal(act.mm, exp.mm);
assert.equal(act.ss, exp.ss);
});
test('should parse mm well', function () {
exp.hh = 0;
exp.mm = 59;
exp.ss = 0;
act = player.parseTime(toSec(exp));
assert.equal(act.hh, exp.hh);
assert.equal(act.mm, exp.mm);
assert.equal(act.ss, exp.ss);
});
test('should parse ss well', function () {
exp.hh = 0;
exp.mm = 0;
exp.ss = 59;
act = player.parseTime(toSec(exp));
assert.equal(act.hh, exp.hh);
assert.equal(act.mm, exp.mm);
assert.equal(act.ss, exp.ss);
});
test('should parse hh, mm and ss well', function () {
exp.hh = 39;
exp.mm = 28;
exp.ss = 17;
act = player.parseTime(toSec(exp));
assert.equal(act.hh, exp.hh);
assert.equal(act.mm, exp.mm);
assert.equal(act.ss, exp.ss);
});
test('should parse zero sec well', function () {
act = player.parseTime(0);
assert.equal(act.hh, 0);
assert.equal(act.mm, 0);
assert.equal(act.ss, 0);
});
test('should parse negative time well', function () {
act = player.parseTime(-1234567890);
assert.equal(act.hh, 0);
assert.equal(act.mm, 0);
assert.equal(act.ss, 0);
});
});
});