From 7daf5a738315b7228ecd5e95315fe255a5bfc115 Mon Sep 17 00:00:00 2001 From: jcass Date: Sat, 16 Apr 2016 17:10:52 +0200 Subject: [PATCH] Modularise controls.js. Fixes 124 Make default click action user-configurable. Fixes #133. Optimise updating of now-playing icons. Fixes #184. --- .eslintrc | 1 + README.rst | 8 + mopidy_musicbox_webclient/__init__.py | 7 + mopidy_musicbox_webclient/ext.conf | 1 + .../static/css/webclient.css | 69 +- mopidy_musicbox_webclient/static/index.html | 109 +- .../static/js/controls.js | 1239 ++++++++--------- .../static/js/functionsvars.js | 39 +- mopidy_musicbox_webclient/static/js/gui.js | 103 +- .../static/js/library.js | 5 +- .../static/js/process_ws.js | 41 +- mopidy_musicbox_webclient/static/mb.appcache | 2 +- mopidy_musicbox_webclient/static/system.html | 4 +- mopidy_musicbox_webclient/web.py | 1 + mopidy_musicbox_webclient/webclient.py | 3 + tests/js/dummy_tracklist.js | 108 ++ tests/js/test_controls.js | 265 ++++ tests/js/test_images.js | 9 +- tests/js/test_synced_timer.js | 8 +- tests/test_extension.py | 2 + tests/test_webclient.py | 3 + 21 files changed, 1221 insertions(+), 806 deletions(-) create mode 100644 tests/js/dummy_tracklist.js create mode 100644 tests/js/test_controls.js diff --git a/.eslintrc b/.eslintrc index dc14e5b..5b838e3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,5 +7,6 @@ "indent": [2, 4, {"SwitchCase": 1}], "no-undef": 0, // TODO: Set this to '2' once Javascript has been modularised. "no-unused-vars": 0, // TODO: Set this to '2' once Javascript has been modularised. + "camelcase": 1, } } diff --git a/README.rst b/README.rst index 70de863..00fa03e 100644 --- a/README.rst +++ b/README.rst @@ -79,12 +79,20 @@ v2.3.0 (UNRELEASED) (Addresses: `#130 `_). - Upgrade Media Progress Timer to version 3.0.0. - Now retrieves album cover and artist images using MusicBrainzID, if available. +- New configuration parameter ``on_track_click`` can be used to customize the action that is performed when the + user clicks on a track in a list. Valid options are: ``PLAY_NOW``, ``PLAY_NEXT``, ``ADD_THIS_BOTTOM``, + ``ADD_ALL_BOTTOM``, ``PLAY_ALL`` (default), and ``DYNAMIC`` (repeats last action). + (Addresses: `#133 `_). +- Optimized updating of 'now playing' icons in tracklists. + (Addresses: `#184 `_). **Fixes** - Don't create Mopidy models manually. (Fixes: `#172 `_). - Context menu is now available for all tracks in browse pane. (Fixes: `#126 `_). - last.fm artist image lookups should now always return the correct image for similarly named artists. +- Ensure that browsed tracks are always added to the queue using the track URI rather than the track's position in the folder. + (Fixes: `#124 `_). v2.2.0 (2016-03-01) ------------------- diff --git a/mopidy_musicbox_webclient/__init__.py b/mopidy_musicbox_webclient/__init__.py index 89f4c5d..2bc628e 100644 --- a/mopidy_musicbox_webclient/__init__.py +++ b/mopidy_musicbox_webclient/__init__.py @@ -22,6 +22,13 @@ class Extension(ext.Extension): schema['musicbox'] = config.Boolean(optional=True) schema['websocket_host'] = config.Hostname(optional=True) schema['websocket_port'] = config.Port(optional=True) + schema['on_track_click'] = config.String(optional=True, + choices=['PLAY_NOW', + 'PLAY_NEXT', + 'ADD_THIS_BOTTOM', + 'ADD_ALL_BOTTOM', + 'PLAY_ALL', + 'DYNAMIC']) return schema def setup(self, registry): diff --git a/mopidy_musicbox_webclient/ext.conf b/mopidy_musicbox_webclient/ext.conf index fa7898e..f5d8a17 100644 --- a/mopidy_musicbox_webclient/ext.conf +++ b/mopidy_musicbox_webclient/ext.conf @@ -3,3 +3,4 @@ enabled = true musicbox = false websocket_host = websocket_port = +on_track_click = PLAY_ALL diff --git a/mopidy_musicbox_webclient/static/css/webclient.css b/mopidy_musicbox_webclient/static/css/webclient.css index 7a2ed05..38637c1 100644 --- a/mopidy_musicbox_webclient/static/css/webclient.css +++ b/mopidy_musicbox_webclient/static/css/webclient.css @@ -244,8 +244,8 @@ } .smalldivider { - font-size: 25% !important; - height: 5px !important; + font-size: 10%; + height: 2px; background-color: #ddd !important; } @@ -342,6 +342,11 @@ text-decoration: none; } +.popupArtistLi, +.popupAlbumLi { + display: none +} + .popupArtistName, .popupTrackName, .popupAlbumName, @@ -361,7 +366,7 @@ margin-top: 10px; } -#controlspopupimage, +#albumCoverImg, #coverpopupimage, #artistpopupimage { display: block; @@ -372,16 +377,56 @@ max-height: 90%; } -#popupTracksLv li, -#popupQueueLv li, -#popupBrowseLv li { - border-bottom: 1px solid #aaa; +/* Override to make buttons more visible in popups.*/ +#popupTracks .ui-btn-up-c, +#popupQueue .ui-btn-up-c { + background: white; } -#popupTracksLv, -#popupQueueLv, -#popupBrowseLv li { - border: 1px solid #aaa; +/* Custom icons for popup listviews - see http://demos.jquerymobile.com/1.3.2/#CustomIcons */ +.ui-icon-playAll:after, +.ui-icon-play:after, +.ui-icon-playNext:after, +.ui-icon-add:after, +.ui-icon-addAll:after, +.ui-icon-remove:after { + color: #34495e; + font-family: 'FontAwesome'; +} + +.ui-icon-playAll:after { + content: '\f050'; +} + +.ui-icon-play:after { + content: '\f04b'; +} + +.ui-icon-playNext:after { + content: '\f149'; +} + +.ui-icon-add:after { + content: '\f196'; +} + +.ui-icon-addAll:after { + content: '\f0fe'; +} + +.ui-icon-remove:after { + content: '\f00d'; +} + +.ui-icon-playAll, +.ui-icon-play, +.ui-icon-playNext, +.ui-icon-add, +.ui-icon-addAll, +.ui-icon-remove { + background-color: unset; + background-image: none; + font-weight: normal; } .popupDialog { @@ -523,7 +568,7 @@ a { overflow: hidden; } - #controlspopupimage { + #albumCoverImg { max-width: 90%; max-height: 90%; margin-bottom: 3px; diff --git a/mopidy_musicbox_webclient/static/index.html b/mopidy_musicbox_webclient/static/index.html index 4c4b832..8620f30 100644 --- a/mopidy_musicbox_webclient/static/index.html +++ b/mopidy_musicbox_webclient/static/index.html @@ -36,7 +36,7 @@ - +
@@ -87,7 +87,7 @@
  • - + @@ -112,47 +112,28 @@ Album artist
    - - -
    + -
    +
    - -
    @@ -426,18 +407,18 @@

    Play a specific stream/track and optionally save it to your favourites. - - -
    @@ -459,7 +440,7 @@

    - +
  • @@ -467,16 +448,16 @@
    - - - + + +
    - - - - - + + + + +
    diff --git a/mopidy_musicbox_webclient/static/js/controls.js b/mopidy_musicbox_webclient/static/js/controls.js index b1bcc9b..1b2e55e 100644 --- a/mopidy_musicbox_webclient/static/js/controls.js +++ b/mopidy_musicbox_webclient/static/js/controls.js @@ -1,663 +1,644 @@ -/** ********************************* - * play tracks from a browse list * - ***********************************/ -function playBrowsedTracks (action, trackIndex) { - $('#popupBrowse').popup('close') - toast('Loading...') +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory) + } else if (typeof module === 'object' && module.exports) { + module.exports = factory() + } else { + root.controls = factory() + } +}(this, function () { + 'use strict' - if (typeof trackIndex === 'undefined') { - trackIndex = $('#popupBrowse').data('tlid') - } - if (action === PLAY_ALL) { - mopidy.tracklist.clear() - // Default for radio streams is to just add the selected URI. - if (isStreamUri(browseTracks[trackIndex].uri)) { - action = PLAY_NOW - } - } - var trackUris = [] - switch (action) { - case PLAY_NOW: - case PLAY_NEXT: - case ADD_THIS_BOTTOM: - trackUris.push(browseTracks[trackIndex].uri) - break - case PLAY_ALL: - case ADD_ALL_BOTTOM: - trackUris = getUris(browseTracks) - break - default: - break - } - var maybePlay = function (tlTracks) { - if (action === PLAY_NOW || action === PLAY_ALL) { - var playIndex = (action === PLAY_ALL) ? trackIndex : 0 - mopidy.playback.play({'tl_track': tlTracks[playIndex]}) - } - } + var controls = { - switch (action) { - case PLAY_NOW: - case PLAY_NEXT: - mopidy.tracklist.index().then(function (currentIndex) { - mopidy.tracklist.add({'at_position': currentIndex + 1, 'uris': trackUris}).then(maybePlay) - }) - break - case ADD_THIS_BOTTOM: - case ADD_ALL_BOTTOM: - case PLAY_ALL: - mopidy.tracklist.add({'uris': trackUris}).then(maybePlay) - break - default: - break - } - return false -} + /** + * 'onClick' handler for tracks that are rendered in a list. + * + * 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. + * ADD_THIS_BOTTOM: add track to bottom of tracklist. + * ADD_ALL_BOTTOM: add all tracks in in the list to bottom of + * tracklist. + * PLAY_ALL: clear tracklist and start playback of the track + * with URI provided in 'trackUri'. + * @param {object} mopidy - The Mopidy.js object that should be used to communicate with the + * Mopidy server. + * @param {string} trackUri - (Optional) The URI of the specific track that the action should + * be performed on. If no URI is provided then the 'data' attribute + * of the popup DIV is assumed to contain the track URI. + * @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. + */ -/** ******************************* - * play an uri from a tracklist * - *********************************/ -function playTrack (action) { - var hash = document.location.hash.split('?') - var divid = hash[0].substr(1) + playTracks: function (action, mopidy, trackUri, playlistUri) { + $('#popupTracks').popup('close') + toast('Loading...') - // Search page default click behaviour adds and plays selected track only. - if (action === PLAY_NOW && divid === 'search') { - action === PLAY_NOW_SEARCH - } - - $('#popupTracks').popup('close') - $('#controlspopup').popup('close') - toast('Loading...') - - playlisturi = $('#popupTracks').data('list') - uri = $('#popupTracks').data('track') - - var trackUris = getTracksFromUri(playlisturi) - // find track that was selected - for (var selected = 0; selected < trackUris.length; selected++) { - if (trackUris[selected] === uri) { - break - } - } - switch (action) { - case ADD_THIS_BOTTOM: - case PLAY_NEXT: - case PLAY_NOW_SEARCH: - trackUris = [trackUris[selected]] - selected = 0 - } - switch (action) { - case PLAY_NOW: - case PLAY_NOW_SEARCH: - mopidy.tracklist.clear().then( - mopidy.tracklist.add({'uris': trackUris}).then( - function (tlTracks) { - mopidy.playback.play({'tl_track': tlTracks[selected]}) - } - ) - ) - break - case PLAY_NEXT: - mopidy.tracklist.index().then(function (currentIndex) { - mopidy.tracklist.add({'at_position': currentIndex + 1, 'uris': trackUris}) - }) - break - case ADD_THIS_BOTTOM: - case ADD_ALL_BOTTOM: - mopidy.tracklist.add({'uris': trackUris}) - break - } - return false -} - -/** * - * Plays a Track given by an URI from the given playlist URI. - * @param track_uri, playlist_uri - * @returns {boolean} - */ -function playTrackByUri (track_uri, playlist_uri) { - function findAndPlayTrack (tlTracks) { - if (tlTracks.length > 0) { - // Find track that was selected - for (var selected = 0; selected < tlTracks.length; selected++) { - if (tlTracks[selected].track.uri === track_uri) { - mopidy.playback.play({'tl_track': tlTracks[selected]}) - return - } + trackUri = trackUri || $('#popupTracks').data('track') + if (typeof trackUri === 'undefined') { + throw new Error('No track URI provided for playback.') } - } - console.error('Failed to find and play selected track ', track_uri) - return - } - - // Stop directly, for user feedback - mopidy.tracklist.clear() - - // this is deprecated, remove when popuptracks is removed completly - $('#popupTracks').popup('close') - $('#controlspopup').popup('close') - // end of deprecated - - toast('Loading...') - - mopidy.tracklist.add({'uris': [playlist_uri]}).then(function (tlTracks) { - // Can fail for all sorts of reasons. If so, just add individually. - if (tlTracks.length === 0) { - var trackUris = getTracksFromUri(playlist_uri, false) - mopidy.tracklist.add({'uris': trackUris}).then(findAndPlayTrack) - } else { - findAndPlayTrack(tlTracks) - } - }) - return false -} - -/** ****************************************************** - * play an uri from the queue - *********************************************************/ - -/** * - * Plays a Track from a Playlist. - * @param uri - * @param tlid - * @returns {boolean} - */ -function playTrackQueueByTlid (uri, tlid) { - // stop directly, for user feedback - mopidy.playback.stop() - $('#popupQueue').popup('close') - toast('Loading...') - - tlid = parseInt(tlid) - mopidy.tracklist.filter({ - 'tlid': [tlid] - }).then( - function (tlTracks) { - if (tlTracks.length > 0) { - mopidy.playback.play({'tl_track': tlTracks[0]}) - return + playlistUri = playlistUri || $('#popupTracks').data('list') + if (typeof playlistUri === 'undefined') { + throw new Error('No playlist URI provided for playback.') } - console.log('Failed to play selected track ', tlid) - } - ) - return false -} -/** * - * @deprecated - * @returns {boolean} - */ -function playTrackQueue () { - uri = $('#popupQueue').data('track') - tlid = $('#popupQueue').data('tlid') - return playTrackQueueByTlid(uri, tlid) -} + action = controls.getAction(action) -/** ********************************* - * remove a track from the queue * - ***********************************/ -function removeTrack () { - $('#popupQueue').popup('close') - toast('Deleting...') + if (action === PLAY_ALL) { + mopidy.tracklist.clear() + } - tlid = parseInt($('#popupQueue').data('tlid')) - console.log(tlid) - mopidy.tracklist.remove({'tlid': [tlid]}) -} - -function clearQueue () { - mopidy.tracklist.clear().then( - resetSong() - ) - return false -} - -function savePressed (key) { - if (key === 13) { - saveQueue() - return false - } - return true -} - -function showSavePopup () { - mopidy.tracklist.getTracks().then(function (tracks) { - if (tracks.length > 0) { - $('#saveinput').val('') - $('#popupSave').popup('open') - } - }) -} - -function saveQueue () { - mopidy.tracklist.getTracks().then(function (tracks) { - var playlistName = $('#saveinput').val().trim() - if (playlistName !== null && playlistName !== '') { - getPlaylistByName(playlistName, 'm3u', false).then(function (exists) { - if (exists) { - $('#popupSave').popup('close') - $('#popupOverwrite').popup('open') - $('#overwriteConfirmBtn').click(function () { - initSave(playlistName, tracks) + var trackUris = controls._getTrackURIsForAction(action, trackUri, playlistUri) + // Add the tracks and start playback if necessary. + 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}) + }) + } + }) }) - } else { - initSave(playlistName, tracks) + break + case ADD_THIS_BOTTOM: + case ADD_ALL_BOTTOM: + case PLAY_ALL: + mopidy.tracklist.add({uris: trackUris}).then(function (tlTracks) { + if (action === PLAY_ALL) { // Start playback of selected track immediately. + mopidy.tracklist.filter({uri: [trackUri]}).then(function (tlTracks) { + mopidy.playback.stop().then(function () { + mopidy.playback.play({tlid: tlTracks[0].tlid}) + }) + }) + } + }) + break + default: + 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)) + } + } + }, + + /* Getter function for 'action' variable. Also checks config settings and cookies if required. */ + getAction: function (action) { + if (typeof action === 'undefined' || action.length === 0) { // Action parameter not provided, use defaults + action = window[$(document.body).data('on-track-click')] + } + if (action === DYNAMIC) { // Re-use last action stored in cookie. + action = $.cookie('onTrackClick') + if (typeof action === 'undefined') { + action = PLAY_ALL // Backwards-compatible default value. + } + } + return action + }, + + /* Retrieves the Font Awesome character for the given action. */ + getIconForAction: function (action) { + action = controls.getAction(action) + + switch (parseInt(action)) { + case PLAY_ALL: + return 'fa fa-fast-forward' + case PLAY_NOW: + return 'fa fa-play' + case PLAY_NEXT: + return 'fa fa-level-down' + case ADD_THIS_BOTTOM: + return 'fa fa-plus-square-o' + case ADD_ALL_BOTTOM: + return 'fa fa-plus-square' + default: + throw new Error('Unkown tracklist action identifier: ' + action) + } + }, + + /* Retrieves the relevant track URIs for the given action. */ + _getTrackURIsForAction: function (action, trackUri, playlistUri) { + var trackUris = [] + // Fill 'trackUris', by determining which tracks should be added. + switch (parseInt(action)) { + case PLAY_NOW: + case PLAY_NEXT: + case ADD_THIS_BOTTOM: + // Process single track + trackUris.push(trackUri) + break + case PLAY_ALL: + case ADD_ALL_BOTTOM: + // Process all tracks in playlist + trackUris = getTracksFromUri(playlistUri, false) + break + default: + throw new Error('Unexpected tracklist action identifier: ' + action) + } + return trackUris + }, + + /** ****************************************************** + * play an uri from the queue + *********************************************************/ + + /** * + * Plays a Track from a Playlist. + * @param tlid + * @returns {boolean} + */ + 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)}) + }, + + /** ********************************* + * remove a track from the queue * + ***********************************/ + removeTrack: function (tlid) { + $('#popupQueue').popup('close') + toast('Deleting...') + + tlid = tlid || $('#popupQueue').data('tlid') + mopidy.tracklist.remove({'tlid': [parseInt(tlid)]}) + }, + + clearQueue: function () { + mopidy.tracklist.clear().then( + resetSong() + ) + return false + }, + + savePressed: function (key) { + if (key === 13) { + controls.saveQueue() + return false + } + return true + }, + + showSavePopup: function () { + mopidy.tracklist.getTracks().then(function (tracks) { + if (tracks.length > 0) { + $('#saveinput').val('') + $('#popupSave').popup('open') } }) - } - }) - return false -} + }, -function initSave (playlistName, tracks) { - $('#popupOverwrite').popup('close') - $('#popupSave').popup('close') - $('#saveinput').val('') - toast('Saving...') - mopidy.playlists.create({'name': playlistName, 'uri_scheme': 'm3u'}).then(function (playlist) { - playlist.tracks = tracks - mopidy.playlists.save({'playlist': playlist}).then() - }) -} + saveQueue: function () { + mopidy.tracklist.getTracks().then(function (tracks) { + var playlistName = $('#saveinput').val().trim() + if (playlistName !== null && playlistName !== '') { + controls.getPlaylistByName(playlistName, 'm3u', false).then(function (exists) { + if (exists) { + $('#popupSave').popup('close') + $('#popupOverwrite').popup('open') + $('#overwriteConfirmBtn').click(function () { + controls.initSave(playlistName, tracks) + }) + } else { + controls.initSave(playlistName, tracks) + } + }) + } + }) + return false + }, -function refreshPlaylists () { - mopidy.playlists.refresh().then(function () { - playlists = {} - $('#playlisttracksdiv').hide() - $('#playlistslistdiv').show() - }) - return false -} + initSave: function (playlistName, tracks) { + $('#popupOverwrite').popup('close') + $('#popupSave').popup('close') + $('#saveinput').val('') + toast('Saving...') + mopidy.playlists.create({'name': playlistName, 'uri_scheme': 'm3u'}).then(function (playlist) { + playlist.tracks = tracks + mopidy.playlists.save({'playlist': playlist}).then() + }) + }, -/** *********** - * Buttons * - *************/ + refreshPlaylists: function () { + mopidy.playlists.refresh().then(function () { + playlists = {} + $('#playlisttracksdiv').hide() + $('#playlistslistdiv').show() + }) + return false + }, -function doShuffle () { - mopidy.playback.stop() - mopidy.tracklist.shuffle() - mopidy.playback.play() -} + /** *********** + * Buttons * + *************/ -/* Toggle state of play button */ -function setPlayState (nwplay) { - if (nwplay) { - $('#btplayNowPlaying >i').removeClass('fa-play').addClass('fa-pause') - $('#btplayNowPlaying').attr('title', 'Pause') - $('#btplay >i').removeClass('fa-play').addClass('fa-pause') - $('#btplay').attr('title', 'Pause') - mopidy.playback.getTimePosition().then(processCurrentposition, console.error) - syncedProgressTimer.start() - } else { - $('#btplayNowPlaying >i').removeClass('fa-pause').addClass('fa-play') - $('#btplayNowPlaying').attr('title', 'Play') - $('#btplay >i').removeClass('fa-pause').addClass('fa-play') - $('#btplay').attr('title', 'Play') - syncedProgressTimer.stop() - } - play = nwplay -} - -// play or pause -function doPlay () { - toast('Please wait...', 250) - if (!play) { - mopidy.playback.play() - } else { - if (isStreamUri(songdata.track.uri)) { + doShuffle: function () { mopidy.playback.stop() - } else { - mopidy.playback.pause() - } - } - setPlayState(!play) -} + mopidy.tracklist.shuffle() + mopidy.playback.play() + }, -function doPrevious () { - toast('Playing previous track...') - mopidy.playback.previous() -} - -function doNext () { - toast('Playing next track...') - mopidy.playback.next() -} - -function backbt () { - history.back() - return false -} - -/** *********** - * Options * - *************/ -function setTracklistOption (name, new_value) { - if (!new_value) { - $('#' + name + 'bt').attr('style', 'color:#2489ce') - } else { - $('#' + name + 'bt').attr('style', 'color:#66DD33') - } - return new_value -} - -function setRepeat (nwrepeat) { - if (repeat !== nwrepeat) { - repeat = setTracklistOption('repeat', nwrepeat) - } -} - -function setRandom (nwrandom) { - if (random !== nwrandom) { - random = setTracklistOption('random', nwrandom) - } -} - -function setConsume (nwconsume) { - if (consume !== nwconsume) { - consume = setTracklistOption('consume', nwconsume) - } -} - -function setSingle (nwsingle) { - if (single !== nwsingle) { - single = setTracklistOption('single', nwsingle) - } -} - -function doRandom () { - mopidy.tracklist.setRandom({'value': !random}).then() -} - -function doRepeat () { - mopidy.tracklist.setRepeat({'value': !repeat}).then() -} - -function doConsume () { - mopidy.tracklist.setConsume({'value': !consume}).then() -} - -function doSingle () { - mopidy.tracklist.setSingle({'value': !single}).then() -} - -/** ********************************************* - * Track Slider * - * Use a timer to prevent looping of commands * - ***********************************************/ -function doSeekPos (value) { - if (!positionChanging) { - positionChanging = value - mopidy.playback.seek({'time_position': Math.round(value)}).then(function () { - positionChanging = null - }) - } -} - -function setPosition (pos) { - if (!positionChanging && $('#trackslider').val() !== pos) { - syncedProgressTimer.set(pos) - } -} - -/** ********************************************* - * Volume slider * - * Use a timer to prevent looping of commands * - ***********************************************/ - -function setVolume (value) { - if (!volumeChanging && !volumeSliding && $('#volumeslider').val() !== value) { - $('#volumeslider').off('change') - $('#volumeslider').val(value).slider('refresh') - $('#volumeslider').on('change', function () { doVolume($(this).val()) }) - } -} - -function doVolume (value) { - if (!volumeChanging) { - volumeChanging = value - mopidy.playback.setVolume({'volume': parseInt(volumeChanging)}).then(function () { - volumeChanging = null - }) - } -} - -function setMute (nwmute) { - if (mute !== nwmute) { - mute = nwmute - if (mute) { - $('#mutebt').attr('class', 'fa fa-volume-off') - } else { - $('#mutebt').attr('class', 'fa fa-volume-up') - } - } -} - -function doMute () { - mopidy.mixer.setMute({'mute': !mute}) -} - -/** ********** - * Stream * - ************/ -function streamPressed (key) { - if (key === 13) { - playStreamUri() - return false - } - return true -} - -function playStreamUri (uri) { - // value of name is based on the passing of an uri as a parameter or not - var nwuri = uri || $('#streamuriinput').val().trim() - var service = $('#selectstreamservice').val() - if (!uri && service) { - nwuri = service + ':' + nwuri - } - if (isServiceUri(nwuri) || isStreamUri(nwuri) || validUri(nwuri)) { - toast('Playing...') - // stop directly, for user feedback - mopidy.playback.stop() - // hide ios/android keyboard - document.activeElement.blur() - clearQueue() - $('input').blur() - mopidy.tracklist.add({'uris': [nwuri]}) - mopidy.playback.play() - } else { - toast('No valid url!') - } - return false -} - -function getCurrentlyPlaying () { - $('#streamuriinput').val(songdata.track.uri) - var name = songdata.track.name - if (songdata.track.artists) { - var artistStr = artistsToString(songdata.track.artists) - if (artistStr) { - name = artistStr + ' - ' + name - } - } - $('#streamnameinput').val(name) - return true -} - -function getUriSchemes () { - uriSchemes = {} - return mopidy.getUriSchemes().then(function (schemes) { - for (var i = 0; i < schemes.length; i++) { - uriSchemes[schemes[i].toLowerCase()] = true - } - }) -} - -function getPlaylistByName (name, scheme, create) { - var uri_scheme = scheme || '' - var uri = '' - if (uri_scheme && !uriSchemes[uri_scheme]) { - return Mopidy.when(false) - } - return mopidy.playlists.asList().catch(console.error.bind(console)).then(function (plists) { - for (var i = 0; i < plists.length; i++) { - if ((plists[i].name === name) && (uri_scheme === '' || getScheme(plists[i].uri) === uri_scheme)) { - return plists[i] - } - } - if (create) { - return mopidy.playlists.create({'name': name, 'uri_scheme': uri_scheme}).done(function (plist) { - console.log("Created playlist '%s'", plist.name) - return plist - }) - } - console.log("Can't find playist '%s", name) - return Mopidy.when(false) - }) -} - -function getPlaylistFull (uri) { - return mopidy.playlists.lookup({'uri': uri}).then(function (pl) { - playlists[uri] = pl - return pl - }) -} - -function getFavourites () { - return getPlaylistByName(STREAMS_PLAYLIST_NAME, - STREAMS_PLAYLIST_SCHEME, - true).then(function (playlist) { - if (playlist) { - return getPlaylistFull(playlist.uri) - } - return Mopidy.when(false) - }) -} - -function addToFavourites (newTracks) { - getFavourites().catch(console.error.bind(console)).then(function (favourites) { - if (favourites) { - if (favourites.tracks) { - Array.prototype.push.apply(favourites.tracks, newTracks) + /* Toggle state of play button */ + setPlayState: function (nwplay) { + if (nwplay) { + $('#btplayNowPlaying >i').removeClass('fa-play').addClass('fa-pause') + $('#btplayNowPlaying').attr('title', 'Pause') + $('#btplay >i').removeClass('fa-play').addClass('fa-pause') + $('#btplay').attr('title', 'Pause') + mopidy.playback.getTimePosition().then(processCurrentposition, console.error) + syncedProgressTimer.start() } else { - favourites.tracks = newTracks + $('#btplayNowPlaying >i').removeClass('fa-pause').addClass('fa-play') + $('#btplayNowPlaying').attr('title', 'Play') + $('#btplay >i').removeClass('fa-pause').addClass('fa-play') + $('#btplay').attr('title', 'Play') + syncedProgressTimer.stop() } - mopidy.playlists.save({'playlist': favourites}).then(function (s) { - showFavourites() - }) - } - }) -} + play = nwplay + }, -function addFavourite (uri, name) { - uri = uri || $('#streamuriinput').val().trim() - name = name || $('#streamnameinput').val().trim() - mopidy.library.lookup({'uris': [uri]}).then(function (results) { - var newTracks = results[uri] - if (newTracks.length === 1) { - // TODO: Supporting adding an entire playlist? - if (name) { - newTracks[0].name = name // User overrides name. - } - addToFavourites(newTracks) - } else { - if (newTracks.length === 0) { - console.log('No tracks to add') + // play or pause + doPlay: function () { + toast('Please wait...', 250) + if (!play) { + mopidy.playback.play() } else { - console.log('Too many tracks (%d) to add', tracks.length) + if (isStreamUri(songdata.track.uri)) { + mopidy.playback.stop() + } else { + mopidy.playback.pause() + } } - } - }) -} + controls.setPlayState(!play) + }, -function deleteFavourite (index) { - getFavourites().then(function (favourites) { - if (favourites && favourites.tracks && index < favourites.tracks.length) { - var name = favourites.tracks[index].name - if (confirm("Are you sure you want to remove '" + name + "'?")) { - favourites.tracks.splice(index, 1) - mopidy.playlists.save({'playlist': favourites}).then(function (s) { - showFavourites() + doPrevious: function () { + toast('Playing previous track...') + mopidy.playback.previous() + }, + + doNext: function () { + toast('Playing next track...') + mopidy.playback.next() + }, + + backbt: function () { + history.back() + return false + }, + + /** *********** + * Options * + *************/ + setTracklistOption: function (name, new_value) { + if (!new_value) { + $('#' + name + 'bt').attr('style', 'color:#2489ce') + } else { + $('#' + name + 'bt').attr('style', 'color:#66DD33') + } + return new_value + }, + + setRepeat: function (nwrepeat) { + if (repeat !== nwrepeat) { + repeat = controls.setTracklistOption('repeat', nwrepeat) + } + }, + + setRandom: function (nwrandom) { + if (random !== nwrandom) { + random = controls.setTracklistOption('random', nwrandom) + } + }, + + setConsume: function (nwconsume) { + if (consume !== nwconsume) { + consume = controls.setTracklistOption('consume', nwconsume) + } + }, + + setSingle: function (nwsingle) { + if (single !== nwsingle) { + single = controls.setTracklistOption('single', nwsingle) + } + }, + + doRandom: function () { + mopidy.tracklist.setRandom({'value': !random}).then() + }, + + doRepeat: function () { + mopidy.tracklist.setRepeat({'value': !repeat}).then() + }, + + doConsume: function () { + mopidy.tracklist.setConsume({'value': !consume}).then() + }, + + doSingle: function () { + mopidy.tracklist.setSingle({'value': !single}).then() + }, + + /** ********************************************* + * Track Slider * + * Use a timer to prevent looping of commands * + ***********************************************/ + doSeekPos: function (value) { + if (!positionChanging) { + positionChanging = value + mopidy.playback.seek({'time_position': Math.round(value)}).then(function () { + positionChanging = null }) } - } - }) -} + }, -function showFavourites () { - $('#streamuristable').empty() - getFavourites().then(function (favourites) { - if (!favourites) { - return - } - var tmp = '' - - $.cookie.json = true - if ($.cookie('streamUris')) { - tmp = '' - } - if (favourites.tracks) { - var child = '' - for (var i = 0; i < favourites.tracks.length; i++) { - child = '
  •  ' + - '' + - ' ' - child += '

    ' + favourites.tracks[i].name + '

  • ' - tmp += child + setPosition: function (pos) { + if (!positionChanging && $('#trackslider').val() !== pos) { + syncedProgressTimer.set(pos) } - } - $('#streamuristable').html(tmp) - }) -} + }, -// TODO: Remove this upgrade path in next major release. -function upgradeStreamUrisToFavourites () { - toast('Converting streamUris...') - $.cookie.json = true - var streamUris = $.cookie('streamUris') // Read the cookie. - if (streamUris) { - var uris = [] // Prepare a list of uris to lookup. - for (var key in streamUris) { - var rs = streamUris[key] - if (rs) { - uris.push(rs[1]) + /** ********************************************* + * Volume slider * + * Use a timer to prevent looping of commands * + ***********************************************/ + + setVolume: function (value) { + if (!volumeChanging && !volumeSliding && $('#volumeslider').val() !== value) { + $('#volumeslider').off('change') + $('#volumeslider').val(value).slider('refresh') + $('#volumeslider').on('change', function () { controls.doVolume($(this).val()) }) } - } - mopidy.library.lookup({'uris': uris}).then(function (results) { - var tracks = [] // Prepare a list of tracks to add. - for (var key in streamUris) { - var rs = streamUris[key] - if (rs) { - var track = results[rs[1]][0] - if (track) { - track.name = rs[0] || track.name // Use custom name if provided. - tracks.push(track) - } else { - console.log('Skipping unplayable streamUri ' + rs[1]) - } + }, + + doVolume: function (value) { + if (!volumeChanging) { + volumeChanging = value + mopidy.playback.setVolume({'volume': parseInt(volumeChanging)}).then(function () { + volumeChanging = null + }) + } + }, + + setMute: function (nwmute) { + if (mute !== nwmute) { + mute = nwmute + if (mute) { + $('#mutebt').attr('class', 'fa fa-volume-off') + } else { + $('#mutebt').attr('class', 'fa fa-volume-up') } } - addToFavourites(tracks) - $.cookie('streamUris', null) // Delete the cookie now we're done. - console.log(tracks.length + ' streamUris added to favourites') - }) - } else { - console.log('No streamUris cookie found') + }, + + doMute: function () { + mopidy.mixer.setMute({'mute': !mute}) + }, + + /** ********** + * Stream * + ************/ + streamPressed: function (key) { + if (key === 13) { + controls.playStreamUri() + return false + } + return true + }, + + playStreamUri: function (uri) { + // value of name is based on the passing of an uri as a parameter or not + var nwuri = uri || $('#streamuriinput').val().trim() + var service = $('#selectstreamservice').val() + if (!uri && service) { + nwuri = service + ':' + nwuri + } + if (isServiceUri(nwuri) || isStreamUri(nwuri) || validUri(nwuri)) { + toast('Playing...') + // stop directly, for user feedback + mopidy.playback.stop() + // hide ios/android keyboard + document.activeElement.blur() + controls.clearQueue() + $('input').blur() + mopidy.tracklist.add({'uris': [nwuri]}) + mopidy.playback.play() + } else { + toast('No valid url!') + } + return false + }, + + getCurrentlyPlaying: function () { + $('#streamuriinput').val(songdata.track.uri) + var name = songdata.track.name + if (songdata.track.artists) { + var artistStr = artistsToString(songdata.track.artists) + if (artistStr) { + name = artistStr + ' - ' + name + } + } + $('#streamnameinput').val(name) + return true + }, + + getUriSchemes: function () { + uriSchemes = {} + return mopidy.getUriSchemes().then(function (schemes) { + for (var i = 0; i < schemes.length; i++) { + uriSchemes[schemes[i].toLowerCase()] = true + } + }) + }, + + getPlaylistByName: function (name, scheme, create) { + var uri_scheme = scheme || '' + var uri = '' + if (uri_scheme && !uriSchemes[uri_scheme]) { + return Mopidy.when(false) + } + return mopidy.playlists.asList().catch(console.error.bind(console)).then(function (plists) { + for (var i = 0; i < plists.length; i++) { + if ((plists[i].name === name) && (uri_scheme === '' || getScheme(plists[i].uri) === uri_scheme)) { + return plists[i] + } + } + if (create) { + return mopidy.playlists.create({'name': name, 'uri_scheme': uri_scheme}).done(function (plist) { + console.log("Created playlist '%s'", plist.name) + return plist + }) + } + console.log("Can't find playist '%s", name) + return Mopidy.when(false) + }) + }, + + getPlaylistFull: function (uri) { + return mopidy.playlists.lookup({'uri': uri}).then(function (pl) { + playlists[uri] = pl + return pl + }) + }, + + getFavourites: function () { + return controls.getPlaylistByName(STREAMS_PLAYLIST_NAME, + STREAMS_PLAYLIST_SCHEME, + true).then(function (playlist) { + if (playlist) { + return controls.getPlaylistFull(playlist.uri) + } + return Mopidy.when(false) + }) + }, + + addToFavourites: function (newTracks) { + controls.getFavourites().catch(console.error.bind(console)).then(function (favourites) { + if (favourites) { + if (favourites.tracks) { + Array.prototype.push.apply(favourites.tracks, newTracks) + } else { + favourites.tracks = newTracks + } + mopidy.playlists.save({'playlist': favourites}).then(function (s) { + controls.showFavourites() + }) + } + }) + }, + + addFavourite: function (uri, name) { + uri = uri || $('#streamuriinput').val().trim() + name = name || $('#streamnameinput').val().trim() + mopidy.library.lookup({'uris': [uri]}).then(function (results) { + var newTracks = results[uri] + if (newTracks.length === 1) { + // TODO: Supporting adding an entire playlist? + if (name) { + newTracks[0].name = name // User overrides name. + } + controls.addToFavourites(newTracks) + } else { + if (newTracks.length === 0) { + console.log('No tracks to add') + } else { + console.log('Too many tracks (%d) to add', tracks.length) + } + } + }) + }, + + deleteFavourite: function (index) { + controls.getFavourites().then(function (favourites) { + if (favourites && favourites.tracks && index < favourites.tracks.length) { + var name = favourites.tracks[index].name + if (confirm("Are you sure you want to remove '" + name + "'?")) { + favourites.tracks.splice(index, 1) + mopidy.playlists.save({'playlist': favourites}).then(function (s) { + controls.showFavourites() + }) + } + } + }) + }, + + showFavourites: function () { + $('#streamuristable').empty() + controls.getFavourites().then(function (favourites) { + if (!favourites) { + return + } + var tmp = '' + + $.cookie.json = true + if ($.cookie('streamUris')) { + tmp = '' + } + if (favourites.tracks) { + var child = '' + for (var i = 0; i < favourites.tracks.length; i++) { + child = '
  •  ' + + '' + + ' ' + child += '

    ' + favourites.tracks[i].name + '

  • ' + tmp += child + } + } + $('#streamuristable').html(tmp) + }) + }, + + // TODO: Remove this upgrade path in next major release. + upgradeStreamUrisToFavourites: function () { + toast('Converting streamUris...') + $.cookie.json = true + var streamUris = $.cookie('streamUris') // Read the cookie. + if (streamUris) { + var uris = [] // Prepare a list of uris to lookup. + for (var key in streamUris) { + var rs = streamUris[key] + if (rs) { + uris.push(rs[1]) + } + } + mopidy.library.lookup({'uris': uris}).then(function (results) { + var tracks = [] // Prepare a list of tracks to add. + for (var key in streamUris) { + var rs = streamUris[key] + if (rs) { + var track = results[rs[1]][0] + if (track) { + track.name = rs[0] || track.name // Use custom name if provided. + tracks.push(track) + } else { + console.log('Skipping unplayable streamUri ' + rs[1]) + } + } + } + controls.addToFavourites(tracks) + $.cookie('streamUris', null) // Delete the cookie now we're done. + console.log(tracks.length + ' streamUris added to favourites') + }) + } else { + console.log('No streamUris cookie found') + } + }, + + haltSystem: function () { + $.post('/settings/shutdown') + toast('Stopping system...', 10000) + setTimeout(function () { + window.history.back() + }, 10000) + }, + + rebootSystem: function () { + $.post('/settings/reboot') + toast('Rebooting...', 10000) + setTimeout(function () { + window.history.back() + }, 10000) + } + } -} - -function haltSystem () { - $.post('/settings/shutdown') - toast('Stopping system...', 10000) - setTimeout(function () { - window.history.back() - }, 10000) -} - -function rebootSystem () { - $.post('/settings/reboot') - toast('Rebooting...', 10000) - setTimeout(function () { - window.history.back() - }, 10000) -} + return controls +})) diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index ea4ff05..83bb401 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -20,7 +20,7 @@ var volumeSliding = false var positionChanging var initgui = true -var popupData = {} +var popupData = {} // TODO: Refactor into one shared cache var songlength = 0 var artistshtml = '' @@ -36,12 +36,11 @@ var STREAMS_PLAYLIST_SCHEME = 'm3u' var uriSchemes = {} // array of cached playlists (not only user-playlists, also search, artist, album-playlists) -var playlists = {} +var playlists = {} // TODO: Refactor into one shared cache var currentplaylist -var customTracklists = [] +var customTracklists = [] // TODO: Refactor into one shared cache var browseStack = [] -var browseTracks = [] var ua = navigator.userAgent var isMobileSafari = /Mac/.test(ua) && /Mobile/.test(ua) @@ -68,9 +67,7 @@ PLAY_NEXT = 1 ADD_THIS_BOTTOM = 2 ADD_ALL_BOTTOM = 3 PLAY_ALL = 4 -PLAY_NOW_SEARCH = 5 - -MAX_TABLEROWS = 50 +DYNAMIC = 5 // the first part of Mopidy extensions which serve radio streams var radioExtensionsList = ['somafm', 'tunein', 'dirble', 'audioaddict'] @@ -185,9 +182,9 @@ function albumTracksToTable (pl, target, uri) { nextTrack = i < pl.length - 1 ? pl[i + 1] : undefined track = pl[i] popupData[track.uri] = track - renderSongLi(previousTrack, track, nextTrack, uri, '', ALBUM_TABLE, i, pl.length) + renderSongLi(previousTrack, track, nextTrack, uri, '', target, i, pl.length) } - updatePlayIcons(songdata.track.uri, songdata.tlid) + updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction()) } function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, currentIndex, listLength) { @@ -204,16 +201,14 @@ function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, curre $(target).append('
  • ' + track.name + ' [Stream]

  • ') return } - // Play by tlid if available. - // TODO: Need to consolidate all of the 'play...' functions - if (tlid && target === BROWSE_TABLE) { - onClick = 'return playBrowsedTracks(PLAY_ALL, ' + tlid + ');' - } else if (tlid) { + + if (target === CURRENT_PLAYLIST_TABLE && typeof tlid === 'number' && tlid >= 0) { // Current queue: Show popup menu icon. onClick plays track. tlidParameter = '\',\'' + tlid - onClick = 'return playTrackQueueByTlid(\'' + track.uri + '\',\'' + tlid + '\');' - } else { - onClick = 'return playTrackByUri(\'' + track.uri + '\',\'' + uri + '\');' + onClick = 'return controls.playQueueTrack(' + tlid + ');' + } else { // All other tracklist: Show default action icon. onClick performs default action + onClick = 'return controls.playTracks(\'\', mopidy, \'' + track.uri + '\', \'' + uri + '\');' } + $(target).append( '
  • ' + '' + @@ -223,8 +218,8 @@ function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, curre if (listLength === 1 || !hasSameAlbum(previousTrack, track) && !hasSameAlbum(track, nextTrack)) { renderSongLiAlbumInfo(track, target) } - // TODO: remove this hard-coded condition for 'ALBUM_TABLE' - if (target !== ALBUM_TABLE && !hasSameAlbum(previousTrack, track)) { + // TODO: remove this hard-coded conditions for 'ALBUM_TABLE' and 'BROWSE_TABLE' + if (target !== ALBUM_TABLE && target !== BROWSE_TABLE && !hasSameAlbum(previousTrack, track)) { // Starting to render a new album in the list. renderSongLiDivider(track, nextTrack, currentIndex, target) } @@ -339,7 +334,7 @@ function resultsToTables (results, target, uri, onClickBack, backIsOptional) { renderSongLi(previousTrack, track, nextTrack, uri, tlid, target, i, results.length) } } - updatePlayIcons(songdata.track.uri, songdata.tlid) + updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction()) } // process updated playlist to gui @@ -397,7 +392,7 @@ function getUris (tracks) { function getTracksFromUri (uri, full_track_data) { var returnTracksOrUris = function (tracks) { - return (full_track_data || false) ? tracks : getUris(tracks) + return full_track_data ? tracks : getUris(tracks) } if (customTracklists[uri]) { return returnTracksOrUris(customTracklists[uri]) @@ -552,7 +547,7 @@ function getjQueryID (identifier, uri, includePrefix) { } else if (identifier.charAt(0) !== '#' && includePrefix) { identifier = '#' + identifier } - return identifier + '-' + fixedEncodeURIComponent(uri).replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '') + return identifier + '-' + fixedEncodeURIComponent(uri).replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '') // eslint-disable-line no-useless-escape } // Strict URI encoding as per https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index abe94d5..41f1dca 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -7,8 +7,8 @@ * Song Info Sreen * ********************/ function resetSong () { - setPlayState(false) - setPosition(0) + controls.setPlayState(false) + controls.setPosition(0) var data = {} data.tlid = -1 data.track = {} @@ -84,7 +84,7 @@ function setSongInfo (data) { data.track.name = decodeURI(name[name.length - 1]) } - updatePlayIcons(data.track.uri, data.tlid) + updatePlayIcons(data.track.uri, data.tlid, controls.getIconForAction()) artistshtml = '' artiststext = '' @@ -131,7 +131,7 @@ function setSongInfo (data) { } else { $('#modalalbum').html('') } - images.setAlbumImage(data.track.uri, '#infocover, #controlspopupimage', mopidy) + images.setAlbumImage(data.track.uri, '#infocover, #albumCoverImg', mopidy) $('#modalartist').html(arttmp) @@ -153,7 +153,6 @@ function closePopups () { $('#artistpopup').popup('close') $('#coverpopup').popup('close') $('#popupQueue').popup('close') - $('#controlspopup').popup('close') } function popupTracks (e, listuri, trackuri, tlid) { @@ -163,6 +162,9 @@ function popupTracks (e, listuri, trackuri, tlid) { $('.popupTrackName').html(popupData[trackuri].name) if (popupData[trackuri].album && popupData[trackuri].album.name) { $('.popupAlbumName').html(popupData[trackuri].album.name) + $('.popupAlbumLi').show() + } else { + $('.popupAlbumLi').hide() } var child = '' @@ -194,9 +196,6 @@ function popupTracks (e, listuri, trackuri, tlid) { if (divid === 'current') { $('.addqueue').hide() popupName = '#popupQueue' - } else if (divid === 'browse') { - $('.addqueue').show() - popupName = '#popupBrowse' } else { $('.addqueue').show() popupName = '#popupTracks' @@ -232,8 +231,8 @@ function initSocketevents () { library.getCurrentPlaylist() updateStatusOfAll() library.getPlaylists() - getUriSchemes().then(function () { - showFavourites() + controls.getUriSchemes().then(function () { + controls.showFavourites() }) library.getBrowseDir() library.getSearchSchemes(searchBlacklist, mopidy) @@ -250,7 +249,7 @@ function initSocketevents () { mopidy.on('event:trackPlaybackStarted', function (data) { setSongInfo(data.tl_track) - setPlayState(true) + controls.setPlayState(true) }) mopidy.on('event:playlistsLoaded', function (data) { @@ -273,21 +272,21 @@ function initSocketevents () { }) mopidy.on('event:volumeChanged', function (data) { - setVolume(data.volume) + controls.setVolume(data.volume) }) mopidy.on('event:muteChanged', function (data) { - setMute(data.mute) + controls.setMute(data.mute) }) mopidy.on('event:playbackStateChanged', function (data) { switch (data.new_state) { case 'paused': case 'stopped': - setPlayState(false) + controls.setPlayState(false) break case 'playing': - setPlayState(true) + controls.setPlayState(true) break } }) @@ -297,7 +296,7 @@ function initSocketevents () { }) mopidy.on('event:seeked', function (data) { - setPosition(parseInt(data.time_position)) + controls.setPosition(parseInt(data.time_position)) if (play) { syncedProgressTimer.start() } @@ -420,9 +419,7 @@ function locationHashChanged () { case 'search': $('#navsearch a').addClass($.mobile.activeBtnClass) $('#searchinput').focus() - if (customTracklists['mbw:allresultscache'] === '') { - library.initSearch($('#searchinput').val()) - } + library.initSearch($('#searchinput').val()) break case 'stream': $('#navstream a').addClass('ui-state-active ui-state-persist ui-btn-active') @@ -519,7 +516,7 @@ $(document).ready(function (event) { $('#songinfo').click(function () { return switchContent('nowPlaying') }) - $('#controlspopupimage').click(function () { + $('#albumCoverImg').click(function () { return switchContent('current') }) $('#navToggleFullscreen').click(function () { @@ -559,15 +556,15 @@ $(document).ready(function (event) { var actualkey = String.fromCharCode(unicode) switch (actualkey) { case ' ': - doPlay() + controls.doPlay() event.preventDefault() break case '>': - doNext() + controls.doNext() event.preventDefault() break case '<': - doPrevious() + controls.doPrevious() event.preventDefault() break } @@ -586,8 +583,8 @@ $(document).ready(function (event) { $.event.special.swipe.durationThreshold = 500 // swipe songinfo and panel - $('#normalFooter, #nowPlayingFooter').on('swiperight', doPrevious) - $('#normalFooter, #nowPlayingFooter').on('swipeleft', doNext) + $('#normalFooter, #nowPlayingFooter').on('swiperight', controls.doPrevious) + $('#normalFooter, #nowPlayingFooter').on('swipeleft', controls.doNext) $('#nowPlayingpane, .ui-body-c, #header, #panel, .pane').on('swiperight', function (event) { if (!$(event.target).is('#normalFooter') && !$(event.target).is('#nowPlayingFooter')) { $('#panel').panel('open') @@ -609,37 +606,55 @@ $(document).ready(function (event) { $('#trackslider').on('slidestop', function () { $('#trackslider').off('change') syncedProgressTimer.updatePosition($(this).val()) - doSeekPos($(this).val()) + controls.doSeekPos($(this).val()) }) $('#volumeslider').on('slidestart', function () { volumeSliding = true }) $('#volumeslider').on('slidestop', function () { volumeSliding = false }) - $('#volumeslider').on('change', function () { doVolume($(this).val()) }) + $('#volumeslider').on('change', function () { controls.doVolume($(this).val()) }) }) -function updatePlayIcons (uri, tlid) { +function updatePlayIcons (uri, tlid, popupMenuIcon) { // Update styles of listviews + if (arguments.length < 3) { + throw new Error('Missing parameters for "updatePlayIcons" function call.') + } var listviews = [PLAYLIST_TABLE, SEARCH_TRACK_TABLE, ARTIST_TABLE, ALBUM_TABLE, BROWSE_TABLE] var target = CURRENT_PLAYLIST_TABLE.substr(1) - $(CURRENT_PLAYLIST_TABLE).children('li').each(function () { - var eachTlid = $(this).attr('tlid') - if (typeof eachTlid !== 'undefined') { - eachTlid = parseInt(eachTlid) - } - if (this.id === getjQueryID(target, uri) && eachTlid === tlid) { - $(this).addClass('currenttrack') - } else { - $(this).removeClass('currenttrack') - } - }) + if (uri && typeof tlid === 'number' && tlid >= 0) { + $(CURRENT_PLAYLIST_TABLE).children('li.song.albumli').each(function () { + var eachTlid = $(this).attr('tlid') + if (typeof eachTlid !== 'undefined') { + eachTlid = parseInt(eachTlid) + } + if (this.id === getjQueryID(target, uri) && eachTlid === tlid) { + if (!$(this).hasClass('currenttrack')) { + $(this).addClass('currenttrack') + } + } else if ($(this).hasClass('currenttrack')) { + $(this).removeClass('currenttrack') + } + }) + } + + var popupElement for (var i = 0; i < listviews.length; i++) { target = listviews[i].substr(1) - $(listviews[i]).children('li').each(function () { - if (this.id === getjQueryID(target, uri)) { - $(this).addClass('currenttrack2') - } else { - $(this).removeClass('currenttrack2') + $(listviews[i]).children('li.song.albumli').each(function () { + if (uri) { + if (this.id === getjQueryID(target, uri)) { + $(this).addClass('currenttrack2') + } else { + $(this).removeClass('currenttrack2') + } + } + if (popupMenuIcon) { + popupElement = $(this).children('a.moreBtn').children('i').first() + if (!popupElement.hasClass(popupMenuIcon)) { + popupElement.removeClass() + popupElement.addClass(popupMenuIcon) + } } }) } diff --git a/mopidy_musicbox_webclient/static/js/library.js b/mopidy_musicbox_webclient/static/js/library.js index dad0606..13e63e8 100644 --- a/mopidy_musicbox_webclient/static/js/library.js +++ b/mopidy_musicbox_webclient/static/js/library.js @@ -37,9 +37,6 @@ document.activeElement.blur() $('input').blur() - delete customTracklists[URI_SCHEME + ':allresultscache'] - delete customTracklists[URI_SCHEME + ':artistresultscache'] - delete customTracklists[URI_SCHEME + ':albumresultscache'] delete customTracklists[URI_SCHEME + ':trackresultscache'] $('#searchartists').hide() $('#searchalbums').hide() @@ -258,7 +255,7 @@ resultsToTables(tracks, PLAYLIST_TABLE, uri, 'return library.togglePlaylists();', true) showLoading(false) }) - updatePlayIcons(uri) + updatePlayIcons(uri, '', controls.getIconForAction()) $('#playlistslist li a').each(function () { $(this).removeClass('playlistactive') if (this.id === uri) { diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index 0b752c2..b6464d4 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -16,49 +16,49 @@ function processCurrenttrack (data) { * process results of volume *********************************************************/ function processVolume (data) { - setVolume(data) + controls.setVolume(data) } /** ****************************************************** * process results of mute *********************************************************/ function processMute (data) { - setMute(data) + controls.setMute(data) } /** ****************************************************** * process results of a repeat *********************************************************/ function processRepeat (data) { - setRepeat(data) + controls.setRepeat(data) } /** ****************************************************** * process results of random *********************************************************/ function processRandom (data) { - setRandom(data) + controls.setRandom(data) } /** ****************************************************** * process results of consume *********************************************************/ function processConsume (data) { - setConsume(data) + controls.setConsume(data) } /** ****************************************************** * process results of single *********************************************************/ function processSingle (data) { - setSingle(data) + controls.setSingle(data) } /** ****************************************************** * process results of current position *********************************************************/ function processCurrentposition (data) { - setPosition(parseInt(data)) + controls.setPosition(parseInt(data)) } /** ****************************************************** @@ -66,9 +66,9 @@ function processCurrentposition (data) { *********************************************************/ function processPlaystate (data) { if (data === 'playing') { - setPlayState(true) + controls.setPlayState(true) } else { - setPlayState(false) + controls.setPlayState(false) } } @@ -84,26 +84,24 @@ function processBrowseDir (resultArr) { showLoading(false) return } - browseTracks = [] uris = [] - var ref, track, previousTrack, nextTrack + var ref, previousRef, nextRef var uri = resultArr[0].uri var length = 0 || resultArr.length + customTracklists[BROWSE_TABLE] = [] for (var 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 ref = resultArr[i] + // TODO: consolidate usage of various arrays for caching URIs, Refs, and Tracks popupData[ref.uri] = ref - browseTracks.push(ref) + customTracklists[BROWSE_TABLE].push(ref) uris.push(ref.uri) - $(BROWSE_TABLE).append( - '
  • ' + - '' + - '' + - '' + - '

    ' + ref.name + '

  • ' - ) + renderSongLi(previousRef, ref, nextRef, BROWSE_TABLE, '', BROWSE_TABLE, index, resultArr.length) + index++ } else { var iconClass = '' @@ -119,7 +117,7 @@ function processBrowseDir (resultArr) { } } - updatePlayIcons(songdata.track.uri, songdata.tlid) + updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction()) if (uris.length > 0) { mopidy.library.lookup({'uris': uris}).then(function (resultDict) { @@ -134,6 +132,7 @@ function processBrowseDir (resultArr) { nextTrack = undefined } track = resultDict[ref.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) } @@ -211,7 +210,7 @@ function processCurrentPlaylist (resultArr) { currentplaylist = resultArr resultsToTables(currentplaylist, CURRENT_PLAYLIST_TABLE) mopidy.playback.getCurrentTlTrack().then(processCurrenttrack, console.error) - updatePlayIcons(songdata.track.uri, songdata.tlid) + updatePlayIcons(songdata.track.uri, songdata.tlid, controls.getIconForAction()) } /** ****************************************************** diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache index 7b69bc6..6e558db 100644 --- a/mopidy_musicbox_webclient/static/mb.appcache +++ b/mopidy_musicbox_webclient/static/mb.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 2016-04-12:v1 +# 2016-04-26:v2 NETWORK: * diff --git a/mopidy_musicbox_webclient/static/system.html b/mopidy_musicbox_webclient/static/system.html index a252a6e..1c4a929 100644 --- a/mopidy_musicbox_webclient/static/system.html +++ b/mopidy_musicbox_webclient/static/system.html @@ -23,8 +23,8 @@
    diff --git a/mopidy_musicbox_webclient/web.py b/mopidy_musicbox_webclient/web.py index e562fde..3ee687b 100644 --- a/mopidy_musicbox_webclient/web.py +++ b/mopidy_musicbox_webclient/web.py @@ -37,6 +37,7 @@ class IndexHandler(tornado.web.RequestHandler): '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() } self.__path = path self.__title = string.Template('MusicBox on $hostname') diff --git a/mopidy_musicbox_webclient/webclient.py b/mopidy_musicbox_webclient/webclient.py index 6db7903..e4c6e79 100644 --- a/mopidy_musicbox_webclient/webclient.py +++ b/mopidy_musicbox_webclient/webclient.py @@ -44,3 +44,6 @@ class Webclient(object): def is_music_box(self): return self.ext_config.get('musicbox', False) + + def get_default_click_action(self): + return self.ext_config.get('on_track_click', 'PLAY_ALL') diff --git a/tests/js/dummy_tracklist.js b/tests/js/dummy_tracklist.js new file mode 100644 index 0000000..0401dba --- /dev/null +++ b/tests/js/dummy_tracklist.js @@ -0,0 +1,108 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory) + } else if (typeof module === 'object' && module.exports) { + module.exports = factory() + } else { + root.DummyTracklist = factory() + } +}(this, function () { + 'use strict' + + /* A dummy tracklist with partial support for mocking mopidy.core.TracklistController. + * + * Returns resolved promises to simulate functionality of Mopidy.js. + */ + function DummyTracklist () { + if (!(this instanceof DummyTracklist)) { + return new DummyTracklist() + } + this._tlTracks = [] + this._nextTlid = 1 + return this + } + + /* Add tracks to the tracklist. params.uris should contain an array of strings for the URIs to be added. */ + DummyTracklist.prototype.add = function (params) { + if (!params || !params.uris) { + throw new Error('No tracks provided to add.') + } + if (params.tracks || params.uri) { + throw new Error('DummyTracklist.add does not support deprecated "tracks" and "uri" parameters.') + } + + // Add tracks to end of tracklist if no position is provided + params.at_position = params.at_position || this._tlTracks.length + var tlTrack + var tlTracks = [] + for (var i = 0; i < params.uris.length; i++) { + tlTrack = { + tlid: this._nextTlid++, + track: { + uri: params.uris[i] + } + } + tlTracks.push(tlTrack) + this._tlTracks.splice(params.at_position + i, 0, tlTrack) + } + + return $.when(tlTracks) + } + + /* Clears the tracklist */ + DummyTracklist.prototype.clear = function () { + this._tlTracks = [] + } + + /** + * Retuns a list containing tlTracks that contain the provided + * criteria.uri or has ID criteria.tlid. + * + */ + DummyTracklist.prototype.filter = function (criteria) { + if (!criteria || (!criteria.uri && !criteria.tlid)) { + throw new Error('No URI or tracklist ID provided to filter on.') + } + + var matches = [] + if (criteria.uri) { // Look for matching URIs + for (var i = 0; i < criteria.uri.length; i++) { + for (var j = 0; j < this._tlTracks.length; j++) { + if (this._tlTracks[j].track.uri === criteria.uri[i]) { + matches.push(this._tlTracks[j]) + } + } + } + } + if (criteria.tlid) { // Look for matching tracklist IDs + for (i = 0; i < criteria.tlid.length; i++) { + for (j = 0; j < this._tlTracks.length; j++) { + if (this._tlTracks[j].tlid === criteria.tlid[i]) { + matches.push(this._tlTracks[j]) + } + } + } + } + return $.when(matches) + } + + /* 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 { + return $.when(0) + } + } + for (var i = 0; i < this._tlTracks.length; i++) { + if (this._tlTracks[i].tlid === params.tlid || (params.tl_track && params.tl_track.tlid === this._tlTracks[i].tlid)) { + return $.when(i) + } + } + return $.when(0) + } + + return DummyTracklist +})) diff --git a/tests/js/test_controls.js b/tests/js/test_controls.js new file mode 100644 index 0000000..6bc5b6f --- /dev/null +++ b/tests/js/test_controls.js @@ -0,0 +1,265 @@ +var chai = require('chai') +var expect = chai.expect +var assert = chai.assert +chai.use(require('chai-string')) +chai.use(require('chai-jquery')) + +var sinon = require('sinon') + +var controls = require('../../mopidy_musicbox_webclient/static/js/controls.js') +var DummyTracklist = require('./dummy_tracklist.js') + +describe('controls', function () { + var mopidy + var div_element + var QUEUE_TRACKS = [ // Simulate an existing queue with three tracks loaded. + {uri: 'track:tlTrackMock1'}, + {uri: 'track:tlTrackMock2'}, + {uri: 'track:tlTrackMock3'} + ] + var NEW_TRACKS = [ // Simulate the user browsing to a folder with three tracks inside it. + {uri: 'track:trackMock1'}, + {uri: 'tunein:track:trackMock2'}, // Stream + {uri: 'track:trackMock3'} + ] + var addSpy + + before(function () { + $(document.body).append('
    ') + $('#popupTracks').popup() // Initialize popup + $(document.body).data('on-track-click', 'PLAY_ALL') // Set default click action + + mopidy = sinon.stub(new Mopidy({callingConvention: 'by-position-or-by-name'})) + + var playback = { + play: sinon.stub(), + stop: sinon.stub() + + } + mopidy.playback = playback + mopidy.playback.stop.returns($.when()) + // Mock the Mopidy tracklist so that we have a predictable state to test against. + mopidy.tracklist = new DummyTracklist() + addSpy = sinon.spy(mopidy.tracklist, 'add') + clearSpy = sinon.spy(mopidy.tracklist, 'clear') + }) + + beforeEach(function () { + mopidy.tracklist.clear() + clearSpy.reset() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + }) + + afterEach(function () { + mopidy.playback.play.reset() + addSpy.reset() + }) + + after(function () { + mopidy.tracklist.add.restore() + mopidy.tracklist.clear.restore() + }) + + 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) + 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) + 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) + 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) + 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) + 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 + + 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() + + 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() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(ADD_THIS_BOTTOM, mopidy, NEW_TRACKS[0].uri) + assert(addSpy.calledWithMatch({uris: [NEW_TRACKS[0].uri]}), 'ADD_THIS_BOTTOM did not add correct track') + }) + + it('PLAY_ALL and ADD_ALL_BOTTOM should add all tracks to tracklist', function () { + controls.playTracks(PLAY_ALL, mopidy, NEW_TRACKS[0].uri) + assert(addSpy.calledWithMatch({uris: getUris(NEW_TRACKS)}), 'PLAY_ALL did not add correct tracks') + addSpy.reset() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(ADD_ALL_BOTTOM, mopidy, NEW_TRACKS[0].uri) + 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 () { + 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') + 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') + }) + + 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') + mopidy.playback.play.reset() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(PLAY_NEXT, mopidy, NEW_TRACKS[0].uri) + assert.isFalse(mopidy.playback.play.called, 'PLAY_NEXT should not have triggered playback to start') + mopidy.playback.play.reset() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(ADD_THIS_BOTTOM, mopidy, NEW_TRACKS[0].uri) + assert.isFalse(mopidy.playback.play.called, 'ADD_THIS_BOTTOM should not have triggered playback to start') + mopidy.playback.play.reset() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(PLAY_ALL, mopidy, NEW_TRACKS[2].uri) + assert(mopidy.playback.play.calledWithMatch({tlid: mopidy.tracklist._tlTracks[2].tlid}), 'PLAY_ALL did not start playback of correct track') + mopidy.playback.play.reset() + + mopidy.tracklist.clear() + mopidy.tracklist.add({uris: getUris(QUEUE_TRACKS)}) + + controls.playTracks(ADD_ALL_BOTTOM, mopidy, NEW_TRACKS[0].uri) + assert.isFalse(mopidy.playback.play.called, 'ADD_ALL_BOTTOM should not have triggered playback to start') + mopidy.playback.play.reset() + }) + + 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) + assert(cookieStub.calledWithMatch('onTrackClick', PLAY_NOW, {expires: 365})) + cookieStub.reset() + + $(document.body).data('on-track-click', 'PLAY_NOW') + controls.playTracks(PLAY_NOW, mopidy, 2) + assert(cookieStub.notCalled) + cookieStub.restore() + }) + }) + + describe('#getAction()', function () { + it('should use default action if none is specified', function () { + window.MOCK_DEFAULT = 99 // Define global variable to test against. + $(document.body).data('on-track-click', 'MOCK_DEFAULT') + assert.equal(controls.getAction(), 99) + }) + + it('should get action from cookie if action is set to "DYNAMIC"', function () { + $(document.body).data('on-track-click', 'DYNAMIC') + var cookieStub = sinon.stub($, 'cookie') + controls.getAction() + assert(cookieStub.called) + cookieStub.restore() + }) + + it('should default to "PLAY_ALL" if no cookie is available for "DYNAMIC"', function () { + $(document.body).data('on-track-click', 'DYNAMIC') + $.removeCookie('onTrackClick') + assert.equal(controls.getAction(), PLAY_ALL) + }) + }) + + describe('#getIconForAction()', function () { + it('should return correct FontAwesome class for each tracklist action', function () { + assert.equal(controls.getIconForAction(PLAY_ALL), 'fa fa-fast-forward') + assert.equal(controls.getIconForAction(PLAY_NOW), 'fa fa-play') + assert.equal(controls.getIconForAction(PLAY_NEXT), 'fa fa-level-down') + assert.equal(controls.getIconForAction(ADD_THIS_BOTTOM), 'fa fa-plus-square-o') + assert.equal(controls.getIconForAction(ADD_ALL_BOTTOM), 'fa fa-plus-square') + }) + + it('should raise error if unknown tracklist action is provided', function () { + assert.throw(function () { controls.getIconForAction(99) }, Error) + }) + + it('should handle action identifier strings in addition to integers', function () { + assert.equal(controls.getIconForAction('0'), 'fa fa-play') + }) + + it('should use default tracklist action if no parameter is provided', function () { + assert.equal(controls.getIconForAction(), 'fa fa-fast-forward') + }) + }) + + describe('#_getTrackURIsForAction()', function () { + it('should return just "trackUri" for PLAY_NOW, PLAY_NEXT, and ADD_THIS_BOTTOM', function () { + assert.equal(controls._getTrackURIsForAction(PLAY_NOW, 'mockUri')[0], 'mockUri') + assert.equal(controls._getTrackURIsForAction(PLAY_NEXT, 'mockUri')[0], 'mockUri') + assert.equal(controls._getTrackURIsForAction(ADD_THIS_BOTTOM, 'mockUri')[0], 'mockUri') + }) + + it('should get tracks from "playlistUri" for PLAY_ALL, and ADD_ALL_BOTTOM', function () { + customTracklists[BROWSE_TABLE] = NEW_TRACKS + + var tracks = controls._getTrackURIsForAction(PLAY_ALL, NEW_TRACKS[0], BROWSE_TABLE) + assert.equal(tracks.length, NEW_TRACKS.length) + for (var i = 0; i < tracks.length; i++) { + assert.equal(tracks[i], NEW_TRACKS[i].uri) + } + }) + + it('should raise error if unknown tracklist action is provided', function () { + assert.throw(function () { controls._getTrackURIsForAction(99) }, Error) + }) + + it('should handle action identifier strings in addition to integers', function () { + assert.equal(controls._getTrackURIsForAction('0', 'mockUri')[0], 'mockUri') + }) + }) +}) diff --git a/tests/js/test_images.js b/tests/js/test_images.js index 11af74c..68004b9 100644 --- a/tests/js/test_images.js +++ b/tests/js/test_images.js @@ -243,6 +243,9 @@ describe('images', function () { getImagesSpy.reset() setDeprecatedAlbumImageSpy.reset() }) + after(function () { + mopidy.library.getImages.restore() + }) it('should use default image if no track URI is provided', function () { images.setAlbumImage('', img_element, mopidy) @@ -302,7 +305,7 @@ describe('images', function () { assert.isTrue(lookupSpy.calledOnce) expect($(img_element).prop('src')).to.endWith('mockAlbumImageUri') - lookupSpy.restore() + mopidy.library.lookup.restore() }) it('should use default image if track.album or track.artist is not available', function () { @@ -313,7 +316,7 @@ describe('images', function () { assert.isTrue(lookupSpy.calledOnce) expect($(img_element).prop('src')).to.endWith(images.DEFAULT_ALBUM_URL) - lookupSpy.restore() + mopidy.library.lookup.restore() }) it('should fall back to retrieving image from last.fm if none provided by Mopidy', function () { @@ -414,7 +417,7 @@ describe('images', function () { getImagesSpy.reset() }) after(function () { - getImagesSpy.restore() + mopidy.library.getImages.restore() }) it('should use default image if no artist URI is provided', function () { diff --git a/tests/js/test_synced_timer.js b/tests/js/test_synced_timer.js index d921eb2..3e296d8 100644 --- a/tests/js/test_synced_timer.js +++ b/tests/js/test_synced_timer.js @@ -223,7 +223,7 @@ describe('SyncedTimer', function () { clock.tick(1001) syncedProgressTimer._scheduleSync(1000) assert(clearSpy.calledWith(scheduleID)) - clearSpy.restore() + window.clearTimeout.restore() }) }) @@ -358,7 +358,7 @@ describe('SyncedTimer', function () { assert(scheduleSpy.calledWith(0)) syncedProgressTimer.stop() - scheduleSpy.restore() + syncedProgressTimer._scheduleSync.restore() }) }) @@ -392,7 +392,7 @@ describe('SyncedTimer', function () { assert.isFalse(syncedProgressTimer._isSyncScheduled) assert(cancelSpy.calledWith(syncedProgressTimer._scheduleID)) - cancelSpy.restore() + window.clearTimeout.restore() }) }) @@ -422,7 +422,7 @@ describe('SyncedTimer', function () { assert.isTrue(formatSpy.called) expect(syncedProgressTimer.positionNode.nodeValue).to.equal('0:01') - formatSpy.restore() + SyncedProgressTimer.format.restore() }) it('should set position to "" if timer has not been initialized', function () { diff --git a/tests/test_extension.py b/tests/test_extension.py index cfaa39d..5ce0d43 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -18,6 +18,7 @@ class ExtensionTests(unittest.TestCase): assert 'enabled = true' in config assert 'websocket_host =' in config assert 'websocket_port =' in config + assert 'on_track_click = PLAY_ALL' in config def test_get_config_schema(self): ext = Extension() @@ -27,6 +28,7 @@ class ExtensionTests(unittest.TestCase): assert 'musicbox' in schema assert 'websocket_host' in schema assert 'websocket_port' in schema + assert 'on_track_click' in schema def test_setup(self): registry = mock.Mock() diff --git a/tests/test_webclient.py b/tests/test_webclient.py index 1ac2657..e15acaa 100644 --- a/tests/test_webclient.py +++ b/tests/test_webclient.py @@ -76,3 +76,6 @@ class WebclientTests(unittest.TestCase): def test_is_musicbox(self): assert not self.mmw.is_music_box() + + def test_default_click_action(self): + assert self.mmw.get_default_click_action() == 'PLAY_ALL'