Merge pull request #216 from jcass77/enhance/75_add_track_uri_to_queue

Add feature for adding track URI directly to the queue
This commit is contained in:
John Cass 2017-01-14 14:19:35 +02:00 committed by GitHub
commit 37b267ded1
9 changed files with 559 additions and 113 deletions

View File

@ -106,12 +106,17 @@ Changelog
v2.4.0 (UNRELEASED)
-------------------
- 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. The popup includes the URI of the track,
which can be inserted into various lists elsewhere in the player.
**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>`_).
- Now shows server name/IP address and port number at the bottom of the navigation pane. (Fixes: `#67 <https://github.com/pimusicbox/mopidy-musicbox-webclient/issues/67>`_).
- 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.
- Use correct icons for folders, audio, and other files when browsing local files.
v2.3.0 (2016-05-15)

View File

@ -1,6 +1,6 @@
/*
* Mopidy Webclient CSS
* (c) Wouter van Wijk 2012-2013
* (c) Wouter van Wijk 2012-2017
*/
/****************************
@ -221,6 +221,17 @@ span.hostInfo {
#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 *
***************/
@ -249,6 +260,38 @@ span.hostInfo {
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;
}
.albumdivider h1, .table li h1 {
font-size: 120% !important;
}
@ -327,7 +370,6 @@ span.hostInfo {
/**********************
* Now Playing area *
**********************/
#nowPlayingFooter {
height: 50px;
line-height: 48px;
@ -406,6 +448,7 @@ span.hostInfo {
.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 {
@ -425,6 +468,10 @@ span.hostInfo {
content: '\f149';
}
.ui-icon-insert:after {
content: '\f177';
}
.ui-icon-add:after {
content: '\f196';
}
@ -448,11 +495,17 @@ span.hostInfo {
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;
@ -461,7 +514,6 @@ span.hostInfo {
/****************
* Common use *
****************/
#playlistspane {
margin: 0 !important;
}
@ -545,13 +597,13 @@ a {
}
/*helper*/
.ui-loader h1 {
color: #efefef;
}
/* panel workaround to make it responsive wrap push on wide viewports once open */
/*tablets and desktop*/
@media (min-width: 35em) {
/* panel workaround to make it responsive wrap push on wide viewports once open */
.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,
@ -569,7 +621,12 @@ a {
.ui-responsive-panel .ui-panel-dismiss-display-reveal {
display: none;
}
.popupDialog {
min-width: 320px;
}
}
/*smartphones*/
@media (max-width: 35em) {
#nowPlayingpane {

View File

@ -111,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">
@ -150,6 +150,9 @@
<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');">Show Track Info...</span></a>
</li>
</ul>
</div>
</div>
@ -160,8 +163,11 @@
<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>
@ -174,26 +180,48 @@
<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');">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>
@ -223,6 +251,48 @@
</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>
<tr>
<td class="label">Name:</td>
<td id="name-cell"></td>
</tr>
<tr id="album-row">
<td class="label">Album:</td>
<td id="album-cell"></td>
</tr>
<tr id="artist-row">
<td class="label">Artist(s):</td>
<td id="artist-cell"></td>
</tr>
<tr id="track-no-row">
<td class="label">Track #:</td>
<td id="track-no-cell"></td>
</tr>
<tr id="length-row">
<td class="label">Length:</td>
<td id="length-cell"></td>
</tr>
<tr id="bitrate-row">
<td class="label">Bitrate:</td>
<td id="bitrate-cell"></td>
</tr>
<tr>
<td class="label label-center">URI:</td>
<td><input type="text" id="uri-cell"></input></td>
</tr>
</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">Initializing...</h1>
@ -343,10 +413,14 @@
<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">
<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>
@ -355,6 +429,7 @@
</button>
</div>
</div>
</div>
<div class="ui-grid">
<ul class="table" id="currenttable"></ul>
</div>
@ -430,7 +505,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,17 +62,16 @@
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:
@ -87,6 +90,7 @@
throw new Error('Unexpected tracklist action identifier: ' + 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')
@ -95,6 +99,10 @@
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,74 @@
})
},
showInfoPopup: function (popupId) {
showLoading(true)
var uri = $(popupId).data('track')
$(popupId).popup('close')
mopidy.library.lookup({'uris': [uri]}).then(function (resultDict) {
var uri = Object.keys(resultDict)[0]
var track = resultDict[uri][0]
$('#popupShowInfo #name-cell').text(track.name)
if (track.album && track.album.name) {
$('#popupShowInfo #album-cell').text(track.album.name)
} else {
$('#popupShowInfo #album-cell').text('(Not available)')
}
var artistNames = ''
if (track.artists && track.artists.length > 0) {
for (var i = 0; i < track.artists.length; i++) {
if (i > 0) {
artistNames = artistNames + ', '
}
artistNames = artistNames + track.artists[i].name
}
}
// Fallback to album artists.
if (artistNames.length === 0 && track.album.artists && track.album.artists.length > 0) {
for (i = 0; i < track.album.artists.length; i++) {
if (i > 0) {
artistNames = artistNames + ', '
}
artistNames = artistNames + track.album.artists[i].name
}
}
if (artistNames.length > 0) {
$('#popupShowInfo #artist-cell').text(artistNames)
$('#popupShowInfo #artist-row').show()
} else {
$('#popupShowInfo #artist-row').hide()
}
if (track.track_no) {
$('#popupShowInfo #track-no-cell').text(track.track_no)
$('#popupShowInfo #track-no-row').show()
} else {
$('#popupShowInfo #track-no-row').hide()
}
if (track.length) {
var duration = new Date(track.length)
$('#popupShowInfo #length-cell').text(duration.getUTCMinutes() + ':' + duration.getUTCSeconds())
$('#popupShowInfo #length-row').show()
} else {
$('#popupShowInfo #length-row').hide()
}
if (track.bitrate) {
$('#popupShowInfo #bitrate-cell').text(track.bitrate)
$('#popupShowInfo #bitrate-row').show()
} else {
$('#popupShowInfo #bitrate-row').hide()
}
$('#popupShowInfo #uri-cell').val(uri)
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-cell').focus()
$('#popupShowInfo #uri-cell').select()
}
}, console.error)
},
refreshPlaylists: function () {
mopidy.playlists.refresh().then(function () {
playlists = {}
@ -448,8 +616,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 +625,7 @@
name = artistStr + ' - ' + name
}
}
$('#streamnameinput').val(name)
$('#' + nameInput).val(name)
return true
},

View File

@ -42,11 +42,9 @@ 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 = $(document.body).data('program-name')
@ -69,6 +67,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']
@ -173,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) {

View File

@ -76,7 +76,6 @@ function setSongTitle (title, refresh_ui) {
}
function setSongInfo (data) {
// console.log(data, songdata);
if (!data) { return }
if (data.tlid === songdata.tlid) { return }
if (!data.track.name || data.track.name === '') {
@ -148,13 +147,6 @@ 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
@ -205,6 +197,7 @@ function popupTracks (e, listuri, trackuri, tlid) {
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,
@ -217,6 +210,11 @@ 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
}
@ -256,6 +254,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()
@ -297,6 +300,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) {

View File

@ -1,6 +1,6 @@
CACHE MANIFEST
# 2017-01-06:v1
# 2017-01-14:v4
NETWORK:
*

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,90 @@ 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)
})
})
})
})