diff --git a/README.rst b/README.rst index 423ea19e..bc148472 100644 --- a/README.rst +++ b/README.rst @@ -74,10 +74,12 @@ v2.3.0 (UNRELEASED) ------------------- - Enhance build workflow to include style checks and syntax validation for HTML, CSS, and Javascript. +- Now displays album and artist info when browsing tracks. (Addresses: `#99 `_). **Fixes** - Don't create Mopidy models manually. (Fixes: `#172 `_). +- Context menu is now available for all tracks in browse pane. (Fixes: `#126 `_). v2.2.0 (2016-03-01) ------------------- diff --git a/mopidy_musicbox_webclient/static/css/webclient.css b/mopidy_musicbox_webclient/static/css/webclient.css index 68ece165..7a2ed059 100644 --- a/mopidy_musicbox_webclient/static/css/webclient.css +++ b/mopidy_musicbox_webclient/static/css/webclient.css @@ -36,7 +36,7 @@ margin-left: 10px; } - #playlisttracksback { + .backnav-optional { display: none; } @@ -74,7 +74,7 @@ width: 100%; } - #playlisttracksback { + .backnav-optional { display: block; } @@ -293,13 +293,17 @@ margin-left: 20px; } -.song .moreBtn{ +.song .moreBtn { float: right; padding: 15px 18px 12px 22px; display: inline-block; line-height: 100%; } +.backnav { + background-color: #ccc !important; +} + /********************** * Now Playing area * @@ -369,12 +373,14 @@ } #popupTracksLv li, -#popupQueueLv li { +#popupQueueLv li, +#popupBrowseLv li { border-bottom: 1px solid #aaa; } #popupTracksLv, -#popupQueueLv { +#popupQueueLv, +#popupBrowseLv li { border: 1px solid #aaa; } diff --git a/mopidy_musicbox_webclient/static/index.html b/mopidy_musicbox_webclient/static/index.html index 22a91585..02e92db9 100644 --- a/mopidy_musicbox_webclient/static/index.html +++ b/mopidy_musicbox_webclient/static/index.html @@ -56,7 +56,6 @@ -

Save current queue to a playlist. @@ -224,7 +221,6 @@

Artists

-

Overwrite existing playlist with same name? @@ -307,12 +303,10 @@

System

-
Album cover -

-

@@ -343,9 +337,6 @@

Playlists

    -
    - Back -
      @@ -356,8 +347,9 @@

      Playlists

      Browse

      -
      -
        +
        +
          +
          @@ -385,7 +377,7 @@

          Play Queue

          -
          +
            @@ -477,7 +469,6 @@

            Streams

            -
            @@ -492,6 +483,7 @@

            Streams

            +
            @@ -507,11 +499,9 @@

            Streams

            -
            - diff --git a/mopidy_musicbox_webclient/static/js/functionsvars.js b/mopidy_musicbox_webclient/static/js/functionsvars.js index cad2ac42..8f31eaf6 100644 --- a/mopidy_musicbox_webclient/static/js/functionsvars.js +++ b/mopidy_musicbox_webclient/static/js/functionsvars.js @@ -52,6 +52,7 @@ var isWebkit = /WebKit/.test(ua) PROGRAM_NAME = 'MusicBox' ARTIST_TABLE = '#artiststable' ALBUM_TABLE = '#albumstable' +BROWSE_TABLE = '#browsetable' PLAYLIST_TABLE = '#playlisttracks' CURRENT_PLAYLIST_TABLE = '#currenttable' SEARCH_ALL_TABLE = '#allresulttable' @@ -162,54 +163,69 @@ function artistsToString (artists, max) { * break up results and put them in album tables *********************************************************/ function albumTracksToTable (pl, target, uri) { - var tmp = '
              ' - var liId = '' - var targetmin = target.substr(1) + var track, previousTrack, nextTrack $(target).empty() + $(target).attr('data', uri) for (var i = 0; i < pl.length; i++) { - popupData[pl[i].uri] = pl[i] - liID = targetmin + '-' + pl[i].uri - tmp += renderSongLi(pl[i], liID, uri) + previousTrack = track || undefined + 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) } - tmp += '
            ' - $(target).html(tmp) - $(target).attr('data', uri) + updatePlayIcons(songdata.track.uri, songdata.tlid) } -function renderSongLi (song, liID, uri, tlid, renderAlbumInfo) { - var name, iconClass - var tlidString = '' +function renderSongLi (previousTrack, track, nextTrack, uri, tlid, target, currentIndex, listLength) { + var name var tlidParameter = '' var onClick = '' - // Determine if the song line item will be rendered as part of an album. - if (!song.album || !song.album.name) { - iconClass = getMediaClass(song.uri) - } else { - iconClass = 'trackname' + var targetmin = target.substr(1) + track.name = validateTrackName(track, currentIndex) + // Leave out unplayable items + if (track.name.substring(0, 12) === '[unplayable]') { + return + } + // Streams + if (track.length === -1) { + $(target).append('
          • ' + track.name + ' [Stream]

          • ') + return } // Play by tlid if available. - if (tlid) { - tlidString = '" tlid="' + tlid + // TODO: Need to consolidate all of the 'play...' functions + if (tlid && target === BROWSE_TABLE) { + onClick = 'return playBrowsedTracks(PLAY_ALL, ' + tlid + ');' + } else if (tlid) { tlidParameter = '\',\'' + tlid - onClick = 'return playTrackQueueByTlid(\'' + song.uri + '\',\'' + tlid + '\');' + onClick = 'return playTrackQueueByTlid(\'' + track.uri + '\',\'' + tlid + '\');' } else { - onClick = 'return playTrackByUri(\'' + song.uri + '\',\'' + uri + '\');' + onClick = 'return playTrackByUri(\'' + track.uri + '\',\'' + uri + '\');' } - songLi = '
          • ' + - '' + - '' + - '' + - '

            ' + song.name + '

            ' - if (renderAlbumInfo) { - songLi += '

            ' - songLi += renderSongLiTrackArtists(song) - if (song.album && song.album.name) { - songLi += ' - ' - songLi += '' + song.album.name + '

            ' - } + $(target).append( + '
          • ' + + '' + + '' + + '

            ' + track.name + '

          • ' + ) + 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)) { + // Starting to render a new album in the list. + renderSongLiDivider(track, nextTrack, currentIndex, target) } - songLi += '' - return songLi +} + +function renderSongLiAlbumInfo (track, target) { + var html = '

            ' + html += renderSongLiTrackArtists(track) + if (track.album && track.album.name) { + html += ' - ' + track.album.name + '

            ' + } + target = getjQueryID(target.substr(1) + '-', track.uri, true) + $(target).children('a').eq(1).append(html) + $(target + ' a h1 i').addClass(getMediaClass(track.uri)) } function renderSongLiTrackArtists (track) { @@ -228,15 +244,49 @@ function renderSongLiTrackArtists (track) { return html } -function isNewAlbumSection (track, previousTrack) { - // 'true' if album name is either not defined or has changed from the previous track. - return !track.album || !track.album.name || !previousTrack || !previousTrack.album || !previousTrack.album.name || - track.album.name !== previousTrack.album.name +function renderSongLiDivider (track, nextTrack, currentIndex, target) { + targetmin = target.substr(1) + target = getjQueryID(targetmin + '-', track.uri, true) + // Render differently if part of an album + if (hasSameAlbum(track, nextTrack)) { + // Large divider with album cover + $(target).before( + '
          • ' + + '' + + '

            ' + track.album.name + '

            ' + + renderSongLiTrackArtists(track) + '

          • ' + ) + // Retrieve album covers + getCover(track.uri, getjQueryID(targetmin + '-cover-', track.uri, true), 'small') + } else if (currentIndex > 0) { + // Small divider + $(target).before('
          •  
          • ') + } } -function isMultiTrackAlbum (track, nextTrack) { - // 'true' if there are more tracks of the same album after this one. - return nextTrack.album && nextTrack.album.name && track.album && track.album.name && track.album.name === nextTrack.album.name +function renderSongLiBackButton (results, target, onClick, optional) { + if (onClick && onClick.length > 0) { + if (!results || results.length === 0) { + $(target).empty() + $(target).append( + '
          • No tracks found...

          • ' + ) + } + var opt = '' + if (optional) { + opt = ' backnav-optional' + } + $(target).prepend( + '
          • Back

          • ' + ) + } +} + +function hasSameAlbum (track1, track2) { + // 'true' if album for each track exists and has the same name + var name1 = track1 ? (track1.album ? track1.album.name : undefined) : undefined + var name2 = track2 ? (track2.album ? track2.album.name : undefined) : undefined + return name1 && name2 && (name1 === name2) } function validateTrackName (track, trackNumber) { @@ -251,86 +301,33 @@ function validateTrackName (track, trackNumber) { return name } -function resultsToTables (results, target, uri) { - if (!results) { +function resultsToTables (results, target, uri, onClickBack, backIsOptional) { + $(target).empty() + renderSongLiBackButton(results, target, onClickBack, backIsOptional) + if (!results || results.length === 0) { return } - $(target).html('') $(target).attr('data', uri) var track, previousTrack, nextTrack, tlid - var albumTrackSeen = 0 - var renderAlbumInfo = true - var liID = '' - // Keep a list of track URIs for retrieving of covers - var coversList = [] - - var html = '' - var tableid, artistname, name, iconClass - var targetmin = target.substr(1) - var length = 0 || results.length // Break into albums and put in tables - for (i = 0; i < length; i++) { - previousTrack = track + for (i = 0; i < results.length; i++) { + previousTrack = track || undefined + nextTrack = i < results.length - 1 ? results[i + 1] : undefined track = results[i] - tlid = '' - if (i < length - 1) { - nextTrack = results[i + 1] - } - if ('tlid' in results[i]) { - // Get track information from TlTrack instance - track = results[i].track - tlid = results[i].tlid - if (i < length - 1) { - nextTrack = results[i + 1].track - } - } - track.name = validateTrackName(track, i) - // Leave out unplayable items - if (track.name.substring(0, 12) === '[unplayable]') { - continue - } - // Streams - if (track.length === -1) { - html += '
          • ' + track.name + ' [Stream]

          • ' - continue - } - - if (isNewAlbumSection(track, previousTrack)) { - // Starting to render a new album in the list. - tableid = 'art' + i - // Render differently if part of an album - if (i < length - 1 && isMultiTrackAlbum(track, nextTrack)) { - // Large divider with album cover - renderAlbumInfo = false - html += '
          • ' - html += '' + - '' + - '

            ' + track.album.name + '

            ' - html += renderSongLiTrackArtists(track) - html += '

          • ' - coversList.push([track.uri, i]) - } else { - renderAlbumInfo = true - if (i > 0) { - // Small divider - html += '
          •  
          • ' - } + if (track) { + if ('tlid' in track) { + // Get track information from TlTrack instance + tlid = track.tlid + track = track.track + nextTrack = nextTrack ? nextTrack.track : undefined } - albumTrackSeen = 0 + popupData[track.uri] = track + renderSongLi(previousTrack, track, nextTrack, uri, tlid, target, i, results.length) } - popupData[track.uri] = track - liID = targetmin + '-' + track.uri - html += renderSongLi(track, liID, uri, tlid, renderAlbumInfo) - albumTrackSeen += 1 - } - tableid = '#' + tableid - $(target).html(html) - // Retrieve album covers - for (i = 0; i < coversList.length; i++) { - getCover(coversList[i][0], target + '-cover-' + coversList[i][1], 'small') } + updatePlayIcons(songdata.track.uri, songdata.tlid) } // process updated playlist to gui @@ -519,3 +516,32 @@ function isSpotifyStarredPlaylist (playlist) { var starredRegex = /spotify:user:.*:starred/g return (starredRegex.test(playlist.uri) && playlist.name === 'Starred') } + +/** + * Converts a URI to a jQuery-safe identifier. jQuery identifiers need to be + * unique per page and cannot contain special characters. + * + * @param {string} identifier - Identifier string to prefix to the URI. Can + * be used to ensure that the generated ID will be unique for the page that + * it will be included on. Can be any string (e.g. ID of parent element). + * + * @param {string} uri - URI to encode, usually the URI of a Mopidy track. + * + * @param {boolean} includePrefix - Will prefix the generated identifier + * with the '#' character if set to 'true', ready to be passed to $() or + * jQuery(). + * + * @return {string} - a string in the format '[#]identifier-encodedURI' that + * is safe to use as a jQuery identifier. + */ +function getjQueryID (identifier, uri, includePrefix) { + var prefix = includePrefix ? '#' : '' + return prefix + identifier + fixedEncodeURIComponent(uri).replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '') +} + +// Strict URI encoding as per https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent +function fixedEncodeURIComponent (str) { + return encodeURIComponent(str).replace(/[!'()*]/g, function (c) { + return '%' + c.charCodeAt(0).toString(16) + }) +} diff --git a/mopidy_musicbox_webclient/static/js/gui.js b/mopidy_musicbox_webclient/static/js/gui.js index dba42413..a622a31f 100644 --- a/mopidy_musicbox_webclient/static/js/gui.js +++ b/mopidy_musicbox_webclient/static/js/gui.js @@ -413,7 +413,6 @@ function locationHashChanged () { break case 'current': $('#navcurrent a').addClass('ui-state-active ui-state-persist ui-btn-active') - getCurrentPlaylist() break case 'playlists': $('#navplaylists a').addClass('ui-state-active ui-state-persist ui-btn-active') @@ -622,55 +621,29 @@ $(document).ready(function (event) { }) function updatePlayIcons (uri, tlid) { - // update styles of listviews - $('#currenttable li').each(function () { + // Update styles of listviews + 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 === 'currenttable-' + uri && eachTlid === tlid) { + if (this.id === getjQueryID(target + '-', uri) && eachTlid === tlid) { $(this).addClass('currenttrack') } else { $(this).removeClass('currenttrack') } }) - $('#playlisttracks li').each(function () { - if (this.id === 'playlisttracks-' + uri) { - $(this).addClass('currenttrack2') - } else { - $(this).removeClass('currenttrack2') - } - }) - - $('#trackresulttable li').each(function () { - if (this.id === 'trackresulttable-' + uri) { - $(this).addClass('currenttrack2') - } else { - $(this).removeClass('currenttrack2') - } - }) - - $('#artiststable li').each(function () { - if (this.id === 'artiststable-' + uri) { - $(this).addClass('currenttrack2') - } else { - $(this).removeClass('currenttrack2') - } - }) - - $('#albumstable li').each(function () { - if (this.id === 'albumstable-' + uri) { - $(this).addClass('currenttrack2') - } else { - $(this).removeClass('currenttrack2') - } - }) - $('#browselist li').each(function () { - if (this.id === 'browselisttracks-' + uri) { - $(this).addClass('currenttrack2') - } else { - $(this).removeClass('currenttrack2') - } - }) + 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') + } + }) + } } diff --git a/mopidy_musicbox_webclient/static/js/images.js b/mopidy_musicbox_webclient/static/js/images.js index d8c66277..5c16f181 100644 --- a/mopidy_musicbox_webclient/static/js/images.js +++ b/mopidy_musicbox_webclient/static/js/images.js @@ -77,7 +77,7 @@ function getCoverFromLastFm (track, images, size) { } } } - }) + }, $(images).attr('src', defUrl)) } function getArtistImage (nwartist, image, size) { @@ -88,5 +88,5 @@ function getArtistImage (nwartist, image, size) { $(image).attr('src', data.artist.image[i]['#text'] || defUrl) } } - }}) + }}, $(images).attr('src', defUrl)) } diff --git a/mopidy_musicbox_webclient/static/js/library.js b/mopidy_musicbox_webclient/static/js/library.js index 9cba4333..5047c5db 100644 --- a/mopidy_musicbox_webclient/static/js/library.js +++ b/mopidy_musicbox_webclient/static/js/library.js @@ -106,6 +106,10 @@ function processSearchResults (resultArr) { customTracklists[URI_SCHEME + ':trackresultscache'] = results.tracks if (emptyResult) { + $('#searchtracks').show() + $(SEARCH_TRACK_TABLE).append( + '
          • No tracks found...

          • ' + ) toast('No results') showLoading(false) return false @@ -193,8 +197,6 @@ function processSearchResults (resultArr) { // Inject list items, refresh listview and hide superfluous items. $(SEARCH_ALBUM_TABLE).html(child).listview('refresh').find('.overflow').hide() - $('#expandsearch').show() - // Track results resultsToTables(results.tracks, SEARCH_TRACK_TABLE, URI_SCHEME + ':trackresultscache') @@ -250,12 +252,13 @@ function togglePlaylists () { } function showTracklist (uri) { + showLoading(true) $(PLAYLIST_TABLE).empty() togglePlaylists() var tracks = getPlaylistTracks(uri).then(function (tracks) { - resultsToTables(tracks, PLAYLIST_TABLE, uri) + resultsToTables(tracks, PLAYLIST_TABLE, uri, 'return togglePlaylists();', true) + showLoading(false) }) - showLoading(false) updatePlayIcons(uri) $('#playlistslist li a').each(function () { $(this).removeClass('playlistactive') diff --git a/mopidy_musicbox_webclient/static/js/process_ws.js b/mopidy_musicbox_webclient/static/js/process_ws.js index c1279c34..1c6ed37c 100644 --- a/mopidy_musicbox_webclient/static/js/process_ws.js +++ b/mopidy_musicbox_webclient/static/js/process_ws.js @@ -76,78 +76,78 @@ function processPlaystate (data) { * process results of a browse list *********************************************************/ function processBrowseDir (resultArr) { - var backHtml = '
          • Back

          • ' - if ((!resultArr) || (resultArr === '') || (resultArr.length === 0)) { - $('#browsepath').html('No tracks found...') - $('#browselist').html(backHtml) + $(BROWSE_TABLE).empty() + if (browseStack.length > 0) { + renderSongLiBackButton(resultArr, BROWSE_TABLE, 'return getBrowseDir();') + } + if (!resultArr || resultArr.length === 0) { showLoading(false) return } - - $('#browselist').empty() - - var child = '' - var rooturi = '' + browseTracks = [] + uris = [] + var ref, track, previousTrack, nextTrack var uri = resultArr[0].uri + var length = 0 || resultArr.length - // check root uri - // find last : or / (spltting the result) - // do it twice, since. - var colonindex = uri.lastIndexOf(':') - var slashindex = uri.lastIndexOf('/') - - var lastindex = (colonindex > slashindex) ? colonindex : slashindex - rooturi = uri.slice(0, lastindex) - if (resultArr[0].type === 'track') { - rooturi = rooturi.replace(':track:', ':directory:') - } - colonindex = rooturi.lastIndexOf(':') - slashindex = rooturi.lastIndexOf('/') - - lastindex = (colonindex > slashindex) ? colonindex : slashindex - rooturi = rooturi.slice(0, lastindex) - - if (browseStack.length > 0) { - child += backHtml - } - - browseTracks = [] for (var i = 0, index = 0; i < resultArr.length; i++) { - iconClass = getMediaClass(resultArr[i].uri) if (resultArr[i].type === 'track') { - // console.log(resultArr[i]); - mopidy.library.lookup({'uris': [resultArr[i].uri]}).then(function (resultDict) { - var lookupUri = Object.keys(resultDict)[0] - popupData[lookupUri] = resultDict[lookupUri][0] - browseTracks.push(resultDict[lookupUri][0]) - }, console.error) - child += '
          • ' + - '' + - '' + - '

            ' + resultArr[i].name + '

          • ' + ref = resultArr[i] + popupData[ref.uri] = ref + browseTracks.push(ref) + uris.push(ref.uri) + + $(BROWSE_TABLE).append( + '
          • ' + + '' + + '' + + '' + + '

            ' + ref.name + '

          • ' + ) index++ } else { + var iconClass = '' if (browseStack.length > 0) { iconClass = 'fa fa-folder-o' + } else { + iconClass = getMediaClass(resultArr[i].uri) } - child += '
          • ' + resultArr[i].name + '

          • ' + $(BROWSE_TABLE).append( + '
          • ' + + '

            ' + resultArr[i].name + '

          • ' + ) } } - $('#browselist').html(child) - if (browseStack.length > 0) { - child = getMediaHuman(uri) - iconClass = getMediaClass(uri) - $('#browsepath').html(' ' + child) - } else { - $('#browsepath').html('') - } - updatePlayIcons(songdata.track.uri, songdata.tlid) - showLoading(false) + if (uris.length > 0) { + mopidy.library.lookup({'uris': uris}).then(function (resultDict) { + // Break into albums and put in tables + var track, previousTrack, nextTrack, uri + $.each(resultArr, function (i, ref) { + if (ref.type === 'track') { + previousTrack = track || undefined + if (i < resultArr.length - 1 && resultDict[resultArr[i + 1].uri]) { + nextTrack = resultDict[resultArr[i + 1].uri][0] + } else { + nextTrack = undefined + } + track = resultDict[ref.uri][0] + if (uris.length === 1 || (previousTrack && !hasSameAlbum(previousTrack, track) && !hasSameAlbum(track, nextTrack))) { + renderSongLiAlbumInfo(track, BROWSE_TABLE) + } + if (!hasSameAlbum(previousTrack, track)) { + // Starting to render a new album in the list. + renderSongLiDivider(track, nextTrack, i, BROWSE_TABLE) + } + } + }) + showLoading(false) + }, console.error) + } else { + showLoading(false) + } } /** ****************************************************** diff --git a/mopidy_musicbox_webclient/static/mb.appcache b/mopidy_musicbox_webclient/static/mb.appcache index 8501ba9d..5844c18b 100644 --- a/mopidy_musicbox_webclient/static/mb.appcache +++ b/mopidy_musicbox_webclient/static/mb.appcache @@ -1,6 +1,6 @@ CACHE MANIFEST -# 2016-03-06:v2 +# 2016-03-14:v1 NETWORK: *