diff --git a/WebFM/README.md b/WebFM/README.md
new file mode 100644
index 0000000..718f0ee
--- /dev/null
+++ b/WebFM/README.md
@@ -0,0 +1,8 @@
+WebFM API 사용하기
+=======================
+
+**소개**
+- WebFM API를 사용해서 샘플 앱을 작성합니다.
+
+**실행 방법**
+- Firefox OS Simulator를 이용하여, 구동해 볼 수 있습니다.
diff --git a/WebFM/index.html b/WebFM/index.html
new file mode 100644
index 0000000..5034d3f
--- /dev/null
+++ b/WebFM/index.html
@@ -0,0 +1,57 @@
+
+
+
+
+
+ FM Radio J
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
헤드셋을 연결해주세요.
+
FM 라디오 신호를 수신하려면 헤드셋이 연결되어야 합니다.
+
+
+
비행 모드로 설정되어 있습니다.
+
FM 라디오를 사용하려면 비행 모드를 해지해주세요.
+
+
+
+
diff --git a/WebFM/js/async_storage.js b/WebFM/js/async_storage.js
new file mode 100644
index 0000000..6cca66d
--- /dev/null
+++ b/WebFM/js/async_storage.js
@@ -0,0 +1,187 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+
+'use strict';
+
+/**
+ * This file defines an asynchronous version of the localStorage API, backed by
+ * an IndexedDB database. It creates a global asyncStorage object that has
+ * methods like the localStorage object.
+ *
+ * To store a value use setItem:
+ *
+ * asyncStorage.setItem('key', 'value');
+ *
+ * If you want confirmation that the value has been stored, pass a callback
+ * function as the third argument:
+ *
+ * asyncStorage.setItem('key', 'newvalue', function() {
+ * console.log('new value stored');
+ * });
+ *
+ * To read a value, call getItem(), but note that you must supply a callback
+ * function that the value will be passed to asynchronously:
+ *
+ * asyncStorage.getItem('key', function(value) {
+ * console.log('The value of key is:', value);
+ * });
+ *
+ * Note that unlike localStorage, asyncStorage does not allow you to store and
+ * retrieve values by setting and querying properties directly. You cannot just
+ * write asyncStorage.key; you have to explicitly call setItem() or getItem().
+ *
+ * removeItem(), clear(), length(), and key() are like the same-named methods of
+ * localStorage, but, like getItem() and setItem() they take a callback
+ * argument.
+ *
+ * The asynchronous nature of getItem() makes it tricky to retrieve multiple
+ * values. But unlike localStorage, asyncStorage does not require the values you
+ * store to be strings. So if you need to save multiple values and want to
+ * retrieve them together, in a single asynchronous operation, just group the
+ * values into a single object. The properties of this object may not include
+ * DOM elements, but they may include things like Blobs and typed arrays.
+ *
+ * Unit tests are in apps/gallery/test/unit/asyncStorage_test.js
+ */
+
+this.asyncStorage = (function() {
+
+ var DBNAME = 'asyncStorage';
+ var DBVERSION = 1;
+ var STORENAME = 'keyvaluepairs';
+ var db = null;
+
+ function withStore(type, f) {
+ if (db) {
+ f(db.transaction(STORENAME, type).objectStore(STORENAME));
+ } else {
+ var openreq = indexedDB.open(DBNAME, DBVERSION);
+ openreq.onerror = function withStoreOnError() {
+ console.error("asyncStorage: can't open database:", openreq.error.name);
+ };
+ openreq.onupgradeneeded = function withStoreOnUpgradeNeeded() {
+ // First time setup: create an empty object store
+ openreq.result.createObjectStore(STORENAME);
+ };
+ openreq.onsuccess = function withStoreOnSuccess() {
+ db = openreq.result;
+ f(db.transaction(STORENAME, type).objectStore(STORENAME));
+ };
+ }
+ }
+
+ function getItem(key, callback) {
+ withStore('readonly', function getItemBody(store) {
+ var req = store.get(key);
+ req.onsuccess = function getItemOnSuccess() {
+ var value = req.result;
+ if (value === undefined)
+ value = null;
+ callback(value);
+ };
+ req.onerror = function getItemOnError() {
+ console.error('Error in asyncStorage.getItem(): ', req.error.name);
+ };
+ });
+ }
+
+ function setItem(key, value, callback) {
+ withStore('readwrite', function setItemBody(store) {
+ var req = store.put(value, key);
+ if (callback) {
+ req.onsuccess = function setItemOnSuccess() {
+ callback();
+ };
+ }
+ req.onerror = function setItemOnError() {
+ console.error('Error in asyncStorage.setItem(): ', req.error.name);
+ };
+ });
+ }
+
+ function removeItem(key, callback) {
+ withStore('readwrite', function removeItemBody(store) {
+ var req = store.delete(key);
+ if (callback) {
+ req.onsuccess = function removeItemOnSuccess() {
+ callback();
+ };
+ }
+ req.onerror = function removeItemOnError() {
+ console.error('Error in asyncStorage.removeItem(): ', req.error.name);
+ };
+ });
+ }
+
+ function clear(callback) {
+ withStore('readwrite', function clearBody(store) {
+ var req = store.clear();
+ if (callback) {
+ req.onsuccess = function clearOnSuccess() {
+ callback();
+ };
+ }
+ req.onerror = function clearOnError() {
+ console.error('Error in asyncStorage.clear(): ', req.error.name);
+ };
+ });
+ }
+
+ function length(callback) {
+ withStore('readonly', function lengthBody(store) {
+ var req = store.count();
+ req.onsuccess = function lengthOnSuccess() {
+ callback(req.result);
+ };
+ req.onerror = function lengthOnError() {
+ console.error('Error in asyncStorage.length(): ', req.error.name);
+ };
+ });
+ }
+
+ function key(n, callback) {
+ if (n < 0) {
+ callback(null);
+ return;
+ }
+
+ withStore('readonly', function keyBody(store) {
+ var advanced = false;
+ var req = store.openCursor();
+ req.onsuccess = function keyOnSuccess() {
+ var cursor = req.result;
+ if (!cursor) {
+ // this means there weren't enough keys
+ callback(null);
+ return;
+ }
+ if (n === 0) {
+ // We have the first key, return it if that's what they wanted
+ callback(cursor.key);
+ } else {
+ if (!advanced) {
+ // Otherwise, ask the cursor to skip ahead n records
+ advanced = true;
+ cursor.advance(n);
+ } else {
+ // When we get here, we've got the nth key.
+ callback(cursor.key);
+ }
+ }
+ };
+ req.onerror = function keyOnError() {
+ console.error('Error in asyncStorage.key(): ', req.error.name);
+ };
+ });
+ }
+
+ return {
+ getItem: getItem,
+ setItem: setItem,
+ removeItem: removeItem,
+ clear: clear,
+ length: length,
+ key: key
+ };
+}());
+
diff --git a/WebFM/js/fm.js b/WebFM/js/fm.js
new file mode 100644
index 0000000..64a85d4
--- /dev/null
+++ b/WebFM/js/fm.js
@@ -0,0 +1,975 @@
+'use strict';
+
+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 || {
+ curLocation: null,
+ latitude: null,
+ longitude: null,
+ stationFreqByLocation: {
+ "서울특별시" : {
+ "91.9" : "S00001",
+ "95.9" : "S00002"
+ },
+ "제주시" : {
+ "90.5" : "S00001",
+ "97.9" : "S00002"
+ }
+ },
+ stationCode: {
+ "S00001" : "MBC FM4U",
+ "S00002" : "MBC 표준FM",
+ "S00003" : "SBS 파워FM",
+ "S00004" : "SBS 러브FM"
+ },
+ timeTable: {
+ "S00001" : [
+ "1600|오후의 발견, 스윗소로우 입니다",
+ "1800|배철수의 음악캠프",
+ "2200|로이킴, 정준영의 친한친구"
+ ],
+ "S00002" : [
+ "1800|뉴스",
+ "1805|왕상한의 세계는 우리는(1,2부)",
+ "1900|뉴스포커스",
+ "1920|왕산한의 세계는 우리는(3,4부)",
+ "2000|뉴스데스크"
+ ]
+ },
+ 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;
+ }
+ },
+
+ setCurrentLocation: function(strLocation) {
+ this.curLocation = strLocation;
+ },
+
+ getCurrentLocation: function() {
+ return this.curLocation ? this.curLocation.split('|')[1] : null;
+ },
+
+ setCurrentLatitude: function(latitude) {
+ this.latitude = latitude;
+ },
+
+ setCurrentLongitude: function(longitude) {
+ this.longitude = longitude;
+ },
+
+ getCurrentLatitude: function(latitude) {
+ return this.latitude;
+ },
+
+ getCurrentLongitude: function(longitude) {
+ return this.longitude;
+ },
+
+ getStationCode: function() {
+ var loc = this.curLocation.split('|');
+ var freq = this.frequency;
+ var areaInfo = this.stationFreqByLocation[loc[1]] || this.stationFreqByLocation[loc[0]];
+ var stationCode = null;
+ if( !!areaInfo && areaInfo[freq] ) {
+ stationCode = areaInfo[freq];
+ }
+ return stationCode;
+ },
+
+ getProgramName: function(stationCode, time) {
+ var _timeTable = this.timeTable[stationCode];
+ var currentProgramName = "";
+
+ if( !!_timeTable ) {
+ for( var i=0, len=_timeTable.length ; i 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_mousedown(event) {
+ event.stopPropagation();
+
+ // Stop animation
+ $('frequency-dialer').classList.remove('animation-on');
+
+ startEvent = currentEvent = cloneEvent(event);
+ tunedFrequency = self._currentFreqency;
+
+ _removeEventListeners();
+ document.body.addEventListener('mousemove', fd_body_mousemove, false);
+ document.body.addEventListener('mouseup', fd_body_mouseup, false);
+ }
+
+ $('dialer-container').addEventListener('mousedown', fd_mousedown, 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 (var i = 0; i < _dialerUnits.length; i++) {
+ _dialerUnits[i].style.left = i * _dialerUnitWidth + 'px';
+ }
+ },
+
+ _addDialerUnit: function(start, end) {
+ var markStart = start - start % this.unit;
+ var html = '';
+
+ // 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;
+ }
+ }
+
+ html += '
';
+ if (startMaskWidth > 0) {
+ html += '';
+ }
+ if (endMaskWidth > 0) {
+ html += '';
+ }
+ html += '
';
+
+ var width = '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;
+ if (showFloor) {
+ html += '
' +
+ frequency + '
';
+ } else {
+ html += '
' + frequency + '
';
+ }
+ }
+
+ html += ' ';
+ var unit = document.createElement('div');
+ unit.className = 'dialer-unit';
+ unit.innerHTML = html;
+ $('frequency-dialer').appendChild(unit);
+ },
+
+ _updateUI: function(frequency, ignoreDialer) {
+ $('frequency').textContent = frequency.toFixed(1);
+ this._updateFreqTitle();
+ 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)';
+ }
+ }
+ },
+
+ _updateFreqTitle: function() {
+ var location = mozFMRadio.getCurrentLocation() || '';
+ if (location) {
+ var title = mozFMRadio.getCurrentProgramName() || '정보 없음';
+ $('frequency-title').textContent = location + ': ' + title;
+ }
+ },
+
+ 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';
+ var html = '';
+ html += '
';
+ html += item.frequency.toFixed(1);
+ html += '
';
+ html += '';
+ elem.innerHTML = html;
+
+ // 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) {
+ var isParentListItem = elem.parentNode.classList.contains('fav-list-item');
+ var listItem = isParentListItem ? elem.parentNode : elem;
+ return parseFloat(listItem.id.substring(listItem.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.
+ this._addItemToListUI(this._favList[freq]).scrollIntoView();
+ }
+ },
+
+ /**
+ * 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');
+ } else {
+ item.classList.remove('selected');
+ }
+ }
+ }
+};
+
+function startWatchPosition() {
+ var options = {
+ enableHighAccuracy: false,
+ timeout: 5000,
+ maximumAge: 0
+ };
+
+ var id = navigator.geolocation.watchPosition(function(pos) {
+ var crd = pos.coords;
+
+ if (mozFMRadio.latitude === crd.latitude && mozFMRadio.longitude === crd.longitude) {
+ // call api, 위/경도 set, getCurrentProgramName(), 제목만 따로 업데이트...
+ console.log('user position is changed...');
+ // navigator.geolocation.clearWatch(id);
+ }
+ }, function(err) {
+
+ }, options);
+ mozFMRadio.watcherId = id;
+}
+
+function renderRegionName(data) {
+ var elem = $('loc');
+ mozFMRadio.setCurrentLocation(data.name1 + '|' + data.name2);
+ console.log('current location: ' + mozFMRadio.curLocation);
+
+ frequencyDialer._updateFreqTitle();
+}
+
+function init() {
+ frequencyDialer.init();
+
+ if ('geolocation' in navigator) {
+ navigator.geolocation.getCurrentPosition(function(position) {
+ var latitude = position.coords.latitude;
+ var longitude = position.coords.longitude;
+
+ mozFMRadio.setCurrentLatitude(latitude);
+ mozFMRadio.setCurrentLongitude(longitude);
+
+ startWatchPosition();
+
+ var scriptElem = document.createElement('script');
+ scriptElem.type = 'text/javascript';
+ scriptElem.charset = 'utf-8';
+ var url = 'http://apis.daum.net/local/geo/coord2addr?apikey=dc0560f309a55019fdf362633c31777f1f0ecb02&longitude=' + longitude + '&latitude=' + latitude + '&output=json&callback=renderRegionName&inputCoordSystem=WGS84';
+ scriptElem.src = url;
+ document.body.appendChild(scriptElem);
+ }, function(error) {
+ alert('ERROR(' + error.code + '): ' + error.message);
+ });
+ } else {
+ alert('GPS를 활성화해주세요!');
+ }
+
+ var seeking = false;
+ function onclick_seekbutton(event) {
+ 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);
+
+ mozFMRadio.onfrequencychange = updateFreqUI;
+ mozFMRadio.onenabled = function() {
+ updateEnablingState(false);
+ };
+ mozFMRadio.ondisabled = function() {
+ updateEnablingState(false);
+ };
+
+ mozFMRadio.onantennaavailablechange = function onAntennaChange() {
+ updateAntennaUI();
+ 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.
+ updateAirplaneModeUI();
+ mozSettings.addObserver('ril.radio.disabled', function(event) {
+ rilDisabled = event.settingValue;
+ updateAirplaneModeUI();
+ });
+
+ 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;
+ updateAntennaUI();
+ favoritesList.init();
+ }
+ updatePowerUI();
+ });
+}
+
+
+window.addEventListener('load', function(e) {
+ var req = mozSettings.createLock().get('ril.radio.disabled');
+ req.onsuccess = function() {
+ rilDisabled = req.result['ril.radio.disabled'];
+ init();
+ };
+ req.onerror = function() {
+ init();
+ };
+}, false);
+
+// Turn off radio immediately when window is unloaded.
+window.addEventListener('unload', function(e) {
+ mozFMRadio.disable();
+}, false);
+
+// Set the 'lang' and 'dir' attributes to when the page is translated
+window.addEventListener('localized', function showBody() {
+ document.documentElement.lang = navigator.mozL10n.language.code;
+ document.documentElement.dir = navigator.mozL10n.language.direction;
+});
+
diff --git a/WebFM/js/mouse_event_shim.js b/WebFM/js/mouse_event_shim.js
new file mode 100644
index 0000000..053ef7f
--- /dev/null
+++ b/WebFM/js/mouse_event_shim.js
@@ -0,0 +1,282 @@
+/**
+ * mouse_event_shim.js: generate mouse events from touch events.
+ *
+ * This library listens for touch events and generates mousedown, mousemove
+ * mouseup, and click events to match them. It captures and dicards any
+ * real mouse events (non-synthetic events with isTrusted true) that are
+ * send by gecko so that there are not duplicates.
+ *
+ * This library does emit mouseover/mouseout and mouseenter/mouseleave
+ * events. You can turn them off by setting MouseEventShim.trackMouseMoves to
+ * false. This means that mousemove events will always have the same target
+ * as the mousedown even that began the series. You can also call
+ * MouseEventShim.setCapture() from a mousedown event handler to prevent
+ * mouse tracking until the next mouseup event.
+ *
+ * This library does not support multi-touch but should be sufficient
+ * to do drags based on mousedown/mousemove/mouseup events.
+ *
+ * This library does not emit dblclick events or contextmenu events
+ */
+
+'use strict';
+
+(function() {
+ // Make sure we don't run more than once
+ if (MouseEventShim)
+ return;
+
+ // Bail if we're not on running on a platform that sends touch
+ // events. We don't need the shim code for mouse events.
+ try {
+ document.createEvent('TouchEvent');
+ } catch (e) {
+ return;
+ }
+
+ var starttouch; // The Touch object that we started with
+ var target; // The element the touch is currently over
+ var emitclick; // Will we be sending a click event after mouseup?
+
+ // Use capturing listeners to discard all mouse events from gecko
+ window.addEventListener('mousedown', discardEvent, true);
+ window.addEventListener('mouseup', discardEvent, true);
+ window.addEventListener('mousemove', discardEvent, true);
+ window.addEventListener('click', discardEvent, true);
+
+ function discardEvent(e) {
+ if (e.isTrusted) {
+ e.stopImmediatePropagation(); // so it goes no further
+ if (e.type === 'click')
+ e.preventDefault(); // so it doesn't trigger a change event
+ }
+ }
+
+ // Listen for touch events that bubble up to the window.
+ // If other code has called stopPropagation on the touch events
+ // then we'll never see them. Also, we'll honor the defaultPrevented
+ // state of the event and will not generate synthetic mouse events
+ window.addEventListener('touchstart', handleTouchStart);
+ window.addEventListener('touchmove', handleTouchMove);
+ window.addEventListener('touchend', handleTouchEnd);
+ window.addEventListener('touchcancel', handleTouchEnd); // Same as touchend
+
+ function handleTouchStart(e) {
+ // If we're already handling a touch, ignore this one
+ if (starttouch)
+ return;
+
+ // Ignore any event that has already been prevented
+ if (e.defaultPrevented)
+ return;
+
+ // Sometimes an unknown gecko bug causes us to get a touchstart event
+ // for an iframe target that we can't use because it is cross origin.
+ // Don't start handling a touch in that case
+ try {
+ e.changedTouches[0].target.ownerDocument;
+ }
+ catch (e) {
+ // Ignore the event if we can't see the properties of the target
+ return;
+ }
+
+ // If there is more than one simultaneous touch, ignore all but the first
+ starttouch = e.changedTouches[0];
+ target = starttouch.target;
+ emitclick = true;
+
+ // Move to the position of the touch
+ emitEvent('mousemove', target, starttouch);
+
+ // Now send a synthetic mousedown
+ var result = emitEvent('mousedown', target, starttouch);
+
+ // If the mousedown was prevented, pass that on to the touch event.
+ // And remember not to send a click event
+ if (!result) {
+ e.preventDefault();
+ emitclick = false;
+ }
+ }
+
+ function handleTouchEnd(e) {
+ if (!starttouch)
+ return;
+
+ // End a MouseEventShim.setCapture() call
+ if (MouseEventShim.capturing) {
+ MouseEventShim.capturing = false;
+ MouseEventShim.captureTarget = null;
+ }
+
+ for (var i = 0; i < e.changedTouches.length; i++) {
+ var touch = e.changedTouches[i];
+ // If the ended touch does not have the same id, skip it
+ if (touch.identifier !== starttouch.identifier)
+ continue;
+
+ emitEvent('mouseup', target, touch);
+
+ // If target is still the same element we started and the touch did not
+ // move more than the threshold and if the user did not prevent
+ // the mousedown, then send a click event, too.
+ if (emitclick)
+ emitEvent('click', starttouch.target, touch);
+
+ starttouch = null;
+ return;
+ }
+ }
+
+ function handleTouchMove(e) {
+ if (!starttouch)
+ return;
+
+ for (var i = 0; i < e.changedTouches.length; i++) {
+ var touch = e.changedTouches[i];
+ // If the ended touch does not have the same id, skip it
+ if (touch.identifier !== starttouch.identifier)
+ continue;
+
+ // Don't send a mousemove if the touchmove was prevented
+ if (e.defaultPrevented)
+ return;
+
+ // See if we've moved too much to emit a click event
+ var dx = Math.abs(touch.screenX - starttouch.screenX);
+ var dy = Math.abs(touch.screenY - starttouch.screenY);
+ if (dx > MouseEventShim.dragThresholdX ||
+ dy > MouseEventShim.dragThresholdY) {
+ emitclick = false;
+ }
+
+ var tracking = MouseEventShim.trackMouseMoves &&
+ !MouseEventShim.capturing;
+
+ if (tracking) {
+ // If the touch point moves, then the element it is over
+ // may have changed as well. Note that calling elementFromPoint()
+ // forces a layout if one is needed.
+ // XXX: how expensive is it to do this on each touchmove?
+ // Can we listen for (non-standard) touchleave events instead?
+ var oldtarget = target;
+ var newtarget = document.elementFromPoint(touch.clientX, touch.clientY);
+ if (newtarget === null) {
+ // this can happen as the touch is moving off of the screen, e.g.
+ newtarget = oldtarget;
+ }
+ if (newtarget !== oldtarget) {
+ leave(oldtarget, newtarget, touch); // mouseout, mouseleave
+ target = newtarget;
+ }
+ }
+ else if (MouseEventShim.captureTarget) {
+ target = MouseEventShim.captureTarget;
+ }
+
+ emitEvent('mousemove', target, touch);
+
+ if (tracking && newtarget !== oldtarget) {
+ enter(newtarget, oldtarget, touch); // mouseover, mouseenter
+ }
+ }
+ }
+
+ // Return true if element a contains element b
+ function contains(a, b) {
+ return (a.compareDocumentPosition(b) & 16) !== 0;
+ }
+
+ // A touch has left oldtarget and entered newtarget
+ // Send out all the events that are required
+ function leave(oldtarget, newtarget, touch) {
+ emitEvent('mouseout', oldtarget, touch, newtarget);
+
+ // If the touch has actually left oldtarget (and has not just moved
+ // into a child of oldtarget) send a mouseleave event. mouseleave
+ // events don't bubble, so we have to repeat this up the hierarchy.
+ for (var e = oldtarget; !contains(e, newtarget); e = e.parentNode) {
+ emitEvent('mouseleave', e, touch, newtarget);
+ }
+ }
+
+ // A touch has entered newtarget from oldtarget
+ // Send out all the events that are required.
+ function enter(newtarget, oldtarget, touch) {
+ emitEvent('mouseover', newtarget, touch, oldtarget);
+
+ // Emit non-bubbling mouseenter events if the touch actually entered
+ // newtarget and wasn't already in some child of it
+ for (var e = newtarget; !contains(e, oldtarget); e = e.parentNode) {
+ emitEvent('mouseenter', e, touch, oldtarget);
+ }
+ }
+
+ function emitEvent(type, target, touch, relatedTarget) {
+ var synthetic = document.createEvent('MouseEvents');
+ var bubbles = (type !== 'mouseenter' && type !== 'mouseleave');
+ var count =
+ (type === 'mousedown' || type === 'mouseup' || type === 'click') ? 1 : 0;
+
+ synthetic.initMouseEvent(type,
+ bubbles, // canBubble
+ true, // cancelable
+ window,
+ count, // detail: click count
+ touch.screenX,
+ touch.screenY,
+ touch.clientX,
+ touch.clientY,
+ false, // ctrlKey: we don't have one
+ false, // altKey: we don't have one
+ false, // shiftKey: we don't have one
+ false, // metaKey: we don't have one
+ 0, // we're simulating the left button
+ relatedTarget || null);
+
+ try {
+ return target.dispatchEvent(synthetic);
+ }
+ catch (e) {
+ console.warn('Exception calling dispatchEvent', type, e);
+ return true;
+ }
+ }
+}());
+
+var MouseEventShim = {
+ // It is a known gecko bug that synthetic events have timestamps measured
+ // in microseconds while regular events have timestamps measured in
+ // milliseconds. This utility function returns a the timestamp converted
+ // to milliseconds, if necessary.
+ getEventTimestamp: function(e) {
+ if (e.isTrusted) // XXX: Are real events always trusted?
+ return e.timeStamp;
+ else
+ return e.timeStamp / 1000;
+ },
+
+ // Set this to false if you don't care about mouseover/out events
+ // and don't want the target of mousemove events to follow the touch
+ trackMouseMoves: true,
+
+ // Call this function from a mousedown event handler if you want to guarantee
+ // that the mousemove and mouseup events will go to the same element
+ // as the mousedown even if they leave the bounds of the element. This is
+ // like setting trackMouseMoves to false for just one drag. It is a
+ // substitute for event.target.setCapture(true)
+ setCapture: function(target) {
+ this.capturing = true; // Will be set back to false on mouseup
+ if (target)
+ this.captureTarget = target;
+ },
+
+ capturing: false,
+
+ // Keep these in sync with ui.dragThresholdX and ui.dragThresholdY prefs.
+ // If a touch ever moves more than this many pixels from its starting point
+ // then we will not synthesize a click event when the touch ends.
+ dragThresholdX: 25,
+ dragThresholdY: 25
+};
diff --git a/WebFM/locales/fm.ar.properties b/WebFM/locales/fm.ar.properties
new file mode 100644
index 0000000..836ef1b
--- /dev/null
+++ b/WebFM/locales/fm.ar.properties
@@ -0,0 +1,2 @@
+noAntenna = قم بتوصيل السماعة
+noAntennaMsg = راديو FM يحتاج إلى سماعات متصلة لاستقبال إشارات الراديو.
diff --git a/WebFM/locales/fm.en-US.properties b/WebFM/locales/fm.en-US.properties
new file mode 100644
index 0000000..0a6c106
--- /dev/null
+++ b/WebFM/locales/fm.en-US.properties
@@ -0,0 +1,4 @@
+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.
diff --git a/WebFM/locales/fm.fr.properties b/WebFM/locales/fm.fr.properties
new file mode 100644
index 0000000..03db52d
--- /dev/null
+++ b/WebFM/locales/fm.fr.properties
@@ -0,0 +1,4 @@
+noAntenna = Casque débranché
+noAntennaMsg = La radio FM nécessite un casque branché pour servir d’antenne.
+airplaneModeHeader = Le mode avion est activé
+airplaneModeMsg = Pour utiliser l’application Radio FM, désactivez le mode avion.
diff --git a/WebFM/locales/fm.ko.properties b/WebFM/locales/fm.ko.properties
new file mode 100644
index 0000000..12503de
--- /dev/null
+++ b/WebFM/locales/fm.ko.properties
@@ -0,0 +1,4 @@
+noAntenna = 헤드셋을 연결해주세요.
+noAntennaMsg = FM 라디오 신호를 수신하려면 헤드셋이 연결되어야 합니다.
+airplaneModeHeader = 비행 모드로 설정되어 있습니다.
+airplaneModeMsg = FM 라디오를 사용하려면 비행 모드를 해지해주세요.
diff --git a/WebFM/locales/fm.zh-TW.properties b/WebFM/locales/fm.zh-TW.properties
new file mode 100644
index 0000000..e66050f
--- /dev/null
+++ b/WebFM/locales/fm.zh-TW.properties
@@ -0,0 +1,3 @@
+noAntenna = 插入耳機
+noAntennaMsg = FM 收音機需要插入的耳機才能接收廣播訊號。
+
diff --git a/WebFM/locales/locales.ini b/WebFM/locales/locales.ini
new file mode 100644
index 0000000..5a17d63
--- /dev/null
+++ b/WebFM/locales/locales.ini
@@ -0,0 +1,11 @@
+@import url(fm.en-US.properties)
+
+[ar]
+@import url(fm.ar.properties)
+
+[fr]
+@import url(fm.fr.properties)
+
+[zh-TW]
+@import url(fm.zh-TW.properties)
+
diff --git a/WebFM/manifest.webapp b/WebFM/manifest.webapp
new file mode 100644
index 0000000..166e88b
--- /dev/null
+++ b/WebFM/manifest.webapp
@@ -0,0 +1,44 @@
+{
+ "name": "FM Radio J",
+ "description": "Gaia FM Radio by J Team",
+ "type": "certified",
+ "launch_path": "/index.html",
+ "developer": {
+ "name": "The Gaia Team J",
+ "url": "https://github.com/mozilla-b2g/gaia"
+ },
+ "permissions": {
+ "storage":{},
+ "fmradio":{},
+ "settings":{ "access": "readonly" },
+ "geolocation": {}
+ },
+ "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 Gaia"
+ },
+ "ko": {
+ "name": "FM 라디오",
+ "description": "FM 라디오"
+ },
+ "zh-TW": {
+ "name": "收音机",
+ "description": "收音机"
+ }
+ },
+ "default_locale": "ko",
+ "icons": {
+ "128": "/style/icons/Fm.png",
+ "60": "/style/icons/60/Fm.png"
+ },
+ "orientation": "portrait-primary"
+}
diff --git a/WebFM/style/fm.css b/WebFM/style/fm.css
new file mode 100644
index 0000000..729338e
--- /dev/null
+++ b/WebFM/style/fm.css
@@ -0,0 +1,461 @@
+html, body {
+ padding: 0;
+ border: 0;
+ margin: 0;
+ height: 100%;
+ overflow: hidden;
+ color: #FFF;
+ font-size: 10px;
+}
+
+
+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: #272d32;
+}
+
+#container > div {
+ position: absolute;
+ left: 0;
+ width: 100%;
+}
+
+/****** styles for frequency view *******/
+#frequency-bar {
+ top: 0;
+ height: 10rem;
+ width: 100%;
+ text-align: center;
+ background-color: #43464B;
+ background-image: -moz-linear-gradient(#33373C, rgba(0, 0, 0, 0));
+ 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;
+ top: -1.5rem;
+}
+
+#frequency-display {
+ position: relative;
+ transition: opacity 680ms ease;
+}
+
+.dim #frequency-display {
+ opacity: 0.1;
+}
+
+#bookmark-button {
+ top: 0;
+ right: 2rem;
+}
+
+#frequency {
+ font-size: 5rem;
+ font-weight: 300;
+ color: #fff;
+ padding-top: .5rem;
+ position: absolute;
+ width: 100%;
+ height: 5rem;
+ display: block;
+ top: 0;
+ left: 0;
+}
+#frequency-title {
+ background-color: #3A3A38;
+ font-size: 1.5rem;
+ color: #FCE700;
+ padding: 1rem 0 0;
+ position: absolute;
+ width: 100%;
+ height: 4.5rem;
+ display: block;
+ top: 6.5rem;
+ left: 0;
+}
+
+#frequency::after {
+ content: "MHz";
+ 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;
+}
+/***** end *****/
+
+/**** styles for frequency dialer *****/
+#dialer-bar {
+ top: 10rem;
+ font-size: 1.4rem;
+ font-weight: 400;
+ height: 9rem;
+ overflow: hidden;
+ background: #4C5055;
+ border-top: .1rem solid #3A3D41;
+ color: #FFF;
+ box-shadow: 0 .3rem .4rem #111;
+}
+
+#dialer-container {
+ padding-left: 50%;
+ width: 100%;
+ height: 100%;
+}
+
+#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;
+}
+
+div.animation-on > div {
+ transition: -moz-transform 0.4s ease 0s;
+}
+
+#frequency-dialer {
+ display: inline-block;
+ display: -moz-inline-box;
+ -moz-user-select: none;
+ width: 100%;
+ margin-left: -0.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: #4C5055;
+ 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 {
+ font-size: 2.3rem;
+ font-weight: 300;
+ height: 4.4rem;
+ margin: 0 2rem 0 2rem;
+ padding-bottom: 0.2rem;
+ clear: both;
+ border-top: .1rem solid #3D4045;
+}
+
+div#fav-list-container > div:active {
+ background-color: #00ABCD;
+}
+
+div#fav-list-container > div:first-child {
+ border-top: none;
+}
+
+div#fav-list-container > div:last-child {
+ border-top: .1rem solid #3D4045;
+}
+
+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% - 0.2rem) 0 0;
+ text-align: right;
+ width: 6rem;
+}
+
+div#fav-list-container div.fav-list-frequency::after {
+ content: "MHz";
+ 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: url(images/popup-texture.png) transparent repeat 0 0 / 100% 100%;
+}
+
+.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 > 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 *******/
+
diff --git a/WebFM/style/icons/60/Fm.png b/WebFM/style/icons/60/Fm.png
new file mode 100644
index 0000000..58d5ee2
Binary files /dev/null and b/WebFM/style/icons/60/Fm.png differ
diff --git a/WebFM/style/icons/60/Fm@1.5x.png b/WebFM/style/icons/60/Fm@1.5x.png
new file mode 100644
index 0000000..9ceffd3
Binary files /dev/null and b/WebFM/style/icons/60/Fm@1.5x.png differ
diff --git a/WebFM/style/icons/60/Fm@2x.png b/WebFM/style/icons/60/Fm@2x.png
new file mode 100644
index 0000000..976ceb4
Binary files /dev/null and b/WebFM/style/icons/60/Fm@2x.png differ
diff --git a/WebFM/style/icons/Fm.png b/WebFM/style/icons/Fm.png
new file mode 100644
index 0000000..58d5ee2
Binary files /dev/null and b/WebFM/style/icons/Fm.png differ
diff --git a/WebFM/style/icons/Fm@1.5x.png b/WebFM/style/icons/Fm@1.5x.png
new file mode 100644
index 0000000..9ceffd3
Binary files /dev/null and b/WebFM/style/icons/Fm@1.5x.png differ
diff --git a/WebFM/style/icons/Fm@2x.png b/WebFM/style/icons/Fm@2x.png
new file mode 100644
index 0000000..976ceb4
Binary files /dev/null and b/WebFM/style/icons/Fm@2x.png differ
diff --git a/WebFM/style/images/app-texture.png b/WebFM/style/images/app-texture.png
new file mode 100644
index 0000000..69c8e5b
Binary files /dev/null and b/WebFM/style/images/app-texture.png differ
diff --git a/WebFM/style/images/dial-notches.png b/WebFM/style/images/dial-notches.png
new file mode 100644
index 0000000..c56a1fa
Binary files /dev/null and b/WebFM/style/images/dial-notches.png differ
diff --git a/WebFM/style/images/dial-notches@1.5x.png b/WebFM/style/images/dial-notches@1.5x.png
new file mode 100644
index 0000000..b78abe0
Binary files /dev/null and b/WebFM/style/images/dial-notches@1.5x.png differ
diff --git a/WebFM/style/images/dial-notches@2x.png b/WebFM/style/images/dial-notches@2x.png
new file mode 100644
index 0000000..8b783e1
Binary files /dev/null and b/WebFM/style/images/dial-notches@2x.png differ
diff --git a/WebFM/style/images/fav-star-selected.png b/WebFM/style/images/fav-star-selected.png
new file mode 100644
index 0000000..d613d3a
Binary files /dev/null and b/WebFM/style/images/fav-star-selected.png differ
diff --git a/WebFM/style/images/fav-star-selected@1.5x.png b/WebFM/style/images/fav-star-selected@1.5x.png
new file mode 100755
index 0000000..fa05a00
Binary files /dev/null and b/WebFM/style/images/fav-star-selected@1.5x.png differ
diff --git a/WebFM/style/images/fav-star-selected@2x.png b/WebFM/style/images/fav-star-selected@2x.png
new file mode 100644
index 0000000..52deea5
Binary files /dev/null and b/WebFM/style/images/fav-star-selected@2x.png differ
diff --git a/WebFM/style/images/fav-star.png b/WebFM/style/images/fav-star.png
new file mode 100644
index 0000000..b4b1bfb
Binary files /dev/null and b/WebFM/style/images/fav-star.png differ
diff --git a/WebFM/style/images/fav-star@1.5x.png b/WebFM/style/images/fav-star@1.5x.png
new file mode 100755
index 0000000..eeb8af6
Binary files /dev/null and b/WebFM/style/images/fav-star@1.5x.png differ
diff --git a/WebFM/style/images/fav-star@2x.png b/WebFM/style/images/fav-star@2x.png
new file mode 100644
index 0000000..4495558
Binary files /dev/null and b/WebFM/style/images/fav-star@2x.png differ
diff --git a/WebFM/style/images/headset.png b/WebFM/style/images/headset.png
new file mode 100644
index 0000000..ccbd1bc
Binary files /dev/null and b/WebFM/style/images/headset.png differ
diff --git a/WebFM/style/images/headset@1.5x.png b/WebFM/style/images/headset@1.5x.png
new file mode 100644
index 0000000..b85e174
Binary files /dev/null and b/WebFM/style/images/headset@1.5x.png differ
diff --git a/WebFM/style/images/headset@2x.png b/WebFM/style/images/headset@2x.png
new file mode 100644
index 0000000..7b32ff5
Binary files /dev/null and b/WebFM/style/images/headset@2x.png differ
diff --git a/WebFM/style/images/play.png b/WebFM/style/images/play.png
new file mode 100644
index 0000000..c6dad02
Binary files /dev/null and b/WebFM/style/images/play.png differ
diff --git a/WebFM/style/images/play@1.5x.png b/WebFM/style/images/play@1.5x.png
new file mode 100755
index 0000000..93ff04c
Binary files /dev/null and b/WebFM/style/images/play@1.5x.png differ
diff --git a/WebFM/style/images/play@2x.png b/WebFM/style/images/play@2x.png
new file mode 100644
index 0000000..90a95bb
Binary files /dev/null and b/WebFM/style/images/play@2x.png differ
diff --git a/WebFM/style/images/popup-texture.png b/WebFM/style/images/popup-texture.png
new file mode 100644
index 0000000..657bccc
Binary files /dev/null and b/WebFM/style/images/popup-texture.png differ
diff --git a/WebFM/style/images/popup-texture@1.5x.png b/WebFM/style/images/popup-texture@1.5x.png
new file mode 100644
index 0000000..9539af9
Binary files /dev/null and b/WebFM/style/images/popup-texture@1.5x.png differ
diff --git a/WebFM/style/images/popup-texture@2x.png b/WebFM/style/images/popup-texture@2x.png
new file mode 100644
index 0000000..cf91572
Binary files /dev/null and b/WebFM/style/images/popup-texture@2x.png differ
diff --git a/WebFM/style/images/reflection-56.png b/WebFM/style/images/reflection-56.png
new file mode 100644
index 0000000..57fa638
Binary files /dev/null and b/WebFM/style/images/reflection-56.png differ
diff --git a/WebFM/style/images/reflection-56@1.5x.png b/WebFM/style/images/reflection-56@1.5x.png
new file mode 100755
index 0000000..be6a9be
Binary files /dev/null and b/WebFM/style/images/reflection-56@1.5x.png differ
diff --git a/WebFM/style/images/reflection-56@2x.png b/WebFM/style/images/reflection-56@2x.png
new file mode 100644
index 0000000..44a68af
Binary files /dev/null and b/WebFM/style/images/reflection-56@2x.png differ
diff --git a/WebFM/style/images/reflection-72.png b/WebFM/style/images/reflection-72.png
new file mode 100644
index 0000000..d981d62
Binary files /dev/null and b/WebFM/style/images/reflection-72.png differ
diff --git a/WebFM/style/images/reflection-72@1.5x.png b/WebFM/style/images/reflection-72@1.5x.png
new file mode 100755
index 0000000..26a47ac
Binary files /dev/null and b/WebFM/style/images/reflection-72@1.5x.png differ
diff --git a/WebFM/style/images/reflection-72@2x.png b/WebFM/style/images/reflection-72@2x.png
new file mode 100644
index 0000000..3af8b76
Binary files /dev/null and b/WebFM/style/images/reflection-72@2x.png differ
diff --git a/WebFM/style/images/seekdown.png b/WebFM/style/images/seekdown.png
new file mode 100644
index 0000000..c2d14ec
Binary files /dev/null and b/WebFM/style/images/seekdown.png differ
diff --git a/WebFM/style/images/seekdown@1.5x.png b/WebFM/style/images/seekdown@1.5x.png
new file mode 100644
index 0000000..62afae8
Binary files /dev/null and b/WebFM/style/images/seekdown@1.5x.png differ
diff --git a/WebFM/style/images/seekdown@2x.png b/WebFM/style/images/seekdown@2x.png
new file mode 100644
index 0000000..77b0af2
Binary files /dev/null and b/WebFM/style/images/seekdown@2x.png differ
diff --git a/WebFM/style/images/seekup.png b/WebFM/style/images/seekup.png
new file mode 100644
index 0000000..6e09220
Binary files /dev/null and b/WebFM/style/images/seekup.png differ
diff --git a/WebFM/style/images/seekup@1.5x.png b/WebFM/style/images/seekup@1.5x.png
new file mode 100644
index 0000000..ef21647
Binary files /dev/null and b/WebFM/style/images/seekup@1.5x.png differ
diff --git a/WebFM/style/images/seekup@2x.png b/WebFM/style/images/seekup@2x.png
new file mode 100644
index 0000000..25d3004
Binary files /dev/null and b/WebFM/style/images/seekup@2x.png differ
diff --git a/WebFM/style/images/selector.png b/WebFM/style/images/selector.png
new file mode 100644
index 0000000..36ddd84
Binary files /dev/null and b/WebFM/style/images/selector.png differ
diff --git a/WebFM/style/images/selector@1.5x.png b/WebFM/style/images/selector@1.5x.png
new file mode 100644
index 0000000..25f65f8
Binary files /dev/null and b/WebFM/style/images/selector@1.5x.png differ
diff --git a/WebFM/style/images/selector@2x.png b/WebFM/style/images/selector@2x.png
new file mode 100644
index 0000000..52835d6
Binary files /dev/null and b/WebFM/style/images/selector@2x.png differ
diff --git a/WebFM/style/images/spinner-FMRadio.png b/WebFM/style/images/spinner-FMRadio.png
new file mode 100644
index 0000000..07d0fab
Binary files /dev/null and b/WebFM/style/images/spinner-FMRadio.png differ
diff --git a/WebFM/style/images/spinner-FMRadio@1.5x.png b/WebFM/style/images/spinner-FMRadio@1.5x.png
new file mode 100755
index 0000000..4b73bab
Binary files /dev/null and b/WebFM/style/images/spinner-FMRadio@1.5x.png differ
diff --git a/WebFM/style/images/spinner-FMRadio@2x.png b/WebFM/style/images/spinner-FMRadio@2x.png
new file mode 100644
index 0000000..a935b75
Binary files /dev/null and b/WebFM/style/images/spinner-FMRadio@2x.png differ
diff --git a/WebFM/style/images/stop.png b/WebFM/style/images/stop.png
new file mode 100644
index 0000000..6ce13b5
Binary files /dev/null and b/WebFM/style/images/stop.png differ
diff --git a/WebFM/style/images/stop@1.5x.png b/WebFM/style/images/stop@1.5x.png
new file mode 100755
index 0000000..5cd1698
Binary files /dev/null and b/WebFM/style/images/stop@1.5x.png differ
diff --git a/WebFM/style/images/stop@2x.png b/WebFM/style/images/stop@2x.png
new file mode 100644
index 0000000..a9be82f
Binary files /dev/null and b/WebFM/style/images/stop@2x.png differ
diff --git a/WebFM/style/images/toggle-fav-star-off.png b/WebFM/style/images/toggle-fav-star-off.png
new file mode 100644
index 0000000..794f05b
Binary files /dev/null and b/WebFM/style/images/toggle-fav-star-off.png differ
diff --git a/WebFM/style/images/toggle-fav-star-off@1.5x.png b/WebFM/style/images/toggle-fav-star-off@1.5x.png
new file mode 100644
index 0000000..4a67da4
Binary files /dev/null and b/WebFM/style/images/toggle-fav-star-off@1.5x.png differ
diff --git a/WebFM/style/images/toggle-fav-star-off@2x.png b/WebFM/style/images/toggle-fav-star-off@2x.png
new file mode 100644
index 0000000..0151a5c
Binary files /dev/null and b/WebFM/style/images/toggle-fav-star-off@2x.png differ
diff --git a/WebFM/style/images/toggle-fav-star-on.png b/WebFM/style/images/toggle-fav-star-on.png
new file mode 100644
index 0000000..967e3ae
Binary files /dev/null and b/WebFM/style/images/toggle-fav-star-on.png differ
diff --git a/WebFM/style/images/toggle-fav-star-on@1.5x.png b/WebFM/style/images/toggle-fav-star-on@1.5x.png
new file mode 100644
index 0000000..0749835
Binary files /dev/null and b/WebFM/style/images/toggle-fav-star-on@1.5x.png differ
diff --git a/WebFM/style/images/toggle-fav-star-on@2x.png b/WebFM/style/images/toggle-fav-star-on@2x.png
new file mode 100644
index 0000000..ac767b0
Binary files /dev/null and b/WebFM/style/images/toggle-fav-star-on@2x.png differ
diff --git a/WebFM/style/images/toggle-fav-star-pressed.png b/WebFM/style/images/toggle-fav-star-pressed.png
new file mode 100644
index 0000000..aff63bf
Binary files /dev/null and b/WebFM/style/images/toggle-fav-star-pressed.png differ
diff --git a/WebFM/style/images/toggle-fav-star-pressed@1.5x.png b/WebFM/style/images/toggle-fav-star-pressed@1.5x.png
new file mode 100644
index 0000000..787e627
Binary files /dev/null and b/WebFM/style/images/toggle-fav-star-pressed@1.5x.png differ
diff --git a/WebFM/style/images/toggle-fav-star-pressed@2x.png b/WebFM/style/images/toggle-fav-star-pressed@2x.png
new file mode 100644
index 0000000..6559a27
Binary files /dev/null and b/WebFM/style/images/toggle-fav-star-pressed@2x.png differ