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 @@
-
-
-
-
-
@@ -343,9 +337,6 @@
Playlists
@@ -477,7 +469,6 @@
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:
*