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.
64
gaia/apps/fm/index.html
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
|
||||
<meta charset="utf-8" />
|
||||
<meta name="theme-color" content="#242d33">
|
||||
<title>FM Radio</title>
|
||||
<link rel="stylesheet" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/gaia-theme.css" type="text/css" />
|
||||
<link rel="stylesheet" href="style/fm.css" type="text/css" />
|
||||
<meta name="defaultLanguage" content="en-US">
|
||||
<meta name="availableLanguages" content="en-US">
|
||||
<link rel="localization" href="locales/fm.{locale}.properties">
|
||||
<script defer src="shared/js/intl/l20n.js"></script>
|
||||
<script defer src="shared/js/async_storage.js"></script>
|
||||
<script defer src="shared/js/airplane_mode_helper.js"></script>
|
||||
|
||||
<script defer src="js/fm.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container" dir="ltr">
|
||||
<div id="frequency-bar">
|
||||
<div id="frequency-display">
|
||||
<a id="speaker-switch" href="#speaker" data-speaker-on="false" data-l10n-id="speaker-switch" role="button" aria-pressed="false"></a>
|
||||
<div id="frequency" role="status" data-l10n-id="frequency-MHz" data-l10n-args='{"value": "87.5"}'>
|
||||
<small></small>
|
||||
</div>
|
||||
<a id="bookmark-button" href="#bookmark" data-bookmarked="false" data-l10n-id="bookmark-button" role="button"></a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dialer-bar">
|
||||
<div id="dialer-container" data-l10n-id="dialer-container" role="slider" aria-valuemin="87.5" aria-valuemax="108" aria-controls="frequency">
|
||||
<div id="frequency-indicator"></div>
|
||||
<div id="frequency-dialer" class="animation-on"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fav-list">
|
||||
<div id="fav-list-container" role="listbox" data-l10n-id="fav-list-container"></div>
|
||||
</div>
|
||||
<div id="action-bar" role="toolbar">
|
||||
<div>
|
||||
<a id="frequency-op-seekdown" href="#seekdown" data-l10n-id="frequency-op-seekdown" role="button"></a>
|
||||
</div>
|
||||
<div>
|
||||
<a id="power-switch" href="#power-switch" data-enabled="false" data-enabling="false" data-l10n-id="power-switch-off" role="button"><span></span></a>
|
||||
</div>
|
||||
<div>
|
||||
<a id="frequency-op-seekup" href="#seekup" data-l10n-id="frequency-op-seekup" role="button"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="antenna-warning" class="warning" hidden>
|
||||
<div></div>
|
||||
<div id="antenna-warning-header" class="warning-header" data-l10n-id="noAntenna">Please plugin your headset</div>
|
||||
<div id="antenna-warning-body" class="warning-body" data-l10n-id="noAntennaMsg">The FM Radio requires a headset to use as an antenna.</div>
|
||||
</div>
|
||||
<div id="airplane-mode-warning" class="warning" hidden>
|
||||
<div class="warning-header" data-l10n-id="airplaneModeHeader">Airplane mode is on</div>
|
||||
<div class="warning-body" data-l10n-id="airplaneModeMsg">Turn off Airplane mode to use FM Radio.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
921
gaia/apps/fm/js/fm.js
Executable file
|
|
@ -0,0 +1,921 @@
|
|||
'use strict';
|
||||
/* global
|
||||
AirplaneModeHelper */
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function $$(expr) {
|
||||
return document.querySelectorAll(expr);
|
||||
}
|
||||
|
||||
// XXX fake mozFMRadio object for UI testing on PC
|
||||
var mozFMRadio = navigator.mozFM || navigator.mozFMRadio || {
|
||||
speakerEnabled: false,
|
||||
|
||||
frequency: null,
|
||||
|
||||
enabled: false,
|
||||
|
||||
antennaAvailable: true,
|
||||
|
||||
signalStrength: 1,
|
||||
|
||||
frequencyLowerBound: 87.5,
|
||||
|
||||
frequencyUpperBound: 108,
|
||||
|
||||
channelWidth: 0.1,
|
||||
|
||||
onsignalstrengthchange: function emptyFunction() { },
|
||||
|
||||
onfrequencychange: function emptyFunction() { },
|
||||
|
||||
onenabled: function emptyFunction() { },
|
||||
|
||||
ondisabled: function emptyFunction() { },
|
||||
|
||||
onantennaavailablechange: function emptyFunction() { },
|
||||
|
||||
disable: function fm_disable() {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.enabled = false;
|
||||
var self = this;
|
||||
window.setTimeout(function() {
|
||||
self.ondisabled();
|
||||
}, 500);
|
||||
|
||||
return {};
|
||||
},
|
||||
|
||||
enable: function fm_enable(frequency) {
|
||||
if (this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.enabled = true;
|
||||
var self = this;
|
||||
window.setTimeout(function() {
|
||||
self.onenabled();
|
||||
self.setFrequency(frequency);
|
||||
}, 500);
|
||||
|
||||
return {};
|
||||
},
|
||||
|
||||
setFrequency: function fm_setFrequency(freq) {
|
||||
freq = parseFloat(freq.toFixed(1));
|
||||
var previousValue = this.frequency;
|
||||
this.frequency = freq;
|
||||
if (previousValue != freq) {
|
||||
this.onfrequencychange();
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
seekUp: function fm_seekUp() {
|
||||
var self = this;
|
||||
if (this._seekRequest) {
|
||||
return;
|
||||
}
|
||||
this._seekRequest = {};
|
||||
this._seekTimeout = window.setTimeout(function su_timeout() {
|
||||
self.setFrequency(self.frequency + 0.5);
|
||||
if (self._seekRequest.onsuccess) {
|
||||
self._seekRequest.onsuccess();
|
||||
}
|
||||
self._clearSeekRequest();
|
||||
}, 1000);
|
||||
return this._seekRequest;
|
||||
},
|
||||
|
||||
seekDown: function fm_seekDown() {
|
||||
var self = this;
|
||||
if (this._seekRequest) {
|
||||
return;
|
||||
}
|
||||
this._seekRequest = {};
|
||||
this._seekTimeout = window.setTimeout(function sd_timeout() {
|
||||
self.setFrequency(self.frequency - 0.5);
|
||||
if (self._seekRequest.onsuccess) {
|
||||
self._seekRequest.onsuccess();
|
||||
}
|
||||
self._clearSeekRequest();
|
||||
}, 1000);
|
||||
return this._seekRequest;
|
||||
},
|
||||
|
||||
cancelSeek: function fm_cancelSeek() {
|
||||
this._clearSeekRequest();
|
||||
var request = {};
|
||||
window.setTimeout(function() {
|
||||
if (request.onsuccess) {
|
||||
request.onsuccess();
|
||||
}
|
||||
}, 0);
|
||||
return request;
|
||||
},
|
||||
|
||||
_clearSeekRequest: function fm_clearSeek() {
|
||||
if (this._seekTimeout) {
|
||||
window.clearTimeout(this._seekTimeout);
|
||||
this._seekTimeout = null;
|
||||
}
|
||||
if (this._seekRequest && this._seekRequest.onerror) {
|
||||
this._seekRequest.onerror();
|
||||
this._seekRequest = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// XXX fake SpeakerManager object for UI testing on PC
|
||||
(function(aGlobal) {
|
||||
aGlobal.SpeakerManager = aGlobal.SpeakerManager || aGlobal.MozSpeakerManager;
|
||||
|
||||
if (aGlobal.SpeakerManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
function SpeakerManager() {
|
||||
this.speakerforced = false;
|
||||
}
|
||||
|
||||
SpeakerManager.prototype = {
|
||||
set forcespeaker(enable) {
|
||||
if (this.speakerforced != enable) {
|
||||
this.speakerforced = enable;
|
||||
if (this.onspeakerforcedchange) {
|
||||
this.onspeakerforcedchange();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
aGlobal.SpeakerManager = SpeakerManager;
|
||||
})(window);
|
||||
|
||||
var enabling = false;
|
||||
function updatePowerUI() {
|
||||
var enabled = mozFMRadio.enabled;
|
||||
var powerSwitch = $('power-switch');
|
||||
if (enabled) {
|
||||
window.performance.mark('fmRadioEnabled');
|
||||
// ACCESSIBILITY - Must set data-l10n-id to reflect Off switch
|
||||
powerSwitch.setAttribute('data-l10n-id', 'power-switch-off');
|
||||
} else {
|
||||
// ACCESSIBILITY - Must set data-l10n-id to reflect On switch
|
||||
powerSwitch.setAttribute('data-l10n-id', 'power-switch-on');
|
||||
}
|
||||
console.log('Power status: ' + (enabled ? 'on' : 'off'));
|
||||
powerSwitch.dataset.enabled = enabled;
|
||||
powerSwitch.dataset.enabling = enabling;
|
||||
}
|
||||
|
||||
var airplaneModeEnabled = false;
|
||||
function updateWarningModeUI() {
|
||||
$('airplane-mode-warning').hidden = !airplaneModeEnabled;
|
||||
$('antenna-warning').hidden = mozFMRadio.antennaAvailable ||
|
||||
airplaneModeEnabled;
|
||||
$('container').classList.toggle('hidden-block', airplaneModeEnabled ||
|
||||
!mozFMRadio.antennaAvailable);
|
||||
}
|
||||
|
||||
function updateFrequencyBarUI() {
|
||||
var frequencyBar = $('frequency-bar');
|
||||
if (enabling) {
|
||||
frequencyBar.classList.add('dim');
|
||||
} else {
|
||||
frequencyBar.classList.remove('dim');
|
||||
}
|
||||
}
|
||||
|
||||
function updateEnablingState(enablingState) {
|
||||
enabling = enablingState;
|
||||
updatePowerUI();
|
||||
updateFrequencyBarUI();
|
||||
}
|
||||
|
||||
function enableFMRadio(frequency) {
|
||||
if (airplaneModeEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
var request = mozFMRadio.enable(frequency);
|
||||
// Request might fail, see bug862672
|
||||
request.onerror = function onerror_enableFMRadio(event) {
|
||||
updateEnablingState(false);
|
||||
};
|
||||
|
||||
updateEnablingState(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the FM radio is seeking currently, cancel it and then set frequency.
|
||||
*
|
||||
* @param {freq} frequency set.
|
||||
*/
|
||||
function cancelSeekAndSetFreq(frequency) {
|
||||
function setFreq() {
|
||||
mozFMRadio.setFrequency(frequency);
|
||||
}
|
||||
|
||||
var seeking = !!$('power-switch').getAttribute('data-seeking');
|
||||
if (!seeking) {
|
||||
setFreq();
|
||||
} else {
|
||||
var request = mozFMRadio.cancelSeek();
|
||||
request.onsuccess = setFreq;
|
||||
request.onerror = setFreq;
|
||||
}
|
||||
}
|
||||
|
||||
var frequencyDialer = {
|
||||
unit: 2,
|
||||
_bandUpperBound: 0,
|
||||
_bandLowerBound: 0,
|
||||
_minFrequency: 0,
|
||||
_maxFrequency: 0,
|
||||
_currentFreqency: 0,
|
||||
_translateX: 0,
|
||||
|
||||
init: function() {
|
||||
// First thing is to show a warning if there // is not antenna.
|
||||
updateWarningModeUI();
|
||||
|
||||
this._initUI();
|
||||
this.setFrequency(mozFMRadio.frequency);
|
||||
this._addEventListeners();
|
||||
},
|
||||
|
||||
_addEventListeners: function() {
|
||||
function _removeEventListeners() {
|
||||
document.body.removeEventListener('touchend', fd_body_touchend, false);
|
||||
document.body.removeEventListener('touchmove', fd_body_touchmove, false);
|
||||
}
|
||||
|
||||
function cloneEvent(evt) {
|
||||
if ('touches' in evt) {
|
||||
evt = evt.touches[0];
|
||||
}
|
||||
return { x: evt.clientX, y: evt.clientX,
|
||||
timestamp: evt.timeStamp };
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var SPEED_THRESHOLD = 0.1;
|
||||
var currentEvent, startEvent, currentSpeed;
|
||||
var tunedFrequency = 0;
|
||||
|
||||
function toFixed(frequency) {
|
||||
return parseFloat(frequency.toFixed(1));
|
||||
}
|
||||
|
||||
function _calcSpeed() {
|
||||
var movingSpace = startEvent.x - currentEvent.x;
|
||||
var deltaTime = currentEvent.timestamp - startEvent.timestamp;
|
||||
var speed = movingSpace / deltaTime;
|
||||
currentSpeed = parseFloat(speed.toFixed(2));
|
||||
}
|
||||
|
||||
function _calcTargetFrequency() {
|
||||
return tunedFrequency - getMovingSpace() / self._space;
|
||||
}
|
||||
|
||||
function getMovingSpace() {
|
||||
var movingSpace = currentEvent.x - startEvent.x;
|
||||
return movingSpace;
|
||||
}
|
||||
|
||||
function fd_body_touchmove(event) {
|
||||
event.stopPropagation();
|
||||
currentEvent = cloneEvent(event);
|
||||
|
||||
_calcSpeed();
|
||||
|
||||
// move dialer
|
||||
var dialer = $('frequency-dialer');
|
||||
var translateX = self._translateX + getMovingSpace();
|
||||
self._translateX = translateX;
|
||||
var count = dialer.childNodes.length;
|
||||
for (var i = 0; i < count; i++) {
|
||||
var child = dialer.childNodes[i];
|
||||
child.style.MozTransform = 'translateX(' + translateX + 'px)';
|
||||
}
|
||||
|
||||
tunedFrequency = _calcTargetFrequency();
|
||||
var roundedFrequency = Math.round(tunedFrequency * 10) / 10;
|
||||
|
||||
if (roundedFrequency != self._currentFreqency) {
|
||||
self.setFrequency(toFixed(roundedFrequency), true);
|
||||
}
|
||||
|
||||
startEvent = currentEvent;
|
||||
}
|
||||
|
||||
function fd_body_touchend(event) {
|
||||
event.stopPropagation();
|
||||
_removeEventListeners();
|
||||
|
||||
// Add animation back
|
||||
$('frequency-dialer').classList.add('animation-on');
|
||||
// Add momentum if speed is higher than a given threshold.
|
||||
if (Math.abs(currentSpeed) > SPEED_THRESHOLD) {
|
||||
var direction = currentSpeed > 0 ? 1 : -1;
|
||||
tunedFrequency += Math.min(Math.abs(currentSpeed) * 3, 3) * direction;
|
||||
}
|
||||
tunedFrequency = self.setFrequency(toFixed(tunedFrequency));
|
||||
cancelSeekAndSetFreq(tunedFrequency);
|
||||
|
||||
// Reset vars
|
||||
currentEvent = null;
|
||||
startEvent = null;
|
||||
currentSpeed = 0;
|
||||
}
|
||||
|
||||
function fd_touchstart(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
// Stop animation
|
||||
$('frequency-dialer').classList.remove('animation-on');
|
||||
|
||||
startEvent = currentEvent = cloneEvent(event);
|
||||
tunedFrequency = self._currentFreqency;
|
||||
|
||||
_removeEventListeners();
|
||||
document.body.addEventListener('touchmove', fd_body_touchmove, false);
|
||||
document.body.addEventListener('touchend', fd_body_touchend, false);
|
||||
}
|
||||
|
||||
function fd_key(event) {
|
||||
if (event.keyCode === event.DOM_VK_UP) {
|
||||
tunedFrequency = self._currentFreqency + 0.1;
|
||||
} else if (event.keyCode === event.DOM_VK_DOWN) {
|
||||
tunedFrequency = self._currentFreqency - 0.1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
tunedFrequency = self.setFrequency(toFixed(tunedFrequency));
|
||||
cancelSeekAndSetFreq(tunedFrequency);
|
||||
}
|
||||
|
||||
var dialerContainer = $('dialer-container');
|
||||
dialerContainer.addEventListener('touchstart', fd_touchstart, false);
|
||||
// ACCESSIBILITY - Add keypress event for screen reader
|
||||
dialerContainer.addEventListener('keypress', fd_key, false);
|
||||
},
|
||||
|
||||
_initUI: function() {
|
||||
$('frequency-dialer').innerHTML = '';
|
||||
var lower = this._bandLowerBound = mozFMRadio.frequencyLowerBound;
|
||||
var upper = this._bandUpperBound = mozFMRadio.frequencyUpperBound;
|
||||
|
||||
var unit = this.unit;
|
||||
this._minFrequency = lower - lower % unit;
|
||||
this._maxFrequency = upper + unit - upper % unit;
|
||||
var unitCount = (this._maxFrequency - this._minFrequency) / unit;
|
||||
|
||||
for (var i = 0; i < unitCount; ++i) {
|
||||
var start = this._minFrequency + i * unit;
|
||||
start = start < lower ? lower : start;
|
||||
var end = this._maxFrequency + i * unit + unit;
|
||||
end = upper < end ? upper : end;
|
||||
this._addDialerUnit(start, end);
|
||||
}
|
||||
|
||||
// cache the size of dialer
|
||||
var _dialerUnits = $$('#frequency-dialer .dialer-unit');
|
||||
var _dialerUnitWidth = _dialerUnits[0].clientWidth;
|
||||
this._dialerWidth = _dialerUnitWidth * _dialerUnits.length;
|
||||
this._space = this._dialerWidth /
|
||||
(this._maxFrequency - this._minFrequency);
|
||||
|
||||
for (i = 0; i < _dialerUnits.length; i++) {
|
||||
_dialerUnits[i].style.left = i * _dialerUnitWidth + 'px';
|
||||
}
|
||||
},
|
||||
|
||||
_addDialerUnit: function(start, end) {
|
||||
var markStart = start - start % this.unit;
|
||||
|
||||
// At the beginning and end of the dial, some of the notches should be
|
||||
// hidden. To do this, we use an absolutely positioned div mask.
|
||||
// startMaskWidth and endMaskWidth track how wide that mask should be.
|
||||
var startMaskWidth = 0;
|
||||
var endMaskWidth = 0;
|
||||
|
||||
// unitWidth is how wide each notch is that needs to be covered.
|
||||
var unitWidth = 16;
|
||||
|
||||
var total = this.unit * 10; // 0.1MHz
|
||||
for (var i = 0; i < total; i++) {
|
||||
var dialValue = markStart + i * 0.1;
|
||||
if (dialValue < start) {
|
||||
startMaskWidth += unitWidth;
|
||||
} else if (dialValue > end) {
|
||||
endMaskWidth += unitWidth;
|
||||
}
|
||||
}
|
||||
|
||||
var container = document.createElement('div');
|
||||
container.classList.add('dialer-unit-mark-box');
|
||||
|
||||
if (startMaskWidth > 0) {
|
||||
var markEl = document.createElement('div');
|
||||
markEl.classList.add('dialer-unit-mark-mask-start');
|
||||
markEl.style.width = startMaskWidth + 'px';
|
||||
|
||||
container.appendChild(markEl);
|
||||
}
|
||||
|
||||
if (endMaskWidth > 0) {
|
||||
var markEnd = document.createElement('div');
|
||||
markEnd.classList.add('dialer-unit-mark-mask-end');
|
||||
markEnd.style.width = endMaskWidth + 'px';
|
||||
|
||||
container.appendChild(markEnd);
|
||||
}
|
||||
|
||||
var width = (100 / this.unit) + '%';
|
||||
// Show the frequencies on dialer
|
||||
for (var j = 0; j < this.unit; j++) {
|
||||
var frequency = Math.floor(markStart) + j;
|
||||
var showFloor = frequency >= start && frequency <= end;
|
||||
|
||||
var unit = document.createElement('div');
|
||||
unit.classList.add('dialer-unit-floor');
|
||||
if (!showFloor) {
|
||||
unit.classList.add('hidden-block');
|
||||
}
|
||||
unit.style.width = width;
|
||||
unit.appendChild(document.createTextNode(frequency));
|
||||
container.appendChild(unit);
|
||||
}
|
||||
|
||||
var dialerUnit = document.createElement('div');
|
||||
dialerUnit.className = 'dialer-unit';
|
||||
dialerUnit.appendChild(container);
|
||||
$('frequency-dialer').appendChild(dialerUnit);
|
||||
},
|
||||
|
||||
_updateUI: function(frequency, ignoreDialer) {
|
||||
document.l10n.setAttributes($('frequency'), 'frequency-MHz', {
|
||||
value: frequency.toFixed(1)
|
||||
});
|
||||
if (true !== ignoreDialer) {
|
||||
this._translateX = (this._minFrequency - frequency) * this._space;
|
||||
var dialer = $('frequency-dialer');
|
||||
var count = dialer.childNodes.length;
|
||||
for (var i = 0; i < count; i++) {
|
||||
dialer.childNodes[i].style.MozTransform =
|
||||
'translateX(' + this._translateX + 'px)';
|
||||
}
|
||||
$('dialer-container').setAttribute('aria-valuenow', frequency);
|
||||
}
|
||||
},
|
||||
|
||||
setFrequency: function(frequency, ignoreDialer) {
|
||||
if (frequency < this._bandLowerBound) {
|
||||
frequency = this._bandLowerBound;
|
||||
}
|
||||
|
||||
if (frequency > this._bandUpperBound) {
|
||||
frequency = this._bandUpperBound;
|
||||
}
|
||||
|
||||
this._currentFreqency = frequency;
|
||||
this._updateUI(frequency, ignoreDialer);
|
||||
|
||||
return frequency;
|
||||
},
|
||||
|
||||
getFrequency: function() {
|
||||
return this._currentFreqency;
|
||||
}
|
||||
};
|
||||
|
||||
var historyList = {
|
||||
|
||||
_historyList: [],
|
||||
|
||||
/**
|
||||
* Storage key name.
|
||||
* @const
|
||||
* @type {string}
|
||||
*/
|
||||
KEYNAME: 'historylist',
|
||||
|
||||
/**
|
||||
* Maximum size of the history
|
||||
* @const
|
||||
* @type {integer}
|
||||
*/
|
||||
SIZE: 1,
|
||||
|
||||
init: function hl_init(callback) {
|
||||
var self = this;
|
||||
window.asyncStorage.getItem(this.KEYNAME, function storage_getItem(value) {
|
||||
self._historyList = value || [];
|
||||
if (typeof callback == 'function') {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_save: function hl_save() {
|
||||
window.asyncStorage.setItem(this.KEYNAME, this._historyList);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add frequency to history list.
|
||||
*
|
||||
* @param {freq} frequency to add.
|
||||
*/
|
||||
add: function hl_add(freq) {
|
||||
if (freq == null) {
|
||||
return;
|
||||
}
|
||||
var self = this;
|
||||
self._historyList.push({
|
||||
name: freq + '',
|
||||
frequency: freq
|
||||
});
|
||||
if (self._historyList.length > self.SIZE) {
|
||||
self._historyList.shift();
|
||||
}
|
||||
self._save();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the last frequency tuned
|
||||
*
|
||||
* @return {freq} the last frequency tuned.
|
||||
*/
|
||||
last: function hl_last() {
|
||||
if (this._historyList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return this._historyList[this._historyList.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var favoritesList = {
|
||||
_favList: null,
|
||||
|
||||
KEYNAME: 'favlist',
|
||||
|
||||
init: function(callback) {
|
||||
var self = this;
|
||||
window.asyncStorage.getItem(this.KEYNAME, function storage_getItem(value) {
|
||||
self._favList = value || { };
|
||||
self._showListUI();
|
||||
|
||||
if (typeof callback == 'function') {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
var _container = $('fav-list-container');
|
||||
_container.addEventListener('click', function _onclick(event) {
|
||||
var frequency = self._getElemFreq(event.target);
|
||||
if (!frequency) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.classList.contains('fav-list-remove-button')) {
|
||||
// Remove the item from the favorites list.
|
||||
self.remove(frequency);
|
||||
updateFreqUI();
|
||||
} else {
|
||||
if (mozFMRadio.enabled) {
|
||||
cancelSeekAndSetFreq(frequency);
|
||||
} else {
|
||||
// If fm is disabled, turn the radio on.
|
||||
enableFMRadio(frequency);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_save: function() {
|
||||
window.asyncStorage.setItem(this.KEYNAME, this._favList);
|
||||
},
|
||||
|
||||
_showListUI: function() {
|
||||
var self = this;
|
||||
this.forEach(function(f) {
|
||||
self._addItemToListUI(f);
|
||||
});
|
||||
},
|
||||
|
||||
_addItemToListUI: function(item) {
|
||||
var container = $('fav-list-container');
|
||||
var elem = document.createElement('div');
|
||||
elem.id = this._getUIElemId(item);
|
||||
elem.className = 'fav-list-item';
|
||||
elem.setAttribute('role', 'option');
|
||||
|
||||
var subElem = document.createElement('div');
|
||||
subElem.className = 'fav-list-frequency';
|
||||
document.l10n.setAttributes(subElem, 'fav-frequency-MHz', {
|
||||
value: item.frequency.toFixed(1)
|
||||
});
|
||||
elem.appendChild(subElem);
|
||||
|
||||
subElem = document.createElement('div');
|
||||
subElem.className = 'fav-list-remove-button';
|
||||
elem.appendChild(subElem);
|
||||
|
||||
// keep list ascending sorted
|
||||
if (container.childNodes.length === 0) {
|
||||
container.appendChild(elem);
|
||||
} else {
|
||||
var childNodes = container.childNodes;
|
||||
for (var i = 0; i < childNodes.length; i++) {
|
||||
var child = childNodes[i];
|
||||
var elemFreq = this._getElemFreq(child);
|
||||
if (item.frequency < elemFreq) {
|
||||
container.insertBefore(elem, child);
|
||||
break;
|
||||
} else if (i == childNodes.length - 1) {
|
||||
container.appendChild(elem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return elem;
|
||||
},
|
||||
|
||||
_removeItemFromListUI: function(freq) {
|
||||
if (!this.contains(freq)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var itemElem = $(this._getUIElemId(this._favList[freq]));
|
||||
if (itemElem) {
|
||||
itemElem.parentNode.removeChild(itemElem);
|
||||
}
|
||||
},
|
||||
|
||||
_getUIElemId: function(item) {
|
||||
return 'frequency-' + item.frequency;
|
||||
},
|
||||
|
||||
_getElemFreq: function(elem) {
|
||||
// ensure we get the closest list-item.
|
||||
elem = elem.closest('.fav-list-item');
|
||||
return parseFloat(elem.id.substring(elem.id.indexOf('-') + 1));
|
||||
},
|
||||
|
||||
forEach: function(callback) {
|
||||
for (var freq in this._favList) {
|
||||
callback(this._favList[freq]);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if frequency is in fav list.
|
||||
*
|
||||
* @param {number} frequence to check.
|
||||
*
|
||||
* @return {boolean} True if freq is in fav list.
|
||||
*/
|
||||
contains: function(freq) {
|
||||
if (!this._favList) {
|
||||
return false;
|
||||
}
|
||||
return typeof this._favList[freq] != 'undefined';
|
||||
},
|
||||
|
||||
/**
|
||||
* Add frequency to fav list.
|
||||
*/
|
||||
add: function(freq) {
|
||||
if (!this.contains(freq)) {
|
||||
this._favList[freq] = {
|
||||
name: freq + '',
|
||||
frequency: freq
|
||||
};
|
||||
|
||||
this._save();
|
||||
|
||||
// show the item in favorites list.
|
||||
var elem = this._addItemToListUI(this._favList[freq]);
|
||||
window.scrollTo(0, elem.offsetTop);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove frequency from fav list.
|
||||
*
|
||||
* @param {number} freq to remove.
|
||||
*
|
||||
* @return {boolean} True if freq to remove is in fav list.
|
||||
*/
|
||||
remove: function(freq) {
|
||||
var exists = this.contains(freq);
|
||||
this._removeItemFromListUI(freq);
|
||||
delete this._favList[freq];
|
||||
this._save();
|
||||
return exists;
|
||||
},
|
||||
|
||||
select: function(freq) {
|
||||
var items = $$('#fav-list-container div.fav-list-item');
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
if (this._getElemFreq(item) == freq) {
|
||||
item.classList.add('selected');
|
||||
item.setAttribute('aria-selected', true);
|
||||
} else {
|
||||
item.classList.remove('selected');
|
||||
item.setAttribute('aria-selected', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function updateFreqUI() {
|
||||
historyList.add(mozFMRadio.frequency);
|
||||
frequencyDialer.setFrequency(mozFMRadio.frequency);
|
||||
var frequency = frequencyDialer.getFrequency();
|
||||
favoritesList.select(frequency);
|
||||
var bookmarkButton = $('bookmark-button');
|
||||
bookmarkButton.dataset.bookmarked = favoritesList.contains(frequency);
|
||||
bookmarkButton.setAttribute('aria-pressed',
|
||||
favoritesList.contains(frequency));
|
||||
}
|
||||
|
||||
function init() {
|
||||
frequencyDialer.init();
|
||||
|
||||
function onclick_seekbutton(event) {
|
||||
/* jshint validthis: true */
|
||||
var seekButton = this;
|
||||
var powerSwitch = $('power-switch');
|
||||
var seeking = !!powerSwitch.getAttribute('data-seeking');
|
||||
var up = seekButton.id == 'frequency-op-seekup';
|
||||
|
||||
function seek() {
|
||||
powerSwitch.dataset.seeking = true;
|
||||
|
||||
var request = up ? mozFMRadio.seekUp() : mozFMRadio.seekDown();
|
||||
|
||||
request.onsuccess = function seek_onsuccess() {
|
||||
powerSwitch.removeAttribute('data-seeking');
|
||||
};
|
||||
|
||||
request.onerror = function seek_onerror() {
|
||||
powerSwitch.removeAttribute('data-seeking');
|
||||
};
|
||||
}
|
||||
|
||||
// If the FM radio is seeking channel currently, cancel it and seek again.
|
||||
if (seeking) {
|
||||
var request = mozFMRadio.cancelSeek();
|
||||
request.onsuccess = seek;
|
||||
request.onerror = seek;
|
||||
} else {
|
||||
seek();
|
||||
}
|
||||
}
|
||||
|
||||
$('frequency-op-seekdown').addEventListener('click',
|
||||
onclick_seekbutton, false);
|
||||
$('frequency-op-seekup').addEventListener('click',
|
||||
onclick_seekbutton, false);
|
||||
|
||||
$('power-switch').addEventListener('click', function toggle_fm() {
|
||||
if (mozFMRadio.enabled) {
|
||||
mozFMRadio.disable();
|
||||
} else {
|
||||
enableFMRadio(frequencyDialer.getFrequency());
|
||||
}
|
||||
}, false);
|
||||
|
||||
$('bookmark-button').addEventListener('click', function toggle_bookmark() {
|
||||
var frequency = frequencyDialer.getFrequency();
|
||||
if (favoritesList.contains(frequency)) {
|
||||
favoritesList.remove(frequency);
|
||||
} else {
|
||||
favoritesList.add(frequency);
|
||||
}
|
||||
updateFreqUI();
|
||||
}, false);
|
||||
|
||||
var speakerManager = new window.SpeakerManager();
|
||||
$('speaker-switch').addEventListener('click', function toggle_speaker() {
|
||||
speakerManager.forcespeaker = !speakerManager.speakerforced;
|
||||
}, false);
|
||||
|
||||
speakerManager.onspeakerforcedchange = function onspeakerforcedchange() {
|
||||
var speakerSwitch = $('speaker-switch');
|
||||
speakerSwitch.dataset.speakerOn = speakerManager.speakerforced;
|
||||
speakerSwitch.setAttribute('aria-pressed', speakerManager.speakerforced);
|
||||
};
|
||||
|
||||
mozFMRadio.onfrequencychange = updateFreqUI;
|
||||
mozFMRadio.onenabled = function() {
|
||||
updateEnablingState(false);
|
||||
};
|
||||
mozFMRadio.ondisabled = function() {
|
||||
updateEnablingState(false);
|
||||
};
|
||||
|
||||
mozFMRadio.onantennaavailablechange = function onAntennaChange() {
|
||||
updateWarningModeUI();
|
||||
if (mozFMRadio.antennaAvailable) {
|
||||
// If the FM radio is enabled or enabling when the antenna is unplugged,
|
||||
// turn the FM radio on again.
|
||||
if (!!window._previousFMRadioState || !!window._previousEnablingState) {
|
||||
enableFMRadio(frequencyDialer.getFrequency());
|
||||
}
|
||||
} else {
|
||||
// Remember the current state of the FM radio
|
||||
window._previousFMRadioState = mozFMRadio.enabled;
|
||||
window._previousEnablingState = enabling;
|
||||
mozFMRadio.disable();
|
||||
}
|
||||
};
|
||||
|
||||
// Disable the power button and the fav list when the airplane mode is on.
|
||||
updateWarningModeUI();
|
||||
|
||||
AirplaneModeHelper.addEventListener('statechange', function(status) {
|
||||
airplaneModeEnabled = status === 'enabled';
|
||||
updateWarningModeUI();
|
||||
});
|
||||
|
||||
// Load the fav list and enable the FM radio if an antenna is available.
|
||||
historyList.init(function hl_ready() {
|
||||
if (mozFMRadio.antennaAvailable) {
|
||||
// Enable FM immediately
|
||||
if (historyList.last() && historyList.last().frequency) {
|
||||
enableFMRadio(historyList.last().frequency);
|
||||
} else {
|
||||
enableFMRadio(mozFMRadio.frequencyLowerBound);
|
||||
}
|
||||
|
||||
favoritesList.init(updateFreqUI);
|
||||
} else {
|
||||
// Mark the previous state as True,
|
||||
// so the FM radio be enabled automatically
|
||||
// when the headset is plugged.
|
||||
window._previousFMRadioState = true;
|
||||
updateWarningModeUI();
|
||||
favoritesList.init();
|
||||
}
|
||||
updatePowerUI();
|
||||
|
||||
// PERFORMANCE MARKER (5): fullyLoaded
|
||||
// Designates that the app is *completely* loaded and all relevant
|
||||
// "below-the-fold" content exists in the DOM, is marked visible,
|
||||
// has its events bound and is ready for user interaction. All
|
||||
// required startup background processing should be complete.
|
||||
window.performance.mark('fullyLoaded');
|
||||
});
|
||||
}
|
||||
|
||||
document.l10n.ready.then(function() {
|
||||
// PERFORMANCE MARKER (1): navigationLoaded
|
||||
// Designates that the app's *core* chrome or navigation interface
|
||||
// exists in the DOM and is marked as ready to be displayed.
|
||||
window.performance.mark('navigationLoaded');
|
||||
|
||||
AirplaneModeHelper.ready(function() {
|
||||
airplaneModeEnabled = AirplaneModeHelper.getStatus() == 'enabled';
|
||||
init();
|
||||
|
||||
// PERFORMANCE MARKER (2): navigationInteractive
|
||||
// Designates that the app's *core* chrome or navigation interface
|
||||
// has its events bound and is ready for user interaction.
|
||||
window.performance.mark('navigationInteractive');
|
||||
|
||||
// PERFORMANCE MARKER (3): visuallyLoaded
|
||||
// Designates that the app is visually loaded (e.g.: all of the
|
||||
// "above-the-fold" content exists in the DOM and is marked as
|
||||
// ready to be displayed).
|
||||
window.performance.mark('visuallyLoaded');
|
||||
|
||||
// PERFORMANCE MARKER (4): contentInteractive
|
||||
// Designates that the app has its events bound for the minimum
|
||||
// set of functionality to allow the user to interact with the
|
||||
// "above-the-fold" content.
|
||||
window.performance.mark('contentInteractive');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Turn off radio immediately when window is unloaded.
|
||||
window.addEventListener('unload', function(e) {
|
||||
mozFMRadio.disable();
|
||||
}, false);
|
||||
|
||||
18
gaia/apps/fm/jsdoc.json
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "fm",
|
||||
"tags": {
|
||||
"dictionaries": ["jsdoc"]
|
||||
},
|
||||
"source": {
|
||||
"include": ["apps/fm/js/**/*.js"]
|
||||
},
|
||||
"plugins": [
|
||||
"plugins/markdown",
|
||||
"plugins/summarize"
|
||||
],
|
||||
"opts": {
|
||||
"private": true,
|
||||
"recurse": true,
|
||||
"destination": "docs/fm"
|
||||
}
|
||||
}
|
||||
18
gaia/apps/fm/locales/fm.en-US.properties
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
noAntenna = Plug in a headset
|
||||
noAntennaMsg = FM Radio requires a plugged in headset to receive radio signals.
|
||||
airplaneModeHeader = Airplane mode is on
|
||||
airplaneModeMsg = Turn off Airplane mode to use FM Radio.
|
||||
|
||||
frequency-MHz = {{ value }} <small>MHz</small>
|
||||
fav-frequency-MHz = {{ value }} <small>MHz</small>
|
||||
|
||||
# The following strings are spoken by screen readers and not shown on the
|
||||
# screen.
|
||||
speaker-switch.ariaLabel = Speaker
|
||||
bookmark-button.ariaLabel = Favorite
|
||||
dialer-container.ariaLabel = Frequency
|
||||
frequency-op-seekdown.ariaLabel = Seek Down
|
||||
frequency-op-seekup.ariaLabel = Seek Up
|
||||
power-switch-on.ariaLabel = On
|
||||
power-switch-off.ariaLabel = Off
|
||||
fav-list-container.ariaLabel = Favorited stations
|
||||
47
gaia/apps/fm/manifest.webapp
Executable file
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "FM Radio",
|
||||
"description": "Gaia FM Radio",
|
||||
"type": "certified",
|
||||
"launch_path": "/index.html",
|
||||
"developer": {
|
||||
"name": "The Gaia Team",
|
||||
"url": "https://github.com/mozilla-b2g/gaia"
|
||||
},
|
||||
"permissions": {
|
||||
"themeable": {},
|
||||
"fmradio": {},
|
||||
"settings": {
|
||||
"access": "readonly"
|
||||
},
|
||||
"speaker-control": {},
|
||||
"audio-channel-content": {},
|
||||
"moz-extremely-unstable-and-will-change-webcomponents": {}
|
||||
},
|
||||
"locales": {
|
||||
"ar": {
|
||||
"name": "FM Radio",
|
||||
"description": "Gaia FM Radio"
|
||||
},
|
||||
"en-US": {
|
||||
"name": "FM Radio",
|
||||
"description": "Gaia FM Radio"
|
||||
},
|
||||
"fr": {
|
||||
"name": "Radio FM",
|
||||
"description": "Radio FM de Gaia"
|
||||
},
|
||||
"zh-TW": {
|
||||
"name": "收音机",
|
||||
"description": "收音机"
|
||||
}
|
||||
},
|
||||
"default_locale": "en-US",
|
||||
"icons": {
|
||||
"84": "/style/icons/fm_84.png",
|
||||
"126": "/style/icons/fm_126.png",
|
||||
"142": "/style/icons/fm_142.png",
|
||||
"189": "/style/icons/fm_189.png",
|
||||
"284": "/style/icons/fm_284.png"
|
||||
},
|
||||
"orientation": "default"
|
||||
}
|
||||
10
gaia/apps/fm/package.json
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"webide": {
|
||||
"prepackage": {
|
||||
"command": "make",
|
||||
"env": ["APP=fm"],
|
||||
"cwd": "../.."
|
||||
},
|
||||
"packageDir": "../../build_stage/fm/"
|
||||
}
|
||||
}
|
||||
522
gaia/apps/fm/style/fm.css
Executable file
|
|
@ -0,0 +1,522 @@
|
|||
html, body {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
color: #FFF;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. No content inside the app
|
||||
* should be user-selectable.
|
||||
*/
|
||||
* {
|
||||
-moz-user-select: none; /* 1 */
|
||||
}
|
||||
|
||||
a {
|
||||
outline: 0 none;
|
||||
text-decoration: none;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: #000;
|
||||
background-color: #00ABCD;
|
||||
}
|
||||
|
||||
.hidden-block {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #2F3337;
|
||||
}
|
||||
|
||||
#container > div {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/****** styles for frequency view *******/
|
||||
#frequency-bar {
|
||||
border-bottom: 1px solid #24292E;
|
||||
box-sizing: border-box;
|
||||
top: 0;
|
||||
height: 10rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background-color: #353A41;
|
||||
overflow: hidden;
|
||||
transition: background-color 680ms ease;
|
||||
}
|
||||
|
||||
#frequency-bar.dim {
|
||||
background-color: #232323;
|
||||
}
|
||||
|
||||
#frequency-bar > div {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#frequency-bar a {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
line-height: 4rem;
|
||||
width: 3.8rem;
|
||||
height: 3.8rem;
|
||||
font-size: 1.4rem;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-top: 3rem;
|
||||
box-shadow: 0 -.1rem .1rem #111;
|
||||
}
|
||||
|
||||
#frequency-display {
|
||||
position: relative;
|
||||
transition: opacity 680ms ease;
|
||||
}
|
||||
|
||||
.dim #frequency-display {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
#bookmark-button {
|
||||
top: 0;
|
||||
right: 2rem;
|
||||
}
|
||||
|
||||
#frequency {
|
||||
font-size: 6rem;
|
||||
font-weight: 300;
|
||||
color: #fff;
|
||||
padding-top: 1.5rem;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#frequency small {
|
||||
font-size: 1.5rem;
|
||||
padding-left: 0.3rem;
|
||||
}
|
||||
|
||||
#bookmark-button {
|
||||
background: url("images/toggle-fav-star-off.png") no-repeat center center / 4rem;
|
||||
}
|
||||
|
||||
#bookmark-button:focus {
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
#bookmark-button:active,
|
||||
#bookmark-button[data-bookmarked="true"]:active {
|
||||
background: #00ABCD url("images/toggle-fav-star-pressed.png") no-repeat center center / 4rem;
|
||||
}
|
||||
|
||||
#bookmark-button[data-bookmarked="true"] {
|
||||
background: url("images/toggle-fav-star-on.png") no-repeat center center / 4rem;
|
||||
}
|
||||
|
||||
#speaker-switch {
|
||||
top: 0;
|
||||
left: 2rem;
|
||||
background: url("images/speaker-off.png") no-repeat center center / 4rem;
|
||||
}
|
||||
|
||||
#speaker-switch:focus {
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
#speaker-switch:active,
|
||||
#speaker-switch[data-speaker-on="true"]:active {
|
||||
background: #00ABCD url("images/speaker-pressed.png") no-repeat center center / 4rem;
|
||||
}
|
||||
|
||||
#speaker-switch[data-speaker-on="true"] {
|
||||
background: url("images/speaker-on.png") no-repeat center center / 4rem;
|
||||
}
|
||||
/***** end *****/
|
||||
|
||||
/**** styles for frequency dialer *****/
|
||||
#dialer-bar {
|
||||
top: 10rem;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
height: 9rem;
|
||||
overflow: hidden;
|
||||
background: #2F3337;
|
||||
color: #FFF;
|
||||
box-shadow: 0 0.2rem 0.4rem rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#dialer-container {
|
||||
box-shadow: inset 0 0.05rem 0 #3A3F44;
|
||||
position: relative;
|
||||
padding-left: 50%;
|
||||
margin: 0 2rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#dialer-container:before,
|
||||
#dialer-container:after {
|
||||
content: "";
|
||||
background-color: #474E57;
|
||||
box-shadow: inset 0 0.05rem 0 #585E65;
|
||||
position: absolute;
|
||||
width: 2rem;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#dialer-container:before {
|
||||
border-right: 0.1rem solid #24292E;
|
||||
left: -2rem;
|
||||
}
|
||||
|
||||
#dialer-container:after {
|
||||
border-left: 0.1rem solid #24292E;
|
||||
right: -2rem;
|
||||
}
|
||||
|
||||
#frequency-indicator {
|
||||
border-radius: 50%/0.1rem;
|
||||
height: 8.6rem;
|
||||
width: 1.6rem;
|
||||
margin: 0.6rem 0 0 -0.8rem;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
background: url(images/selector.png) no-repeat center center / 1.6rem;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
div.animation-on > div {
|
||||
transition: -moz-transform var(--transition-duration) ease 0s;
|
||||
}
|
||||
|
||||
#frequency-dialer {
|
||||
display: inline-block;
|
||||
display: -moz-inline-box;
|
||||
-moz-user-select: none;
|
||||
width: 100%;
|
||||
margin-left: -2.2rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#frequency-dialer .dialer-unit-mark-box {
|
||||
background: url(images/dial-notches.png) no-repeat 0 3.4rem / 32rem;
|
||||
overflow: hidden;
|
||||
height: 5.5rem;
|
||||
}
|
||||
|
||||
#frequency-dialer .dialer-unit {
|
||||
text-align: left;
|
||||
height: 7.5rem;
|
||||
position: absolute;
|
||||
width: 32rem;
|
||||
}
|
||||
|
||||
/* The beginning and end of the dial are masked to hide notches that
|
||||
are out of range. */
|
||||
.dialer-unit-mark-mask-start,
|
||||
.dialer-unit-mark-mask-end {
|
||||
background-color: #2F3337;
|
||||
display: block;
|
||||
height: 5.5rem;
|
||||
position: absolute;
|
||||
}
|
||||
/* The mask at the end should be jogged left by 0.3rem, since the
|
||||
mask is intended to cover everything except the first notch. */
|
||||
.dialer-unit-mark-mask-end {
|
||||
margin-left: .3rem;
|
||||
}
|
||||
|
||||
#frequency-dialer .dialer-unit-floor {
|
||||
color: #AAA;
|
||||
float: left;
|
||||
}
|
||||
/**** end *******/
|
||||
|
||||
/***** styles for favorites list ******/
|
||||
div#fav-list {
|
||||
overflow: auto;
|
||||
top: 19rem;
|
||||
bottom: 9rem;
|
||||
}
|
||||
|
||||
div#fav-list-container {
|
||||
max-height: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div#fav-list-container > div:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 2rem;
|
||||
right: 2rem;
|
||||
width: calc(100% - 4rem);
|
||||
height: .1rem;
|
||||
background-color: #3D4045;
|
||||
}
|
||||
|
||||
div#fav-list-container > div {
|
||||
position: relative;
|
||||
font-size: 2.3rem;
|
||||
font-weight: 300;
|
||||
height: 4.4rem;
|
||||
padding-bottom: 0.2rem;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
div#fav-list-container > div:active {
|
||||
background-color: rgba(52,140,158, .6);
|
||||
}
|
||||
|
||||
div#fav-list-container > div:active:before,
|
||||
div#fav-list-container > div:active + div:before {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
div#fav-list-container div.fav-list-remove-button {
|
||||
height: 100%;
|
||||
width: 4rem;
|
||||
margin-left: 2rem;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
background: url("images/fav-star.png") no-repeat center center / 1.4rem;
|
||||
}
|
||||
|
||||
div#fav-list-container div.selected div.fav-list-remove-button {
|
||||
background: url("images/fav-star-selected.png") no-repeat center center / 1.4rem;
|
||||
}
|
||||
|
||||
div#fav-list-container div.fav-list-frequency {
|
||||
float: right;
|
||||
padding: 0.8rem -moz-calc(50% - 2.4rem) 0 0;
|
||||
text-align: right;
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
div#fav-list-container div.fav-list-frequency small {
|
||||
font-size: 1.5rem;
|
||||
position: absolute;
|
||||
padding: .6rem .2rem 0 1rem;
|
||||
}
|
||||
/***** end *****/
|
||||
|
||||
/***** styles for status bars *****/
|
||||
#action-bar {
|
||||
bottom: 0;
|
||||
border-top: .1rem solid #3A3E46;
|
||||
box-shadow: 0 0 .2rem #111;
|
||||
height: 9rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Styles for action bar */
|
||||
/* FIX: it doesn't hide itself */
|
||||
#action-bar[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#action-bar > div {
|
||||
text-align: center;
|
||||
padding: 1rem 0 0;
|
||||
width: 33.333%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#action-bar > div:first-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#action-bar > div:last-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#action-bar a {
|
||||
display: inline-block;
|
||||
border: none;
|
||||
font-size: 2.5rem;
|
||||
width: 7rem;
|
||||
height: 7rem;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
box-shadow: 0 .1rem .1rem #A8A8A8 inset, 0 .2rem 0 #1e2226;
|
||||
}
|
||||
|
||||
#action-bar a:active {
|
||||
color: #FFF;
|
||||
background-color: #00ABCD;
|
||||
box-shadow: 0 .1rem .1rem #3F6978 inset, 0 .2rem 0 #1e2226;
|
||||
}
|
||||
|
||||
#power-switch, #power-switch span {
|
||||
background-image: url(images/play.png), url(images/reflection-72.png);
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
background-color: transparent;
|
||||
background-position: center center, 0 0;
|
||||
background-size: 7.2rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#power-switch:active {
|
||||
background-image: url(images/play.png);
|
||||
}
|
||||
|
||||
#power-switch[data-enabled="true"] {
|
||||
background-image: url(images/stop.png), url(images/reflection-72.png);
|
||||
}
|
||||
|
||||
#power-switch[data-enabled="true"]:active {
|
||||
background-image: url(images/stop.png);
|
||||
}
|
||||
|
||||
#power-switch span {
|
||||
background-image: url(images/spinner-FMRadio.png);
|
||||
background-size: 6.6rem;
|
||||
display: none;
|
||||
height: 6.6rem;
|
||||
left: 0.2rem;
|
||||
position: absolute;
|
||||
top: 0.2rem;
|
||||
width: 6.6rem;
|
||||
}
|
||||
|
||||
#power-switch[data-enabling="true"] span,
|
||||
#power-switch[data-seeking="true"] span {
|
||||
animation: 0.9s spinner-animation infinite steps(30);
|
||||
display: block;
|
||||
}
|
||||
|
||||
@keyframes spinner-animation {
|
||||
from {
|
||||
transform: rotate(1deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
a#frequency-op-seekdown {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
margin: 1rem 0;
|
||||
background-image: url(images/seekdown.png), url(images/reflection-56.png);
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
background-color: transparent;
|
||||
background-position: center center;
|
||||
background-size: 5.6rem;
|
||||
}
|
||||
|
||||
a#frequency-op-seekdown:active {
|
||||
background-image: url(images/seekdown.png);
|
||||
}
|
||||
|
||||
a#frequency-op-seekup {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
margin: 1rem 0;
|
||||
background-image: url(images/seekup.png), url(images/reflection-56.png);
|
||||
background-repeat: no-repeat, no-repeat;
|
||||
background-color: transparent;
|
||||
background-position: center center;
|
||||
background-size: 5.6rem;
|
||||
}
|
||||
|
||||
a#frequency-op-seekup:active {
|
||||
background-image: url(images/seekup.png);
|
||||
}
|
||||
/**** end ****/
|
||||
|
||||
/*** styles for warning box ****/
|
||||
.warning {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
color: #FFF;
|
||||
background-color: #2d2d2d;
|
||||
}
|
||||
|
||||
.warning .warning-header {
|
||||
padding: 1rem 3rem 1rem 3rem;
|
||||
font-weight: normal;
|
||||
font-size: 1.9rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.warning .warning-body {
|
||||
font-weight: 300;
|
||||
font-size: 2.5rem;
|
||||
padding: .5rem;
|
||||
border-top: .1rem solid #686868;
|
||||
margin: 0 2.5rem;
|
||||
}
|
||||
|
||||
/* Press (default & recommend) */
|
||||
.warning .menu button:active {
|
||||
border-color: #008aaa;
|
||||
color: #333;
|
||||
}
|
||||
/**** end ****/
|
||||
|
||||
/*** styles for antenna warning box ****/
|
||||
#antenna-warning {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#antenna-warning:not([hidden]) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#antenna-warning > div:first-child {
|
||||
height: 60%;
|
||||
background: url(images/headset.png) no-repeat center center / 18.4rem;
|
||||
}
|
||||
|
||||
#antenna-warning .warning-header {
|
||||
margin-top: -2.5rem;
|
||||
}
|
||||
/******* end *******/
|
||||
|
||||
/*** styles for airplane mode warning box ****/
|
||||
#airplane-mode-warning {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#airplane-mode-warning .warning-header {
|
||||
margin-top: 50%;
|
||||
}
|
||||
/******* end *******/
|
||||
|
||||
/*** styles for RTL languages ****/
|
||||
/* The scrollbar should be on the left.
|
||||
* But direction is explicitly set to LTR on #container
|
||||
So we force #fav-list to be RTL, while keeping the children LTR
|
||||
*/
|
||||
html[dir=rtl] #fav-list {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
html[dir=rtl] #fav-list > * {
|
||||
direction: ltr;
|
||||
}
|
||||
/******* end *******/
|
||||
BIN
gaia/apps/fm/style/icons/fm_126.png
Executable file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
gaia/apps/fm/style/icons/fm_142.png
Executable file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
gaia/apps/fm/style/icons/fm_189.png
Executable file
|
After Width: | Height: | Size: 5 KiB |
BIN
gaia/apps/fm/style/icons/fm_284.png
Executable file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
gaia/apps/fm/style/icons/fm_84.png
Executable file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
gaia/apps/fm/style/images/dial-notches.png
Executable file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
gaia/apps/fm/style/images/dial-notches@1.5x.png
Executable file
|
After Width: | Height: | Size: 504 B |
BIN
gaia/apps/fm/style/images/dial-notches@2.25x.png
Executable file
|
After Width: | Height: | Size: 432 B |
BIN
gaia/apps/fm/style/images/dial-notches@2x.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
gaia/apps/fm/style/images/fav-star-selected.png
Executable file
|
After Width: | Height: | Size: 270 B |
BIN
gaia/apps/fm/style/images/fav-star-selected@1.5x.png
Executable file
|
After Width: | Height: | Size: 624 B |
BIN
gaia/apps/fm/style/images/fav-star-selected@2.25x.png
Executable file
|
After Width: | Height: | Size: 504 B |
BIN
gaia/apps/fm/style/images/fav-star-selected@2x.png
Executable file
|
After Width: | Height: | Size: 451 B |
BIN
gaia/apps/fm/style/images/fav-star.png
Executable file
|
After Width: | Height: | Size: 285 B |
BIN
gaia/apps/fm/style/images/fav-star@1.5x.png
Executable file
|
After Width: | Height: | Size: 624 B |
BIN
gaia/apps/fm/style/images/fav-star@2.25x.png
Executable file
|
After Width: | Height: | Size: 554 B |
BIN
gaia/apps/fm/style/images/fav-star@2x.png
Executable file
|
After Width: | Height: | Size: 486 B |
BIN
gaia/apps/fm/style/images/headset.png
Executable file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
gaia/apps/fm/style/images/headset@1.5x.png
Executable file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
gaia/apps/fm/style/images/headset@2.25x.png
Executable file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
gaia/apps/fm/style/images/headset@2x.png
Executable file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
gaia/apps/fm/style/images/play.png
Executable file
|
After Width: | Height: | Size: 397 B |
BIN
gaia/apps/fm/style/images/play@1.5x.png
Executable file
|
After Width: | Height: | Size: 586 B |
BIN
gaia/apps/fm/style/images/play@2.25x.png
Executable file
|
After Width: | Height: | Size: 732 B |
BIN
gaia/apps/fm/style/images/play@2x.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
gaia/apps/fm/style/images/reflection-56.png
Executable file
|
After Width: | Height: | Size: 521 B |
BIN
gaia/apps/fm/style/images/reflection-56@1.5x.png
Executable file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
gaia/apps/fm/style/images/reflection-56@2.25x.png
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
gaia/apps/fm/style/images/reflection-56@2x.png
Executable file
|
After Width: | Height: | Size: 1,017 B |
BIN
gaia/apps/fm/style/images/reflection-72.png
Executable file
|
After Width: | Height: | Size: 695 B |
BIN
gaia/apps/fm/style/images/reflection-72@1.5x.png
Executable file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
gaia/apps/fm/style/images/reflection-72@2.25x.png
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
gaia/apps/fm/style/images/reflection-72@2x.png
Executable file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
gaia/apps/fm/style/images/seekdown.png
Executable file
|
After Width: | Height: | Size: 308 B |
BIN
gaia/apps/fm/style/images/seekdown@1.5x.png
Executable file
|
After Width: | Height: | Size: 463 B |
BIN
gaia/apps/fm/style/images/seekdown@2.25x.png
Executable file
|
After Width: | Height: | Size: 609 B |
BIN
gaia/apps/fm/style/images/seekdown@2x.png
Executable file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
gaia/apps/fm/style/images/seekup.png
Executable file
|
After Width: | Height: | Size: 334 B |
BIN
gaia/apps/fm/style/images/seekup@1.5x.png
Executable file
|
After Width: | Height: | Size: 474 B |
BIN
gaia/apps/fm/style/images/seekup@2.25x.png
Executable file
|
After Width: | Height: | Size: 683 B |
BIN
gaia/apps/fm/style/images/seekup@2x.png
Executable file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
gaia/apps/fm/style/images/selector.png
Executable file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
gaia/apps/fm/style/images/selector@1.5x.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
gaia/apps/fm/style/images/selector@2.25x.png
Executable file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
gaia/apps/fm/style/images/selector@2x.png
Executable file
|
After Width: | Height: | Size: 3 KiB |
BIN
gaia/apps/fm/style/images/speaker-off.png
Executable file
|
After Width: | Height: | Size: 298 B |
BIN
gaia/apps/fm/style/images/speaker-off@1.5x.png
Executable file
|
After Width: | Height: | Size: 575 B |
BIN
gaia/apps/fm/style/images/speaker-off@2.25x.png
Executable file
|
After Width: | Height: | Size: 795 B |
BIN
gaia/apps/fm/style/images/speaker-off@2x.png
Executable file
|
After Width: | Height: | Size: 759 B |
BIN
gaia/apps/fm/style/images/speaker-on.png
Executable file
|
After Width: | Height: | Size: 457 B |
BIN
gaia/apps/fm/style/images/speaker-on@1.5x.png
Executable file
|
After Width: | Height: | Size: 679 B |
BIN
gaia/apps/fm/style/images/speaker-on@2.25x.png
Executable file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
gaia/apps/fm/style/images/speaker-on@2x.png
Executable file
|
After Width: | Height: | Size: 844 B |
BIN
gaia/apps/fm/style/images/speaker-pressed.png
Executable file
|
After Width: | Height: | Size: 287 B |
BIN
gaia/apps/fm/style/images/speaker-pressed@1.5x.png
Executable file
|
After Width: | Height: | Size: 402 B |
BIN
gaia/apps/fm/style/images/speaker-pressed@2.25x.png
Executable file
|
After Width: | Height: | Size: 743 B |
BIN
gaia/apps/fm/style/images/speaker-pressed@2x.png
Executable file
|
After Width: | Height: | Size: 526 B |
BIN
gaia/apps/fm/style/images/spinner-FMRadio.png
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
gaia/apps/fm/style/images/spinner-FMRadio@1.5x.png
Executable file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
gaia/apps/fm/style/images/spinner-FMRadio@2.25x.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
gaia/apps/fm/style/images/spinner-FMRadio@2x.png
Executable file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
gaia/apps/fm/style/images/stop.png
Executable file
|
After Width: | Height: | Size: 211 B |
BIN
gaia/apps/fm/style/images/stop@1.5x.png
Executable file
|
After Width: | Height: | Size: 247 B |
BIN
gaia/apps/fm/style/images/stop@2.25x.png
Executable file
|
After Width: | Height: | Size: 273 B |
BIN
gaia/apps/fm/style/images/stop@2x.png
Executable file
|
After Width: | Height: | Size: 546 B |
BIN
gaia/apps/fm/style/images/toggle-fav-star-off.png
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
gaia/apps/fm/style/images/toggle-fav-star-off@1.5x.png
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
gaia/apps/fm/style/images/toggle-fav-star-off@2.25x.png
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
gaia/apps/fm/style/images/toggle-fav-star-off@2x.png
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
gaia/apps/fm/style/images/toggle-fav-star-on.png
Executable file
|
After Width: | Height: | Size: 406 B |
BIN
gaia/apps/fm/style/images/toggle-fav-star-on@1.5x.png
Executable file
|
After Width: | Height: | Size: 554 B |
BIN
gaia/apps/fm/style/images/toggle-fav-star-on@2.25x.png
Executable file
|
After Width: | Height: | Size: 825 B |
BIN
gaia/apps/fm/style/images/toggle-fav-star-on@2x.png
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
gaia/apps/fm/style/images/toggle-fav-star-pressed.png
Executable file
|
After Width: | Height: | Size: 312 B |
BIN
gaia/apps/fm/style/images/toggle-fav-star-pressed@1.5x.png
Executable file
|
After Width: | Height: | Size: 441 B |
BIN
gaia/apps/fm/style/images/toggle-fav-star-pressed@2.25x.png
Executable file
|
After Width: | Height: | Size: 769 B |
BIN
gaia/apps/fm/style/images/toggle-fav-star-pressed@2x.png
Executable file
|
After Width: | Height: | Size: 666 B |
27
gaia/apps/fm/test/build/integration/fm_test.js
Executable file
|
|
@ -0,0 +1,27 @@
|
|||
'use strict';
|
||||
|
||||
/* jshint node: true, mocha: true */
|
||||
/* global suiteSetup */
|
||||
|
||||
var helper = require('helper');
|
||||
|
||||
suite('FM tests', function() {
|
||||
suiteSetup(helper.cleanupWorkspace);
|
||||
teardown(helper.cleanupWorkspace);
|
||||
|
||||
test('APP=fm make', function(done) {
|
||||
helper.exec('APP=fm make', function(error, stdout, stderr) {
|
||||
helper.checkError(error, stdout, stderr);
|
||||
|
||||
var zipPath = process.cwd() +
|
||||
'/profile/webapps/fm.gaiamobile.org/application.zip';
|
||||
var checkList = ['manifest.webapp'];
|
||||
|
||||
checkList.forEach(function(filePath) {
|
||||
helper.checkFilePathInZip(zipPath, filePath);
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
146
gaia/apps/fm/test/marionette/fm_test.js
Executable file
|
|
@ -0,0 +1,146 @@
|
|||
/* global marionette, require, setup, suite, test */
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
var Fm = require('./lib/fm.js');
|
||||
|
||||
|
||||
marionette('FM Radio test', function() {
|
||||
|
||||
var client = marionette.client({
|
||||
profile: {},
|
||||
desiredCapabilities: { raisesAccessibilityExceptions: false }
|
||||
});
|
||||
|
||||
|
||||
var fm;
|
||||
|
||||
setup(function() {
|
||||
fm = new Fm(client);
|
||||
|
||||
fm.launch();
|
||||
fm.waitForLoaded();
|
||||
|
||||
});
|
||||
|
||||
|
||||
suite('FM Radio UI', function() {
|
||||
test('Test basic UI', function() {
|
||||
try {
|
||||
var freq = fm.getCurrentFrequency();
|
||||
assert.equal(freq, '87.5');
|
||||
|
||||
var dialFreq = fm.getDialFreq();
|
||||
assert.equal(dialFreq, '87.5');
|
||||
|
||||
assert.equal(freq, dialFreq);
|
||||
|
||||
// test seek up and down
|
||||
var startFreq = freq;
|
||||
|
||||
fm.seekUp();
|
||||
|
||||
freq = fm.getCurrentFrequency();
|
||||
assert.notEqual(freq, startFreq);
|
||||
|
||||
dialFreq = fm.getDialFreq();
|
||||
assert.equal(parseFloat(dialFreq), parseFloat(freq));
|
||||
|
||||
fm.seekDown();
|
||||
|
||||
freq = fm.getCurrentFrequency();
|
||||
assert.equal(freq, startFreq);
|
||||
|
||||
dialFreq = fm.getDialFreq();
|
||||
assert.equal(parseFloat(dialFreq), parseFloat(freq));
|
||||
|
||||
// see about the frequency being saved.
|
||||
fm.seekUp();
|
||||
startFreq = fm.getCurrentFrequency();
|
||||
fm.close();
|
||||
|
||||
fm.launch();
|
||||
fm.waitForLoaded();
|
||||
freq = fm.getCurrentFrequency();
|
||||
assert.equal(parseFloat(freq), parseFloat(startFreq));
|
||||
|
||||
} catch(e) {
|
||||
assert.ok(false, e.stack);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
suite('FM Radio favourites', function() {
|
||||
test('Test favouriting', function() {
|
||||
try {
|
||||
fm.seekUp();
|
||||
|
||||
var isFav = fm.isFav();
|
||||
assert.ok(!isFav, 'Frequency shouldn\'t have been favourited');
|
||||
|
||||
fm.fav();
|
||||
isFav = fm.isFav();
|
||||
assert.ok(isFav, 'Frequency should be favourited');
|
||||
|
||||
fm.close();
|
||||
|
||||
fm.launch();
|
||||
fm.waitForLoaded();
|
||||
|
||||
isFav = fm.isFav();
|
||||
assert.ok(isFav, 'Frequency should be favourited after reboot.');
|
||||
|
||||
// checking we have a selected favourite.
|
||||
var elem = fm.selectedFavItem();
|
||||
assert.ok(elem, 'Can\'t find selected favourite');
|
||||
|
||||
var freq = fm.getCurrentFrequency();
|
||||
|
||||
// checking the text content of the favourite.
|
||||
var favFreq = fm.selectedFavItemText();
|
||||
assert.equal(favFreq, freq,
|
||||
'Current frequency and selected favourite do not match');
|
||||
|
||||
// checking the id of the favourite.
|
||||
var id = elem.getAttribute('id');
|
||||
favFreq = id.substring(id.indexOf('-') + 1);
|
||||
assert.equal(
|
||||
parseFloat(favFreq), parseFloat(freq),
|
||||
'Current frequency and selected favourite ID do not match');
|
||||
|
||||
fm.seekUp();
|
||||
var newFreq = fm.getCurrentFrequency();
|
||||
|
||||
assert.notEqual(freq, newFreq);
|
||||
|
||||
fm.fav();
|
||||
assert.ok(fm.isFav());
|
||||
|
||||
// count number of favs. Should be 2.
|
||||
var elems = client.findElement(Fm.Selector.favList).
|
||||
findElements(Fm.Selector.favItem);
|
||||
assert.equal(elems.length, 2);
|
||||
|
||||
favFreq = fm.selectedFavItemText();
|
||||
assert.equal(favFreq, newFreq);
|
||||
|
||||
elem.tap();
|
||||
assert.ok(fm.isFav());
|
||||
|
||||
favFreq = fm.selectedFavItemText();
|
||||
assert.notEqual(favFreq, newFreq);
|
||||
|
||||
// remove the favourite.
|
||||
fm.fav();
|
||||
assert.ok(!fm.isFav());
|
||||
|
||||
// count number of favs. Should be 1.
|
||||
elems = client.findElement(Fm.Selector.favList).
|
||||
findElements(Fm.Selector.favItem);
|
||||
assert.equal(elems.length, 1);
|
||||
} catch(e) {
|
||||
assert.ok(false, e.stack);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
141
gaia/apps/fm/test/marionette/lib/fm.js
Executable file
|
|
@ -0,0 +1,141 @@
|
|||
/* global require, module */
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
function Fm(client, origin) {
|
||||
this.client = client;
|
||||
this.origin = origin || ('app://' + Fm.DEFAULT_ORIGIN);
|
||||
this.actions = client.loader.getActions();
|
||||
}
|
||||
|
||||
module.exports = Fm;
|
||||
|
||||
Fm.DEFAULT_ORIGIN = 'fm.gaiamobile.org';
|
||||
|
||||
Fm.Selector = Object.freeze({
|
||||
|
||||
currentFrequency: '#frequency',
|
||||
dialerContainer: '#dialer-container',
|
||||
|
||||
seekDown: '#frequency-op-seekdown',
|
||||
seekUp: '#frequency-op-seekup',
|
||||
powerSwitch: '#power-switch',
|
||||
favButton: '#bookmark-button',
|
||||
favList: '#fav-list',
|
||||
favListContainer: '#fav-list-container',
|
||||
favItemSelected: '.fav-list-item.selected',
|
||||
favItemSelectedFreq:'.fav-list-item.selected .fav-list-frequency',
|
||||
favItem: '.fav-list-item',
|
||||
});
|
||||
|
||||
Fm.prototype = {
|
||||
|
||||
client: null,
|
||||
|
||||
|
||||
get powerSwitch() {
|
||||
return this.client.findElement(Fm.Selector.powerSwitch);
|
||||
},
|
||||
|
||||
get favButton() {
|
||||
return this.client.findElement(Fm.Selector.favButton);
|
||||
},
|
||||
|
||||
get favListContainer() {
|
||||
return this.client.findElement(Fm.Selector.favListContainer);
|
||||
},
|
||||
|
||||
// debug function.
|
||||
debugDocument: function() {
|
||||
var debug = this.client.executeScript(function() {
|
||||
return document.documentElement.innerHTML;
|
||||
});
|
||||
console.log('debug', debug);
|
||||
},
|
||||
|
||||
launch: function() {
|
||||
this.client.switchToFrame();
|
||||
this.client.apps.launch(this.origin);
|
||||
this.client.apps.switchToApp(this.origin);
|
||||
this.client.helper.waitForElement('body');
|
||||
},
|
||||
|
||||
close: function() {
|
||||
this.client.apps.close(this.origin);
|
||||
},
|
||||
|
||||
// since we can't get the child text isolated with marionette.
|
||||
// You might want to call _trimFreqTextContent() on the result.
|
||||
_getTextNodeContent: function(sel) {
|
||||
return this.client.executeScript(function(sel) {
|
||||
return document.querySelector(sel).firstChild.textContent;
|
||||
}, [sel]);
|
||||
},
|
||||
|
||||
// make sure we have just the frequency
|
||||
_trimFreqTextContent: function(freq) {
|
||||
// for some reason these unicode delimiters are inserted
|
||||
// in the text node
|
||||
return freq.trim().replace(/[\u2068\u2069]/g, '');
|
||||
},
|
||||
|
||||
getCurrentFrequency: function() {
|
||||
var elem = this.client.findElement(Fm.Selector.currentFrequency);
|
||||
assert.ok(elem, 'Couldn\'t find frequency');
|
||||
|
||||
var freq = this._getTextNodeContent(Fm.Selector.currentFrequency);
|
||||
return this._trimFreqTextContent(freq);
|
||||
},
|
||||
|
||||
getDialFreq: function() {
|
||||
var elem = this.client.findElement(Fm.Selector.dialerContainer);
|
||||
assert.ok(elem, 'Couldn\'t find dialer container');
|
||||
var dialFreq = elem.getAttribute('aria-valuenow');
|
||||
return dialFreq;
|
||||
},
|
||||
|
||||
seekDown: function() {
|
||||
var elem = this.client.findElement(Fm.Selector.seekDown);
|
||||
assert.ok(elem, 'Seek Down button not found');
|
||||
elem.click();
|
||||
|
||||
this.client.waitFor(function() {
|
||||
return !this.powerSwitch.getAttribute('data-seeking');
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
seekUp: function() {
|
||||
var elem = this.client.findElement(Fm.Selector.seekUp);
|
||||
assert.ok(elem, 'Seek Down button not found');
|
||||
elem.click();
|
||||
|
||||
this.client.waitFor(function() {
|
||||
return !this.powerSwitch.getAttribute('data-seeking');
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
fav: function() {
|
||||
var elem = this.favButton;
|
||||
elem.tap();
|
||||
},
|
||||
|
||||
selectedFavItem: function() {
|
||||
return this.favListContainer.findElement(Fm.Selector.favItemSelected);
|
||||
},
|
||||
|
||||
selectedFavItemText: function() {
|
||||
var freq = this._getTextNodeContent(Fm.Selector.favItemSelectedFreq);
|
||||
return this._trimFreqTextContent(freq);
|
||||
},
|
||||
|
||||
isFav: function() {
|
||||
return this.favButton.getAttribute('data-bookmarked') == 'true';
|
||||
},
|
||||
|
||||
waitForLoaded: function() {
|
||||
this.client.waitFor(function() {
|
||||
return this.powerSwitch.getAttribute('data-enabled');
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
17
gaia/apps/fm/test/unit/.jshintrc
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"extends": "../../../../.jshintrc",
|
||||
"predef": [
|
||||
"assert",
|
||||
"suite",
|
||||
"test",
|
||||
"setup",
|
||||
"teardown",
|
||||
"suiteSetup",
|
||||
"suiteTeardown",
|
||||
"require",
|
||||
"requireApp",
|
||||
"sinon",
|
||||
"mocha",
|
||||
"loadBodyHTML"
|
||||
]
|
||||
}
|
||||
386
gaia/apps/fm/test/unit/fm_test.js
Executable file
|
|
@ -0,0 +1,386 @@
|
|||
'use strict';
|
||||
/* global
|
||||
$,
|
||||
$$,
|
||||
airplaneModeEnabled:true,
|
||||
enabling,
|
||||
favoritesList,
|
||||
frequencyDialer,
|
||||
historyList,
|
||||
MockL10n,
|
||||
mozFMRadio,
|
||||
updateEnablingState,
|
||||
updateFreqUI,
|
||||
updateWarningModeUI */
|
||||
|
||||
requireApp('shared/js/airplane_mode_helper.js');
|
||||
requireApp('fm/js/fm.js');
|
||||
require('/shared/test/unit/load_body_html_helper.js');
|
||||
require('/shared/test/unit/mocks/mock_l20n.js');
|
||||
|
||||
suite('FM', function() {
|
||||
function setFrequency(frequency) {
|
||||
var setFreq = frequencyDialer.setFrequency(frequency);
|
||||
if (frequency < mozFMRadio.frequencyLowerBound) {
|
||||
assert.equal(setFreq, mozFMRadio.frequencyLowerBound);
|
||||
} else if (frequency > mozFMRadio.frequencyUpperBound) {
|
||||
assert.equal(setFreq, mozFMRadio.frequencyUpperBound);
|
||||
} else {
|
||||
assert.equal(setFreq, frequency);
|
||||
}
|
||||
}
|
||||
|
||||
function testScreenReaderSwipe(swipeDirection, startFreq, expectedFreq) {
|
||||
var modifier, key;
|
||||
|
||||
if (swipeDirection === 'up') {
|
||||
modifier = 0.1;
|
||||
key = 38; // DOM_VK_UP = 38
|
||||
} else if (swipeDirection === 'down') {
|
||||
modifier = -0.1;
|
||||
key = 40; // DOM_VK_DOWN = 40
|
||||
}
|
||||
|
||||
var keyEvent = document.createEvent('KeyboardEvent');
|
||||
keyEvent.initKeyEvent('keypress',
|
||||
true, true, window,
|
||||
false, false, false, false,
|
||||
key, 0);
|
||||
|
||||
frequencyDialer.setFrequency(startFreq);
|
||||
|
||||
$('dialer-container').dispatchEvent(keyEvent);
|
||||
assert.equal(frequencyDialer._currentFreqency, expectedFreq);
|
||||
}
|
||||
|
||||
function testPowerSwitchLabel() {
|
||||
assert.equal(mozFMRadio.enabled ? 'power-switch-off' : 'power-switch-on',
|
||||
$('power-switch').getAttribute('data-l10n-id'));
|
||||
}
|
||||
|
||||
suite('frequency dialer', function() {
|
||||
var nativeL10n = document.l10n;
|
||||
|
||||
suiteSetup(function() {
|
||||
sinon.stub(favoritesList, '_save').returns(true);
|
||||
favoritesList._favList = {};
|
||||
sinon.stub(historyList, '_save').returns(true);
|
||||
|
||||
loadBodyHTML('/index.html');
|
||||
document.l10n = MockL10n;
|
||||
frequencyDialer.init();
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
document.l10n = nativeL10n;
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
test('resolved frequency within bounds', function() {
|
||||
setFrequency(92);
|
||||
});
|
||||
|
||||
test('resolved frequency exceeding upperbound', function() {
|
||||
setFrequency(mozFMRadio.frequencyUpperBound + 10);
|
||||
});
|
||||
|
||||
test('resolved frequency exceeding lowerbound', function() {
|
||||
setFrequency(mozFMRadio.frequencyLowerBound - 10);
|
||||
});
|
||||
|
||||
test('retrieved frequency', function() {
|
||||
assert.equal(frequencyDialer.getFrequency(), 87.5);
|
||||
});
|
||||
|
||||
test('updated #frequency dom display digits', function() {
|
||||
assert.deepEqual(
|
||||
document.l10n.getAttributes($('frequency')),
|
||||
{ id: 'frequency-MHz', args: { value: '87.5' } });
|
||||
});
|
||||
|
||||
test('compare new set frequency with aria-valuenow', function() {
|
||||
var frequency = 95.7;
|
||||
frequencyDialer.setFrequency(frequency);
|
||||
assert.equal($('dialer-container').getAttribute('aria-valuenow'),
|
||||
frequency.toString());
|
||||
});
|
||||
|
||||
test('screen reader swipe up on dialer', function() {
|
||||
testScreenReaderSwipe('up', 95.7, 95.8);
|
||||
testScreenReaderSwipe('down', 95.7, 95.6);
|
||||
});
|
||||
|
||||
test('maintain aria-pressed on bookmark button', function() {
|
||||
var bookmarkButton = $('bookmark-button');
|
||||
|
||||
// Start at arbitrary station
|
||||
mozFMRadio.setFrequency(88.6);
|
||||
updateFreqUI();
|
||||
assert.equal('false', bookmarkButton.getAttribute('aria-pressed'));
|
||||
// Add station to favorites
|
||||
favoritesList.add(88.6);
|
||||
updateFreqUI();
|
||||
assert.equal('true', bookmarkButton.getAttribute('aria-pressed'));
|
||||
// Browse to different station
|
||||
mozFMRadio.setFrequency(99.5);
|
||||
updateFreqUI();
|
||||
assert.equal('false', bookmarkButton.getAttribute('aria-pressed'));
|
||||
// Come back to first station
|
||||
mozFMRadio.setFrequency(88.6);
|
||||
updateFreqUI();
|
||||
assert.equal('true', bookmarkButton.getAttribute('aria-pressed'));
|
||||
// Remove station from favorites
|
||||
favoritesList.remove(88.6);
|
||||
updateFreqUI();
|
||||
assert.equal('false', bookmarkButton.getAttribute('aria-pressed'));
|
||||
});
|
||||
|
||||
// temporarily removing due to test not passing on TBPL Bug 876265
|
||||
// test('changed horizontal position of dialer', function() {
|
||||
// var prevX = frequencyDialer._translateX;
|
||||
// frequencyDialer.setFrequency(100);
|
||||
// assert.notEqual(frequencyDialer._translateX, prevX);
|
||||
// });
|
||||
|
||||
test('#frequency display precision to one decimal point', function() {
|
||||
var freq = document.l10n.getAttributes($('frequency')).args.value;
|
||||
assert.ok(freq.indexOf('.') > -1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('history list', function() {
|
||||
setup(function() {
|
||||
historyList._save = function() {
|
||||
return true;
|
||||
};
|
||||
});
|
||||
|
||||
test('item added to history list', function() {
|
||||
historyList.add(100);
|
||||
assert.equal(historyList._historyList.length, 1);
|
||||
});
|
||||
|
||||
test('gets the last frequency tuned', function() {
|
||||
assert.equal(historyList.last().frequency, 100);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('favorite list', function() {
|
||||
var nativeL10n = document.l10n;
|
||||
|
||||
|
||||
suiteSetup(function() {
|
||||
loadBodyHTML('/index.html');
|
||||
document.l10n = MockL10n;
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
document.l10n = nativeL10n;
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
test('item added to favorite list', function() {
|
||||
favoritesList.add(100);
|
||||
assert.equal(Object.keys(favoritesList._favList).length, 1);
|
||||
});
|
||||
|
||||
test('added item is updated in DOM #fav-list', function() {
|
||||
assert.equal($('fav-list-container').childNodes.length, 1);
|
||||
});
|
||||
|
||||
test('contains a frequency', function() {
|
||||
assert.ok(favoritesList.contains(100));
|
||||
});
|
||||
|
||||
test('selected frequency in DOM #fav-list', function() {
|
||||
favoritesList.select(100);
|
||||
assert.ok($$('#fav-list-container div.fav-list-item')[0]
|
||||
.classList.contains('selected'));
|
||||
});
|
||||
|
||||
test('removed frequency from list', function() {
|
||||
assert.ok(favoritesList.remove(100));
|
||||
});
|
||||
|
||||
test('removed item is updated in DOM #fav-list', function() {
|
||||
assert.equal($('fav-list-container').childNodes.length, 0);
|
||||
});
|
||||
|
||||
test('items are in desceding order', function() {
|
||||
favoritesList.add(100);
|
||||
favoritesList.add(101);
|
||||
favoritesList.add(102);
|
||||
var items = $$('#fav-list-container div.fav-list-item');
|
||||
var isAscending = true;
|
||||
for (var i = 0, len = items.length; i < len - 1; i++) {
|
||||
var itemA = items[i].childNodes[0].textContent;
|
||||
var itemB = items[i + 1].childNodes[0].textContent;
|
||||
if (itemA > itemB) {
|
||||
isAscending = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert.ok(isAscending);
|
||||
});
|
||||
|
||||
test('set aria-selected = true on active favorite stations, else false',
|
||||
function() {
|
||||
var testFreqs = [88.6, 103.7, 104.8, 55.6];
|
||||
var favorites = $$('#fav-list-container div.fav-list-item');
|
||||
|
||||
favoritesList.add(88.6);
|
||||
favoritesList.add(103.7);
|
||||
favoritesList.add(104.8);
|
||||
|
||||
testFreqs.forEach(function(testFreq, index, array) {
|
||||
favoritesList.select(testFreq);
|
||||
for (var i = 0; i < favorites.length; i++) {
|
||||
assert.equal(favoritesList._getElemFreq(favorites[i]) === testFreq ?
|
||||
'true' : 'false', favorites[i].getAttribute('aria-selected'));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('update display states', function() {
|
||||
var nativeL10n = document.l10n;
|
||||
|
||||
suiteSetup(function() {
|
||||
mozFMRadio.enabled = true;
|
||||
mozFMRadio.antennaAvailable = true;
|
||||
loadBodyHTML('/index.html');
|
||||
document.l10n = MockL10n;
|
||||
updateEnablingState(true);
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
document.l10n = nativeL10n;
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
suite('enabling UI', function() {
|
||||
|
||||
test('#power-switch enabled attribute is correctly set', function() {
|
||||
assert.equal(!!$('power-switch').dataset.enabled, mozFMRadio.enabled);
|
||||
});
|
||||
|
||||
test('#power-switchh enabling attribute is correctly set', function() {
|
||||
assert.equal(!!$('power-switch').dataset.enabling, enabling);
|
||||
});
|
||||
|
||||
test('#frequency-bar display is dimmed', function() {
|
||||
assert.ok($('frequency-bar').classList.contains('dim'));
|
||||
});
|
||||
|
||||
test('#antenna-warning is hidden', function() {
|
||||
assert.ok(!!$('antenna-warning').hidden, mozFMRadio.antennaAvailable);
|
||||
});
|
||||
|
||||
test('#power-switch has appropriate aria-label based on enabled status',
|
||||
function() {
|
||||
testPowerSwitchLabel();
|
||||
$('power-switch').click();
|
||||
testPowerSwitchLabel();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('update UI based on the airplane mode status', function() {
|
||||
var nativeL10n = document.l10n;
|
||||
|
||||
suiteSetup(function() {
|
||||
loadBodyHTML('/index.html');
|
||||
document.l10n = MockL10n;
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
document.l10n = nativeL10n;
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
suite('airplane mode on', function() {
|
||||
setup(function() {
|
||||
airplaneModeEnabled = true;
|
||||
updateWarningModeUI();
|
||||
});
|
||||
|
||||
test('#airplane-mode-warning is shown', function() {
|
||||
assert.equal(!!$('airplane-mode-warning').hidden, false);
|
||||
});
|
||||
|
||||
test('#container is hidden', function() {
|
||||
assert.equal($('container').classList.contains('hidden-block'), true);
|
||||
});
|
||||
|
||||
test('#antenna-warning is hidden', function() {
|
||||
assert.equal(!!$('antenna-warning').hidden, true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('airplane mode off', function() {
|
||||
setup(function() {
|
||||
airplaneModeEnabled = false;
|
||||
updateWarningModeUI();
|
||||
});
|
||||
|
||||
test('#airplane-mode-warning is hidden', function() {
|
||||
assert.equal(!!$('airplane-mode-warning').hidden, true);
|
||||
});
|
||||
|
||||
test('#antenna-warning is on and container is hidden', function() {
|
||||
mozFMRadio.antennaAvailable = false;
|
||||
updateWarningModeUI();
|
||||
assert.equal(!!$('antenna-warning').hidden, false);
|
||||
assert.equal($('container').classList.contains('hidden-block'), true);
|
||||
});
|
||||
|
||||
test('#antenna-warning is off and container is visible', function() {
|
||||
mozFMRadio.antennaAvailable = true;
|
||||
updateWarningModeUI();
|
||||
assert.equal(!!$('antenna-warning').hidden, true);
|
||||
assert.equal($('container').classList.contains('hidden-block'), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('update UI based on the antenna status', function() {
|
||||
var nativeL10n = document.l10n;
|
||||
|
||||
suiteSetup(function() {
|
||||
loadBodyHTML('/index.html');
|
||||
document.l10n = MockL10n;
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
document.l10n = nativeL10n;
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
suite('antenna is plugged in', function() {
|
||||
setup(function() {
|
||||
mozFMRadio.antennaAvailable = true;
|
||||
updateWarningModeUI();
|
||||
});
|
||||
|
||||
test('#antenna-warning is hidden', function() {
|
||||
assert.equal(!!$('antenna-warning').hidden, true);
|
||||
});
|
||||
});
|
||||
|
||||
suite('antenna is not plugged in', function() {
|
||||
setup(function() {
|
||||
mozFMRadio.antennaAvailable = false;
|
||||
updateWarningModeUI();
|
||||
});
|
||||
|
||||
test('#antenna-warning is shown', function() {
|
||||
assert.equal(!!$('antenna-warning').hidden, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||