Release v2.4.0

This commit is contained in:
Nick Steel 2017-03-15 00:04:59 +00:00
commit 17f85d783d
40 changed files with 4492 additions and 1096 deletions

View File

@ -6,10 +6,6 @@ Mopidy-MusicBox-Webclient
:target: https://pypi.python.org/pypi/Mopidy-MusicBox-Webclient/
:alt: Latest PyPI version
.. image:: https://img.shields.io/pypi/dm/Mopidy-MusicBox-Webclient.svg?style=flat
:target: https://pypi.python.org/pypi/Mopidy-MusicBox-Webclient/
:alt: Number of PyPI downloads
.. image:: https://img.shields.io/travis/pimusicbox/mopidy-musicbox-webclient/develop.svg?style=flat
:target: https://travis-ci.org/pimusicbox/mopidy-musicbox-webclient
:alt: Travis CI build status
@ -38,7 +34,9 @@ Features
- Deep integration with, and additional features for, the `Pi MusicBox <http://www.pimusicbox.com/>`_.
- Fullscreen mode.
.. image:: https://github.com/pimusicbox/mopidy-musicbox-webclient/raw/develop/screenshots/queue_desktop.png
.. image:: https://github.com/pimusicbox/mopidy-musicbox-webclient/raw/develop/screenshots/overview.png
:width: 1312
:height: 723
Dependencies
============
@ -107,6 +105,33 @@ Project resources
Changelog
=========
v2.4.0 (2017-03-15)
-------------------
- Now shows server name/IP address and port number at the bottom of the navigation pane. (Addresses: `#67 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/67>`_).
- Add ability to insert a track anywhere in the current queue. (Addresses: `#75 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/75>`_).
- Add 'Show Track Info' popup which can be activated from any context menu or by clicking on either the 'info' icon next
to the album cover or the track's title text on the 'Now Playing' pane. The popup includes the URI of the track, which
can be inserted into various lists elsewhere in the player.
- Updated icon set for font-awesome 4.7.0.
- Added 'Refresh' button for refreshing libraries. (Addresses: `#75 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/75>`_).
**Fixes**
- Only show 'Show Album' or 'Show Artist' options in popup menus if URI's for those resources are available.
(Fixes: `#213 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/213>`_).
- Now shows correct hostname information in loader popup. (Fixes: `#209 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/209>`_).
- Reset 'Now Playing' info when the last track in the tracklist is deleted. Fixes an issue where info of the last song played would be displayed even after the queue had been cleared.
- Now initializes the GUI properly, even if the user is offline or the Mopidy server cannot be reached.
- Fixed `Alarm Clock <https://pypi.python.org/pypi/Mopidy-AlarmClock/>`_ detection.
- Unplayable files are shown with a different icon in track lists.
- Show all available track information in the 'Show Track Info...' popup. (Fixes: `#227 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/227>`_).
- The last scroll position is now always saved when navigating between pages or browsing the library.
(Fixes: `#73 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/73>`_, `#93 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/93>`_).
- Playlists will now list tracks even if they are no longer available in the library. (Fixes: `#226 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/226>`_).
- Fixed an issue on Safari where the first page to load would be too wide to fit on the screen.
- Refreshing album or artist info pages no longer raises an exception. (Fixes: `#230 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/230>`_).
v2.3.0 (2016-05-15)
-------------------

View File

@ -4,7 +4,7 @@ import os
from mopidy import config, ext
__version__ = '2.3.0'
__version__ = '2.4.0'
class Extension(ext.Extension):

View File

@ -1,6 +1,6 @@
/*
* Mopidy Webclient CSS
* (c) Wouter van Wijk 2012-2013
* (c) Wouter van Wijk 2012-2017
*/
/****************************
@ -118,7 +118,6 @@
/******************
* Track Slider *
******************/
#trackslider {
display: inline;
width: 100%;
@ -160,6 +159,20 @@
display: inline;
}
div.hostInfo {
width: 100%;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
}
span.hostInfo {
font-weight: normal;
font-size: 0.75em;
overflow: hidden;
text-overflow: ellipsis;
}
/********************
* Pages, content *
********************/
@ -207,6 +220,17 @@
#homerows div i {
font-size: 28px;
}
.ui-block-a-min {
float: left !important;
width: initial !important;
}
.ui-block-b-min {
float:right !important;
width: initial !important;
}
/***************
* listviews *
***************/
@ -235,6 +259,45 @@
border-bottom: 1px solid #CECECE;
}
.info-table {
display: table !important;
}
.info-table thead {
visibility: collapse;
}
.info-table th {
border-bottom: none !important;
}
.info-table tr {
border-bottom: 1px solid #f2f2f2
}
.info-table td {
color: #555 !important;
padding: 2px;
padding-right: 14px;
padding-left: 14px;
border: none !important;
}
.info-table td.label {
font-weight: bold;
}
.info-table td.label-center {
vertical-align: middle;
}
.info-table input {
color: #555;
border: none;
font-size: 1em;
width: 100%;
}
.albumdivider h1, .table li h1 {
font-size: 120% !important;
}
@ -305,15 +368,33 @@
font-size: initial;
}
.infoBtn {
top: 0;
width: 90%;
position: absolute;
}
.infoBtn i {
font-size: 1.33em;
color: #ddd;
background: white;
border-radius: 50%;
height: 1em;
width: 1em;
}
.backnav {
background-color: #ccc !important;
}
.refreshLibraryBtnDiv {
display: none;
}
/**********************
* Now Playing area *
**********************/
#nowPlayingFooter {
height: 50px;
line-height: 48px;
@ -342,11 +423,17 @@
/************
* Popups *
************/
#modalalbum a, #modalartist a {
#modalalbum a, #modalartist a, #modalname a {
color: #444;
text-decoration: none;
}
#modalinfo {
position: relative;
display: inline-block;
padding-top: .5em;
}
.popupArtistLi,
.popupAlbumLi {
display: none
@ -392,6 +479,7 @@
.ui-icon-playAll:after,
.ui-icon-play:after,
.ui-icon-playNext:after,
.ui-icon-insert:after,
.ui-icon-add:after,
.ui-icon-addAll:after,
.ui-icon-remove:after {
@ -411,6 +499,10 @@
content: '\f149';
}
.ui-icon-insert:after {
content: '\f177';
}
.ui-icon-add:after {
content: '\f196';
}
@ -434,11 +526,17 @@
font-weight: normal;
}
.popupDialog {
.popupDialog,
.popupDialog-full-width {
padding: 10px;
text-align: center;
}
.popupDialog-full-width {
padding-left: 0;
padding-right: 0;
}
/*dont hide clear buttons in text input */
.ui-input-clear-hidden {
display: block !important;
@ -447,7 +545,6 @@
/****************
* Common use *
****************/
#playlistspane {
margin: 0 !important;
}
@ -531,31 +628,40 @@ a {
}
/*helper*/
.ui-loader h1 {
color: #efefef;
}
/* panel workaround to make it responsive wrap push on wide viewports once open */
@media (min-width: 35em){
.ui-responsive-panel.ui-page-panel-open .ui-panel-content-fixed-toolbar-open.ui-panel-content-fixed-toolbar-display-push,
.ui-responsive-panel.ui-page-panel-open .ui-panel-content-fixed-toolbar-open.ui-panel-content-fixed-toolbar-display-reveal,
.ui-responsive-panel.ui-page-panel-open .ui-panel-content-wrap-open.ui-panel-content-wrap-display-push,
.ui-responsive-panel.ui-page-panel-open .ui-panel-content-wrap-open.ui-panel-content-wrap-display-reveal {
/*desktop*/
@media (min-width: 55em) {
/* panel workaround to make it responsive wrap push on wide viewports once open */
.ui-responsive-panel.ui-page-panel .ui-panel-content-fixed-toolbar-open.ui-panel-content-fixed-toolbar-display-push,
.ui-responsive-panel.ui-page-panel .ui-panel-content-fixed-toolbar-open.ui-panel-content-fixed-toolbar-display-reveal,
.ui-responsive-panel.ui-page-panel .ui-panel-content-wrap-open.ui-panel-content-wrap-display-push,
.ui-responsive-panel.ui-page-panel .ui-panel-content-wrap-open.ui-panel-content-wrap-display-reveal {
margin-right: 17em;
width: auto;
}
.ui-responsive-panel.ui-page-panel-open .ui-panel-content-fixed-toolbar-open.ui-panel-content-wrap-display-push.ui-panel-content-fixed-toolbar-position-right,
.ui-responsive-panel.ui-page-panel-open .ui-panel-content-fixed-toolbar-open.ui-panel-content-wrap-display-reveal.ui-panel-content-fixed-toolbar-position-right,
.ui-responsive-panel.ui-page-panel-open .ui-panel-content-wrap-open.ui-panel-content-wrap-display-push.ui-panel-content-wrap-position-right,
.ui-responsive-panel.ui-page-panel-open .ui-panel-content-wrap-open.ui-panel-content-wrap-display-reveal.ui-panel-content-wrap-position-right {
.ui-responsive-panel.ui-page-panel .ui-panel-content-fixed-toolbar-open.ui-panel-content-wrap-display-push.ui-panel-content-fixed-toolbar-position-right,
.ui-responsive-panel.ui-page-panel .ui-panel-content-fixed-toolbar-open.ui-panel-content-wrap-display-reveal.ui-panel-content-fixed-toolbar-position-right,
.ui-responsive-panel.ui-page-panel .ui-panel-content-wrap-open.ui-panel-content-wrap-display-push.ui-panel-content-wrap-position-right,
.ui-responsive-panel.ui-page-panel .ui-panel-content-wrap-open.ui-panel-content-wrap-display-reveal.ui-panel-content-wrap-position-right {
margin: 0 0 0 17em;
}
}
/*tablets and desktop*/
@media (min-width: 35em) {
.ui-responsive-panel .ui-panel-dismiss-display-reveal {
display: none;
}
.popupDialog {
min-width: 320px;
}
}
/*smartphones*/
@media (max-width: 35em) {
#nowPlayingpane {

View File

@ -16,7 +16,7 @@
<link rel="shortcut icon" type="image/x-icon" href="images/icons/musicbox32.gif" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="MusicBox" />
<meta name="apple-mobile-web-app-title" content="{{ title }}" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
@ -36,8 +36,8 @@
</head>
<body data-websocket-url="{{websocketUrl}}" data-is-musicbox="{{isMusicBox}}" data-has-alarmclock="{{hasAlarmClock}}" data-on-track-click="{{onTrackClick}}">
<div data-role="page" id="page" class="ui-responsive-panel" data-theme="c">
<body data-websocket-url="{{ websocketUrl }}" data-is-musicbox="{{ isMusicBox }}" data-has-alarmclock="{{ hasAlarmClock }}" data-on-track-click="{{ onTrackClick }}" data-program-name="{{ programName }}" data-hostname="{{ hostname }}">
<div data-role="page" id="page" class="ui-responsive-panel" data-theme="c" data-title="{{ title }}">
<div data-role="panel" id="panel" data-position="left" data-theme="a" data-display="reveal" data-position-fixed="true">
<ul class="ui-listview mainNav" data-role="listview" data-theme="a">
@ -93,6 +93,15 @@
value="0" max="100"/>
</div>
</li>
<li data-icon="false">
<div class="hostInfo">
{% if hostname == serverIP %}
<span class="hostInfo">{{programName}} running on {{ hostname }}:{{ serverPort}}</span>
{% else %}
<span class="hostInfo">{{programName}} running on {{ hostname }} at {{ serverIP }}:{{ serverPort}}</span>
{% end %}
</div>
</li>
</ul>
</div>
@ -102,14 +111,14 @@
<h3 id="coverpopupalbumname"></h3>
<h4 id="coverpopupartist"></h4>
<a href="#" onclick="closePopups();"><img id="coverpopupimage" src="" alt="Album cover"/></a>
<a href="#" onclick="$('#coverpopup').popup('close');"><img id="coverpopupimage" src="" alt="Album cover"/></a>
</div>
<div id="artistpopup" data-role="popup" data-theme="c">
<a href="#" data-rel="back" data-role="button" data-icon="delete" data-iconpos="notext"
class="ui-btn-right">Close</a>
<h4 id="artistpopupname">&nbsp;</h4>
<a href="#" onclick="closePopups();"><img id="artistpopupimage" src="" alt="Album artist"/></a>
<a href="#" onclick="$('#artistpopup').popup('close');"><img id="artistpopupimage" src="" alt="Album artist"/></a>
</div>
<div data-role="popup" data-transition="none" data-theme="b" id="popupTracks">
@ -121,26 +130,29 @@
<li data-icon="playAll" data-iconshadow="false">
<a href="#" onclick="return controls.playTracks(PLAY_ALL, mopidy);">Play All</a>
</li>
<li data-icon="playNext" class="addqueue">
<li data-icon="playNext">
<a href="#" onclick="return controls.playTracks(PLAY_NEXT, mopidy);">Play Track Next</a>
</li>
<li data-icon="add" class="addqueue">
<li data-icon="add">
<a href="#" onclick="return controls.playTracks(ADD_THIS_BOTTOM, mopidy);">Add Track to Bottom of Queue</a>
</li>
<li data-icon="addAll" class="addqueue">
<li data-icon="addAll">
<a href="#" onclick="return controls.playTracks(ADD_ALL_BOTTOM, mopidy);">Add All to Bottom of Queue</a>
</li>
<li class="popupAlbumLi">
<a href="#" onclick="showAlbumPopup('#popupTracks')">Show Album <span class="popupAlbumName"></span></a>
<a href="#" onclick="showAlbumPopup('#popupTracks', mopidy)">Show Album <span class="popupAlbumName"></span></a>
</li>
<li class="popupArtistsLi">
<a href="#" onclick="showArtist()" class="popupArtistHref">Show Artist <span class="popupArtistName"></span>
<a href="#" onclick="library.showArtist(null, mopidy)" class="popupArtistHref">Show Artist <span class="popupArtistName"></span>
</a>
</li>
<div data-role="collapsible" data-icon="false" data-inset="false" class="popupArtistsDiv">
<h2>Artists</h2>
<ul data-icon="false" data-inset="false" data-role="listview" class="popupArtistsLv"></ul>
</div>
<li>
<a href="#" onclick="return controls.showInfoPopup('', '#popupTracks', mopidy);">Show Track Info...</span></a>
</li>
</ul>
</div>
</div>
@ -151,40 +163,65 @@
<li data-icon="play">
<a href="#" onclick="return controls.playQueueTrack();">Play <span class="popupTrackName"></span></a>
</li>
<li data-icon="insert">
<a href="#" onclick="return controls.showAddTrackPopup();">Add a Track Below <span class="popupTrackName"></span></a>
</li>
<li data-icon="remove">
<a href="#" onclick="return controls.removeTrack();">Remove from Queue</a>
<a href="#" onclick="return controls.removeTrack('', mopidy);">Remove from Queue</a>
</li>
<li class="popupAlbumLi">
<a href="#" onclick="showAlbumPopup('#popupQueue')">Show Album <span class="popupAlbumName"></span></a>
<a href="#" onclick="showAlbumPopup('#popupQueue', mopidy)">Show Album <span class="popupAlbumName"></span></a>
</li>
<li class="popupArtistsLi">
<a href="#" onclick="showArtist()" class="popupArtistHref">Show Artist <span class="popupArtistName"></span>
<a href="#" onclick="library.showArtist(null, mopidy)" class="popupArtistHref">Show Artist <span class="popupArtistName"></span>
</a>
</li>
<div data-role="collapsible" data-icon="false" data-inset="false" class="popupArtistsDiv">
<h2>Artists</h2>
<ul data-icon="false" data-inset="false" data-role="listview" class="popupArtistsLv"></ul>
</div>
<li>
<a href="#" onclick="return controls.showInfoPopup('', '#popupQueue', mopidy);">Show Track Info...</span></a>
</li>
</ul>
</div>
</div>
<div data-role="popup" data-theme="b" id="popupAddTrack" class="popupDialog">
<form>
<p>Add a Track to the Queue
<button class="btn" type="button" id="getPlayingBtn" title="Use URI of currently playing track" onclick="return controls.getCurrentlyPlaying('addTrackInput');">
Get Currently Playing URI
</button>
<input id="addTrackInput" placeholder="Track URI" class="span2" data-clear-btn="true"
onkeypress="return controls.checkDefaultButtonClick(event.keyCode, '#popupAddTrack');" type="text"/>
<select name="select-add" id="select-add"></select>
<div data-role="controlgroup" data-type="horizontal" align="center">
<button class="btn" type="button" onclick="return $('#popupAddTrack').popup('close');">
Cancel
</button>
<button class="btn" type="button" data-default-btn="true" onclick="return controls.addTrack($('#addTrackInput').val(), mopidy);">
Add
</button>
</div>
</form>
</div><!--/add track to queue-->
<div data-role="popup" data-theme="b" id="popupSave" class="popupDialog">
<form>
<p>Save Current Queue to a Playlist
<input id="saveinput" placeholder="Playlist name" class="span2" data-clear-btn="true"
onkeypress="return controls.savePressed(event.keyCode);" type="text"/>
onkeypress="return controls.checkDefaultButtonClick(event.keyCode, '#popupSave');" type="text"/>
<div data-role="controlgroup" data-type="horizontal" align="center">
<button class="btn" type="button" onclick="return $('#popupSave').popup('close');">
Cancel
</button>
<button class="btn" type="button" onclick="return controls.saveQueue();">
<button class="btn" type="button" data-default-btn="true" onclick="return controls.saveQueue();">
Save
</button>
</div>
</form>
</div>
<!--/save queue to playlist-->
</div><!--/save queue to playlist-->
<div data-role="popup" data-theme="b" id="popupOverwrite" class="popupDialog">
<form>
@ -214,9 +251,22 @@
</form>
</div><!--/confirm delete stream-->
<div data-role="popup" data-theme="b" id="popupShowInfo" class="popupDialog-full-width">
<a href="#" data-rel="back" data-role="button" data-icon="delete" data-iconpos="notext" class="ui-btn-right">Close</a>
<table data-role="table" data-mode="reflow" class="ui-responsive table-stroke info-table">
<thead>
<tr>
<th data-priority="persist"></th>
<th data-priority="persist"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div><!--/show track info-->
<div data-role="header" data-tap-toggle="false" id="header" data-position="fixed" class="header-breakpoint headerbtn">
<a id="headermenubtn" href="#panel"><i class="fa fa-align-justify"></i></a>
<h1 id="contentHeadline">Musicbox</h1>
<h1 id="contentHeadline"></h1>
<a id="headersearchbtn" href="#" onclick="switchContent('search' ); return false;" title="Search"><i class="fa fa-search"></i></a>
</div>
<!-- /header -->
@ -284,10 +334,12 @@
<div id="nowPlayingpane" data-role="content" class="pane">
<img id="albumCoverImg" src="images/default_cover.png" alt="Album cover"/>
<div id="modalinfo">
<img id="albumCoverImg" src="images/default_cover.png" alt="Album cover"/>
</div>
<div class="nowPlaying-artistInfo">
<h3 id="modalname"></h3>
<h3 id="modalname"></h3>
<p class="artistAlbumLine"><span id="modalartist"></span> - <span id="modalalbum"></span></p>
</div>
@ -325,7 +377,16 @@
<!--/playlistspane-->
<div data-role="content" id="browsepane" class="pane">
<h4>Browse</h4>
<div class="ui-grid-a">
<div class="ui-block-a">
<h4>Browse</h4>
</div>
<div align="right" class="ui-block-b refreshLibraryBtnDiv" data-role="controlgroup" data-type="horizontal">
<button id="refreshLibraryBtn" class="btn" type="button" title="Refresh library">
<i class="fa fa-refresh"></i>
</button>
</div>
</div>
<div class="ui-grid">
<ul id="browsetable" class="table"></ul>
</div>
@ -334,16 +395,21 @@
<div data-role="content" class="pane" id="currentpane">
<div class="ui-grid-a">
<div class="ui-block-a">
<div class="ui-block-a ui-block-a-min">
<h4>Play Queue</h4>
</div>
<div align="right" class="ui-block-b" data-role="controlgroup" data-type="horizontal">
<button class="btn" type="button" title="Save queue to playlist" onclick="return controls.showSavePopup();">
<i class="fa fa-bookmark-o"></i>
</button>
<button class="btn" type="button" title="Clear queue" onclick="return controls.clearQueue();">
<i class="fa fa-times"></i>
</button>
<div align="right" class="ui-block-b ui-block-b-min">
<div data-role="controlgroup" data-type="horizontal">
<button class="btn" type="button" title="Add a track to the queue" onclick="return controls.showAddTrackPopup();">
<i class="fa fa-plus"></i>
</button>
<button class="btn" type="button" title="Save queue to playlist" onclick="return controls.showSavePopup();">
<i class="fa fa-bookmark-o"></i>
</button>
<button class="btn" type="button" title="Clear queue" onclick="return controls.clearQueue();">
<i class="fa fa-times"></i>
</button>
</div>
</div>
</div>
<div class="ui-grid">
@ -421,7 +487,7 @@
<div class="ui-block-a" style="padding: 5px">
<form>
<p>Play a specific stream/track and optionally save it to your favourites.
<button class="btn" type="button" onclick="return controls.getCurrentlyPlaying();">
<button class="btn" type="button" onclick="return controls.getCurrentlyPlaying('streamuriinput', 'streamnameinput');">
Get currently playing
</button>
<input id="streamuriinput" placeholder="URI" class="span2" data-clear-btn="true"

View File

@ -17,8 +17,10 @@
* Adds tracks to current tracklist and starts playback if necessary.
*
* @param {string} action - The action to perform. Valid actions are:
* PLAY_NOW: add the track at 'trackIndex' and start playback.
* PLAY_NEXT: insert track after currently playing track.
* PLAY_NOW: add the track at the current queue position and
* start playback immediately.
* PLAY_NEXT: insert track after the reference track, if 'index'
* is provided, or after the current track otherwise.
* ADD_THIS_BOTTOM: add track to bottom of tracklist.
* ADD_ALL_BOTTOM: add all tracks in in the list to bottom of
* tracklist.
@ -32,17 +34,19 @@
* @param {string} playlistUri - (Optional) The URI of the playlist containing the tracks
* to be played. If no URI is provided then the 'list' attribute
* of the popup DIV is assumed to contain the playlist URI.
* @param {string} index - (Optional) The tracklist index of the reference track that the
* action should be performed on. Defaults to the index of the currently
* playing track.
*/
playTracks: function (action, mopidy, trackUri, playlistUri) {
$('#popupTracks').popup('close')
toast('Loading...')
playTracks: function (action, mopidy, trackUri, playlistUri, index) {
toast('Updating queue...')
trackUri = trackUri || $('#popupTracks').data('track')
trackUri = trackUri || $('#popupTracks').data('track') || $('#popupQueue').data('track')
if (typeof trackUri === 'undefined') {
throw new Error('No track URI provided for playback.')
}
playlistUri = playlistUri || $('#popupTracks').data('list')
playlistUri = playlistUri || $('#popupTracks').data('list') || $('#popupQueue').data('list')
if (typeof playlistUri === 'undefined') {
throw new Error('No playlist URI provided for playback.')
}
@ -58,18 +62,17 @@
switch (action) {
case PLAY_NOW:
case PLAY_NEXT:
// Find track that is currently playing.
mopidy.tracklist.index().then(function (currentIndex) {
// Add browsed track just below it.
mopidy.tracklist.add({at_position: currentIndex + 1, uris: trackUris}).then(function (tlTracks) {
if (action === PLAY_NOW) { // Start playback immediately.
mopidy.playback.stop().then(function () {
mopidy.playback.play({tlid: tlTracks[0].tlid})
})
}
})
if (currentIndex === null && action === PLAY_NEXT) {
// Tracklist is empty, start playing new tracks immediately.
action = PLAY_NOW
}
controls._addTrackAtIndex(action, mopidy, trackUris, currentIndex)
})
break
case INSERT_AT_INDEX:
controls._addTrackAtIndex(action, mopidy, trackUris, index)
break
case ADD_THIS_BOTTOM:
case ADD_ALL_BOTTOM:
case PLAY_ALL:
@ -87,14 +90,19 @@
throw new Error('Unexpected tracklist action identifier: ' + action)
}
if (window[$(document.body).data('on-track-click')] === DYNAMIC) {
// Save last 'action' - will become default for future 'onClick' events
var previousAction = $.cookie('onTrackClick')
if (typeof previousAction === 'undefined' || action !== previousAction) {
$.cookie('onTrackClick', action, { expires: 365 })
updatePlayIcons('', '', controls.getIconForAction(action))
if (action !== INSERT_AT_INDEX) { // TODO: Add support for 'INSERT_AT_INDEX' to allow user to insert tracks in any playlist.
if (window[$(document.body).data('on-track-click')] === DYNAMIC) {
// Save last 'action' - will become default for future 'onClick' events
var previousAction = $.cookie('onTrackClick')
if (typeof previousAction === 'undefined' || action !== previousAction) {
$.cookie('onTrackClick', action, { expires: 365 })
updatePlayIcons('', '', controls.getIconForAction(action))
}
}
}
$('#popupTracks').popup('close')
$('#popupQueue').popup('close')
},
/* Getter function for 'action' variable. Also checks config settings and cookies if required. */
@ -120,6 +128,8 @@
return 'fa fa-play-circle'
case PLAY_NOW:
return 'fa fa-play-circle-o'
case INSERT_AT_INDEX:
return 'fa fa-long-arrow-left'
case PLAY_NEXT:
return 'fa fa-level-down'
case ADD_THIS_BOTTOM:
@ -138,6 +148,7 @@
switch (parseInt(action)) {
case PLAY_NOW:
case PLAY_NEXT:
case INSERT_AT_INDEX:
case ADD_THIS_BOTTOM:
// Process single track
trackUris.push(trackUri)
@ -153,6 +164,27 @@
return trackUris
},
_addTrackAtIndex: function (action, mopidy, trackUris, index) {
if (typeof index === 'undefined' || index === '') {
throw new Error('No index provided for insertion.')
}
var pos = index
if (pos === null) {
pos = 0
} else {
pos += 1
}
mopidy.tracklist.add({at_position: pos, uris: trackUris}).then(function (tlTracks) {
if (action === PLAY_NOW) { // Start playback immediately.
mopidy.playback.stop().then(function () {
mopidy.playback.play({tlid: tlTracks[0].tlid})
})
}
})
},
/** ******************************************************
* play an uri from the queue
*********************************************************/
@ -165,39 +197,107 @@
playQueueTrack: function (tlid) {
// Stop directly, for user feedback
mopidy.playback.stop()
$('#popupQueue').popup('close')
toast('Loading...')
tlid = tlid || $('#popupQueue').data('tlid')
mopidy.playback.play({'tlid': parseInt(tlid)})
$('#popupQueue').popup('close')
},
/** *********************************
* remove a track from the queue *
***********************************/
removeTrack: function (tlid) {
$('#popupQueue').popup('close')
removeTrack: function (tlid, mopidy) {
toast('Deleting...')
tlid = tlid || $('#popupQueue').data('tlid')
mopidy.tracklist.remove({'tlid': [parseInt(tlid)]})
$('#popupQueue').popup('close')
},
clearQueue: function () {
mopidy.tracklist.clear().then(
resetSong()
)
mopidy.tracklist.clear()
return false
},
savePressed: function (key) {
checkDefaultButtonClick: function (key, parentElement) {
// Click the default button on parentElement when the user presses the enter key.
if (key === 13) {
controls.saveQueue()
return false
$(parentElement).find('button' + '[data-default-btn="true"]').click()
}
return true
},
showAddTrackPopup: function (tlid) {
$('#addTrackInput').val('')
$('#select-add').empty()
tlid = tlid || $('#popupQueue').data('tlid')
if (typeof tlid !== 'undefined' && tlid !== '') {
// Store the tlid of the track after which we want to perform the insert
$('#popupAddTrack').data('tlid', $('#popupQueue').data('tlid'))
$('#popupAddTrack').one('popupafterclose', function (event, ui) {
// Ensure that popup attributes are reset when the popup is closed.
$(this).removeData('tlid')
})
var trackName = popupData[$('#popupQueue').data('track')].name
$('#select-add').append('<option value="6" selected="selected">Add Track Below \'' + trackName + '\'</option>')
}
if (typeof songdata.track.uri !== 'undefined' && songdata.track.uri !== '') {
$('#getPlayingBtn').button('enable')
} else {
$('#getPlayingBtn').button('disable')
}
$('#select-add').append('<option value="1">Play Added Track Next</option>') // PLAY_NEXT
$('#select-add').append('<option value="2">Add Track to Bottom of Queue</option>') // ADD_THIS_BOTTOM
$('#select-add').trigger('change')
$('#popupQueue').popup('close')
$('#popupAddTrack').popup('open')
},
addTrack: function (trackUri, mopidy) {
var selection = parseInt($('#select-add').val())
if (selection === ADD_THIS_BOTTOM) {
controls.addTrackToBottom(trackUri, mopidy)
} else if (selection === PLAY_NEXT) {
controls.insertTrack(trackUri, mopidy)
} else if (selection === INSERT_AT_INDEX) {
var tlid = $('#popupAddTrack').data('tlid')
controls.insertTrack(trackUri, mopidy, tlid)
} else {
throw new Error('Unkown tracklist action selection option: ' + selection)
}
},
insertTrack: function (trackUri, mopidy, tlid) {
if (typeof trackUri === 'undefined' || trackUri === '') {
throw new Error('No track URI provided to insert.')
}
if (typeof tlid !== 'undefined' && tlid !== '') {
mopidy.tracklist.index({tlid: parseInt(tlid)}).then(function (index) {
controls.playTracks(INSERT_AT_INDEX, mopidy, trackUri, CURRENT_PLAYLIST_TABLE, index)
})
} else {
// No tlid provided, insert after current track.
controls.playTracks(PLAY_NEXT, mopidy, trackUri, CURRENT_PLAYLIST_TABLE)
}
$('#popupAddTrack').popup('close')
return false
},
addTrackToBottom: function (trackUri, mopidy) {
if (typeof trackUri === 'undefined' || trackUri === '') {
throw new Error('No track URI provided to add.')
}
controls.playTracks(ADD_THIS_BOTTOM, mopidy, trackUri, CURRENT_PLAYLIST_TABLE)
$('#popupAddTrack').popup('close')
return false
},
showSavePopup: function () {
mopidy.tracklist.getTracks().then(function (tracks) {
if (tracks.length > 0) {
@ -238,6 +338,134 @@
})
},
showInfoPopup: function (uri, popupId, mopidy) {
showLoading(true)
var trackUri = uri || $(popupId).data('track')
if (popupId && popupId.length > 0) {
$(popupId).popup('close')
}
$('#popupShowInfo tbody').empty()
mopidy.library.lookup({'uris': [trackUri]}).then(function (resultDict) {
var uri = Object.keys(resultDict)[0]
var track = resultDict[uri][0]
var html = ''
var rowTemplate = '<tr><td class="label">{label}:</td><td id="{label}-cell">{text}</td></tr>'
var row = {'label': '', 'text': ''}
row.label = 'Name'
if (track.name) {
row.text = track.name
} else {
row.text = '(Not available)'
}
html += stringFromTemplate(rowTemplate, row)
row.label = 'Album'
if (track.album && track.album.name) {
row.text = track.album.name
} else {
row.text = '(Not available)'
}
html += stringFromTemplate(rowTemplate, row)
var artists = artistsToString(track.artists)
// Fallback to album artists.
if (artists.length === 0 && track.album && track.album.artists) {
artists = artistsToString(track.album.artists)
}
if (artists.length > 0) {
row.label = 'Artist'
if (track.artists && track.artists.length > 1 || track.album && track.album.artists && track.album.artists.length > 1) {
row.label += 's'
}
row.text = artists
html += stringFromTemplate(rowTemplate, row)
}
var composers = artistsToString(track.composers)
if (composers.length > 0) {
row.label = 'Composer'
if (track.composers.length > 1) {
row.label += 's'
}
row.text = composers
html += stringFromTemplate(rowTemplate, row)
}
var performers = artistsToString(track.performers)
if (performers.length > 0) {
row.label = 'Performer'
if (track.performers.length > 1) {
row.label += 's'
}
row.text = performers
html += stringFromTemplate(rowTemplate, row)
}
if (track.genre) {
row = {'label': 'Genre', 'text': track.genre}
html += stringFromTemplate(rowTemplate, row)
}
if (track.track_no) {
row = {'label': 'Track #', 'text': track.track_no}
html += stringFromTemplate(rowTemplate, row)
}
if (track.disc_no) {
row = {'label': 'Disc #', 'text': track.disc_no}
html += stringFromTemplate(rowTemplate, row)
}
if (track.date) {
row = {'label': 'Date', 'text': new Date(track.date).toLocaleString()}
html += stringFromTemplate(rowTemplate, row)
}
if (track.length) {
row = {'label': 'Length', 'text': timeFromSeconds(track.length / 1000)}
html += stringFromTemplate(rowTemplate, row)
}
if (track.bitrate) {
row = {'label': 'Bitrate', 'text': track.bitrate}
html += stringFromTemplate(rowTemplate, row)
}
if (track.comment) {
row = {'label': 'Comment', 'text': track.comment}
html += stringFromTemplate(rowTemplate, row)
}
if (track.musicbrainz_id) {
row = {'label': 'MusicBrainz ID', 'text': track.musicbrainz_id}
html += stringFromTemplate(rowTemplate, row)
}
if (track.last_modified) {
row = {'label': 'Modified', 'text': track.last_modified}
html += stringFromTemplate(rowTemplate, row)
}
rowTemplate = '<tr><td class="label label-center">{label}:</td><td><input type="text" id="uri-input" value="{text}"></input></td></tr>'
row = {'label': 'URI', 'text': uri}
html += stringFromTemplate(rowTemplate, row)
$('#popupShowInfo tbody').append(html)
showLoading(false)
$('#popupShowInfo').popup('open')
if (!isMobile) {
// Set focus and select URI text on desktop systems (don't want the keyboard to pop up automatically on mobile devices)
$('#popupShowInfo #uri-input').focus()
$('#popupShowInfo #uri-input').select()
}
}, console.error)
return false
},
refreshPlaylists: function () {
mopidy.playlists.refresh().then(function () {
playlists = {}
@ -247,6 +475,14 @@
return false
},
refreshLibrary: function () {
var uri = $('#refreshLibraryBtn').data('url')
mopidy.library.refresh({'uri': uri}).then(function () {
library.getBrowseDir(uri)
})
return false
},
/** ***********
* Buttons *
*************/
@ -448,8 +684,8 @@
return false
},
getCurrentlyPlaying: function () {
$('#streamuriinput').val(songdata.track.uri)
getCurrentlyPlaying: function (uriInput, nameInput) {
$('#' + uriInput).val(songdata.track.uri)
var name = songdata.track.name
if (songdata.track.artists) {
var artistStr = artistsToString(songdata.track.artists)
@ -457,7 +693,7 @@
name = artistStr + ' - ' + name
}
}
$('#streamnameinput').val(name)
$('#' + nameInput).val(name)
return true
},
@ -652,7 +888,6 @@
window.history.back()
}, 10000)
}
}
return controls
}))

View File

@ -20,6 +20,17 @@
$(document).bind('mobileinit', configureJQueryMobile)
// Extension: timeout to detect end of scrolling action.
$.fn.scrollEnd = function (callback, timeout) {
$(this).scroll(function () {
var $this = $(this)
if ($this.data('scrollTimeout')) {
clearTimeout($this.data('scrollTimeout'))
}
$this.data('scrollTimeout', setTimeout(callback, timeout))
})
}
return configureJQueryMobile
}))

View File

@ -28,8 +28,7 @@ var artiststext = ''
var songname = ''
var songdata = {'track': {}, 'tlid': -1}
var playlisttracksScroll
var playlistslistScroll
var pageScrollPos = {}
var STREAMS_PLAYLIST_NAME = '[Radio Streams]'
var STREAMS_PLAYLIST_SCHEME = 'm3u'
@ -42,14 +41,13 @@ var customTracklists = [] // TODO: Refactor into one shared cache
var browseStack = []
var ua = navigator.userAgent
var ua = navigator.userAgent || navigator.vendor || window.opera
var isMobileSafari = /Mac/.test(ua) && /Mobile/.test(ua)
var isMobileWebkit = /WebKit/.test(ua) && /Mobile/.test(ua)
var isMobile = /Mobile/.test(ua)
var isWebkit = /WebKit/.test(ua)
var isMobile = isMobileAll()
// constants
PROGRAM_NAME = 'MusicBox'
PROGRAM_NAME = $(document.body).data('program-name')
HOSTNAME = $(document.body).data('hostname')
ARTIST_TABLE = '#artiststable'
ALBUM_TABLE = '#albumstable'
BROWSE_TABLE = '#browsetable'
@ -68,6 +66,7 @@ ADD_THIS_BOTTOM = 2
ADD_ALL_BOTTOM = 3
PLAY_ALL = 4
DYNAMIC = 5
INSERT_AT_INDEX = 6
// the first part of Mopidy extensions which serve radio streams
var radioExtensionsList = ['somafm', 'tunein', 'dirble', 'audioaddict']
@ -75,8 +74,9 @@ var radioExtensionsList = ['somafm', 'tunein', 'dirble', 'audioaddict']
var uriClassList = [
['spotify', 'fa-spotify'],
['spotifytunigo', 'fa-spotify'],
['spotifyweb', 'fa-spotify'],
['local', 'fa-file-sound-o'],
['file', 'fa-folder-o'],
['file', 'fa-file-sound-o'],
['m3u', 'fa-file-sound-o'],
['podcast', 'fa-rss-square'],
['podcast+file', 'fa-rss-square'],
@ -102,7 +102,8 @@ var uriClassList = [
var uriHumanList = [
['spotify', 'Spotify'],
['spotifytunigo', 'Spotify browse'],
['local', 'Local files'],
['spotifyweb', 'Spotify browse'],
['local', 'Local media'],
['m3u', 'Local playlists'],
['podcast', 'Podcasts'],
['podcast+itunes', 'iTunes Store: Podcasts'],
@ -130,10 +131,37 @@ var searchBlacklist = [
'yt'
]
// List of known audio file extensions
// TODO: consider querying GStreamer for supported audio formats - see:https://discuss.mopidy.com/t/supported-codecs-file-formats/473
var audioExt = [
'aa', 'aax', // Audible.com
'aac', // Advanced Audio Coding format
'aiff', // Apple
'au', // Sun Microsystems
'flac', // Free Lossless Audio Codec
'gsm',
'iklax',
'ivs',
'm4a',
'm4b',
'm4p',
'mp3',
'mpc', // Musepack
'ogg', 'oga', 'mogg', // Ogg-Vorbis
'opus', // Internet Engineering Task Force (IETF)
'ra', 'rm', // RealAudio
'raw',
'tta', // True Audio
'vox',
'wav',
'wma', // Microsoft
'wv',
'webm' // HTML5 video
]
function scrollToTop () {
var divtop = 0
$('body,html').animate({
scrollTop: divtop
scrollTop: 0
}, 250)
}
@ -144,6 +172,14 @@ function scrollToTracklist () {
}, 250)
}
function isMobileAll () {
// Checks for known mobile and tablet devices - see http://stackoverflow.com/questions/11381673/detecting-a-mobile-browser
var regexpMobile = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i
var regexpTablet = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i
var uaString = ua.substr(0, 4)
return isMobileSafari || regexpMobile.test(uaString) || regexpTablet.test(uaString)
}
// A hack to find the name of the first artist of a playlist. this is not yet returned by mopidy
// does not work wel with multiple artists of course
function getArtist (pl) {
@ -168,12 +204,14 @@ function getAlbum (pl) {
function artistsToString (artists, max) {
var result = ''
max = max || 3
for (var i = 0; i < artists.length && i < max; i++) {
if (artists[i].name) {
if (i > 0) {
result += ', '
if (artists && artists.length > 0) {
for (var i = 0; i < artists.length && i < max; i++) {
if (artists[i].name) {
if (i > 0) {
result += ', '
}
result += artists[i].name
}
result += artists[i].name
}
}
return result
@ -204,13 +242,9 @@ function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, curre
var onClick = ''
var html = ''
track.name = validateTrackName(track, currentIndex)
// Leave out unplayable items
if (track.name.substring(0, 12) === '[unplayable]') {
return html
}
// Streams
if (track.length === -1) {
html += '<li class="albumli"><a href="#"><h1><i class="' + getMediaClass(track.uri) + '"></i> ' + track.name + ' [Stream]</h1></a></li>'
html += '<li class="albumli"><a href="#"><h1><i class="' + getMediaClass(track) + '"></i> ' + track.name + ' [Stream]</h1></a></li>'
return html
}
@ -221,11 +255,13 @@ function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, curre
onClick = 'return controls.playTracks(\'\', mopidy, \'' + track.uri + '\', \'' + uri + '\');'
}
html +=
'<li class="song albumli" id="' + getjQueryID(target, track.uri) + '" tlid="' + tlid + '">' +
'<a href="#" class="moreBtn" onclick="return popupTracks(event, \'' + uri + '\',\'' + track.uri + tlidParameter + '\');">' +
'<i class="fa fa-play-circle-o"></i></a>' +
'<a href="#" onclick="' + onClick + '"><h1><i class="' + getMediaClass(track.uri) + '"></i> ' + track.name + '</h1>'
html += '<li class="song albumli" id="' + getjQueryID(target, track.uri) + '" tlid="' + tlid + '">'
if (isPlayable(track)) {
// Show popup icon for audio files or 'tracks' of other scheme types
html += '<a href="#" class="moreBtn" onclick="return popupTracks(event, \'' + uri + '\',\'' + track.uri + tlidParameter + '\');">' +
'<i class="fa fa-play-circle-o"></i></a>'
}
html += '<a href="#" onclick="' + onClick + '"><h1><i class="' + getMediaClass(track) + '"></i> ' + track.name + '</h1>'
if (listLength === 1 || (!hasSameAlbum(previousTrack, track) && !hasSameAlbum(track, nextTrack))) {
html += renderSongLiAlbumInfo(track)
@ -271,9 +307,9 @@ function renderSongLiDivider (previousTrack, track, nextTrack, target) {
if (!hasSameAlbum(previousTrack, track) && hasSameAlbum(track, nextTrack)) {
// Large divider with album cover.
html +=
'<li class="albumdivider"><a href="#" onclick="return library.showAlbum(\'' + track.album.uri + '\');">' +
'<li class="albumdivider"><a href="#" onclick="return library.showAlbum(\'' + track.album.uri + '\', mopidy);">' +
'<img id="' + getjQueryID(target + '-cover', track.uri) + '" class="artistcover" width="30" height="30"/>' +
'<h1><i class="' + getMediaClass(track.uri) + '"></i> ' + track.album.name + '</h1><p>' +
'<h1>' + track.album.name + '</h1><p>' +
renderSongLiTrackArtists(track) + '</p></a></li>'
// Retrieve album covers
images.setAlbumImage(track.uri, getjQueryID(target + '-cover', track.uri, true), mopidy, 'small')
@ -281,7 +317,7 @@ function renderSongLiDivider (previousTrack, track, nextTrack, target) {
// Small divider
html += '<li class="smalldivider"> &nbsp;</li>'
}
if (typeof target !== 'undefined' && target.length > 0) {
if (html.length > 0 && typeof target !== 'undefined' && target.length > 0) {
target = getjQueryID(target, track.uri, true)
$(target).before(html)
}
@ -357,40 +393,6 @@ function resultsToTables (results, target, uri, onClickBack, backIsOptional) {
updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction())
}
// process updated playlist to gui
function playlisttotable (pl, target, uri) {
var tmp = ''
$(target).html('')
var targetmin = target.substr(1)
var child = ''
for (var i = 0; i < pl.length; i++) {
if (pl[i]) {
popupData[pl[i].uri] = pl[i]
child = '<li id="' + targetmin + '-' + pl[i].uri + '"><a href="#" onclick="return popupTracks(event, \'' + uri + '\',\'' + pl[i].uri + '\');">'
child += '<h1>' + pl[i].name + 'h1>'
child += '<p>'
child += '<span style="float: right;">' + timeFromSeconds(pl[i].length / 1000) + '</span>'
for (var j = 0; j < pl[i].artists.length; j++) {
if (pl[i].artists[j]) {
child += pl[i].artists[j].name
child += (j === pl[i].artists.length - 1) ? '' : ' / '
// stop after 3
if (j > 2) {
child += '...'
break
}
}
}
child += ' / <em>' + pl[i].album.name + '</em></p>'
child += '</a></li>'
tmp += child
}
}
$(target).html(tmp)
$(target).attr('data', uri)
}
function getPlaylistTracks (uri) {
if (playlists[uri] && playlists[uri].tracks) {
return Mopidy.when(playlists[uri].tracks)
@ -456,7 +458,7 @@ function showLoading (on) {
if (on) {
$('body').css('cursor', 'progress')
$.mobile.loading('show', {
text: 'Loading data from ' + PROGRAM_NAME + '. Please wait...',
text: 'Loading data from ' + PROGRAM_NAME + ' on ' + HOSTNAME + '. Please wait...',
textVisible: true,
theme: 'a'
})
@ -469,7 +471,7 @@ function showLoading (on) {
function showOffline (on) {
if (on) {
$.mobile.loading('show', {
text: 'Trying to reach ' + PROGRAM_NAME + '. Please wait...',
text: 'Trying to reach ' + PROGRAM_NAME + ' on ' + HOSTNAME + '. Please wait...',
textVisible: true,
theme: 'a'
})
@ -479,9 +481,9 @@ function showOffline (on) {
}
// from http://dzone.com/snippets/validate-url-regexp
function validUri (str) {
function validUri (uri) {
var regexp = /^(mms|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/
return regexp.test(str)
return regexp.test(uri)
}
function validServiceUri (str) {
@ -492,18 +494,52 @@ function getScheme (uri) {
return uri.split(':')[0].toLowerCase()
}
function isStreamUri (uri) {
var a = validUri(uri)
var b = radioExtensionsList.indexOf(getScheme(uri)) >= 0
return a || b
function isPlayable (track) {
if (typeof track.type === 'undefined' || track.type === 'track') {
if (track.uri && getScheme(track.uri) === 'file') {
var ext = track.uri.split('.').pop().toLowerCase()
if ($.inArray(ext, audioExt) === -1) {
// Files must have the correct extension
return false
}
}
return true
}
return false
}
function getMediaClass (uri) {
var scheme = getScheme(uri)
for (var i = 0; i < uriClassList.length; i++) {
if (scheme === uriClassList[i][0]) {
return 'fa ' + uriClassList[i][1]
function isStreamUri (uri) {
return validUri(uri) || radioExtensionsList.indexOf(getScheme(uri)) >= 0
}
function getMediaClass (track) {
var defaultIcon = 'fa-file-sound-o'
var type = track.type
if (typeof type === 'undefined' || type === 'track') {
if (!isPlayable(track)) {
return 'fa fa-file-o' // Unplayable file
} else if (isStreamUri(track.uri)) {
return 'fa fa-rss' // Stream
}
} else if (type === 'directory') {
return 'fa fa-folder-o'
} else if (type === 'album') {
// return 'fa fa-bullseye' // Album
defaultIcon = 'fa-folder-o'
} else if (type === 'artist') {
// return 'fa fa-user-circle-o' // Artist
defaultIcon = 'fa-folder-o'
} else if (type === 'playlist') {
// return 'fa fa-star' // Playlist
}
if (track.uri) {
var scheme = getScheme(track.uri)
for (var i = 0; i < uriClassList.length; i++) {
if (scheme === uriClassList[i][0]) {
return 'fa ' + uriClassList[i][1]
}
}
return 'fa ' + defaultIcon
}
return ''
}
@ -544,6 +580,13 @@ function isSpotifyStarredPlaylist (playlist) {
return (starredRegex.test(playlist.uri) && playlist.name === 'Starred')
}
// Returns a string where {x} in template is replaced by tokens[x].
function stringFromTemplate (template, tokens) {
return template.replace(/{[^}]+}/g, function (match) {
return tokens[match.slice(1, -1)]
})
}
/**
* Converts a URI to a jQuery-safe identifier. jQuery identifiers need to be
* unique per page and cannot contain special characters.

View File

@ -20,63 +20,30 @@ function resetSong () {
}
function resizeMb () {
if ($(window).width() < 880) {
$('#panel').panel('close')
} else {
$('#panel').panel('open')
}
$('#infoname').html(songdata.track.name)
$('#infoartist').html(artiststext)
if ($(window).width() <= 960) {
// $('#playlisttracksdiv').hide();
// $('#playlistslistdiv').show();
} else {
if ($(window).width() > 960) {
$('#playlisttracksdiv').show()
$('#playlistslistdiv').show()
}
// //set height of playlist scrollers
/* if ($(window).width() > 960) {
$('#playlisttracksdiv').show();
$('#playlistslistdiv').show();
$('.scroll').removeClass('height').removeClass('width');
$('#playlistspane').removeClass('height').removeClass('width');
} else {
if ( $('#playlisttracksdiv').is(':visible') == $('#playlistslistdiv').is(':visible')) {
$('#playlisttracksdiv').hide();
$('#playlistslistdiv').show();
$('.scroll').addClass('height', '99%').addClass('width', '99%');
$('#playlistspane').addClass('height', '99%').addClass('width', '99%');
}
}
if ($('#playlisttracksdiv').is(':visible') && !$('#playlisttracksback').is(':visible') ) {
$('.scroll').height($(window).height() - 96);
//jqm added something which it shouldnt (at least in this case) I guess
// $('#playlistspane').removeClass('height').height($(window).height() - 110);
$('.scroll').removeClass('height').removeClass('width');
$('#playlistspane').removeClass('height').removeClass('width');
$('#playlisttracksdiv').show();
$('#playlistslistdiv').show();
} else {
$('.scroll').addClass('height', '99%').addClass('width', '99%');
$('#playlistspane').addClass('height', '99%').addClass('width', '99%');
$('#playlisttracksdiv').show();
$('#playlistslistdiv').show();
}
if (isMobileWebkit && ($(window).width() > 480)) {
playlistslistScroll.refresh();
playlisttracksScroll.refresh();
}
*/
}
function setSongTitle (title, refresh_ui) {
songdata.track.name = title
$('#modalname').html(title)
function setSongTitle (track, refresh_ui) {
songdata.track.name = track.name
$('#modalname').html('<a href="#" onclick="return controls.showInfoPopup(\'' + track.uri + '\', \'\', mopidy);">' + track.name + '</span></a>')
if (refresh_ui) {
resizeMb()
}
}
function setSongInfo (data) {
// console.log(data, songdata);
if (!data) { return }
if (data.tlid === songdata.tlid) { return }
if (!data.track.name || data.track.name === '') {
@ -99,7 +66,7 @@ function setSongInfo (data) {
songdata = data
setSongTitle(data.track.name, false)
setSongTitle(data.track, false)
songlength = Infinity
if (!data.track.length || data.track.length === 0) {
@ -117,7 +84,7 @@ function setSongInfo (data) {
if (data.track.artists) {
for (var j = 0; j < data.track.artists.length; j++) {
artistshtml += '<a href="#" onclick="return library.showArtist(\'' + data.track.artists[j].uri + '\');">' + data.track.artists[j].name + '</a>'
artistshtml += '<a href="#" onclick="return library.showArtist(\'' + data.track.artists[j].uri + '\', mopidy);">' + data.track.artists[j].name + '</a>'
artiststext += data.track.artists[j].name
if (j !== data.track.artists.length - 1) {
artistshtml += ', '
@ -127,11 +94,17 @@ function setSongInfo (data) {
arttmp = artistshtml
}
if (data.track.album && data.track.album.name) {
$('#modalalbum').html('<a href="#" onclick="return library.showAlbum(\'' + data.track.album.uri + '\');">' + data.track.album.name + '</a>')
$('#modalalbum').html('<a href="#" onclick="return library.showAlbum(\'' + data.track.album.uri + '\', mopidy);">' + data.track.album.name + '</a>')
} else {
$('#modalalbum').html('')
}
images.setAlbumImage(data.track.uri, '#infocover, #albumCoverImg', mopidy)
if (data.track.uri) {
// Add 'Show Info' icon to album image
$('#modalinfo').append(
'<a href="#" class="infoBtn" onclick="return controls.showInfoPopup(\'' + data.track.uri + '\', \'undefined\', mopidy);">' +
'<i class="fa fa-info-circle"></i></a>')
}
$('#modalartist').html(arttmp)
@ -148,19 +121,12 @@ function setSongInfo (data) {
/** ****************
* display popups *
******************/
function closePopups () {
$('#popupTracks').popup('close')
$('#artistpopup').popup('close')
$('#coverpopup').popup('close')
$('#popupQueue').popup('close')
}
function popupTracks (e, listuri, trackuri, tlid) {
if (!e) {
e = window.event
}
$('.popupTrackName').html(popupData[trackuri].name)
if (popupData[trackuri].album && popupData[trackuri].album.name) {
if (popupData[trackuri].album && popupData[trackuri].album.name && popupData[trackuri].album.uri) {
$('.popupAlbumName').html(popupData[trackuri].album.name)
$('.popupAlbumLi').show()
} else {
@ -168,39 +134,42 @@ function popupTracks (e, listuri, trackuri, tlid) {
}
var child = ''
$('.popupArtistsLi').hide()
$('.popupArtistsDiv').hide()
if (popupData[trackuri].artists) {
if (popupData[trackuri].artists.length === 1) {
child = '<a href="#" onclick="library.showArtist(\'' + popupData[trackuri].artists[0].uri + '\');">Show Artist</a>'
if (popupData[trackuri].artists.length === 1 && popupData[trackuri].artists[0].uri) {
child = '<a href="#" onclick="library.showArtist(\'' + popupData[trackuri].artists[0].uri + '\', mopidy);">Show Artist</a>'
$('.popupArtistName').html(popupData[trackuri].artists[0].name)
$('.popupArtistHref').attr('onclick', 'library.showArtist("' + popupData[trackuri].artists[0].uri + '");')
$('.popupArtistHref').attr('onclick', 'library.showArtist(\'' + popupData[trackuri].artists[0].uri + '\', mopidy);')
$('.popupArtistsDiv').hide()
$('.popupArtistsLi').show()
} else {
var isValidArtistURI = false
for (var j = 0; j < popupData[trackuri].artists.length; j++) {
child += '<li><a href="#" onclick="library.showArtist(\'' + popupData[trackuri].artists[j].uri + '\');"><span class="popupArtistName">' + popupData[trackuri].artists[j].name + '</span></a></li>'
if (popupData[trackuri].artists[j].uri) {
isValidArtistURI = true
child += '<li><a href="#" onclick="library.showArtist(\'' + popupData[trackuri].artists[j].uri + '\', mopidy);"><span class="popupArtistName">' + popupData[trackuri].artists[j].name + '</span></a></li>'
}
}
if (isValidArtistURI) {
$('.popupArtistsLv').html(child).show()
$('.popupArtistsDiv').show()
// this makes the viewport of the window resize somehow
$('.popupArtistsLv').listview('refresh')
}
$('.popupArtistsLi').hide()
$('.popupArtistsLv').html(child).show()
$('.popupArtistsDiv').show()
// this makes the viewport of the window resize somehow
$('.popupArtistsLv').listview('refresh')
}
} else {
$('.popupArtistsDiv').hide()
$('.popupArtistsLi').hide()
}
var hash = document.location.hash.split('?')
var divid = hash[0].substr(1)
var popupName = ''
if (divid === 'current') {
$('.addqueue').hide()
popupName = '#popupQueue'
} else {
$('.addqueue').show()
popupName = '#popupTracks'
}
// Set playlist, trackUri, and tlid of clicked item.
if (typeof tlid !== 'undefined' && tlid !== '') {
$(popupName).data('list', listuri).data('track', trackuri).data('tlid', tlid).popup('open', {
x: e.pageX,
@ -213,12 +182,17 @@ function popupTracks (e, listuri, trackuri, tlid) {
})
}
$(popupName).one('popupafterclose', function (event, ui) {
// Ensure that popup attributes are reset when the popup is closed.
$(this).removeData('list').removeData('track').removeData('tlid')
})
return false
}
function showAlbumPopup (popupId) {
uri = $(popupId).data('track')
library.showAlbum(popupData[uri].album.uri)
library.showAlbum(popupData[uri].album.uri, mopidy)
}
/** ********************
@ -252,6 +226,11 @@ function initSocketevents () {
controls.setPlayState(true)
})
mopidy.on('event:trackPlaybackResumed', function (data) {
setSongInfo(data.tl_track)
controls.setPlayState(true)
})
mopidy.on('event:playlistsLoaded', function (data) {
showLoading(true)
library.getPlaylists()
@ -293,6 +272,12 @@ function initSocketevents () {
mopidy.on('event:tracklistChanged', function (data) {
library.getCurrentPlaylist()
mopidy.tracklist.getTracks().then(function (tracks) {
if (tracks.length === 0) {
// Last track in queue was deleted, reset UI.
resetSong()
}
})
})
mopidy.on('event:seeked', function (data) {
@ -303,14 +288,11 @@ function initSocketevents () {
})
mopidy.on('event:streamTitleChanged', function (data) {
setSongTitle(data.title, true)
// Update all track info.
mopidy.playback.getCurrentTlTrack().then(processCurrenttrack, console.error)
})
}
$(document).bind('pageinit', function () {
resizeMb()
})
/** ************
* gui stuff *
**************/
@ -387,66 +369,58 @@ function locationHashChanged () {
var hash = document.location.hash.split('?')
// remove #
var divid = hash[0].substr(1)
var uri = hash[1]
setHeadline(divid)
var uri = hash[1]
$('.mainNav a').removeClass('ui-state-active ui-state-persist ui-btn-active')
// i don't know why some li elements have those classes, but they do, so we need to remove them
$('.mainNav li').removeClass('ui-state-active ui-state-persist ui-btn-active')
if ($(window).width() < 560) {
if ($(window).width() < 880) {
$('#panel').panel('close')
}
$('.pane').hide()
$('#' + divid + 'pane').show()
$('.mainNav a').removeClass($.mobile.activeBtnClass)
// i don't know why some li elements have those classes, but they do, so we need to remove them
$('.mainNav li').removeClass($.mobile.activeBtnClass)
$('#nav' + divid + ' a').addClass($.mobile.activeBtnClass) // Update navigation pane
$('.pane').hide() // Hide all pages
$('#' + divid + 'pane').show() // Switch to active pane
if (divid === 'browse' && browseStack.length > 0) {
window.scrollTo(0, browseStack[browseStack.length - 1].scrollPos || 0) // Restore scroll position - browsing library.
} else if (typeof pageScrollPos[divid] !== 'undefined') { // Restore scroll position - pages
window.scrollTo(0, pageScrollPos[divid])
}
switch (divid) {
case 'home':
$('#navhome a').addClass('ui-state-active ui-state-persist ui-btn-active')
break
case 'nowPlaying':
$('#navnowPlaying a').addClass('ui-state-active ui-state-persist ui-btn-active')
break
case 'current':
$('#navcurrent a').addClass('ui-state-active ui-state-persist ui-btn-active')
break
case 'playlists':
$('#navplaylists a').addClass('ui-state-active ui-state-persist ui-btn-active')
break
case 'browse':
$('#navbrowse a').addClass('ui-state-active ui-state-persist ui-btn-active')
case 'nowPlaying': // Show 'now playing' footer
$('#normalFooter').hide()
$('#nowPlayingFooter').show()
break
case 'search':
$('#navsearch a').addClass($.mobile.activeBtnClass)
$('#searchinput').focus()
break
case 'stream':
$('#navstream a').addClass('ui-state-active ui-state-persist ui-btn-active')
break
case 'artists':
if (uri !== '') {
library.showArtist(uri)
if (mopidy) {
library.showArtist(uri, mopidy)
} else {
showOffline(true) // Page refreshed - wait for mopidy object to be initialized.
}
}
break
case 'albums':
if (uri !== '') {
library.showAlbum(uri)
if (mopidy) {
library.showAlbum(uri, mopidy)
} else {
showOffline(true) // Page refreshed - wait for mopidy object to be initialized.
}
}
break
}
// switch the footer
switch (divid) {
case 'nowPlaying':
$('#normalFooter').hide()
$('#nowPlayingFooter').show()
break
default:
default: // Default footer
$('#normalFooter').show()
$('#nowPlayingFooter').hide()
}
// Set the page title based on the hash.
document.title = PROGRAM_NAME
return false
}
@ -454,6 +428,7 @@ function locationHashChanged () {
* initialize software *
***********************/
$(document).ready(function (event) {
showOffline(true)
// check for websockets
if (!window.WebSocket) {
switchContent('playlists')
@ -465,40 +440,23 @@ $(document).ready(function (event) {
$('.ui-panel-dismiss').on('tap', function () { $('#panel').panel('close') })
// end of workaround
$(window).hashchange()
// Connect to server
var websocketUrl = $(document.body).data('websocket-url')
if (websocketUrl) {
try {
mopidy = new Mopidy({
webSocketUrl: websocketUrl,
callingConvention: 'by-position-or-by-name'
})
} catch (e) {
showOffline(true)
}
} else {
try {
mopidy = new Mopidy({callingConvention: 'by-position-or-by-name'})
} catch (e) {
showOffline(true)
}
}
// initialize events
initSocketevents()
syncedProgressTimer = new SyncedProgressTimer(8, mopidy)
resetSong()
window.onhashchange = locationHashChanged
if (location.hash.length < 2) {
switchContent('home')
}
$(window).hashchange()
// Remember scroll position for each page and browsed folder
$(window).scrollEnd(function () {
var divid = document.location.hash.split('?')[0].substr(1)
if (divid === 'browse' && browseStack.length > 0) {
browseStack[browseStack.length - 1].scrollPos = window.pageYOffset
} else {
pageScrollPos[divid] = window.pageYOffset
}
}, 250)
initgui = false
window.onhashchange = locationHashChanged
// only show backbutton if in UIWebview
if (window.navigator.standalone) {
@ -507,10 +465,6 @@ $(document).ready(function (event) {
$('#btback').hide()
}
$(window).resize(function () {
resizeMb()
})
// navigation temporary, rewrite this!
$('#songinfo').click(function () {
return switchContent('nowPlaying')
@ -571,12 +525,6 @@ $(document).ready(function (event) {
}
})
if ($(window).width() < 980) {
$('#panel').panel('close')
} else {
$('#panel').panel('open')
}
$.event.special.swipe.horizontalDistanceThreshold = 125 // (default: 30px) Swipe horizontal displacement must be more than this.
$.event.special.swipe.verticalDistanceThreshold = 50 // (default: 75px) Swipe vertical displacement must be less than this.
$.event.special.swipe.durationThreshold = 500
@ -611,6 +559,21 @@ $(document).ready(function (event) {
$('#volumeslider').on('slidestart', function () { volumeSliding = true })
$('#volumeslider').on('slidestop', function () { volumeSliding = false })
$('#volumeslider').on('change', function () { controls.doVolume($(this).val()) })
$(window).resize(resizeMb).resize()
// Connect to server
var websocketUrl = $(document.body).data('websocket-url')
var connectOptions = {callingConvention: 'by-position-or-by-name'}
if (websocketUrl) {
connectOptions['webSocketUrl'] = websocketUrl
}
mopidy = new Mopidy(connectOptions)
// initialize events
initSocketevents()
syncedProgressTimer = new SyncedProgressTimer(8, mopidy)
resetSong()
})
function updatePlayIcons (uri, tlid, popupMenuIcon) {

View File

@ -126,35 +126,28 @@
$('#searchtracks').show()
}
// Returns a string where {x} in template is replaced by tokens[x].
function theme (template, tokens) {
return template.replace(/{[^}]+}/g, function (match) {
return tokens[match.slice(1, -1)]
})
}
// 'Show more' pattern
var showMorePattern = '<li onclick="$(this).hide().siblings().show(); return false;"><a>Show {count} more</a></li>'
// 'Show more' template
var showMoreTemplate = '<li onclick="$(this).hide().siblings().show(); return false;"><a>Show {count} more</a></li>'
// Artist results
var child = ''
var pattern = '<li><a href="#" onclick="return library.showArtist(this.id)" id={id}><i class="{class}"></i> <strong>{name}</strong></a></li>'
var template = '<li><a href="#" onclick="return library.showArtist(this.id, mopidy)" id={id}><i class="{class}"></i> <strong>{name}</strong></a></li>'
var tokens
for (i = 0; i < results.artists.length; i++) {
tokens = {
'id': results.artists[i].uri,
'name': results.artists[i].name,
'class': getMediaClass(results.artists[i].uri)
'class': getMediaClass(results.artists[i])
}
// Add 'Show all' item after a certain number of hits.
if (i === 4 && results.artists.length > 5) {
child += theme(showMorePattern, {'count': results.artists.length - i})
pattern = pattern.replace('<li>', '<li class="overflow">')
child += stringFromTemplate(showMoreTemplate, {'count': results.artists.length - i})
template = template.replace('<li>', '<li class="overflow">')
}
child += theme(pattern, tokens)
child += stringFromTemplate(template, tokens)
}
// Inject list items, refresh listview and hide superfluous items.
@ -162,10 +155,10 @@
// Album results
child = ''
pattern = '<li><a href="#" onclick="return library.showAlbum(this.id)" id="{albumId}">'
pattern += '<h5 data-role="heading"><i class="{class}"></i> {albumName}</h5>'
pattern += '<p data-role="desc">{artistName}</p>'
pattern += '</a></li>'
template = '<li><a href="#" onclick="return library.showAlbum(this.id, mopidy)" id="{albumId}">'
template += '<h5 data-role="heading"><i class="{class}"></i> {albumName}</h5>'
template += '<p data-role="desc">{artistName}</p>'
template += '</a></li>'
for (i = 0; i < results.albums.length; i++) {
tokens = {
@ -173,7 +166,7 @@
'albumName': results.albums[i].name,
'artistName': '',
'albumYear': results.albums[i].date,
'class': getMediaClass(results.albums[i].uri)
'class': getMediaClass(results.albums[i])
}
if (results.albums[i].artists) {
for (j = 0; j < results.albums[i].artists.length; j++) {
@ -187,11 +180,11 @@
}
// Add 'Show all' item after a certain number of hits.
if (i === 4 && results.albums.length > 5) {
child += theme(showMorePattern, {'count': results.albums.length - i})
pattern = pattern.replace('<li>', '<li class="overflow">')
child += stringFromTemplate(showMoreTemplate, {'count': results.albums.length - i})
template = template.replace('<li>', '<li class="overflow">')
}
child += theme(pattern, tokens)
child += stringFromTemplate(template, tokens)
}
// Inject list items, refresh listview and hide superfluous items.
$(SEARCH_ALBUM_TABLE).html(child).listview('refresh').find('.overflow').hide()
@ -215,14 +208,25 @@
showLoading(true)
if (!rootdir) {
browseStack.pop()
rootdir = browseStack[browseStack.length - 1]
} else {
browseStack.push(rootdir)
if (browseStack.length > 0) {
rootdir = browseStack[browseStack.length - 1].uri // Navigated one level up
} else {
rootdir = null // Navigated to top of library
}
} else if (browseStack.length === 0 || rootdir !== browseStack[browseStack.length - 1].uri) {
browseStack.push({'uri': rootdir, 'scrollPos': 0}) // Navigated one level down
}
if (!rootdir) {
rootdir = null
}
mopidy.library.browse({'uri': rootdir}).then(processBrowseDir, console.error)
mopidy.library.browse({'uri': rootdir}).then(function (resultArr) {
processBrowseDir(resultArr)
if (rootdir === null) {
$('.refreshLibraryBtnDiv').hide() // Mopidy does not support refreshing list of backends.
} else {
$('.refreshLibraryBtnDiv').show()
$('#refreshLibraryBtn').data('url', rootdir)
$('#refreshLibraryBtn').off('click')
$('#refreshLibraryBtn').one('click', controls.refreshLibrary)
}
}, console.error)
},
getCurrentPlaylist: function () {
@ -265,7 +269,7 @@
return false
},
showArtist: function (nwuri) {
showArtist: function (nwuri, mopidy) {
$('#popupQueue').popup('close')
$('#popupTracks').popup('close')
$('#controlsmodal').popup('close')
@ -284,7 +288,7 @@
return false
},
showAlbum: function (uri) {
showAlbum: function (uri, mopidy) {
$('#popupQueue').popup('close')
$('#popupTracks').popup('close')
$('#controlsmodal').popup('close')

View File

@ -90,8 +90,10 @@ function processBrowseDir (resultArr) {
var length = 0 || resultArr.length
customTracklists[BROWSE_TABLE] = []
var html = ''
var i
for (var i = 0, index = 0; i < resultArr.length; i++) {
// Render list of tracks
for (i = 0, index = 0; i < resultArr.length; i++) {
if (resultArr[i].type === 'track') {
previousRef = ref || undefined
nextRef = i < resultArr.length - 1 ? resultArr[i + 1] : undefined
@ -105,41 +107,39 @@ function processBrowseDir (resultArr) {
index++
} else {
var iconClass = ''
if (browseStack.length > 0) {
iconClass = 'fa fa-folder-o'
} else {
iconClass = getMediaClass(resultArr[i].uri)
}
html += '<li><a href="#" onclick="return library.getBrowseDir(this.id);" id="' + resultArr[i].uri + '">' +
'<h1><i class="' + iconClass + '"></i> ' + resultArr[i].name + '</h1></a></li>'
'<h1><i class="' + getMediaClass(resultArr[i]) + '"></i> ' + resultArr[i].name + '</h1></a></li>'
}
}
$(BROWSE_TABLE).append(html)
if (browseStack.length > 0) {
window.scrollTo(0, browseStack[browseStack.length - 1].scrollPos || 0) // Restore scroll position
}
updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction())
// Look up track details and add album headers
if (uris.length > 0) {
mopidy.library.lookup({'uris': uris}).then(function (resultDict) {
// Break into albums and put in tables
var track, previousTrack, nextTrack, uri
$.each(resultArr, function (i, ref) {
if (ref.type === 'track') {
for (i = 0, index = 0; i < resultArr.length; i++) {
if (resultArr[i].type === 'track') {
previousTrack = track || undefined
if (i < resultArr.length - 1 && resultDict[resultArr[i + 1].uri]) {
nextTrack = resultDict[resultArr[i + 1].uri][0]
} else {
nextTrack = undefined
}
track = resultDict[ref.uri][0]
track = resultDict[resultArr[i].uri][0]
popupData[track.uri] = track // Need full track info in popups in order to display albums and artists.
if (uris.length === 1 || (previousTrack && !hasSameAlbum(previousTrack, track) && !hasSameAlbum(track, nextTrack))) {
renderSongLiAlbumInfo(track, BROWSE_TABLE)
}
renderSongLiDivider(previousTrack, track, nextTrack, BROWSE_TABLE)
}
})
}
showLoading(false)
}, console.error)
} else {
@ -166,7 +166,7 @@ function processGetPlaylists (resultArr) {
} else if (isFavouritesPlaylist(resultArr[i])) {
favourites = li_html + '&hearts; Musicbox Favourites</a></li>'
} else {
tmp = tmp + li_html + '<i class="' + getMediaClass(resultArr[i].uri) + '"></i> ' + resultArr[i].name + '</a></li>'
tmp = tmp + li_html + '<i class="' + getMediaClass(resultArr[i]) + '"></i> ' + resultArr[i].name + '</a></li>'
}
}
// Prepend the user's Spotify "Starred" playlist and favourites to the results. (like Spotify official client).
@ -192,9 +192,11 @@ function processPlaylistItems (resultDict) {
return mopidy.library.lookup({'uris': trackUris}).then(function (tracks) {
// Transform from dict to list and cache result
var newplaylisturi = resultDict.uri
var track
playlists[newplaylisturi] = {'uri': newplaylisturi, 'tracks': []}
for (i = 0; i < trackUris.length; i++) {
playlists[newplaylisturi].tracks.push(tracks[trackUris[i]][0])
track = tracks[trackUris[i]][0] || resultDict.items[i] // Fall back to using track Ref if lookup failed.
playlists[newplaylisturi].tracks.push(track)
}
showLoading(false)
return playlists[newplaylisturi].tracks

View File

@ -1,6 +1,6 @@
CACHE MANIFEST
# 2016-05-15:v1
# 2017-02-26:v1
NETWORK:
*
@ -48,6 +48,7 @@ vendors/font_awesome/less/list.less
vendors/font_awesome/less/mixins.less
vendors/font_awesome/less/path.less
vendors/font_awesome/less/rotated-flipped.less
vendors/font_awesome/less/screen-reader.less
vendors/font_awesome/less/stacked.less
vendors/font_awesome/less/variables.less
vendors/font_awesome/scss/_animated.scss
@ -60,6 +61,7 @@ vendors/font_awesome/scss/_list.scss
vendors/font_awesome/scss/_mixins.scss
vendors/font_awesome/scss/_path.scss
vendors/font_awesome/scss/_rotated-flipped.scss
vendors/font_awesome/scss/_screen-reader.scss
vendors/font_awesome/scss/_stacked.scss
vendors/font_awesome/scss/_variables.scss
vendors/font_awesome/scss/font-awesome.scss

View File

@ -1,13 +1,13 @@
/*!
* Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/
/* FONT PATH
* -------------------------- */
@font-face {
font-family: 'FontAwesome';
src: url('../fonts/fontawesome-webfont.eot?v=4.5.0');
src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg');
src: url('../fonts/fontawesome-webfont.eot?v=4.7.0');
src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');
font-weight: normal;
font-style: normal;
}
@ -118,31 +118,31 @@
}
}
.fa-rotate-90 {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
}
.fa-rotate-180 {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
transform: rotate(180deg);
}
.fa-rotate-270 {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";
-webkit-transform: rotate(270deg);
-ms-transform: rotate(270deg);
transform: rotate(270deg);
}
.fa-flip-horizontal {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";
-webkit-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
transform: scale(-1, 1);
}
.fa-flip-vertical {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";
-webkit-transform: scale(1, -1);
-ms-transform: scale(1, -1);
transform: scale(1, -1);
@ -1383,7 +1383,7 @@
.fa-digg:before {
content: "\f1a6";
}
.fa-pied-piper:before {
.fa-pied-piper-pp:before {
content: "\f1a7";
}
.fa-pied-piper-alt:before {
@ -1509,6 +1509,7 @@
content: "\f1ce";
}
.fa-ra:before,
.fa-resistance:before,
.fa-rebel:before {
content: "\f1d0";
}
@ -1831,6 +1832,7 @@
content: "\f23e";
}
.fa-battery-4:before,
.fa-battery:before,
.fa-battery-full:before {
content: "\f240";
}
@ -2084,3 +2086,252 @@
.fa-percent:before {
content: "\f295";
}
.fa-gitlab:before {
content: "\f296";
}
.fa-wpbeginner:before {
content: "\f297";
}
.fa-wpforms:before {
content: "\f298";
}
.fa-envira:before {
content: "\f299";
}
.fa-universal-access:before {
content: "\f29a";
}
.fa-wheelchair-alt:before {
content: "\f29b";
}
.fa-question-circle-o:before {
content: "\f29c";
}
.fa-blind:before {
content: "\f29d";
}
.fa-audio-description:before {
content: "\f29e";
}
.fa-volume-control-phone:before {
content: "\f2a0";
}
.fa-braille:before {
content: "\f2a1";
}
.fa-assistive-listening-systems:before {
content: "\f2a2";
}
.fa-asl-interpreting:before,
.fa-american-sign-language-interpreting:before {
content: "\f2a3";
}
.fa-deafness:before,
.fa-hard-of-hearing:before,
.fa-deaf:before {
content: "\f2a4";
}
.fa-glide:before {
content: "\f2a5";
}
.fa-glide-g:before {
content: "\f2a6";
}
.fa-signing:before,
.fa-sign-language:before {
content: "\f2a7";
}
.fa-low-vision:before {
content: "\f2a8";
}
.fa-viadeo:before {
content: "\f2a9";
}
.fa-viadeo-square:before {
content: "\f2aa";
}
.fa-snapchat:before {
content: "\f2ab";
}
.fa-snapchat-ghost:before {
content: "\f2ac";
}
.fa-snapchat-square:before {
content: "\f2ad";
}
.fa-pied-piper:before {
content: "\f2ae";
}
.fa-first-order:before {
content: "\f2b0";
}
.fa-yoast:before {
content: "\f2b1";
}
.fa-themeisle:before {
content: "\f2b2";
}
.fa-google-plus-circle:before,
.fa-google-plus-official:before {
content: "\f2b3";
}
.fa-fa:before,
.fa-font-awesome:before {
content: "\f2b4";
}
.fa-handshake-o:before {
content: "\f2b5";
}
.fa-envelope-open:before {
content: "\f2b6";
}
.fa-envelope-open-o:before {
content: "\f2b7";
}
.fa-linode:before {
content: "\f2b8";
}
.fa-address-book:before {
content: "\f2b9";
}
.fa-address-book-o:before {
content: "\f2ba";
}
.fa-vcard:before,
.fa-address-card:before {
content: "\f2bb";
}
.fa-vcard-o:before,
.fa-address-card-o:before {
content: "\f2bc";
}
.fa-user-circle:before {
content: "\f2bd";
}
.fa-user-circle-o:before {
content: "\f2be";
}
.fa-user-o:before {
content: "\f2c0";
}
.fa-id-badge:before {
content: "\f2c1";
}
.fa-drivers-license:before,
.fa-id-card:before {
content: "\f2c2";
}
.fa-drivers-license-o:before,
.fa-id-card-o:before {
content: "\f2c3";
}
.fa-quora:before {
content: "\f2c4";
}
.fa-free-code-camp:before {
content: "\f2c5";
}
.fa-telegram:before {
content: "\f2c6";
}
.fa-thermometer-4:before,
.fa-thermometer:before,
.fa-thermometer-full:before {
content: "\f2c7";
}
.fa-thermometer-3:before,
.fa-thermometer-three-quarters:before {
content: "\f2c8";
}
.fa-thermometer-2:before,
.fa-thermometer-half:before {
content: "\f2c9";
}
.fa-thermometer-1:before,
.fa-thermometer-quarter:before {
content: "\f2ca";
}
.fa-thermometer-0:before,
.fa-thermometer-empty:before {
content: "\f2cb";
}
.fa-shower:before {
content: "\f2cc";
}
.fa-bathtub:before,
.fa-s15:before,
.fa-bath:before {
content: "\f2cd";
}
.fa-podcast:before {
content: "\f2ce";
}
.fa-window-maximize:before {
content: "\f2d0";
}
.fa-window-minimize:before {
content: "\f2d1";
}
.fa-window-restore:before {
content: "\f2d2";
}
.fa-times-rectangle:before,
.fa-window-close:before {
content: "\f2d3";
}
.fa-times-rectangle-o:before,
.fa-window-close-o:before {
content: "\f2d4";
}
.fa-bandcamp:before {
content: "\f2d5";
}
.fa-grav:before {
content: "\f2d6";
}
.fa-etsy:before {
content: "\f2d7";
}
.fa-imdb:before {
content: "\f2d8";
}
.fa-ravelry:before {
content: "\f2d9";
}
.fa-eercast:before {
content: "\f2da";
}
.fa-microchip:before {
content: "\f2db";
}
.fa-snowflake-o:before {
content: "\f2dc";
}
.fa-superpowers:before {
content: "\f2dd";
}
.fa-wpexplorer:before {
content: "\f2de";
}
.fa-meetup:before {
content: "\f2e0";
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.sr-only-focusable:active,
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 434 KiB

View File

@ -1,5 +1,5 @@
/*!
* Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/
@ -15,3 +15,4 @@
@import "rotated-flipped.less";
@import "stacked.less";
@import "icons.less";
@import "screen-reader.less";

View File

@ -438,7 +438,7 @@
.@{fa-css-prefix}-stumbleupon:before { content: @fa-var-stumbleupon; }
.@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; }
.@{fa-css-prefix}-digg:before { content: @fa-var-digg; }
.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; }
.@{fa-css-prefix}-pied-piper-pp:before { content: @fa-var-pied-piper-pp; }
.@{fa-css-prefix}-pied-piper-alt:before { content: @fa-var-pied-piper-alt; }
.@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; }
.@{fa-css-prefix}-joomla:before { content: @fa-var-joomla; }
@ -488,6 +488,7 @@
.@{fa-css-prefix}-life-ring:before { content: @fa-var-life-ring; }
.@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-o-notch; }
.@{fa-css-prefix}-ra:before,
.@{fa-css-prefix}-resistance:before,
.@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; }
.@{fa-css-prefix}-ge:before,
.@{fa-css-prefix}-empire:before { content: @fa-var-empire; }
@ -604,6 +605,7 @@
.@{fa-css-prefix}-opencart:before { content: @fa-var-opencart; }
.@{fa-css-prefix}-expeditedssl:before { content: @fa-var-expeditedssl; }
.@{fa-css-prefix}-battery-4:before,
.@{fa-css-prefix}-battery:before,
.@{fa-css-prefix}-battery-full:before { content: @fa-var-battery-full; }
.@{fa-css-prefix}-battery-3:before,
.@{fa-css-prefix}-battery-three-quarters:before { content: @fa-var-battery-three-quarters; }
@ -695,3 +697,93 @@
.@{fa-css-prefix}-bluetooth:before { content: @fa-var-bluetooth; }
.@{fa-css-prefix}-bluetooth-b:before { content: @fa-var-bluetooth-b; }
.@{fa-css-prefix}-percent:before { content: @fa-var-percent; }
.@{fa-css-prefix}-gitlab:before { content: @fa-var-gitlab; }
.@{fa-css-prefix}-wpbeginner:before { content: @fa-var-wpbeginner; }
.@{fa-css-prefix}-wpforms:before { content: @fa-var-wpforms; }
.@{fa-css-prefix}-envira:before { content: @fa-var-envira; }
.@{fa-css-prefix}-universal-access:before { content: @fa-var-universal-access; }
.@{fa-css-prefix}-wheelchair-alt:before { content: @fa-var-wheelchair-alt; }
.@{fa-css-prefix}-question-circle-o:before { content: @fa-var-question-circle-o; }
.@{fa-css-prefix}-blind:before { content: @fa-var-blind; }
.@{fa-css-prefix}-audio-description:before { content: @fa-var-audio-description; }
.@{fa-css-prefix}-volume-control-phone:before { content: @fa-var-volume-control-phone; }
.@{fa-css-prefix}-braille:before { content: @fa-var-braille; }
.@{fa-css-prefix}-assistive-listening-systems:before { content: @fa-var-assistive-listening-systems; }
.@{fa-css-prefix}-asl-interpreting:before,
.@{fa-css-prefix}-american-sign-language-interpreting:before { content: @fa-var-american-sign-language-interpreting; }
.@{fa-css-prefix}-deafness:before,
.@{fa-css-prefix}-hard-of-hearing:before,
.@{fa-css-prefix}-deaf:before { content: @fa-var-deaf; }
.@{fa-css-prefix}-glide:before { content: @fa-var-glide; }
.@{fa-css-prefix}-glide-g:before { content: @fa-var-glide-g; }
.@{fa-css-prefix}-signing:before,
.@{fa-css-prefix}-sign-language:before { content: @fa-var-sign-language; }
.@{fa-css-prefix}-low-vision:before { content: @fa-var-low-vision; }
.@{fa-css-prefix}-viadeo:before { content: @fa-var-viadeo; }
.@{fa-css-prefix}-viadeo-square:before { content: @fa-var-viadeo-square; }
.@{fa-css-prefix}-snapchat:before { content: @fa-var-snapchat; }
.@{fa-css-prefix}-snapchat-ghost:before { content: @fa-var-snapchat-ghost; }
.@{fa-css-prefix}-snapchat-square:before { content: @fa-var-snapchat-square; }
.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; }
.@{fa-css-prefix}-first-order:before { content: @fa-var-first-order; }
.@{fa-css-prefix}-yoast:before { content: @fa-var-yoast; }
.@{fa-css-prefix}-themeisle:before { content: @fa-var-themeisle; }
.@{fa-css-prefix}-google-plus-circle:before,
.@{fa-css-prefix}-google-plus-official:before { content: @fa-var-google-plus-official; }
.@{fa-css-prefix}-fa:before,
.@{fa-css-prefix}-font-awesome:before { content: @fa-var-font-awesome; }
.@{fa-css-prefix}-handshake-o:before { content: @fa-var-handshake-o; }
.@{fa-css-prefix}-envelope-open:before { content: @fa-var-envelope-open; }
.@{fa-css-prefix}-envelope-open-o:before { content: @fa-var-envelope-open-o; }
.@{fa-css-prefix}-linode:before { content: @fa-var-linode; }
.@{fa-css-prefix}-address-book:before { content: @fa-var-address-book; }
.@{fa-css-prefix}-address-book-o:before { content: @fa-var-address-book-o; }
.@{fa-css-prefix}-vcard:before,
.@{fa-css-prefix}-address-card:before { content: @fa-var-address-card; }
.@{fa-css-prefix}-vcard-o:before,
.@{fa-css-prefix}-address-card-o:before { content: @fa-var-address-card-o; }
.@{fa-css-prefix}-user-circle:before { content: @fa-var-user-circle; }
.@{fa-css-prefix}-user-circle-o:before { content: @fa-var-user-circle-o; }
.@{fa-css-prefix}-user-o:before { content: @fa-var-user-o; }
.@{fa-css-prefix}-id-badge:before { content: @fa-var-id-badge; }
.@{fa-css-prefix}-drivers-license:before,
.@{fa-css-prefix}-id-card:before { content: @fa-var-id-card; }
.@{fa-css-prefix}-drivers-license-o:before,
.@{fa-css-prefix}-id-card-o:before { content: @fa-var-id-card-o; }
.@{fa-css-prefix}-quora:before { content: @fa-var-quora; }
.@{fa-css-prefix}-free-code-camp:before { content: @fa-var-free-code-camp; }
.@{fa-css-prefix}-telegram:before { content: @fa-var-telegram; }
.@{fa-css-prefix}-thermometer-4:before,
.@{fa-css-prefix}-thermometer:before,
.@{fa-css-prefix}-thermometer-full:before { content: @fa-var-thermometer-full; }
.@{fa-css-prefix}-thermometer-3:before,
.@{fa-css-prefix}-thermometer-three-quarters:before { content: @fa-var-thermometer-three-quarters; }
.@{fa-css-prefix}-thermometer-2:before,
.@{fa-css-prefix}-thermometer-half:before { content: @fa-var-thermometer-half; }
.@{fa-css-prefix}-thermometer-1:before,
.@{fa-css-prefix}-thermometer-quarter:before { content: @fa-var-thermometer-quarter; }
.@{fa-css-prefix}-thermometer-0:before,
.@{fa-css-prefix}-thermometer-empty:before { content: @fa-var-thermometer-empty; }
.@{fa-css-prefix}-shower:before { content: @fa-var-shower; }
.@{fa-css-prefix}-bathtub:before,
.@{fa-css-prefix}-s15:before,
.@{fa-css-prefix}-bath:before { content: @fa-var-bath; }
.@{fa-css-prefix}-podcast:before { content: @fa-var-podcast; }
.@{fa-css-prefix}-window-maximize:before { content: @fa-var-window-maximize; }
.@{fa-css-prefix}-window-minimize:before { content: @fa-var-window-minimize; }
.@{fa-css-prefix}-window-restore:before { content: @fa-var-window-restore; }
.@{fa-css-prefix}-times-rectangle:before,
.@{fa-css-prefix}-window-close:before { content: @fa-var-window-close; }
.@{fa-css-prefix}-times-rectangle-o:before,
.@{fa-css-prefix}-window-close-o:before { content: @fa-var-window-close-o; }
.@{fa-css-prefix}-bandcamp:before { content: @fa-var-bandcamp; }
.@{fa-css-prefix}-grav:before { content: @fa-var-grav; }
.@{fa-css-prefix}-etsy:before { content: @fa-var-etsy; }
.@{fa-css-prefix}-imdb:before { content: @fa-var-imdb; }
.@{fa-css-prefix}-ravelry:before { content: @fa-var-ravelry; }
.@{fa-css-prefix}-eercast:before { content: @fa-var-eercast; }
.@{fa-css-prefix}-microchip:before { content: @fa-var-microchip; }
.@{fa-css-prefix}-snowflake-o:before { content: @fa-var-snowflake-o; }
.@{fa-css-prefix}-superpowers:before { content: @fa-var-superpowers; }
.@{fa-css-prefix}-wpexplorer:before { content: @fa-var-wpexplorer; }
.@{fa-css-prefix}-meetup:before { content: @fa-var-meetup; }

View File

@ -12,15 +12,49 @@
}
.fa-icon-rotate(@degrees, @rotation) {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})";
-webkit-transform: rotate(@degrees);
-ms-transform: rotate(@degrees);
transform: rotate(@degrees);
}
.fa-icon-flip(@horiz, @vert, @rotation) {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1);
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)";
-webkit-transform: scale(@horiz, @vert);
-ms-transform: scale(@horiz, @vert);
transform: scale(@horiz, @vert);
}
// Only display content to screen readers. A la Bootstrap 4.
//
// See: http://a11yproject.com/posts/how-to-hide-content/
.sr-only() {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
// Use in conjunction with .sr-only to only display content when it's focused.
//
// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
//
// Credit: HTML5 Boilerplate
.sr-only-focusable() {
&:active,
&:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}
}

View File

@ -9,7 +9,7 @@
url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'),
url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'),
url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg');
// src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
// src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
font-weight: normal;
font-style: normal;
}

View File

@ -0,0 +1,5 @@
// Screen Readers
// -------------------------
.sr-only { .sr-only(); }
.sr-only-focusable { .sr-only-focusable(); }

View File

@ -4,14 +4,18 @@
@fa-font-path: "../fonts";
@fa-font-size-base: 14px;
@fa-line-height-base: 1;
//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.5.0/fonts"; // for referencing Bootstrap CDN font files directly
//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts"; // for referencing Bootstrap CDN font files directly
@fa-css-prefix: fa;
@fa-version: "4.5.0";
@fa-version: "4.7.0";
@fa-border-color: #eee;
@fa-inverse: #fff;
@fa-li-width: (30em / 14);
@fa-var-500px: "\f26e";
@fa-var-address-book: "\f2b9";
@fa-var-address-book-o: "\f2ba";
@fa-var-address-card: "\f2bb";
@fa-var-address-card-o: "\f2bc";
@fa-var-adjust: "\f042";
@fa-var-adn: "\f170";
@fa-var-align-center: "\f037";
@ -20,6 +24,7 @@
@fa-var-align-right: "\f038";
@fa-var-amazon: "\f270";
@fa-var-ambulance: "\f0f9";
@fa-var-american-sign-language-interpreting: "\f2a3";
@fa-var-anchor: "\f13d";
@fa-var-android: "\f17b";
@fa-var-angellist: "\f209";
@ -50,17 +55,24 @@
@fa-var-arrows-alt: "\f0b2";
@fa-var-arrows-h: "\f07e";
@fa-var-arrows-v: "\f07d";
@fa-var-asl-interpreting: "\f2a3";
@fa-var-assistive-listening-systems: "\f2a2";
@fa-var-asterisk: "\f069";
@fa-var-at: "\f1fa";
@fa-var-audio-description: "\f29e";
@fa-var-automobile: "\f1b9";
@fa-var-backward: "\f04a";
@fa-var-balance-scale: "\f24e";
@fa-var-ban: "\f05e";
@fa-var-bandcamp: "\f2d5";
@fa-var-bank: "\f19c";
@fa-var-bar-chart: "\f080";
@fa-var-bar-chart-o: "\f080";
@fa-var-barcode: "\f02a";
@fa-var-bars: "\f0c9";
@fa-var-bath: "\f2cd";
@fa-var-bathtub: "\f2cd";
@fa-var-battery: "\f240";
@fa-var-battery-0: "\f244";
@fa-var-battery-1: "\f243";
@fa-var-battery-2: "\f242";
@ -86,6 +98,7 @@
@fa-var-bitbucket-square: "\f172";
@fa-var-bitcoin: "\f15a";
@fa-var-black-tie: "\f27e";
@fa-var-blind: "\f29d";
@fa-var-bluetooth: "\f293";
@fa-var-bluetooth-b: "\f294";
@fa-var-bold: "\f032";
@ -94,6 +107,7 @@
@fa-var-book: "\f02d";
@fa-var-bookmark: "\f02e";
@fa-var-bookmark-o: "\f097";
@fa-var-braille: "\f2a1";
@fa-var-briefcase: "\f0b1";
@fa-var-btc: "\f15a";
@fa-var-bug: "\f188";
@ -196,6 +210,8 @@
@fa-var-dashboard: "\f0e4";
@fa-var-dashcube: "\f210";
@fa-var-database: "\f1c0";
@fa-var-deaf: "\f2a4";
@fa-var-deafness: "\f2a4";
@fa-var-dedent: "\f03b";
@fa-var-delicious: "\f1a5";
@fa-var-desktop: "\f108";
@ -206,18 +222,25 @@
@fa-var-dot-circle-o: "\f192";
@fa-var-download: "\f019";
@fa-var-dribbble: "\f17d";
@fa-var-drivers-license: "\f2c2";
@fa-var-drivers-license-o: "\f2c3";
@fa-var-dropbox: "\f16b";
@fa-var-drupal: "\f1a9";
@fa-var-edge: "\f282";
@fa-var-edit: "\f044";
@fa-var-eercast: "\f2da";
@fa-var-eject: "\f052";
@fa-var-ellipsis-h: "\f141";
@fa-var-ellipsis-v: "\f142";
@fa-var-empire: "\f1d1";
@fa-var-envelope: "\f0e0";
@fa-var-envelope-o: "\f003";
@fa-var-envelope-open: "\f2b6";
@fa-var-envelope-open-o: "\f2b7";
@fa-var-envelope-square: "\f199";
@fa-var-envira: "\f299";
@fa-var-eraser: "\f12d";
@fa-var-etsy: "\f2d7";
@fa-var-eur: "\f153";
@fa-var-euro: "\f153";
@fa-var-exchange: "\f0ec";
@ -231,6 +254,7 @@
@fa-var-eye: "\f06e";
@fa-var-eye-slash: "\f070";
@fa-var-eyedropper: "\f1fb";
@fa-var-fa: "\f2b4";
@fa-var-facebook: "\f09a";
@fa-var-facebook-f: "\f09a";
@fa-var-facebook-official: "\f230";
@ -265,6 +289,7 @@
@fa-var-fire: "\f06d";
@fa-var-fire-extinguisher: "\f134";
@fa-var-firefox: "\f269";
@fa-var-first-order: "\f2b0";
@fa-var-flag: "\f024";
@fa-var-flag-checkered: "\f11e";
@fa-var-flag-o: "\f11d";
@ -277,11 +302,13 @@
@fa-var-folder-open: "\f07c";
@fa-var-folder-open-o: "\f115";
@fa-var-font: "\f031";
@fa-var-font-awesome: "\f2b4";
@fa-var-fonticons: "\f280";
@fa-var-fort-awesome: "\f286";
@fa-var-forumbee: "\f211";
@fa-var-forward: "\f04e";
@fa-var-foursquare: "\f180";
@fa-var-free-code-camp: "\f2c5";
@fa-var-frown-o: "\f119";
@fa-var-futbol-o: "\f1e3";
@fa-var-gamepad: "\f11b";
@ -300,15 +327,21 @@
@fa-var-github: "\f09b";
@fa-var-github-alt: "\f113";
@fa-var-github-square: "\f092";
@fa-var-gitlab: "\f296";
@fa-var-gittip: "\f184";
@fa-var-glass: "\f000";
@fa-var-glide: "\f2a5";
@fa-var-glide-g: "\f2a6";
@fa-var-globe: "\f0ac";
@fa-var-google: "\f1a0";
@fa-var-google-plus: "\f0d5";
@fa-var-google-plus-circle: "\f2b3";
@fa-var-google-plus-official: "\f2b3";
@fa-var-google-plus-square: "\f0d4";
@fa-var-google-wallet: "\f1ee";
@fa-var-graduation-cap: "\f19d";
@fa-var-gratipay: "\f184";
@fa-var-grav: "\f2d6";
@fa-var-group: "\f0c0";
@fa-var-h-square: "\f0fd";
@fa-var-hacker-news: "\f1d4";
@ -325,6 +358,8 @@
@fa-var-hand-scissors-o: "\f257";
@fa-var-hand-spock-o: "\f259";
@fa-var-hand-stop-o: "\f256";
@fa-var-handshake-o: "\f2b5";
@fa-var-hard-of-hearing: "\f2a4";
@fa-var-hashtag: "\f292";
@fa-var-hdd-o: "\f0a0";
@fa-var-header: "\f1dc";
@ -347,8 +382,12 @@
@fa-var-houzz: "\f27c";
@fa-var-html5: "\f13b";
@fa-var-i-cursor: "\f246";
@fa-var-id-badge: "\f2c1";
@fa-var-id-card: "\f2c2";
@fa-var-id-card-o: "\f2c3";
@fa-var-ils: "\f20b";
@fa-var-image: "\f03e";
@fa-var-imdb: "\f2d8";
@fa-var-inbox: "\f01c";
@fa-var-indent: "\f03c";
@fa-var-industry: "\f275";
@ -386,6 +425,7 @@
@fa-var-link: "\f0c1";
@fa-var-linkedin: "\f0e1";
@fa-var-linkedin-square: "\f08c";
@fa-var-linode: "\f2b8";
@fa-var-linux: "\f17c";
@fa-var-list: "\f03a";
@fa-var-list-alt: "\f022";
@ -397,6 +437,7 @@
@fa-var-long-arrow-left: "\f177";
@fa-var-long-arrow-right: "\f178";
@fa-var-long-arrow-up: "\f176";
@fa-var-low-vision: "\f2a8";
@fa-var-magic: "\f0d0";
@fa-var-magnet: "\f076";
@fa-var-mail-forward: "\f064";
@ -417,8 +458,10 @@
@fa-var-meanpath: "\f20c";
@fa-var-medium: "\f23a";
@fa-var-medkit: "\f0fa";
@fa-var-meetup: "\f2e0";
@fa-var-meh-o: "\f11a";
@fa-var-mercury: "\f223";
@fa-var-microchip: "\f2db";
@fa-var-microphone: "\f130";
@fa-var-microphone-slash: "\f131";
@fa-var-minus: "\f068";
@ -468,8 +511,9 @@
@fa-var-photo: "\f03e";
@fa-var-picture-o: "\f03e";
@fa-var-pie-chart: "\f200";
@fa-var-pied-piper: "\f1a7";
@fa-var-pied-piper: "\f2ae";
@fa-var-pied-piper-alt: "\f1a8";
@fa-var-pied-piper-pp: "\f1a7";
@fa-var-pinterest: "\f0d2";
@fa-var-pinterest-p: "\f231";
@fa-var-pinterest-square: "\f0d3";
@ -482,6 +526,7 @@
@fa-var-plus-circle: "\f055";
@fa-var-plus-square: "\f0fe";
@fa-var-plus-square-o: "\f196";
@fa-var-podcast: "\f2ce";
@fa-var-power-off: "\f011";
@fa-var-print: "\f02f";
@fa-var-product-hunt: "\f288";
@ -490,10 +535,13 @@
@fa-var-qrcode: "\f029";
@fa-var-question: "\f128";
@fa-var-question-circle: "\f059";
@fa-var-question-circle-o: "\f29c";
@fa-var-quora: "\f2c4";
@fa-var-quote-left: "\f10d";
@fa-var-quote-right: "\f10e";
@fa-var-ra: "\f1d0";
@fa-var-random: "\f074";
@fa-var-ravelry: "\f2d9";
@fa-var-rebel: "\f1d0";
@fa-var-recycle: "\f1b8";
@fa-var-reddit: "\f1a1";
@ -507,6 +555,7 @@
@fa-var-repeat: "\f01e";
@fa-var-reply: "\f112";
@fa-var-reply-all: "\f122";
@fa-var-resistance: "\f1d0";
@fa-var-retweet: "\f079";
@fa-var-rmb: "\f157";
@fa-var-road: "\f018";
@ -519,6 +568,7 @@
@fa-var-rub: "\f158";
@fa-var-ruble: "\f158";
@fa-var-rupee: "\f156";
@fa-var-s15: "\f2cd";
@fa-var-safari: "\f267";
@fa-var-save: "\f0c7";
@fa-var-scissors: "\f0c4";
@ -543,9 +593,12 @@
@fa-var-shopping-bag: "\f290";
@fa-var-shopping-basket: "\f291";
@fa-var-shopping-cart: "\f07a";
@fa-var-shower: "\f2cc";
@fa-var-sign-in: "\f090";
@fa-var-sign-language: "\f2a7";
@fa-var-sign-out: "\f08b";
@fa-var-signal: "\f012";
@fa-var-signing: "\f2a7";
@fa-var-simplybuilt: "\f215";
@fa-var-sitemap: "\f0e8";
@fa-var-skyatlas: "\f216";
@ -554,6 +607,10 @@
@fa-var-sliders: "\f1de";
@fa-var-slideshare: "\f1e7";
@fa-var-smile-o: "\f118";
@fa-var-snapchat: "\f2ab";
@fa-var-snapchat-ghost: "\f2ac";
@fa-var-snapchat-square: "\f2ad";
@fa-var-snowflake-o: "\f2dc";
@fa-var-soccer-ball-o: "\f1e3";
@fa-var-sort: "\f0dc";
@fa-var-sort-alpha-asc: "\f15d";
@ -599,6 +656,7 @@
@fa-var-subway: "\f239";
@fa-var-suitcase: "\f0f2";
@fa-var-sun-o: "\f185";
@fa-var-superpowers: "\f2dd";
@fa-var-superscript: "\f12b";
@fa-var-support: "\f1cd";
@fa-var-table: "\f0ce";
@ -608,6 +666,7 @@
@fa-var-tags: "\f02c";
@fa-var-tasks: "\f0ae";
@fa-var-taxi: "\f1ba";
@fa-var-telegram: "\f2c6";
@fa-var-television: "\f26c";
@fa-var-tencent-weibo: "\f1d5";
@fa-var-terminal: "\f120";
@ -616,6 +675,18 @@
@fa-var-th: "\f00a";
@fa-var-th-large: "\f009";
@fa-var-th-list: "\f00b";
@fa-var-themeisle: "\f2b2";
@fa-var-thermometer: "\f2c7";
@fa-var-thermometer-0: "\f2cb";
@fa-var-thermometer-1: "\f2ca";
@fa-var-thermometer-2: "\f2c9";
@fa-var-thermometer-3: "\f2c8";
@fa-var-thermometer-4: "\f2c7";
@fa-var-thermometer-empty: "\f2cb";
@fa-var-thermometer-full: "\f2c7";
@fa-var-thermometer-half: "\f2c9";
@fa-var-thermometer-quarter: "\f2ca";
@fa-var-thermometer-three-quarters: "\f2c8";
@fa-var-thumb-tack: "\f08d";
@fa-var-thumbs-down: "\f165";
@fa-var-thumbs-o-down: "\f088";
@ -625,6 +696,8 @@
@fa-var-times: "\f00d";
@fa-var-times-circle: "\f057";
@fa-var-times-circle-o: "\f05c";
@fa-var-times-rectangle: "\f2d3";
@fa-var-times-rectangle-o: "\f2d4";
@fa-var-tint: "\f043";
@fa-var-toggle-down: "\f150";
@fa-var-toggle-left: "\f191";
@ -655,6 +728,7 @@
@fa-var-umbrella: "\f0e9";
@fa-var-underline: "\f0cd";
@fa-var-undo: "\f0e2";
@fa-var-universal-access: "\f29a";
@fa-var-university: "\f19c";
@fa-var-unlink: "\f127";
@fa-var-unlock: "\f09c";
@ -664,20 +738,28 @@
@fa-var-usb: "\f287";
@fa-var-usd: "\f155";
@fa-var-user: "\f007";
@fa-var-user-circle: "\f2bd";
@fa-var-user-circle-o: "\f2be";
@fa-var-user-md: "\f0f0";
@fa-var-user-o: "\f2c0";
@fa-var-user-plus: "\f234";
@fa-var-user-secret: "\f21b";
@fa-var-user-times: "\f235";
@fa-var-users: "\f0c0";
@fa-var-vcard: "\f2bb";
@fa-var-vcard-o: "\f2bc";
@fa-var-venus: "\f221";
@fa-var-venus-double: "\f226";
@fa-var-venus-mars: "\f228";
@fa-var-viacoin: "\f237";
@fa-var-viadeo: "\f2a9";
@fa-var-viadeo-square: "\f2aa";
@fa-var-video-camera: "\f03d";
@fa-var-vimeo: "\f27d";
@fa-var-vimeo-square: "\f194";
@fa-var-vine: "\f1ca";
@fa-var-vk: "\f189";
@fa-var-volume-control-phone: "\f2a0";
@fa-var-volume-down: "\f027";
@fa-var-volume-off: "\f026";
@fa-var-volume-up: "\f028";
@ -687,11 +769,20 @@
@fa-var-weixin: "\f1d7";
@fa-var-whatsapp: "\f232";
@fa-var-wheelchair: "\f193";
@fa-var-wheelchair-alt: "\f29b";
@fa-var-wifi: "\f1eb";
@fa-var-wikipedia-w: "\f266";
@fa-var-window-close: "\f2d3";
@fa-var-window-close-o: "\f2d4";
@fa-var-window-maximize: "\f2d0";
@fa-var-window-minimize: "\f2d1";
@fa-var-window-restore: "\f2d2";
@fa-var-windows: "\f17a";
@fa-var-won: "\f159";
@fa-var-wordpress: "\f19a";
@fa-var-wpbeginner: "\f297";
@fa-var-wpexplorer: "\f2de";
@fa-var-wpforms: "\f298";
@fa-var-wrench: "\f0ad";
@fa-var-xing: "\f168";
@fa-var-xing-square: "\f169";
@ -702,6 +793,7 @@
@fa-var-yc-square: "\f1d4";
@fa-var-yelp: "\f1e9";
@fa-var-yen: "\f157";
@fa-var-yoast: "\f2b1";
@fa-var-youtube: "\f167";
@fa-var-youtube-play: "\f16a";
@fa-var-youtube-square: "\f166";

View File

@ -438,7 +438,7 @@
.#{$fa-css-prefix}-stumbleupon:before { content: $fa-var-stumbleupon; }
.#{$fa-css-prefix}-delicious:before { content: $fa-var-delicious; }
.#{$fa-css-prefix}-digg:before { content: $fa-var-digg; }
.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; }
.#{$fa-css-prefix}-pied-piper-pp:before { content: $fa-var-pied-piper-pp; }
.#{$fa-css-prefix}-pied-piper-alt:before { content: $fa-var-pied-piper-alt; }
.#{$fa-css-prefix}-drupal:before { content: $fa-var-drupal; }
.#{$fa-css-prefix}-joomla:before { content: $fa-var-joomla; }
@ -488,6 +488,7 @@
.#{$fa-css-prefix}-life-ring:before { content: $fa-var-life-ring; }
.#{$fa-css-prefix}-circle-o-notch:before { content: $fa-var-circle-o-notch; }
.#{$fa-css-prefix}-ra:before,
.#{$fa-css-prefix}-resistance:before,
.#{$fa-css-prefix}-rebel:before { content: $fa-var-rebel; }
.#{$fa-css-prefix}-ge:before,
.#{$fa-css-prefix}-empire:before { content: $fa-var-empire; }
@ -604,6 +605,7 @@
.#{$fa-css-prefix}-opencart:before { content: $fa-var-opencart; }
.#{$fa-css-prefix}-expeditedssl:before { content: $fa-var-expeditedssl; }
.#{$fa-css-prefix}-battery-4:before,
.#{$fa-css-prefix}-battery:before,
.#{$fa-css-prefix}-battery-full:before { content: $fa-var-battery-full; }
.#{$fa-css-prefix}-battery-3:before,
.#{$fa-css-prefix}-battery-three-quarters:before { content: $fa-var-battery-three-quarters; }
@ -695,3 +697,93 @@
.#{$fa-css-prefix}-bluetooth:before { content: $fa-var-bluetooth; }
.#{$fa-css-prefix}-bluetooth-b:before { content: $fa-var-bluetooth-b; }
.#{$fa-css-prefix}-percent:before { content: $fa-var-percent; }
.#{$fa-css-prefix}-gitlab:before { content: $fa-var-gitlab; }
.#{$fa-css-prefix}-wpbeginner:before { content: $fa-var-wpbeginner; }
.#{$fa-css-prefix}-wpforms:before { content: $fa-var-wpforms; }
.#{$fa-css-prefix}-envira:before { content: $fa-var-envira; }
.#{$fa-css-prefix}-universal-access:before { content: $fa-var-universal-access; }
.#{$fa-css-prefix}-wheelchair-alt:before { content: $fa-var-wheelchair-alt; }
.#{$fa-css-prefix}-question-circle-o:before { content: $fa-var-question-circle-o; }
.#{$fa-css-prefix}-blind:before { content: $fa-var-blind; }
.#{$fa-css-prefix}-audio-description:before { content: $fa-var-audio-description; }
.#{$fa-css-prefix}-volume-control-phone:before { content: $fa-var-volume-control-phone; }
.#{$fa-css-prefix}-braille:before { content: $fa-var-braille; }
.#{$fa-css-prefix}-assistive-listening-systems:before { content: $fa-var-assistive-listening-systems; }
.#{$fa-css-prefix}-asl-interpreting:before,
.#{$fa-css-prefix}-american-sign-language-interpreting:before { content: $fa-var-american-sign-language-interpreting; }
.#{$fa-css-prefix}-deafness:before,
.#{$fa-css-prefix}-hard-of-hearing:before,
.#{$fa-css-prefix}-deaf:before { content: $fa-var-deaf; }
.#{$fa-css-prefix}-glide:before { content: $fa-var-glide; }
.#{$fa-css-prefix}-glide-g:before { content: $fa-var-glide-g; }
.#{$fa-css-prefix}-signing:before,
.#{$fa-css-prefix}-sign-language:before { content: $fa-var-sign-language; }
.#{$fa-css-prefix}-low-vision:before { content: $fa-var-low-vision; }
.#{$fa-css-prefix}-viadeo:before { content: $fa-var-viadeo; }
.#{$fa-css-prefix}-viadeo-square:before { content: $fa-var-viadeo-square; }
.#{$fa-css-prefix}-snapchat:before { content: $fa-var-snapchat; }
.#{$fa-css-prefix}-snapchat-ghost:before { content: $fa-var-snapchat-ghost; }
.#{$fa-css-prefix}-snapchat-square:before { content: $fa-var-snapchat-square; }
.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; }
.#{$fa-css-prefix}-first-order:before { content: $fa-var-first-order; }
.#{$fa-css-prefix}-yoast:before { content: $fa-var-yoast; }
.#{$fa-css-prefix}-themeisle:before { content: $fa-var-themeisle; }
.#{$fa-css-prefix}-google-plus-circle:before,
.#{$fa-css-prefix}-google-plus-official:before { content: $fa-var-google-plus-official; }
.#{$fa-css-prefix}-fa:before,
.#{$fa-css-prefix}-font-awesome:before { content: $fa-var-font-awesome; }
.#{$fa-css-prefix}-handshake-o:before { content: $fa-var-handshake-o; }
.#{$fa-css-prefix}-envelope-open:before { content: $fa-var-envelope-open; }
.#{$fa-css-prefix}-envelope-open-o:before { content: $fa-var-envelope-open-o; }
.#{$fa-css-prefix}-linode:before { content: $fa-var-linode; }
.#{$fa-css-prefix}-address-book:before { content: $fa-var-address-book; }
.#{$fa-css-prefix}-address-book-o:before { content: $fa-var-address-book-o; }
.#{$fa-css-prefix}-vcard:before,
.#{$fa-css-prefix}-address-card:before { content: $fa-var-address-card; }
.#{$fa-css-prefix}-vcard-o:before,
.#{$fa-css-prefix}-address-card-o:before { content: $fa-var-address-card-o; }
.#{$fa-css-prefix}-user-circle:before { content: $fa-var-user-circle; }
.#{$fa-css-prefix}-user-circle-o:before { content: $fa-var-user-circle-o; }
.#{$fa-css-prefix}-user-o:before { content: $fa-var-user-o; }
.#{$fa-css-prefix}-id-badge:before { content: $fa-var-id-badge; }
.#{$fa-css-prefix}-drivers-license:before,
.#{$fa-css-prefix}-id-card:before { content: $fa-var-id-card; }
.#{$fa-css-prefix}-drivers-license-o:before,
.#{$fa-css-prefix}-id-card-o:before { content: $fa-var-id-card-o; }
.#{$fa-css-prefix}-quora:before { content: $fa-var-quora; }
.#{$fa-css-prefix}-free-code-camp:before { content: $fa-var-free-code-camp; }
.#{$fa-css-prefix}-telegram:before { content: $fa-var-telegram; }
.#{$fa-css-prefix}-thermometer-4:before,
.#{$fa-css-prefix}-thermometer:before,
.#{$fa-css-prefix}-thermometer-full:before { content: $fa-var-thermometer-full; }
.#{$fa-css-prefix}-thermometer-3:before,
.#{$fa-css-prefix}-thermometer-three-quarters:before { content: $fa-var-thermometer-three-quarters; }
.#{$fa-css-prefix}-thermometer-2:before,
.#{$fa-css-prefix}-thermometer-half:before { content: $fa-var-thermometer-half; }
.#{$fa-css-prefix}-thermometer-1:before,
.#{$fa-css-prefix}-thermometer-quarter:before { content: $fa-var-thermometer-quarter; }
.#{$fa-css-prefix}-thermometer-0:before,
.#{$fa-css-prefix}-thermometer-empty:before { content: $fa-var-thermometer-empty; }
.#{$fa-css-prefix}-shower:before { content: $fa-var-shower; }
.#{$fa-css-prefix}-bathtub:before,
.#{$fa-css-prefix}-s15:before,
.#{$fa-css-prefix}-bath:before { content: $fa-var-bath; }
.#{$fa-css-prefix}-podcast:before { content: $fa-var-podcast; }
.#{$fa-css-prefix}-window-maximize:before { content: $fa-var-window-maximize; }
.#{$fa-css-prefix}-window-minimize:before { content: $fa-var-window-minimize; }
.#{$fa-css-prefix}-window-restore:before { content: $fa-var-window-restore; }
.#{$fa-css-prefix}-times-rectangle:before,
.#{$fa-css-prefix}-window-close:before { content: $fa-var-window-close; }
.#{$fa-css-prefix}-times-rectangle-o:before,
.#{$fa-css-prefix}-window-close-o:before { content: $fa-var-window-close-o; }
.#{$fa-css-prefix}-bandcamp:before { content: $fa-var-bandcamp; }
.#{$fa-css-prefix}-grav:before { content: $fa-var-grav; }
.#{$fa-css-prefix}-etsy:before { content: $fa-var-etsy; }
.#{$fa-css-prefix}-imdb:before { content: $fa-var-imdb; }
.#{$fa-css-prefix}-ravelry:before { content: $fa-var-ravelry; }
.#{$fa-css-prefix}-eercast:before { content: $fa-var-eercast; }
.#{$fa-css-prefix}-microchip:before { content: $fa-var-microchip; }
.#{$fa-css-prefix}-snowflake-o:before { content: $fa-var-snowflake-o; }
.#{$fa-css-prefix}-superpowers:before { content: $fa-var-superpowers; }
.#{$fa-css-prefix}-wpexplorer:before { content: $fa-var-wpexplorer; }
.#{$fa-css-prefix}-meetup:before { content: $fa-var-meetup; }

View File

@ -12,15 +12,49 @@
}
@mixin fa-icon-rotate($degrees, $rotation) {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation});
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})";
-webkit-transform: rotate($degrees);
-ms-transform: rotate($degrees);
transform: rotate($degrees);
}
@mixin fa-icon-flip($horiz, $vert, $rotation) {
filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation});
-ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)";
-webkit-transform: scale($horiz, $vert);
-ms-transform: scale($horiz, $vert);
transform: scale($horiz, $vert);
}
// Only display content to screen readers. A la Bootstrap 4.
//
// See: http://a11yproject.com/posts/how-to-hide-content/
@mixin sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
// Use in conjunction with .sr-only to only display content when it's focused.
//
// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
//
// Credit: HTML5 Boilerplate
@mixin sr-only-focusable {
&:active,
&:focus {
position: static;
width: auto;
height: auto;
margin: 0;
overflow: visible;
clip: auto;
}
}

View File

@ -0,0 +1,5 @@
// Screen Readers
// -------------------------
.sr-only { @include sr-only(); }
.sr-only-focusable { @include sr-only-focusable(); }

View File

@ -4,14 +4,18 @@
$fa-font-path: "../fonts" !default;
$fa-font-size-base: 14px !default;
$fa-line-height-base: 1 !default;
//$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.5.0/fonts" !default; // for referencing Bootstrap CDN font files directly
//$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts" !default; // for referencing Bootstrap CDN font files directly
$fa-css-prefix: fa !default;
$fa-version: "4.5.0" !default;
$fa-version: "4.7.0" !default;
$fa-border-color: #eee !default;
$fa-inverse: #fff !default;
$fa-li-width: (30em / 14) !default;
$fa-var-500px: "\f26e";
$fa-var-address-book: "\f2b9";
$fa-var-address-book-o: "\f2ba";
$fa-var-address-card: "\f2bb";
$fa-var-address-card-o: "\f2bc";
$fa-var-adjust: "\f042";
$fa-var-adn: "\f170";
$fa-var-align-center: "\f037";
@ -20,6 +24,7 @@ $fa-var-align-left: "\f036";
$fa-var-align-right: "\f038";
$fa-var-amazon: "\f270";
$fa-var-ambulance: "\f0f9";
$fa-var-american-sign-language-interpreting: "\f2a3";
$fa-var-anchor: "\f13d";
$fa-var-android: "\f17b";
$fa-var-angellist: "\f209";
@ -50,17 +55,24 @@ $fa-var-arrows: "\f047";
$fa-var-arrows-alt: "\f0b2";
$fa-var-arrows-h: "\f07e";
$fa-var-arrows-v: "\f07d";
$fa-var-asl-interpreting: "\f2a3";
$fa-var-assistive-listening-systems: "\f2a2";
$fa-var-asterisk: "\f069";
$fa-var-at: "\f1fa";
$fa-var-audio-description: "\f29e";
$fa-var-automobile: "\f1b9";
$fa-var-backward: "\f04a";
$fa-var-balance-scale: "\f24e";
$fa-var-ban: "\f05e";
$fa-var-bandcamp: "\f2d5";
$fa-var-bank: "\f19c";
$fa-var-bar-chart: "\f080";
$fa-var-bar-chart-o: "\f080";
$fa-var-barcode: "\f02a";
$fa-var-bars: "\f0c9";
$fa-var-bath: "\f2cd";
$fa-var-bathtub: "\f2cd";
$fa-var-battery: "\f240";
$fa-var-battery-0: "\f244";
$fa-var-battery-1: "\f243";
$fa-var-battery-2: "\f242";
@ -86,6 +98,7 @@ $fa-var-bitbucket: "\f171";
$fa-var-bitbucket-square: "\f172";
$fa-var-bitcoin: "\f15a";
$fa-var-black-tie: "\f27e";
$fa-var-blind: "\f29d";
$fa-var-bluetooth: "\f293";
$fa-var-bluetooth-b: "\f294";
$fa-var-bold: "\f032";
@ -94,6 +107,7 @@ $fa-var-bomb: "\f1e2";
$fa-var-book: "\f02d";
$fa-var-bookmark: "\f02e";
$fa-var-bookmark-o: "\f097";
$fa-var-braille: "\f2a1";
$fa-var-briefcase: "\f0b1";
$fa-var-btc: "\f15a";
$fa-var-bug: "\f188";
@ -196,6 +210,8 @@ $fa-var-cutlery: "\f0f5";
$fa-var-dashboard: "\f0e4";
$fa-var-dashcube: "\f210";
$fa-var-database: "\f1c0";
$fa-var-deaf: "\f2a4";
$fa-var-deafness: "\f2a4";
$fa-var-dedent: "\f03b";
$fa-var-delicious: "\f1a5";
$fa-var-desktop: "\f108";
@ -206,18 +222,25 @@ $fa-var-dollar: "\f155";
$fa-var-dot-circle-o: "\f192";
$fa-var-download: "\f019";
$fa-var-dribbble: "\f17d";
$fa-var-drivers-license: "\f2c2";
$fa-var-drivers-license-o: "\f2c3";
$fa-var-dropbox: "\f16b";
$fa-var-drupal: "\f1a9";
$fa-var-edge: "\f282";
$fa-var-edit: "\f044";
$fa-var-eercast: "\f2da";
$fa-var-eject: "\f052";
$fa-var-ellipsis-h: "\f141";
$fa-var-ellipsis-v: "\f142";
$fa-var-empire: "\f1d1";
$fa-var-envelope: "\f0e0";
$fa-var-envelope-o: "\f003";
$fa-var-envelope-open: "\f2b6";
$fa-var-envelope-open-o: "\f2b7";
$fa-var-envelope-square: "\f199";
$fa-var-envira: "\f299";
$fa-var-eraser: "\f12d";
$fa-var-etsy: "\f2d7";
$fa-var-eur: "\f153";
$fa-var-euro: "\f153";
$fa-var-exchange: "\f0ec";
@ -231,6 +254,7 @@ $fa-var-external-link-square: "\f14c";
$fa-var-eye: "\f06e";
$fa-var-eye-slash: "\f070";
$fa-var-eyedropper: "\f1fb";
$fa-var-fa: "\f2b4";
$fa-var-facebook: "\f09a";
$fa-var-facebook-f: "\f09a";
$fa-var-facebook-official: "\f230";
@ -265,6 +289,7 @@ $fa-var-filter: "\f0b0";
$fa-var-fire: "\f06d";
$fa-var-fire-extinguisher: "\f134";
$fa-var-firefox: "\f269";
$fa-var-first-order: "\f2b0";
$fa-var-flag: "\f024";
$fa-var-flag-checkered: "\f11e";
$fa-var-flag-o: "\f11d";
@ -277,11 +302,13 @@ $fa-var-folder-o: "\f114";
$fa-var-folder-open: "\f07c";
$fa-var-folder-open-o: "\f115";
$fa-var-font: "\f031";
$fa-var-font-awesome: "\f2b4";
$fa-var-fonticons: "\f280";
$fa-var-fort-awesome: "\f286";
$fa-var-forumbee: "\f211";
$fa-var-forward: "\f04e";
$fa-var-foursquare: "\f180";
$fa-var-free-code-camp: "\f2c5";
$fa-var-frown-o: "\f119";
$fa-var-futbol-o: "\f1e3";
$fa-var-gamepad: "\f11b";
@ -300,15 +327,21 @@ $fa-var-git-square: "\f1d2";
$fa-var-github: "\f09b";
$fa-var-github-alt: "\f113";
$fa-var-github-square: "\f092";
$fa-var-gitlab: "\f296";
$fa-var-gittip: "\f184";
$fa-var-glass: "\f000";
$fa-var-glide: "\f2a5";
$fa-var-glide-g: "\f2a6";
$fa-var-globe: "\f0ac";
$fa-var-google: "\f1a0";
$fa-var-google-plus: "\f0d5";
$fa-var-google-plus-circle: "\f2b3";
$fa-var-google-plus-official: "\f2b3";
$fa-var-google-plus-square: "\f0d4";
$fa-var-google-wallet: "\f1ee";
$fa-var-graduation-cap: "\f19d";
$fa-var-gratipay: "\f184";
$fa-var-grav: "\f2d6";
$fa-var-group: "\f0c0";
$fa-var-h-square: "\f0fd";
$fa-var-hacker-news: "\f1d4";
@ -325,6 +358,8 @@ $fa-var-hand-rock-o: "\f255";
$fa-var-hand-scissors-o: "\f257";
$fa-var-hand-spock-o: "\f259";
$fa-var-hand-stop-o: "\f256";
$fa-var-handshake-o: "\f2b5";
$fa-var-hard-of-hearing: "\f2a4";
$fa-var-hashtag: "\f292";
$fa-var-hdd-o: "\f0a0";
$fa-var-header: "\f1dc";
@ -347,8 +382,12 @@ $fa-var-hourglass-start: "\f251";
$fa-var-houzz: "\f27c";
$fa-var-html5: "\f13b";
$fa-var-i-cursor: "\f246";
$fa-var-id-badge: "\f2c1";
$fa-var-id-card: "\f2c2";
$fa-var-id-card-o: "\f2c3";
$fa-var-ils: "\f20b";
$fa-var-image: "\f03e";
$fa-var-imdb: "\f2d8";
$fa-var-inbox: "\f01c";
$fa-var-indent: "\f03c";
$fa-var-industry: "\f275";
@ -386,6 +425,7 @@ $fa-var-line-chart: "\f201";
$fa-var-link: "\f0c1";
$fa-var-linkedin: "\f0e1";
$fa-var-linkedin-square: "\f08c";
$fa-var-linode: "\f2b8";
$fa-var-linux: "\f17c";
$fa-var-list: "\f03a";
$fa-var-list-alt: "\f022";
@ -397,6 +437,7 @@ $fa-var-long-arrow-down: "\f175";
$fa-var-long-arrow-left: "\f177";
$fa-var-long-arrow-right: "\f178";
$fa-var-long-arrow-up: "\f176";
$fa-var-low-vision: "\f2a8";
$fa-var-magic: "\f0d0";
$fa-var-magnet: "\f076";
$fa-var-mail-forward: "\f064";
@ -417,8 +458,10 @@ $fa-var-maxcdn: "\f136";
$fa-var-meanpath: "\f20c";
$fa-var-medium: "\f23a";
$fa-var-medkit: "\f0fa";
$fa-var-meetup: "\f2e0";
$fa-var-meh-o: "\f11a";
$fa-var-mercury: "\f223";
$fa-var-microchip: "\f2db";
$fa-var-microphone: "\f130";
$fa-var-microphone-slash: "\f131";
$fa-var-minus: "\f068";
@ -468,8 +511,9 @@ $fa-var-phone-square: "\f098";
$fa-var-photo: "\f03e";
$fa-var-picture-o: "\f03e";
$fa-var-pie-chart: "\f200";
$fa-var-pied-piper: "\f1a7";
$fa-var-pied-piper: "\f2ae";
$fa-var-pied-piper-alt: "\f1a8";
$fa-var-pied-piper-pp: "\f1a7";
$fa-var-pinterest: "\f0d2";
$fa-var-pinterest-p: "\f231";
$fa-var-pinterest-square: "\f0d3";
@ -482,6 +526,7 @@ $fa-var-plus: "\f067";
$fa-var-plus-circle: "\f055";
$fa-var-plus-square: "\f0fe";
$fa-var-plus-square-o: "\f196";
$fa-var-podcast: "\f2ce";
$fa-var-power-off: "\f011";
$fa-var-print: "\f02f";
$fa-var-product-hunt: "\f288";
@ -490,10 +535,13 @@ $fa-var-qq: "\f1d6";
$fa-var-qrcode: "\f029";
$fa-var-question: "\f128";
$fa-var-question-circle: "\f059";
$fa-var-question-circle-o: "\f29c";
$fa-var-quora: "\f2c4";
$fa-var-quote-left: "\f10d";
$fa-var-quote-right: "\f10e";
$fa-var-ra: "\f1d0";
$fa-var-random: "\f074";
$fa-var-ravelry: "\f2d9";
$fa-var-rebel: "\f1d0";
$fa-var-recycle: "\f1b8";
$fa-var-reddit: "\f1a1";
@ -507,6 +555,7 @@ $fa-var-reorder: "\f0c9";
$fa-var-repeat: "\f01e";
$fa-var-reply: "\f112";
$fa-var-reply-all: "\f122";
$fa-var-resistance: "\f1d0";
$fa-var-retweet: "\f079";
$fa-var-rmb: "\f157";
$fa-var-road: "\f018";
@ -519,6 +568,7 @@ $fa-var-rss-square: "\f143";
$fa-var-rub: "\f158";
$fa-var-ruble: "\f158";
$fa-var-rupee: "\f156";
$fa-var-s15: "\f2cd";
$fa-var-safari: "\f267";
$fa-var-save: "\f0c7";
$fa-var-scissors: "\f0c4";
@ -543,9 +593,12 @@ $fa-var-shirtsinbulk: "\f214";
$fa-var-shopping-bag: "\f290";
$fa-var-shopping-basket: "\f291";
$fa-var-shopping-cart: "\f07a";
$fa-var-shower: "\f2cc";
$fa-var-sign-in: "\f090";
$fa-var-sign-language: "\f2a7";
$fa-var-sign-out: "\f08b";
$fa-var-signal: "\f012";
$fa-var-signing: "\f2a7";
$fa-var-simplybuilt: "\f215";
$fa-var-sitemap: "\f0e8";
$fa-var-skyatlas: "\f216";
@ -554,6 +607,10 @@ $fa-var-slack: "\f198";
$fa-var-sliders: "\f1de";
$fa-var-slideshare: "\f1e7";
$fa-var-smile-o: "\f118";
$fa-var-snapchat: "\f2ab";
$fa-var-snapchat-ghost: "\f2ac";
$fa-var-snapchat-square: "\f2ad";
$fa-var-snowflake-o: "\f2dc";
$fa-var-soccer-ball-o: "\f1e3";
$fa-var-sort: "\f0dc";
$fa-var-sort-alpha-asc: "\f15d";
@ -599,6 +656,7 @@ $fa-var-subscript: "\f12c";
$fa-var-subway: "\f239";
$fa-var-suitcase: "\f0f2";
$fa-var-sun-o: "\f185";
$fa-var-superpowers: "\f2dd";
$fa-var-superscript: "\f12b";
$fa-var-support: "\f1cd";
$fa-var-table: "\f0ce";
@ -608,6 +666,7 @@ $fa-var-tag: "\f02b";
$fa-var-tags: "\f02c";
$fa-var-tasks: "\f0ae";
$fa-var-taxi: "\f1ba";
$fa-var-telegram: "\f2c6";
$fa-var-television: "\f26c";
$fa-var-tencent-weibo: "\f1d5";
$fa-var-terminal: "\f120";
@ -616,6 +675,18 @@ $fa-var-text-width: "\f035";
$fa-var-th: "\f00a";
$fa-var-th-large: "\f009";
$fa-var-th-list: "\f00b";
$fa-var-themeisle: "\f2b2";
$fa-var-thermometer: "\f2c7";
$fa-var-thermometer-0: "\f2cb";
$fa-var-thermometer-1: "\f2ca";
$fa-var-thermometer-2: "\f2c9";
$fa-var-thermometer-3: "\f2c8";
$fa-var-thermometer-4: "\f2c7";
$fa-var-thermometer-empty: "\f2cb";
$fa-var-thermometer-full: "\f2c7";
$fa-var-thermometer-half: "\f2c9";
$fa-var-thermometer-quarter: "\f2ca";
$fa-var-thermometer-three-quarters: "\f2c8";
$fa-var-thumb-tack: "\f08d";
$fa-var-thumbs-down: "\f165";
$fa-var-thumbs-o-down: "\f088";
@ -625,6 +696,8 @@ $fa-var-ticket: "\f145";
$fa-var-times: "\f00d";
$fa-var-times-circle: "\f057";
$fa-var-times-circle-o: "\f05c";
$fa-var-times-rectangle: "\f2d3";
$fa-var-times-rectangle-o: "\f2d4";
$fa-var-tint: "\f043";
$fa-var-toggle-down: "\f150";
$fa-var-toggle-left: "\f191";
@ -655,6 +728,7 @@ $fa-var-twitter-square: "\f081";
$fa-var-umbrella: "\f0e9";
$fa-var-underline: "\f0cd";
$fa-var-undo: "\f0e2";
$fa-var-universal-access: "\f29a";
$fa-var-university: "\f19c";
$fa-var-unlink: "\f127";
$fa-var-unlock: "\f09c";
@ -664,20 +738,28 @@ $fa-var-upload: "\f093";
$fa-var-usb: "\f287";
$fa-var-usd: "\f155";
$fa-var-user: "\f007";
$fa-var-user-circle: "\f2bd";
$fa-var-user-circle-o: "\f2be";
$fa-var-user-md: "\f0f0";
$fa-var-user-o: "\f2c0";
$fa-var-user-plus: "\f234";
$fa-var-user-secret: "\f21b";
$fa-var-user-times: "\f235";
$fa-var-users: "\f0c0";
$fa-var-vcard: "\f2bb";
$fa-var-vcard-o: "\f2bc";
$fa-var-venus: "\f221";
$fa-var-venus-double: "\f226";
$fa-var-venus-mars: "\f228";
$fa-var-viacoin: "\f237";
$fa-var-viadeo: "\f2a9";
$fa-var-viadeo-square: "\f2aa";
$fa-var-video-camera: "\f03d";
$fa-var-vimeo: "\f27d";
$fa-var-vimeo-square: "\f194";
$fa-var-vine: "\f1ca";
$fa-var-vk: "\f189";
$fa-var-volume-control-phone: "\f2a0";
$fa-var-volume-down: "\f027";
$fa-var-volume-off: "\f026";
$fa-var-volume-up: "\f028";
@ -687,11 +769,20 @@ $fa-var-weibo: "\f18a";
$fa-var-weixin: "\f1d7";
$fa-var-whatsapp: "\f232";
$fa-var-wheelchair: "\f193";
$fa-var-wheelchair-alt: "\f29b";
$fa-var-wifi: "\f1eb";
$fa-var-wikipedia-w: "\f266";
$fa-var-window-close: "\f2d3";
$fa-var-window-close-o: "\f2d4";
$fa-var-window-maximize: "\f2d0";
$fa-var-window-minimize: "\f2d1";
$fa-var-window-restore: "\f2d2";
$fa-var-windows: "\f17a";
$fa-var-won: "\f159";
$fa-var-wordpress: "\f19a";
$fa-var-wpbeginner: "\f297";
$fa-var-wpexplorer: "\f2de";
$fa-var-wpforms: "\f298";
$fa-var-wrench: "\f0ad";
$fa-var-xing: "\f168";
$fa-var-xing-square: "\f169";
@ -702,6 +793,7 @@ $fa-var-yc: "\f23b";
$fa-var-yc-square: "\f1d4";
$fa-var-yelp: "\f1e9";
$fa-var-yen: "\f157";
$fa-var-yoast: "\f2b1";
$fa-var-youtube: "\f167";
$fa-var-youtube-play: "\f16a";
$fa-var-youtube-square: "\f166";

View File

@ -1,5 +1,5 @@
/*!
* Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/
@ -15,3 +15,4 @@
@import "rotated-flipped";
@import "stacked";
@import "icons";
@import "screen-reader";

View File

@ -3,7 +3,9 @@ from __future__ import absolute_import, division, print_function, unicode_litera
import json
import logging
import socket
import string
import urlparse
import tornado.web
@ -33,23 +35,34 @@ class IndexHandler(tornado.web.RequestHandler):
webclient = mmw.Webclient(config)
if webclient.is_music_box():
program_name = 'MusicBox'
else:
program_name = 'Mopidy'
url = urlparse.urlparse('%s://%s' % (self.request.protocol, self.request.host))
port = url.port or 80
self.__dict = {
'isMusicBox': json.dumps(webclient.is_music_box()),
'websocketUrl': webclient.get_websocket_url(self.request),
'hasAlarmClock': json.dumps(webclient.has_alarm_clock()),
'onTrackClick': webclient.get_default_click_action()
'onTrackClick': webclient.get_default_click_action(),
'programName': program_name,
'hostname': url.hostname,
'serverIP': socket.gethostbyname(url.hostname),
'serverPort': port
}
self.__path = path
self.__title = string.Template('MusicBox on $hostname')
self.__title = string.Template('{} on $hostname'.format(program_name))
def get(self, path):
return self.render(path, title=self.get_title(), **self.__dict)
def get_title(self):
hostname, sep, port = self.request.host.rpartition(':')
if not sep or not port.isdigit():
hostname, port = self.request.host, '80'
return self.__title.safe_substitute(hostname=hostname, port=port)
url = urlparse.urlparse('%s://%s' % (self.request.protocol, self.request.host))
return self.__title.safe_substitute(hostname=url.hostname)
def get_template_path(self):
return self.__path

View File

@ -26,21 +26,21 @@ class Webclient(object):
if host or port:
if not host:
host = request.host.partition(':')[0]
logger.warning('Musicbox websocket_host not specified, '
logger.warning('Mopidy websocket_host not specified, '
'using %s', host)
elif not port:
port = self.config['http']['port']
logger.warning('Musicbox websocket_port not specified, '
logger.warning('Mopidy websocket_port not specified, '
'using %s', port)
protocol = 'ws'
if request.protocol == 'https':
protocol = 'wss'
ws_url = "%s://%s:%d/mopidy/ws" % (protocol, host, port)
ws_url = '%s://%s:%d/mopidy/ws' % (protocol, host, port)
return ws_url
def has_alarm_clock(self):
return self.ext_config.get('alarmclock', {}).get('enabled', False)
return self.config.get('alarmclock', {}).get('enabled', False)
def is_music_box(self):
return self.ext_config.get('musicbox', False)

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

BIN
screenshots/overview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View File

@ -14,7 +14,7 @@ def get_version(filename):
setup(
name='Mopidy-MusicBox-Webclient',
version=get_version('mopidy_musicbox_webclient/__init__.py'),
url='https://github.com/woutervanwijk/mopidy-musicbox-webclient',
url='https://github.com/pimusicbox/mopidy-musicbox-webclient',
license='Apache License, Version 2.0',
author='Wouter van Wijk',
author_email='woutervanwijk@gmail.com',

View File

@ -31,10 +31,13 @@
throw new Error('DummyTracklist.add does not support deprecated "tracks" and "uri" parameters.')
}
var position = params.at_position
// Add tracks to end of tracklist if no position is provided
params.at_position = params.at_position || this._tlTracks.length
if (typeof position === 'undefined') {
position = Math.max(0, this._tlTracks.length)
}
var tlTrack
var tlTracks = []
for (var i = 0; i < params.uris.length; i++) {
tlTrack = {
tlid: this._nextTlid++,
@ -42,11 +45,10 @@
uri: params.uris[i]
}
}
tlTracks.push(tlTrack)
this._tlTracks.splice(params.at_position + i, 0, tlTrack)
this._tlTracks.splice(position++, 0, tlTrack)
}
return $.when(tlTracks)
return $.when(this._tlTracks)
}
/* Clears the tracklist */
@ -54,6 +56,21 @@
this._tlTracks = []
}
/* Remove the matching tracks from the tracklist */
DummyTracklist.prototype.remove = function (criteria) {
this.filter(criteria).then( function (matches) {
for (var i = 0; i < matches.length; i++) {
for (var j = 0; j < this._tlTracks.length; j++) {
if (this._tlTracks[j].track.uri === matches[i].track.uri) {
this._tlTracks.splice(j, 1)
}
}
}
}.bind(this))
return $.when(this._tlTracks)
}
/**
* Retuns a list containing tlTracks that contain the provided
* criteria.uri or has ID criteria.tlid.
@ -89,11 +106,11 @@
/* Retuns index of the currently 'playing' track. */
DummyTracklist.prototype.index = function (params) {
if (!params) {
if (this._tlTracks.length > 1) {
// Always just assume that the second track is playing
return $.when(1)
} else {
if (this._tlTracks.length > 0) {
// Always just assume that the first track is playing
return $.when(0)
} else {
return $.when(null)
}
}
for (var i = 0; i < this._tlTracks.length; i++) {
@ -101,7 +118,17 @@
return $.when(i)
}
}
return $.when(0)
return $.when(null)
}
/* Returns the tracks in the tracklist */
DummyTracklist.prototype.get_tl_tracks = function () {
return $.when(this._tlTracks)
}
/* Returns the length of the tracklist */
DummyTracklist.prototype.get_length = function () {
return $.when(this._tlTracks.length)
}
return DummyTracklist

View File

@ -13,7 +13,7 @@ describe('controls', function () {
var mopidy
var div_element
var QUEUE_TRACKS = [ // Simulate an existing queue with three tracks loaded.
{uri: 'track:tlTrackMock1'},
{uri: 'track:tlTrackMock1'}, // <-- Currently playing track
{uri: 'track:tlTrackMock2'},
{uri: 'track:tlTrackMock3'}
]
@ -48,11 +48,8 @@ describe('controls', function () {
mopidy.tracklist.clear()
clearSpy.reset()
mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)})
})
afterEach(function () {
mopidy.playback.play.reset()
addSpy.reset()
mopidy.playback.play.reset()
})
after(function () {
@ -62,62 +59,62 @@ describe('controls', function () {
describe('#playTracks()', function () {
it('PLAY_ALL should clear tracklist first before populating with tracks', function () {
customTracklists[BROWSE_TABLE] = NEW_TRACKS
controls.playTracks(PLAY_ALL, mopidy, NEW_TRACKS[0].uri, BROWSE_TABLE)
customTracklists[CURRENT_PLAYLIST_TABLE] = NEW_TRACKS
controls.playTracks(PLAY_ALL, mopidy, NEW_TRACKS[0].uri, CURRENT_PLAYLIST_TABLE)
assert(clearSpy.called)
})
it('should not clear tracklist for events other than PLAY_ALL', function () {
customTracklists[BROWSE_TABLE] = NEW_TRACKS
controls.playTracks(PLAY_NOW, mopidy, NEW_TRACKS[0].uri, BROWSE_TABLE)
customTracklists[CURRENT_PLAYLIST_TABLE] = NEW_TRACKS
controls.playTracks(PLAY_NOW, mopidy, NEW_TRACKS[0].uri, CURRENT_PLAYLIST_TABLE)
assert(clearSpy.notCalled)
})
it('should raise exception if trackUri parameter is not provided and "track" data attribute is empty', function () {
assert.throw(function () { controls.playTracks('', mopidy) }, Error)
controls.playTracks(PLAY_ALL, mopidy, NEW_TRACKS[0].uri, BROWSE_TABLE)
controls.playTracks(PLAY_ALL, mopidy, NEW_TRACKS[0].uri, CURRENT_PLAYLIST_TABLE)
assert(mopidy.playback.play.calledWithMatch({tlid: mopidy.tracklist._tlTracks[0].tlid}))
})
it('should raise exception if playListUri parameter is not provided and "track" data attribute is empty', function () {
assert.throw(function () { controls.playTracks('', mopidy, NEW_TRACKS[0].uri) }, Error)
controls.playTracks(PLAY_ALL, mopidy, NEW_TRACKS[0].uri, BROWSE_TABLE)
controls.playTracks(PLAY_ALL, mopidy, NEW_TRACKS[0].uri, CURRENT_PLAYLIST_TABLE)
assert(mopidy.playback.play.calledWithMatch({tlid: mopidy.tracklist._tlTracks[0].tlid}))
})
it('should raise exception if unknown tracklist action is provided', function () {
var getTrackURIsForActionStub = sinon.stub(controls, '_getTrackURIsForAction') // Stub to bypass earlier exception
assert.throw(function () { controls.playTracks('99', mopidy, NEW_TRACKS[0].uri, BROWSE_TABLE) }, Error)
assert.throw(function () { controls.playTracks('99', mopidy, NEW_TRACKS[0].uri, CURRENT_PLAYLIST_TABLE) }, Error)
getTrackURIsForActionStub.restore()
})
it('should use "track" and "list" data attributes as fallback if parameters are not provided', function () {
$('#popupTracks').data('track', 'track:trackMock1') // Simulate 'track:trackMock1' being clicked.
$('#popupTracks').data('list', BROWSE_TABLE)
customTracklists[BROWSE_TABLE] = NEW_TRACKS
$('#popupTracks').data('list', CURRENT_PLAYLIST_TABLE)
customTracklists[CURRENT_PLAYLIST_TABLE] = NEW_TRACKS
controls.playTracks(PLAY_ALL, mopidy)
assert(mopidy.playback.play.calledWithMatch({tlid: mopidy.tracklist._tlTracks[0].tlid}))
})
it('PLAY_NOW, PLAY_NEXT, and ADD_THIS_BOTTOM should only add one track to the tracklist', function () {
controls.playTracks(PLAY_NOW, mopidy, NEW_TRACKS[0].uri)
assert(addSpy.calledWithMatch({at_position: 2, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NOW did not add correct track')
addSpy.reset()
controls.playTracks(PLAY_NOW, mopidy, NEW_TRACKS[0].uri, CURRENT_PLAYLIST_TABLE)
assert(addSpy.calledWithMatch({at_position: 1, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NOW did not add correct track')
mopidy.tracklist.clear()
mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)})
controls.playTracks(PLAY_NEXT, mopidy, NEW_TRACKS[0].uri)
assert(addSpy.calledWithMatch({at_position: 2, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NEXT did not add correct track')
addSpy.reset()
controls.playTracks(PLAY_NEXT, mopidy, NEW_TRACKS[0].uri, CURRENT_PLAYLIST_TABLE)
assert(addSpy.calledWithMatch({at_position: 1, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NEXT did not add correct track')
mopidy.tracklist.clear()
mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)})
addSpy.reset()
controls.playTracks(ADD_THIS_BOTTOM, mopidy, NEW_TRACKS[0].uri)
controls.playTracks(ADD_THIS_BOTTOM, mopidy, NEW_TRACKS[0].uri, CURRENT_PLAYLIST_TABLE)
assert(addSpy.calledWithMatch({uris: [NEW_TRACKS[0].uri]}), 'ADD_THIS_BOTTOM did not add correct track')
})
@ -133,21 +130,36 @@ describe('controls', function () {
assert(addSpy.calledWithMatch({uris: getUris(NEW_TRACKS)}), 'ADD_ALL_BOTTOM did not add correct tracks')
})
it('PLAY_NOW and PLAY_NEXT should insert track after currently playing track', function () {
it('PLAY_NEXT should insert track after currently playing track by default', function () {
controls.playTracks(PLAY_NOW, mopidy, NEW_TRACKS[0].uri)
assert(addSpy.calledWithMatch({at_position: 2, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NOW did not insert track at correct position')
assert(addSpy.calledWithMatch({at_position: 1, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NEXT did not insert track at correct position')
})
it('PLAY_NEXT should insert track after reference track index, if provided', function () {
controls.playTracks(PLAY_NEXT, mopidy, NEW_TRACKS[0].uri, '', 0)
assert(addSpy.calledWithMatch({at_position: 1, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NEXT did not insert track at correct position')
})
it('PLAY_NEXT should insert track even if queue is empty', function () {
mopidy.tracklist.clear()
controls.playTracks(PLAY_NEXT, mopidy, NEW_TRACKS[0].uri)
assert(addSpy.calledWithMatch({at_position: 0, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NEXT did not insert track at correct position')
})
it('PLAY_NOW should always insert track at current index', function () {
controls.playTracks(PLAY_NOW, mopidy, NEW_TRACKS[0].uri)
assert(addSpy.calledWithMatch({at_position: 1, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NOW did not insert track at correct position')
addSpy.reset()
mopidy.tracklist.clear()
mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)})
controls.playTracks(PLAY_NEXT, mopidy, NEW_TRACKS[0].uri)
assert(addSpy.calledWithMatch({at_position: 2, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NEXT did not insert track at correct position')
controls.playTracks(PLAY_NOW, mopidy, NEW_TRACKS[0].uri)
assert(addSpy.calledWithMatch({at_position: 0, uris: [NEW_TRACKS[0].uri]}), 'PLAY_NOW did not insert track at correct position')
})
it('only PLAY_NOW and PLAY_ALL should trigger playback', function () {
controls.playTracks(PLAY_NOW, mopidy, 2)
assert(mopidy.playback.play.calledWithMatch({tlid: mopidy.tracklist._tlTracks[2].tlid}), 'PLAY_NOW did not start playback of correct track')
controls.playTracks(PLAY_NOW, mopidy)
assert(mopidy.playback.play.calledWithMatch({tlid: mopidy.tracklist._tlTracks[0].tlid}), 'PLAY_NOW did not start playback of correct track')
mopidy.playback.play.reset()
mopidy.tracklist.clear()
@ -182,12 +194,12 @@ describe('controls', function () {
it('should store last action in cookie if on-track-click mode is set to "DYNAMIC"', function () {
$(document.body).data('on-track-click', 'DYNAMIC')
var cookieStub = sinon.stub($, 'cookie')
controls.playTracks(PLAY_NOW, mopidy, 2)
controls.playTracks(PLAY_NOW, mopidy)
assert(cookieStub.calledWithMatch('onTrackClick', PLAY_NOW, {expires: 365}))
cookieStub.reset()
$(document.body).data('on-track-click', 'PLAY_NOW')
controls.playTracks(PLAY_NOW, mopidy, 2)
controls.playTracks(PLAY_NOW, mopidy)
assert(cookieStub.notCalled)
cookieStub.restore()
})
@ -245,9 +257,9 @@ describe('controls', function () {
})
it('should get tracks from "playlistUri" for PLAY_ALL, and ADD_ALL_BOTTOM', function () {
customTracklists[BROWSE_TABLE] = NEW_TRACKS
customTracklists[CURRENT_PLAYLIST_TABLE] = NEW_TRACKS
var tracks = controls._getTrackURIsForAction(PLAY_ALL, NEW_TRACKS[0], BROWSE_TABLE)
var tracks = controls._getTrackURIsForAction(PLAY_ALL, NEW_TRACKS[0], CURRENT_PLAYLIST_TABLE)
assert.equal(tracks.length, NEW_TRACKS.length)
for (var i = 0; i < tracks.length; i++) {
assert.equal(tracks[i], NEW_TRACKS[i].uri)
@ -262,4 +274,144 @@ describe('controls', function () {
assert.equal(controls._getTrackURIsForAction('0', 'mockUri')[0], 'mockUri')
})
})
describe('#insertTrack()', function () {
it('should raise exception if no uri is provided', function () {
assert.throw(function () { controls.insertTrack() }, Error)
})
it('should insert track after currently playing track by default', function () {
var tracklistLength = QUEUE_TRACKS.length
var insertUri = NEW_TRACKS[0].uri
controls.insertTrack(insertUri, mopidy)
mopidy.tracklist.get_length().then(function (length) {
assert.equal(length, tracklistLength + 1)
})
mopidy.tracklist.index().then(function (index) {
mopidy.tracklist.get_tl_tracks().then(function (tlTracks) {
assert.equal(tlTracks[index + 1].track.uri, insertUri)
})
})
})
it('should insert track at provided index', function () {
var tracklistLength = QUEUE_TRACKS.length
var insertUri = NEW_TRACKS[0].uri
mopidy.tracklist.get_tl_tracks().then(function (tlTracks) {
controls.insertTrack(insertUri, mopidy, tlTracks[1].tlid)
})
mopidy.tracklist.get_length().then(function (length) {
assert.equal(length, tracklistLength + 1)
})
mopidy.tracklist.get_tl_tracks().then(function (tlTracks) {
assert.equal(tlTracks[2].track.uri, insertUri)
})
})
})
describe('#addTrackToBottom()', function () {
it('should raise exception if no uri is provided', function () {
assert.throw(function () { controls.addTrackToBottom() }, Error)
})
it('should add track at bottom of tracklist', function () {
var tracklistLength = QUEUE_TRACKS.length
var insertUri = NEW_TRACKS[0].uri
controls.addTrackToBottom(insertUri, mopidy)
mopidy.tracklist.get_length().then(function (length) {
assert.equal(length, tracklistLength + 1)
})
mopidy.tracklist.get_tl_tracks().then(function (tlTracks) {
assert.equal(tlTracks[tlTracks.length - 1].track.uri, insertUri)
})
})
})
describe('#removeTrack()', function () {
it('should remove track', function () {
var tracklistLength = QUEUE_TRACKS.length
var deleteUri = QUEUE_TRACKS[1].uri
mopidy.tracklist.get_tl_tracks().then(function (tlTracks) {
controls.removeTrack(tlTracks[1].tlid, mopidy)
})
mopidy.tracklist.get_length().then(function (length) {
assert.equal(length, tracklistLength - 1)
})
mopidy.tracklist.get_tl_tracks().then(function (tlTracks) {
var found = false
for (var i = 0; i < tlTracks.length; i++) {
if (tlTracks[i].track.uri === deleteUri) {
found = true
}
}
assert(!found)
})
})
})
describe('#showInfoPopup()', function () {
var track
var popup = $('<div data-role="popup" id="popupShowInfo"><table><thead><tr><th></th><th></th></tr></thead><tbody></tbody></div>')
before(function () {
track = {
'uri': QUEUE_TRACKS[0].uri,
'length': 61000,
'artists': [
{
'uri': 'artistUri1',
'name': 'nameMock1'
}, {
'uri': 'artistUri2',
'name': 'nameMock2'
}
]
}
var library = {
lookup: sinon.stub()
}
mopidy.library = library
mopidy.library.lookup.returns($.when({'track:tlTrackMock1': [track]}))
$(document.body).append(popup)
$('#popupShowInfo').data(track, track.uri) // Simulate selection from context menu
$('#popupShowInfo').popup() // Initialize popup
})
afterEach(function () {
mopidy.library.lookup.reset()
})
it('should default track name', function () {
controls.showInfoPopup('', '#popupShowInfo', mopidy)
assert.equal($('td:contains("Name:")').siblings('td').text(), '(Not available)')
})
it('should default album name', function () {
controls.showInfoPopup('', '#popupShowInfo', mopidy)
assert.equal($('td:contains("Album:")').siblings('td').text(), '(Not available)')
})
it('should add leading zero if seconds length < 10', function () {
controls.showInfoPopup('', '#popupShowInfo', mopidy)
assert.equal($('td:contains("Length:")').siblings('td').text(), '1:01')
})
it('should show plural for artist name', function () {
controls.showInfoPopup('', '#popupShowInfo', mopidy)
assert.isOk($('td:contains("Artists:")'))
})
})
})

View File

@ -46,13 +46,13 @@ class RedirectHandlerTest(BaseTest):
response.headers['Location'].endswith('index.html')
class IndexHandlerTest(BaseTest):
class IndexHandlerTestMusicBox(BaseTest):
def test_index_handler(self):
response = self.fetch('/index.html', method='GET')
assert response.code == 200
def test_get_title(self):
def test_get_title_musicbox(self):
response = self.fetch('/index.html', method='GET')
body = tornado.escape.to_unicode(response.body)
@ -65,3 +65,32 @@ class IndexHandlerTest(BaseTest):
assert 'data-is-musicbox="true"' in body
assert 'data-has-alarmclock="false"' in body
assert 'data-websocket-url=""' in body
assert 'data-on-track-click="' in body
assert 'data-program-name="' in body
assert 'data-hostname="' in body
class IndexHandlerTestMopidy(BaseTest):
def get_app(self):
extension = Extension()
self.config = config.Proxy({'musicbox_webclient': {
'enabled': True,
'musicbox': False,
'websocket_host': '',
'websocket_port': '',
}
})
return tornado.web.Application(extension.factory(self.config, mock.Mock()))
def test_initialize_sets_dictionary_objects(self):
response = self.fetch('/index.html', method='GET')
body = tornado.escape.to_unicode(response.body)
assert 'data-is-musicbox="false"' in body
def test_get_title_mopidy(self):
response = self.fetch('/index.html', method='GET')
body = tornado.escape.to_unicode(response.body)
assert '<title>Mopidy on localhost</title>' in body

View File

@ -20,7 +20,10 @@ class WebclientTests(unittest.TestCase):
'musicbox': False,
'websocket_host': 'host_mock',
'websocket_port': 999,
}
},
'alarmclock': {
'enabled': True,
}
})
self.ext = Extension()
@ -72,7 +75,7 @@ class WebclientTests(unittest.TestCase):
assert self.mmw.get_websocket_url(request_mock) == 'wss://127.0.0.1:999/mopidy/ws'
def test_has_alarmclock(self):
assert not self.mmw.has_alarm_clock()
assert self.mmw.has_alarm_clock()
def test_is_musicbox(self):
assert not self.mmw.is_music_box()