(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' var controls = { /** * '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 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. * 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. * @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, index) { toast('Updating queue...') 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') || $('#popupQueue').data('list') if (typeof playlistUri === 'undefined') { throw new Error('No playlist URI provided for playback.') } action = controls.getAction(action) if (action === PLAY_ALL) { mopidy.tracklist.clear() } var trackUris = controls._getTrackURIsForAction(action, trackUri, playlistUri) // Add the tracks and start playback if necessary. switch (action) { case PLAY_NOW: case PLAY_NEXT: mopidy.tracklist.index().then(function (currentIndex) { if (currentIndex === null && action === PLAY_NEXT) { // Tracklist is empty, start playing new tracks immediately. action = PLAY_NOW } controls._addTrackAtIndex(action, mopidy, trackUris, currentIndex) }) break case INSERT_AT_INDEX: controls._addTrackAtIndex(action, mopidy, trackUris, index) break case ADD_THIS_BOTTOM: case ADD_ALL_BOTTOM: case PLAY_ALL: 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 (action !== INSERT_AT_INDEX) { // TODO: Add support for 'INSERT_AT_INDEX' to allow user to insert tracks in any playlist. if (window[$(document.body).data('on-track-click')] === DYNAMIC) { // Save last 'action' - will become default for future 'onClick' events var previousAction = $.cookie('onTrackClick') if (typeof previousAction === 'undefined' || action !== previousAction) { $.cookie('onTrackClick', action, { expires: 365 }) updatePlayIcons('', '', controls.getIconForAction(action)) } } } $('#popupTracks').popup('close') $('#popupQueue').popup('close') }, /* Getter function for 'action' variable. Also checks config settings and cookies if required. */ 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-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: 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 INSERT_AT_INDEX: 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 }, _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 *********************************************************/ /** * * Plays a Track from a Playlist. * @param tlid * @returns {boolean} */ playQueueTrack: function (tlid) { // Stop directly, for user feedback mopidy.playback.stop() 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, mopidy) { toast('Deleting...') tlid = tlid || $('#popupQueue').data('tlid') mopidy.tracklist.remove({'tlid': [parseInt(tlid)]}) $('#popupQueue').popup('close') }, clearQueue: function () { mopidy.tracklist.clear() return false }, checkDefaultButtonClick: function (key, parentElement) { // Click the default button on parentElement when the user presses the enter key. if (key === 13) { $(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('') } if (typeof songdata.track.uri !== 'undefined' && songdata.track.uri !== '') { $('#getPlayingBtn').button('enable') } else { $('#getPlayingBtn').button('disable') } $('#select-add').append('') // PLAY_NEXT $('#select-add').append('') // 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) { $('#saveinput').val('') $('#popupSave').popup('open') } }) }, 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 }, 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() }) }, showInfoPopup: function (uri, popupId, mopidy) { showLoading(true) var trackUri = uri || $(popupId).data('track') $(popupId).popup('close') $('#popupShowInfo tbody').empty() mopidy.library.lookup({'uris': [trackUri]}).then(function (resultDict) { var uri = Object.keys(resultDict)[0] var track = resultDict[uri][0] var html = '' var rowTemplate = '{label}:{text}' var row = {'label': '', 'text': ''} row.label = 'Name' if (track.name) { row.text = track.name } else { row.text = '(Not available)' } html += stringFromTemplate(rowTemplate, row) row.label = 'Album' if (track.album && track.album.name) { row.text = track.album.name } else { row.text = '(Not available)' } html += stringFromTemplate(rowTemplate, row) var artists = artistsToString(track.artists) // Fallback to album artists. if (artists.length === 0 && track.album && track.album.artists) { artists = artistsToString(track.album.artists) } if (artists.length > 0) { if (track.artists && track.artists.length > 1 || track.album && track.album.artists && track.album.artists.length > 1) { row.label = 'Artists' } else { row.label = 'Artist' } row.text = artists html += stringFromTemplate(rowTemplate, row) } var composers = artistsToString(track.composers) if (composers.length > 0) { if (track.composers.length > 1) { row.label = 'Composers' } else { row.label = 'Composer' } row.text = composers html += stringFromTemplate(rowTemplate, row) } var performers = artistsToString(track.performers) if (performers.length > 0) { if (track.performers.length > 1) { row.label = 'Performers' } else { row.label = 'Performer' } row.text = performers html += stringFromTemplate(rowTemplate, row) } if (track.genre) { row = {'label': 'Genre', 'text': track.genre} html += stringFromTemplate(rowTemplate, row) } if (track.track_no) { row = {'label': 'Track #', 'text': track.track_no} html += stringFromTemplate(rowTemplate, row) } if (track.disc_no) { row = {'label': 'Disc #', 'text': track.disc_no} html += stringFromTemplate(rowTemplate, row) } if (track.date) { row = {'label': 'Date', 'text': new Date(track.date)} html += stringFromTemplate(rowTemplate, row) } if (track.length) { row = {'label': 'Length', 'text': timeFromSeconds(track.length / 1000)} html += stringFromTemplate(rowTemplate, row) } if (track.bitrate) { row = {'label': 'Bitrate', 'text': track.bitrate} html += stringFromTemplate(rowTemplate, row) } if (track.comment) { row = {'label': 'Comment', 'text': track.comment} html += stringFromTemplate(rowTemplate, row) } if (track.musicbrainz_id) { row = {'label': 'MusicBrainz ID', 'text': track.musicbrainz_id} html += stringFromTemplate(rowTemplate, row) } if (track.last_modified) { row = {'label': 'Modified', 'text': track.last_modified} html += stringFromTemplate(rowTemplate, row) } rowTemplate = '{label}:' row = {'label': 'URI', 'text': uri} html += stringFromTemplate(rowTemplate, row) $('#popupShowInfo tbody').append(html) showLoading(false) $('#popupShowInfo').popup('open') if (!isMobile) { // Set focus and select URI text on desktop systems (don't want the keyboard to pop up automatically on mobile devices) $('#popupShowInfo #uri-input').focus() $('#popupShowInfo #uri-input').select() } }, console.error) return false }, refreshPlaylists: function () { mopidy.playlists.refresh().then(function () { playlists = {} $('#playlisttracksdiv').hide() $('#playlistslistdiv').show() }) return false }, refreshLibrary: function () { var uri = $('#refreshLibraryBtn').data('url') mopidy.library.refresh({'uri': uri}).then(function () { library.getBrowseDir(uri) }) return false }, /** *********** * Buttons * *************/ doShuffle: function () { mopidy.playback.stop() mopidy.tracklist.shuffle() mopidy.playback.play() }, /* 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 { $('#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 doPlay: function () { toast('Please wait...', 250) if (!play) { mopidy.playback.play() } else { if (isStreamUri(songdata.track.uri)) { mopidy.playback.stop() } else { mopidy.playback.pause() } } controls.setPlayState(!play) }, 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 }) } }, setPosition: function (pos) { if (!positionChanging && $('#trackslider').val() !== pos) { syncedProgressTimer.set(pos) } }, /** ********************************************* * 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()) }) } }, 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') } } }, 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 (uriInput, nameInput) { $('#' + uriInput).val(songdata.track.uri) var name = songdata.track.name if (songdata.track.artists) { var artistStr = artistsToString(songdata.track.artists) if (artistStr) { name = artistStr + ' - ' + name } } $('#' + nameInput).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) } } }) }, showDeleteStreamPopup: function (index) { controls.getFavourites().then(function (favourites) { if (favourites && favourites.tracks && index < favourites.tracks.length) { var name = favourites.tracks[index].name $('.popupStreamName').html(favourites.tracks[index].name) $('#popupConfirmDelete').data('index', index) $('#popupConfirmDelete').popup('open') } }) }, deleteFavourite: function (index) { index = index || $('#popupConfirmDelete').data('index') controls.getFavourites().then(function (favourites) { if (favourites && favourites.tracks && index < favourites.tracks.length) { favourites.tracks.splice(index, 1) mopidy.playlists.save({'playlist': favourites}).then(function (s) { controls.showFavourites() }) $('#popupConfirmDelete').popup('close') } }) }, 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 } } else { tmp = 'Your saved favourite streams/tracks will be shown here' } $('#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) } } return controls }))