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
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+ + + + + 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