diff --git a/src/assets/css/flexstyles.css b/src/assets/css/flexstyles.css index a5a479f2f5..429ed7a650 100644 --- a/src/assets/css/flexstyles.css +++ b/src/assets/css/flexstyles.css @@ -30,6 +30,10 @@ align-items: flex-start; } +.align-items-flex-end { + align-items: flex-end; +} + .justify-content-center { justify-content: center; } @@ -38,6 +42,10 @@ justify-content: flex-end; } +.justify-content-space-between { + justify-content: space-between; +} + .flex-wrap-wrap { flex-wrap: wrap; } diff --git a/src/components/cardbuilder/card.css b/src/components/cardbuilder/card.css index d77fe5660c..ef5ea6604c 100644 --- a/src/components/cardbuilder/card.css +++ b/src/components/cardbuilder/card.css @@ -167,8 +167,9 @@ button::-moz-focus-inner { position: relative; background-clip: content-box !important; color: inherit; +} - /* This is only needed for scalable cards */ +.cardScalable .cardImageContainer { height: 100%; contain: strict; } diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 6b37e023b9..04e6a3fcaf 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -1537,6 +1537,8 @@ import 'programStyles'; case 'MusicArtist': case 'Person': return ''; + case 'Audio': + return ''; case 'Movie': return ''; case 'Series': diff --git a/src/components/itemContextMenu.js b/src/components/itemContextMenu.js index c0f04c2839..96f8f2d356 100644 --- a/src/components/itemContextMenu.js +++ b/src/components/itemContextMenu.js @@ -28,6 +28,23 @@ define(['apphost', 'globalize', 'connectionManager', 'itemHelper', 'appRouter', } } + if (playbackManager.getCurrentPlayer() !== null) { + if (options.stopPlayback) { + commands.push({ + name: globalize.translate('StopPlayback'), + id: 'stopPlayback', + icon: 'stop' + }); + } + if (options.clearQueue) { + commands.push({ + name: globalize.translate('ClearQueue'), + id: 'clearQueue', + icon: 'clear_all' + }); + } + } + if (playbackManager.canQueue(item)) { if (options.queue !== false) { commands.push({ @@ -44,13 +61,6 @@ define(['apphost', 'globalize', 'connectionManager', 'itemHelper', 'appRouter', icon: 'playlist_add' }); } - - //if (options.queueAllFromHere) { - // commands.push({ - // name: globalize.translate("QueueAllFromHere"), - // id: "queueallfromhere" - // }); - //} } if (item.IsFolder || item.Type === 'MusicArtist' || item.Type === 'MusicGenre') { @@ -288,10 +298,11 @@ define(['apphost', 'globalize', 'connectionManager', 'itemHelper', 'appRouter', icon: 'album' }); } - - if (options.openArtist !== false && item.ArtistItems && item.ArtistItems.length) { + // Show Album Artist by default, as a song can have multiple artists, which specific one would this option refer to? + // Although some albums can have multiple artists, it's not as common as songs. + if (options.openArtist !== false && item.AlbumArtists && item.AlbumArtists.length) { commands.push({ - name: globalize.translate('ViewArtist'), + name: globalize.translate('ViewAlbumArtist'), id: 'artist', icon: 'person' }); @@ -430,6 +441,12 @@ define(['apphost', 'globalize', 'connectionManager', 'itemHelper', 'appRouter', play(item, false, true, true); getResolveFunction(resolve, id)(); break; + case 'stopPlayback': + playbackManager.stop(); + break; + case 'clearQueue': + playbackManager.clearQueue(); + break; case 'record': require(['recordingCreator'], function (recordingCreator) { recordingCreator.show(itemId, serverId).then(getResolveFunction(resolve, id, true), getResolveFunction(resolve, id)); @@ -458,7 +475,7 @@ define(['apphost', 'globalize', 'connectionManager', 'itemHelper', 'appRouter', getResolveFunction(resolve, id)(); break; case 'artist': - appRouter.showItem(item.ArtistItems[0].Id, item.ServerId); + appRouter.showItem(item.AlbumArtists[0].Id, item.ServerId); getResolveFunction(resolve, id)(); break; case 'playallfromhere': diff --git a/src/components/listview/listview.js b/src/components/listview/listview.js index b2fa2d6971..22e5e51325 100644 --- a/src/components/listview/listview.js +++ b/src/components/listview/listview.js @@ -1,4 +1,4 @@ -define(['itemHelper', 'mediaInfo', 'indicators', 'connectionManager', 'layoutManager', 'globalize', 'datetime', 'apphost', 'css!./listview', 'emby-ratingbutton', 'emby-playstatebutton'], function (itemHelper, mediaInfo, indicators, connectionManager, layoutManager, globalize, datetime, appHost) { +define(['itemHelper', 'mediaInfo', 'indicators', 'connectionManager', 'layoutManager', 'globalize', 'datetime', 'cardBuilder', 'css!./listview', 'emby-ratingbutton', 'emby-playstatebutton'], function (itemHelper, mediaInfo, indicators, connectionManager, layoutManager, globalize, datetime, cardBuilder) { 'use strict'; function getIndex(item, options) { @@ -267,8 +267,12 @@ define(['itemHelper', 'mediaInfo', 'indicators', 'connectionManager', 'layoutMan if (options.image !== false) { let imgData = options.imageSource === 'channel' ? getChannelImageUrl(item, downloadWidth) : getImageUrl(item, downloadWidth); - let imgUrl = imgData.url; - let blurhash = imgData.blurhash; + let imgUrl; + let blurhash; + if (imgData) { + imgUrl = imgData.url; + blurhash = imgData.blurhash; + } let imageClass = isLargeStyle ? 'listItemImage listItemImage-large' : 'listItemImage'; if (isLargeStyle && layoutManager.tv) { @@ -291,7 +295,7 @@ define(['itemHelper', 'mediaInfo', 'indicators', 'connectionManager', 'layoutMan if (imgUrl) { html += '
'; } else { - html += '
'; + html += '
' + cardBuilder.getDefaultText(item, options); } var indicatorsHtml = ''; diff --git a/src/components/nowPlayingBar/nowPlayingBar.css b/src/components/nowPlayingBar/nowPlayingBar.css index b1e77715ff..e545d82d1e 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.css +++ b/src/components/nowPlayingBar/nowPlayingBar.css @@ -56,8 +56,8 @@ text-align: left; flex-grow: 1; font-size: 92%; - margin-right: 2.4em; - margin-left: 1em; + margin-right: 1em; + margin-left: 0.5em; } .nowPlayingBarCenter { @@ -133,33 +133,50 @@ .toggleRepeatButton { display: none !important; } -} -@media all and (max-width: 62em) { - .nowPlayingBarCenter .nowPlayingBarCurrentTime { + .nowPlayingBar .btnShuffleQueue { display: none !important; } } +@media all and (max-width: 80em) { + .nowPlayingBarCenter .nowPlayingBarCurrentTime, + .nowPlayingBarCenter .stopButton { + display: none !important; + } + + .nowPlayingBarInfoContainer { + width: 45%; + } +} + +.layout-mobile .nowPlayingBarRight button:not(.playPauseButton, .nextTrackButton) { + display: none; +} + +.layout-desktop .nowPlayingBarRight .playPauseButton, +.layout-tv .nowPlayingBarRight .playPauseButton { + display: none; +} + +.layout-mobile .nowPlayingBarRight input, +.layout-mobile .nowPlayingBarRight div { + display: none; +} + @media all and (max-width: 56em) { .nowPlayingBarCenter { display: none !important; } } -@media all and (min-width: 56em) { - .nowPlayingBarRight .playPauseButton { - display: none; - } -} - -@media all and (max-width: 36em) { +@media all and (max-width: 60em) { .nowPlayingBarRight .nowPlayingBarVolumeSliderContainer { display: none !important; } .nowPlayingBarInfoContainer { - width: 70%; + width: 100%; } } diff --git a/src/components/nowPlayingBar/nowPlayingBar.js b/src/components/nowPlayingBar/nowPlayingBar.js index bc9c3c1a88..a229fab4ba 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.js +++ b/src/components/nowPlayingBar/nowPlayingBar.js @@ -47,7 +47,9 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', html += ''; html += ''; - html += ''; + if (!layoutManager.mobile) { + html += ''; + } html += '
'; html += '
'; @@ -61,12 +63,17 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', html += '
'; html += ''; + html += ''; html += '
'; html += '
'; html += ''; - html += ''; + if (layoutManager.mobile) { + html += ''; + } else { + html += ''; + } html += '
'; html += ''; @@ -117,8 +124,13 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', nowPlayingImageElement = elem.querySelector('.nowPlayingImage'); nowPlayingTextElement = elem.querySelector('.nowPlayingBarText'); nowPlayingUserData = elem.querySelector('.nowPlayingBarUserDataButtons'); - + positionSlider = elem.querySelector('.nowPlayingBarPositionSlider'); muteButton = elem.querySelector('.muteButton'); + playPauseButtons = elem.querySelectorAll('.playPauseButton'); + toggleRepeatButton = elem.querySelector('.toggleRepeatButton'); + volumeSlider = elem.querySelector('.nowPlayingBarVolumeSlider'); + volumeSliderContainer = elem.querySelector('.nowPlayingBarVolumeSliderContainer'); + muteButton.addEventListener('click', function () { if (currentPlayer) { @@ -134,7 +146,6 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', } }); - playPauseButtons = elem.querySelectorAll('.playPauseButton'); playPauseButtons.forEach((button) => { button.addEventListener('click', onPlayPauseClick); }); @@ -146,42 +157,52 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', } }); - elem.querySelector('.previousTrackButton').addEventListener('click', function () { + elem.querySelector('.previousTrackButton').addEventListener('click', function (e) { + if (currentPlayer) { + if (lastPlayerState.NowPlayingItem.MediaType === 'Audio' && (currentPlayer._currentTime >= 5 || !playbackManager.previousTrack(currentPlayer))) { + // Cancel this event if doubleclick is fired + if (e.detail > 1 && playbackManager.previousTrack(currentPlayer)) { + return; + } + playbackManager.seekPercent(0, currentPlayer); + // This is done automatically by playbackManager, however, setting this here gives instant visual feedback. + // TODO: Check why seekPercentage doesn't reflect the changes inmmediately, so we can remove this workaround. + positionSlider.value = 0; + } else { + playbackManager.previousTrack(currentPlayer); + } + } + }); + elem.querySelector('.previousTrackButton').addEventListener('dblclick', function () { if (currentPlayer) { playbackManager.previousTrack(currentPlayer); } }); + elem.querySelector('.btnShuffleQueue').addEventListener('click', function () { + if (currentPlayer) { + playbackManager.toggleQueueShuffleMode(); + } + }); + toggleRepeatButton = elem.querySelector('.toggleRepeatButton'); toggleRepeatButton.addEventListener('click', function () { - - if (currentPlayer) { - - switch (playbackManager.getRepeatMode(currentPlayer)) { - case 'RepeatAll': - playbackManager.setRepeatMode('RepeatOne', currentPlayer); - break; - case 'RepeatOne': - playbackManager.setRepeatMode('RepeatNone', currentPlayer); - break; - default: - playbackManager.setRepeatMode('RepeatAll', currentPlayer); - break; - } + switch (playbackManager.getRepeatMode()) { + case 'RepeatAll': + playbackManager.setRepeatMode('RepeatOne'); + break; + case 'RepeatOne': + playbackManager.setRepeatMode('RepeatNone'); + break; + case 'RepeatNone': + playbackManager.setRepeatMode('RepeatAll'); } }); toggleRepeatButtonIcon = toggleRepeatButton.querySelector('.material-icons'); - volumeSlider = elem.querySelector('.nowPlayingBarVolumeSlider'); - volumeSliderContainer = elem.querySelector('.nowPlayingBarVolumeSliderContainer'); - - if (appHost.supports('physicalvolumecontrol')) { - volumeSliderContainer.classList.add('hide'); - } else { - volumeSliderContainer.classList.remove('hide'); - } + volumeSliderContainer.classList.toggle('hide', appHost.supports('physicalvolumecontrol')); function setVolume() { if (currentPlayer) { @@ -193,7 +214,6 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', volumeSlider.addEventListener('mousemove', setVolume); volumeSlider.addEventListener('touchmove', setVolume); - positionSlider = elem.querySelector('.nowPlayingBarPositionSlider'); positionSlider.addEventListener('change', function () { if (currentPlayer) { @@ -257,6 +277,11 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', parentContainer.insertAdjacentHTML('afterbegin', getNowPlayingBarHtml()); nowPlayingBarElement = parentContainer.querySelector('.nowPlayingBar'); + if (layoutManager.mobile) { + hideButton(nowPlayingBarElement.querySelector('.btnShuffleQueue')); + hideButton(nowPlayingBarElement.querySelector('.nowPlayingBarCenter')); + } + if (browser.safari && browser.slow) { // Not handled well here. The wrong elements receive events, bar doesn't update quickly enough, etc. nowPlayingBarElement.classList.add('noMediaProgress'); @@ -309,7 +334,8 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', toggleRepeatButton.classList.remove('hide'); } - updateRepeatModeDisplay(playState.RepeatMode); + updateRepeatModeDisplay(playbackManager.getRepeatMode()); + onQueueShuffleModeChange(); updatePlayerVolumeState(playState.IsMuted, playState.VolumeLevel); @@ -329,16 +355,22 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', function updateRepeatModeDisplay(repeatMode) { toggleRepeatButtonIcon.classList.remove('repeat', 'repeat_one'); + const cssClass = 'repeatButton-active'; - if (repeatMode === 'RepeatAll') { - toggleRepeatButtonIcon.classList.add('repeat'); - toggleRepeatButton.classList.add('repeatButton-active'); - } else if (repeatMode === 'RepeatOne') { - toggleRepeatButtonIcon.classList.add('repeat_one'); - toggleRepeatButton.classList.add('repeatButton-active'); - } else { - toggleRepeatButtonIcon.classList.add('repeat'); - toggleRepeatButton.classList.remove('repeatButton-active'); + switch (repeatMode) { + case 'RepeatAll': + toggleRepeatButtonIcon.classList.add('repeat'); + toggleRepeatButton.classList.add(cssClass); + break; + case 'RepeatOne': + toggleRepeatButtonIcon.classList.add('repeat_one'); + toggleRepeatButton.classList.add(cssClass); + break; + case 'RepeatNone': + default: + toggleRepeatButtonIcon.classList.add('repeat'); + toggleRepeatButton.classList.remove(cssClass); + break; } } @@ -408,11 +440,7 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', // See bindEvents for why this is necessary if (volumeSlider) { - if (showVolumeSlider) { - volumeSliderContainer.classList.remove('hide'); - } else { - volumeSliderContainer.classList.add('hide'); - } + volumeSliderContainer.classList.toggle('hide', !showVolumeSlider); if (!volumeSlider.dragging) { volumeSlider.value = volumeLevel || 0; @@ -420,15 +448,6 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', } } - function getTextActionButton(item, text) { - - if (!text) { - text = itemHelper.getDisplayName(item); - } - - return `${text}`; - } - function seriesImageUrl(item, options) { if (!item) { @@ -501,21 +520,28 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', var nowPlayingItem = state.NowPlayingItem; var textLines = nowPlayingItem ? nowPlayingHelper.getNowPlayingNames(nowPlayingItem) : []; - if (textLines.length > 1) { - textLines[1].secondary = true; - } - nowPlayingTextElement.innerHTML = textLines.map(function (nowPlayingName) { - - var cssClass = nowPlayingName.secondary ? ' class="nowPlayingBarSecondaryText"' : ''; - - if (nowPlayingName.item) { - var nowPlayingText = getTextActionButton(nowPlayingName.item, nowPlayingName.text); - return `
${nowPlayingText}
`; + nowPlayingTextElement.innerHTML = ''; + if (textLines) { + let itemText = document.createElement('div'); + let secondaryText = document.createElement('div'); + secondaryText.classList.add('nowPlayingBarSecondaryText'); + if (textLines.length > 1) { + textLines[1].secondary = true; + if (textLines[1].text) { + let text = document.createElement('a'); + text.innerHTML = textLines[1].text; + secondaryText.appendChild(text); + } } - return `
${nowPlayingText}
`; - - }).join(''); + if (textLines[0].text) { + let text = document.createElement('a'); + text.innerHTML = textLines[0].text; + itemText.appendChild(text); + } + nowPlayingTextElement.appendChild(itemText); + nowPlayingTextElement.appendChild(secondaryText); + } var imgHeight = 70; @@ -533,8 +559,12 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', if (url) { imageLoader.lazyImage(nowPlayingImageElement, url); + nowPlayingImageElement.style.display = null; + nowPlayingTextElement.style.marginLeft = null; } else { nowPlayingImageElement.style.backgroundImage = ''; + nowPlayingImageElement.style.display = 'none'; + nowPlayingTextElement.style.marginLeft = '1em'; } } @@ -545,21 +575,28 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', apiClient.getItem(apiClient.getCurrentUserId(), nowPlayingItem.Id).then(function (item) { var userData = item.UserData || {}; var likes = userData.Likes == null ? '' : userData.Likes; - var contextButton = document.querySelector('.btnToggleContextMenu'); - var options = { - play: false, - queue: false, - positionTo: contextButton - }; - nowPlayingUserData.innerHTML = ''; - apiClient.getCurrentUser().then(function(user) { - contextButton.addEventListener('click', function () { - itemContextMenu.show(Object.assign({ - item: item, - user: user - }, options )); + if (!layoutManager.mobile) { + let contextButton = nowPlayingBarElement.querySelector('.btnToggleContextMenu'); + // We remove the previous event listener by replacing the item in each update event + let contextButtonClone = contextButton.cloneNode(true); + contextButton.parentNode.replaceChild(contextButtonClone, contextButton); + contextButton = nowPlayingBarElement.querySelector('.btnToggleContextMenu'); + let options = { + play: false, + queue: false, + clearQueue: true, + positionTo: contextButton + }; + apiClient.getCurrentUser().then(function (user) { + contextButton.addEventListener('click', function () { + itemContextMenu.show(Object.assign({ + item: item, + user: user + }, options)); + }); }); - }); + } + nowPlayingUserData.innerHTML = ''; }); } } else { @@ -575,15 +612,34 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', onStateChanged.call(player, e, state); } - function onRepeatModeChange(e) { + function onRepeatModeChange() { if (!isEnabled) { return; } - var player = this; + updateRepeatModeDisplay(playbackManager.getRepeatMode()); + } - updateRepeatModeDisplay(playbackManager.getRepeatMode(player)); + function onQueueShuffleModeChange() { + if (!isEnabled) { + return; + } + + let shuffleMode = playbackManager.getQueueShuffleMode(); + let context = nowPlayingBarElement; + const cssClass = 'shuffleQueue-active'; + let toggleShuffleButton = context.querySelector('.btnShuffleQueue'); + + switch (shuffleMode) { + case 'Shuffle': + toggleShuffleButton.classList.add(cssClass); + break; + case 'Sorted': + default: + toggleShuffleButton.classList.remove(cssClass); + break; + } } function showNowPlayingBar() { @@ -691,6 +747,7 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', events.off(player, 'playbackstart', onPlaybackStart); events.off(player, 'statechange', onPlaybackStart); events.off(player, 'repeatmodechange', onRepeatModeChange); + events.off(player, 'shufflequeuemodechange', onQueueShuffleModeChange); events.off(player, 'playbackstop', onPlaybackStopped); events.off(player, 'volumechange', onVolumeChanged); events.off(player, 'pause', onPlayPauseStateChanged); @@ -739,6 +796,7 @@ define(['require', 'datetime', 'itemHelper', 'events', 'browser', 'imageLoader', events.on(player, 'playbackstart', onPlaybackStart); events.on(player, 'statechange', onPlaybackStart); events.on(player, 'repeatmodechange', onRepeatModeChange); + events.on(player, 'shufflequeuemodechange', onQueueShuffleModeChange); events.on(player, 'playbackstop', onPlaybackStopped); events.on(player, 'volumechange', onVolumeChanged); events.on(player, 'pause', onPlayPauseStateChanged); diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 73f07a05f2..c79294442e 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -2097,6 +2097,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla state.PlayState.IsMuted = player.isMuted(); state.PlayState.IsPaused = player.paused(); state.PlayState.RepeatMode = self.getRepeatMode(player); + state.PlayState.ShuffleMode = self.getQueueShuffleMode(player); state.PlayState.MaxStreamingBitrate = self.getMaxStreamingBitrate(player); state.PlayState.PositionTicks = getCurrentTicks(player); @@ -2877,11 +2878,11 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla } }; - self.queue = function (options, player) { + self.queue = function (options, player = this._currentPlayer) { queue(options, '', player); }; - self.queueNext = function (options, player) { + self.queueNext = function (options, player = this._currentPlayer) { queue(options, 'next', player); }; @@ -2969,6 +2970,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla } else { self._playQueueManager.queue(items); } + events.trigger(player, 'playlistitemadd'); } function onPlayerProgressInterval() { @@ -3304,6 +3306,11 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla sendProgressUpdate(player, 'repeatmodechange'); } + function onShuffleQueueModeChange() { + var player = this; + sendProgressUpdate(player, 'shufflequeuemodechange'); + } + function onPlaylistItemMove(e) { var player = this; sendProgressUpdate(player, 'playlistitemmove', true); @@ -3358,6 +3365,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla events.on(player, 'unpause', onPlaybackUnpause); events.on(player, 'volumechange', onPlaybackVolumeChange); events.on(player, 'repeatmodechange', onRepeatModeChange); + events.on(player, 'shufflequeuemodechange', onShuffleQueueModeChange); events.on(player, 'playlistitemmove', onPlaylistItemMove); events.on(player, 'playlistitemremove', onPlaylistItemRemove); events.on(player, 'playlistitemadd', onPlaylistItemAdd); @@ -3370,6 +3378,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla events.on(player, 'unpause', onPlaybackUnpause); events.on(player, 'volumechange', onPlaybackVolumeChange); events.on(player, 'repeatmodechange', onRepeatModeChange); + events.on(player, 'shufflequeuemodechange', onShuffleQueueModeChange); events.on(player, 'playlistitemmove', onPlaylistItemMove); events.on(player, 'playlistitemremove', onPlaylistItemRemove); events.on(player, 'playlistitemadd', onPlaylistItemAdd); @@ -3701,10 +3710,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla return textStreamUrl; }; - PlaybackManager.prototype.stop = function (player) { - - player = player || this._currentPlayer; - + PlaybackManager.prototype.stop = function (player = this._currentPlayer) { if (player) { if (enableLocalPlaylistManagement(player)) { @@ -3811,7 +3817,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla }); }; - PlaybackManager.prototype.shuffle = function (shuffleItem, player, queryOptions) { + PlaybackManager.prototype.shuffle = function (shuffleItem, player) { player = player || this._currentPlayer; if (player && player.shuffle) { @@ -3878,6 +3884,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla 'GoToSearch', 'DisplayMessage', 'SetRepeatMode', + 'SetShuffleQueue', 'PlayMediaSource', 'PlayTrailers' ]; @@ -3911,9 +3918,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla return info ? info.supportedCommands : []; }; - PlaybackManager.prototype.setRepeatMode = function (value, player) { - - player = player || this._currentPlayer; + PlaybackManager.prototype.setRepeatMode = function (value, player = this._currentPlayer) { if (player && !enableLocalPlaylistManagement(player)) { return player.setRepeatMode(value); } @@ -3922,9 +3927,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla events.trigger(player, 'repeatmodechange'); }; - PlaybackManager.prototype.getRepeatMode = function (player) { - - player = player || this._currentPlayer; + PlaybackManager.prototype.getRepeatMode = function (player = this._currentPlayer) { if (player && !enableLocalPlaylistManagement(player)) { return player.getRepeatMode(); } @@ -3932,6 +3935,52 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla return this._playQueueManager.getRepeatMode(); }; + PlaybackManager.prototype.setQueueShuffleMode = function (value, player = this._currentPlayer) { + if (player && !enableLocalPlaylistManagement(player)) { + return player.setQueueShuffleMode(value); + } + + this._playQueueManager.setShuffleMode(value); + events.trigger(player, 'shufflequeuemodechange'); + }; + + PlaybackManager.prototype.getQueueShuffleMode = function (player = this._currentPlayer) { + if (player && !enableLocalPlaylistManagement(player)) { + return player.getQueueShuffleMode(); + } + + return this._playQueueManager.getShuffleMode(); + }; + + PlaybackManager.prototype.toggleQueueShuffleMode = function (player = this._currentPlayer) { + let currentvalue; + if (player && !enableLocalPlaylistManagement(player)) { + currentvalue = player.getQueueShuffleMode(); + switch (currentvalue) { + case 'Shuffle': + player.setQueueShuffleMode('Sorted'); + break; + case 'Sorted': + player.setQueueShuffleMode('Shuffle'); + break; + default: + throw new TypeError('current value for shufflequeue is invalid'); + } + } else { + this._playQueueManager.toggleShuffleMode(); + } + events.trigger(player, 'shufflequeuemodechange'); + }; + + PlaybackManager.prototype.clearQueue = function (clearCurrentItem = false, player = this._currentPlayer) { + if (player && !enableLocalPlaylistManagement(player)) { + return player.clearQueue(clearCurrentItem); + } + + this._playQueueManager.clearPlaylist(clearCurrentItem); + events.trigger(player, 'playlistitemremove'); + }; + PlaybackManager.prototype.trySetActiveDeviceName = function (name) { name = normalizeName(name); @@ -4000,6 +4049,9 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla case 'SetRepeatMode': this.setRepeatMode(cmd.Arguments.RepeatMode, player); break; + case 'SetShuffleQueue': + this.setQueueShuffleMode(cmd.Arguments.ShuffleMode, player); + break; case 'VolumeUp': this.volumeUp(player); break; diff --git a/src/components/playback/playqueuemanager.js b/src/components/playback/playqueuemanager.js index 565cb6993e..2f411091c6 100644 --- a/src/components/playback/playqueuemanager.js +++ b/src/components/playback/playqueuemanager.js @@ -24,8 +24,10 @@ define([], function () { function PlayQueueManager() { + this._sortedPlaylist = []; this._playlist = []; this._repeatMode = 'RepeatNone'; + this._shuffleMode = 'Sorted'; } PlayQueueManager.prototype.getPlaylist = function () { @@ -56,6 +58,40 @@ define([], function () { } }; + PlayQueueManager.prototype.shufflePlaylist = function () { + this._sortedPlaylist = []; + for (const item of this._playlist) { + this._sortedPlaylist.push(item); + } + const currentPlaylistItem = this._playlist.splice(this.getCurrentPlaylistIndex(), 1)[0]; + + for (let i = this._playlist.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * i); + const temp = this._playlist[i]; + this._playlist[i] = this._playlist[j]; + this._playlist[j] = temp; + } + this._playlist.unshift(currentPlaylistItem); + this._shuffleMode = 'Shuffle'; + }; + + PlayQueueManager.prototype.sortShuffledPlaylist = function () { + this._playlist = []; + for (let item of this._sortedPlaylist) { + this._playlist.push(item); + } + this._sortedPlaylist = []; + this._shuffleMode = 'Sorted'; + }; + + PlayQueueManager.prototype.clearPlaylist = function (clearCurrentItem = false) { + const currentPlaylistItem = this._playlist.splice(this.getCurrentPlaylistIndex(), 1)[0]; + this._playlist = []; + if (!clearCurrentItem) { + this._playlist.push(currentPlaylistItem); + } + }; + function arrayInsertAt(destArray, pos, arrayToInsert) { var args = []; args.push(pos); // where to insert @@ -116,9 +152,7 @@ define([], function () { PlayQueueManager.prototype.removeFromPlaylist = function (playlistItemIds) { - var playlist = this.getPlaylist(); - - if (playlist.length <= playlistItemIds.length) { + if (this._playlist.length <= playlistItemIds.length) { return { result: 'empty' }; @@ -127,8 +161,12 @@ define([], function () { var currentPlaylistItemId = this.getCurrentPlaylistItemId(); var isCurrentIndex = playlistItemIds.indexOf(currentPlaylistItemId) !== -1; - this._playlist = playlist.filter(function (item) { - return playlistItemIds.indexOf(item.PlaylistItemId) === -1; + this._sortedPlaylist = this._sortedPlaylist.filter(function (item) { + return !playlistItemIds.includes(item.PlaylistItemId); + }); + + this._playlist = this._playlist.filter(function (item) { + return !playlistItemIds.includes(item.PlaylistItemId); }); return { @@ -176,21 +214,56 @@ define([], function () { PlayQueueManager.prototype.reset = function () { + this._sortedPlaylist = []; this._playlist = []; this._currentPlaylistItemId = null; this._repeatMode = 'RepeatNone'; + this._shuffleMode = 'Sorted'; }; PlayQueueManager.prototype.setRepeatMode = function (value) { - - this._repeatMode = value; + const repeatModes = ['RepeatOne', 'RepeatAll', 'RepeatNone']; + if (repeatModes.includes(value)) { + this._repeatMode = value; + } else { + throw new TypeError('invalid value provided for setRepeatMode'); + } }; PlayQueueManager.prototype.getRepeatMode = function () { - return this._repeatMode; }; + PlayQueueManager.prototype.setShuffleMode = function (value) { + switch (value) { + case 'Shuffle': + this.shufflePlaylist(); + break; + case 'Sorted': + this.sortShuffledPlaylist(); + break; + default: + throw new TypeError('invalid value provided to setShuffleMode'); + } + }; + + PlayQueueManager.prototype.toggleShuffleMode = function () { + switch (this._shuffleMode) { + case 'Shuffle': + this.setShuffleMode('Sorted'); + break; + case 'Sorted': + this.setShuffleMode('Shuffle'); + break; + default: + throw new TypeError('current value for shufflequeue is invalid'); + } + }; + + PlayQueueManager.prototype.getShuffleMode = function () { + return this._shuffleMode; + }; + PlayQueueManager.prototype.getNextItemInfo = function () { var newIndex; diff --git a/src/components/remotecontrol/remotecontrol.css b/src/components/remotecontrol/remotecontrol.css index 073c925339..d4511a9dd7 100644 --- a/src/components/remotecontrol/remotecontrol.css +++ b/src/components/remotecontrol/remotecontrol.css @@ -157,43 +157,110 @@ } .nowPlayingSecondaryButtons { - display: -webkit-box; - display: -webkit-flex; display: flex; - -webkit-box-align: center; - -webkit-align-items: center; align-items: center; - -webkit-flex-wrap: wrap; flex-wrap: wrap; - -webkit-box-pack: end; - -webkit-justify-content: flex-end; justify-content: flex-end; z-index: 0; } +.layout-mobile .playlistSectionButtonTransparent { + background: rgba(0, 0, 0, 0) !important; +} + +.layout-mobile .playlistSection .playlist, +.layout-mobile .playlistSection .contextMenu { + position: absolute; + top: 12.2em; + bottom: 4.2em; + overflow: scroll; + padding: 0 1em; + display: inline-block; + left: 0; + right: 0; + z-index: 1000; +} + +.layout-mobile .playlistSectionButton { + position: fixed; + bottom: 0; + left: 0; + height: 4.2em; + right: 0; + padding-left: 7.3%; + padding-right: 7.3%; +} + +.layout-desktop .playlistSectionButton, +.layout-tv .playlistSectionButton { + background: none; +} + +.layout-desktop .nowPlayingPlaylist, +.layout-tv .nowPlayingPlaylist { + background: none; +} + +.layout-mobile .playlistSectionButton .btnTogglePlaylist { + font-size: larger; + margin: 0; +} + +.layout-mobile .playlistSectionButton .btnSavePlaylist { + margin: 0; + border-radius: 0; +} + +.layout-mobile .playlistSectionButton .volumecontrol { + margin: 0; + padding-right: 0; + border-radius: 0; +} + +.layout-mobile .playlistSectionButton .btnToggleContextMenu { + font-size: larger; + margin: 0; +} + +.layout-mobile .nowPlayingSecondaryButtons .btnShuffleQueue { + display: none; +} + +.layout-mobile .nowPlayingSecondaryButtons .volumecontrol { + display: none; +} + +.layout-mobile .nowPlayingSecondaryButtons .btnRepeat { + display: none; +} + +.layout-desktop .nowPlayingInfoButtons .btnRepeat, +.layout-tv .nowPlayingInfoButtons .btnRepeat { + display: none; +} + +.layout-desktop .nowPlayingInfoButtons .btnShuffleQueue, +.layout-tv .nowPlayingInfoButtons .btnShuffleQueue { + display: none; +} + +.layout-desktop .playlistSectionButton .volumecontrol, +.layout-tv .playlistSectionButton .volumecontrol { + display: none; +} + +.nowPlayingInfoControls .nowPlayingPageUserDataButtonsTitle { + display: none; +} + +.layout-mobile .nowPlayingPageUserDataButtons { + display: none; +} + @media all and (min-width: 63em) { .nowPlayingPage { padding: 8em 0 0 0 !important; } - - .nowPlayingSecondaryButtons { - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - flex-grow: 1; - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - justify-content: flex-end; - } - - .nowPlayingPageUserDataButtonsTitle { - display: none !important; - } - - .playlistSectionButton, - .nowPlayingPlaylist, - .nowPlayingContextMenu { - background: unset !important; - } } @media all and (min-width: 80em) { @@ -202,7 +269,7 @@ } } -@media all and (orientation: portrait) and (max-width: 47em) { +@media all and (orientation: portrait) and (max-width: 43em) { .remoteControlContent { padding-left: 7.3% !important; padding-right: 7.3% !important; @@ -280,6 +347,7 @@ .nowPlayingInfoControls .nowPlayingPageUserDataButtonsTitle { width: 20%; font-size: large; + display: unset; } .nowPlayingInfoControls .nowPlayingPageUserDataButtonsTitle button { @@ -290,7 +358,7 @@ border-radius: 0; } - .nowPlayingInfoButtons .btnRewind { + .nowPlayingInfoButtons .btnRepeat { position: absolute; left: 0; margin-left: 0; @@ -298,7 +366,7 @@ font-size: smaller; } - .nowPlayingInfoButtons .btnFastForward { + .nowPlayingInfoButtons .btnShuffleQueue { position: absolute; right: 0; margin-right: 0; @@ -342,250 +410,6 @@ width: auto; } - #nowPlayingPage .playlistSection .playlist, - #nowPlayingPage .playlistSection .contextMenu { - position: absolute; - top: 12.2em; - bottom: 4.2em; - overflow: scroll; - padding: 0 1em; - display: inline-block; - left: 0; - right: 0; - z-index: 1000; - } - - .playlistSectionButton { - position: fixed; - bottom: 0; - left: 0; - height: 4.2em; - right: 0; - padding-left: 7.3%; - padding-right: 7.3%; - } - - .playlistSectionButton .btnTogglePlaylist { - font-size: larger; - margin: 0; - padding-left: 0; - } - - .playlistSectionButton .btnSavePlaylist { - margin: 0; - padding-right: 0; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - flex-grow: 1; - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - justify-content: flex-end; - border-radius: 0; - } - - .playlistSectionButton .btnToggleContextMenu { - font-size: larger; - margin: 0; - padding-right: 0; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - flex-grow: 1; - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - justify-content: flex-end; - border-radius: 0; - } - - .playlistSectionButton .volumecontrol { - width: 100%; - } - - .remoteControlSection { - margin: 0; - padding: 0 0 4.2em 0; - } - - .nowPlayingButtonsContainer { - display: flex; - height: 100%; - flex-direction: column; - } -} - -@media all and (orientation: landscape) and (max-width: 63em) { - .remoteControlContent { - padding-left: 4.3% !important; - padding-right: 4.3% !important; - display: flex; - height: 100%; - flex-direction: column; - } - - .nowPlayingInfoContainer { - -webkit-box-orient: horizontal !important; - -webkit-box-direction: normal !important; - -webkit-flex-direction: row !important; - flex-direction: row !important; - -webkit-box-align: center; - -webkit-align-items: center; - align-items: center; - width: 100%; - height: calc(100% - 4.2em); - } - - .nowPlayingPageTitle { - /* text-align: center; */ - margin: 0; - } - - .nowPlayingInfoContainerMedia { - text-align: left !important; - width: 80%; - } - - .nowPlayingPositionSliderContainer { - margin: 0.2em 1em 0.2em 1em; - } - - .nowPlayingInfoButtons { - /* margin: 1.5em 0 0 0; */ - -webkit-box-pack: center; - -webkit-justify-content: center; - justify-content: center; - font-size: x-large; - height: 100%; - } - - .nowPlayingPageImageContainer { - width: 30%; - margin: auto 1em auto auto; - } - - .nowPlayingPageImageContainerNoAlbum .cardImageContainer .cardImageIcon { - font-size: 12em; - color: inherit; - } - - .nowPlayingInfoControls { - margin: 0.5em 0 1em 0; - width: 100%; - -webkit-box-pack: start !important; - -webkit-justify-content: start !important; - justify-content: start !important; - } - - .nowPlayingSecondaryButtons { - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - flex-grow: 1; - -webkit-box-pack: center; - -webkit-justify-content: center; - justify-content: center; - } - - .nowPlayingInfoControls .nowPlayingPageUserDataButtonsTitle { - width: 20%; - font-size: large; - } - - .nowPlayingInfoControls .nowPlayingPageUserDataButtonsTitle button { - padding-top: 0; - padding-right: 0; - margin-right: 0; - float: right; - border-radius: 0; - } - - .paper-icon-button-light:hover { - color: #fff !important; - background-color: transparent !important; - } - - .btnPlayPause { - padding: 0; - margin: 0; - font-size: 1.7em; - } - - .btnPlayPause:hover { - background-color: transparent !important; - } - - .nowPlayingPageImage { - /* width: inherit; */ - overflow-y: hidden; - overflow: hidden; - margin: 0 auto; - } - - .nowPlayingPageImage.nowPlayingPageImageAudio { - width: 100%; - } - - .nowPlayingPageImageContainer.nowPlayingPageImagePoster { - height: 100%; - overflow: hidden; - } - - .nowPlayingPageImageContainer.nowPlayingPageImagePoster img { - height: 100%; - width: auto; - } - - #nowPlayingPage .playlistSection .playlist, - #nowPlayingPage .playlistSection .contextMenu { - position: absolute; - top: 7.2em; - bottom: 4.2em; - overflow: scroll; - padding: 0 1em; - display: inline-block; - left: 0; - right: 0; - z-index: 1000; - } - - .playlistSectionButton { - position: fixed; - bottom: 0; - left: 0; - height: 4.2em; - right: 0; - padding-left: 4.3%; - padding-right: 4.3%; - } - - .playlistSectionButton .btnTogglePlaylist { - font-size: larger; - margin: 0; - padding-left: 0; - } - - .playlistSectionButton .btnSavePlaylist { - margin: 0; - padding-right: 0; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - flex-grow: 1; - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - justify-content: flex-end; - border-radius: 0; - } - - .playlistSectionButton .btnToggleContextMenu { - font-size: larger; - margin: 0; - padding-right: 0; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - flex-grow: 1; - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - justify-content: flex-end; - border-radius: 0; - } - .playlistSectionButton .volumecontrol { width: 100%; } @@ -627,6 +451,10 @@ background-image: url(../../assets/img/equalizer.gif) !important; } +.playlistIndexIndicatorImage > * { + display: none; +} + .hideVideoButtons .videoButton { display: none; } @@ -636,7 +464,6 @@ } @media all and (max-width: 63em) { - .nowPlayingSecondaryButtons .nowPlayingPageUserDataButtons, .nowPlayingSecondaryButtons .repeatToggleButton, .nowPlayingInfoButtons .playlist .listItemMediaInfo, .nowPlayingInfoButtons .btnStop { diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index d620ea5d1e..33c44ab400 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -1,5 +1,7 @@ -define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageLoader', 'playbackManager', 'nowPlayingHelper', 'events', 'connectionManager', 'apphost', 'globalize', 'layoutManager', 'userSettings', 'cardBuilder', 'cardStyle', 'emby-itemscontainer', 'css!./remotecontrol.css', 'emby-ratingbutton'], function (browser, datetime, backdrop, libraryBrowser, listView, imageLoader, playbackManager, nowPlayingHelper, events, connectionManager, appHost, globalize, layoutManager, userSettings, cardBuilder) { +define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageLoader', 'playbackManager', 'nowPlayingHelper', 'events', 'connectionManager', 'apphost', 'globalize', 'layoutManager', 'userSettings', 'cardBuilder', 'itemContextMenu', 'cardStyle', 'emby-itemscontainer', 'css!./remotecontrol.css', 'emby-ratingbutton'], function (browser, datetime, backdrop, libraryBrowser, listView, imageLoader, playbackManager, nowPlayingHelper, events, connectionManager, appHost, globalize, layoutManager, userSettings, cardBuilder, itemContextMenu) { 'use strict'; + var showMuteButton = true; + var showVolumeSlider = true; function showAudioMenu(context, player, button, item) { var currentIndex = playbackManager.getAudioStreamIndex(player); @@ -118,30 +120,41 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL if (item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') { var songName = item.Name; if (item.Album != null && item.Artists != null) { + var artistsSeries = ''; var albumName = item.Album; - var artistName; if (item.ArtistItems != null) { - artistName = item.ArtistItems[0].Name; - context.querySelector('.nowPlayingAlbum').innerHTML = '${albumName}`; - context.querySelector('.nowPlayingArtist').innerHTML = '${artistName}`; - context.querySelector('.contextMenuAlbum').innerHTML = ' ` + globalize.translate('ViewAlbum') + ''; - context.querySelector('.contextMenuArtist').innerHTML = ' ` + globalize.translate('ViewArtist') + ''; - } else { - artistName = item.Artists; - context.querySelector('.nowPlayingAlbum').innerHTML = albumName; - context.querySelector('.nowPlayingArtist').innerHTML = artistName; + for (const artist of item.ArtistItems) { + let artistName = artist.Name; + let artistId = artist.Id; + artistsSeries += `${artistName}`; + if (artist !== item.ArtistItems.slice(-1)[0]) { + artistsSeries += ', '; + } + } + } else if (item.Artists) { + // For some reason, Chromecast Player doesn't return a item.ArtistItems object, so we need to fallback + // to normal item.Artists item. + // TODO: Normalise fields returned by all the players + for (const artist of item.Artists) { + artistsSeries += `${artist}`; + if (artist !== item.Artists.slice(-1)[0]) { + artistsSeries += ', '; + } + } } + context.querySelector('.nowPlayingArtist').innerHTML = artistsSeries; + context.querySelector('.nowPlayingAlbum').innerHTML = '${albumName}`; } context.querySelector('.nowPlayingSongName').innerHTML = songName; } else if (item.Type == 'Episode') { if (item.SeasonName != null) { var seasonName = item.SeasonName; - context.querySelector('.nowPlayingSeason').innerHTML = '${seasonName}`; + context.querySelector('.nowPlayingSeason').innerHTML = '${seasonName}`; } if (item.SeriesName != null) { var seriesName = item.SeriesName; if (item.SeriesId != null) { - context.querySelector('.nowPlayingSerie').innerHTML = '${seriesName}`; + context.querySelector('.nowPlayingSerie').innerHTML = '${seriesName}`; } else { context.querySelector('.nowPlayingSerie').innerHTML = seriesName; } @@ -163,11 +176,38 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL maxHeight: 300 * 2 }) : null; - console.debug('updateNowPlayingInfo'); + let contextButton = context.querySelector('.btnToggleContextMenu'); + // We remove the previous event listener by replacing the item in each update event + const autoFocusContextButton = document.activeElement === contextButton; + let contextButtonClone = contextButton.cloneNode(true); + contextButton.parentNode.replaceChild(contextButtonClone, contextButton); + contextButton = context.querySelector('.btnToggleContextMenu'); + if (autoFocusContextButton) { + contextButton.focus(); + } + const stopPlayback = !!layoutManager.mobile; + var options = { + play: false, + queue: false, + stopPlayback: stopPlayback, + clearQueue: true, + openAlbum: false, + positionTo: contextButton + }; + var apiClient = connectionManager.getApiClient(item.ServerId); + apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) { + apiClient.getCurrentUser().then(function (user) { + contextButton.addEventListener('click', function () { + itemContextMenu.show(Object.assign({ + item: fullItem, + user: user + }, options)); + }); + }); + }); setImageUrl(context, state, url); if (item) { backdrop.setBackdrops([item]); - var apiClient = connectionManager.getApiClient(item.ServerId); apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) { var userData = fullItem.UserData || {}; var likes = null == userData.Likes ? '' : userData.Likes; @@ -219,20 +259,16 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL var currentImgUrl; return function () { - function toggleRepeat(player) { - if (player) { - switch (playbackManager.getRepeatMode(player)) { - case 'RepeatNone': - playbackManager.setRepeatMode('RepeatAll', player); - break; - - case 'RepeatAll': - playbackManager.setRepeatMode('RepeatOne', player); - break; - - case 'RepeatOne': - playbackManager.setRepeatMode('RepeatNone', player); - } + function toggleRepeat() { + switch (playbackManager.getRepeatMode()) { + case 'RepeatAll': + playbackManager.setRepeatMode('RepeatOne'); + break; + case 'RepeatOne': + playbackManager.setRepeatMode('RepeatNone'); + break; + case 'RepeatNone': + playbackManager.setRepeatMode('RepeatAll'); } } @@ -275,8 +311,13 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL buttonVisible(context.querySelector('.btnStop'), null != item); buttonVisible(context.querySelector('.btnNextTrack'), null != item); buttonVisible(context.querySelector('.btnPreviousTrack'), null != item); - buttonVisible(context.querySelector('.btnRewind'), null != item); - buttonVisible(context.querySelector('.btnFastForward'), null != item); + if (layoutManager.mobile) { + buttonVisible(context.querySelector('.btnRewind'), false); + buttonVisible(context.querySelector('.btnFastForward'), false); + } else { + buttonVisible(context.querySelector('.btnRewind'), null != item); + buttonVisible(context.querySelector('.btnFastForward'), null != item); + } var positionSlider = context.querySelector('.nowPlayingPositionSlider'); if (positionSlider && item && item.RunTimeTicks) { @@ -300,7 +341,8 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL context.classList.add('hideVideoButtons'); } - updateRepeatModeDisplay(playState.RepeatMode); + updateRepeatModeDisplay(playbackManager.getRepeatMode()); + onShuffleQueueModeChange(false); updateNowPlayingInfo(context, state); } @@ -316,25 +358,32 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL function updateRepeatModeDisplay(repeatMode) { var context = dlg; - var toggleRepeatButton = context.querySelector('.repeatToggleButton'); + let toggleRepeatButtons = context.querySelectorAll('.repeatToggleButton'); + const cssClass = 'repeatButton-active'; + let innHtml = ''; + let repeatOn = true; - if ('RepeatAll' == repeatMode) { - toggleRepeatButton.innerHTML = ""; - toggleRepeatButton.classList.add('repeatButton-active'); - } else if ('RepeatOne' == repeatMode) { - toggleRepeatButton.innerHTML = ""; - toggleRepeatButton.classList.add('repeatButton-active'); - } else { - toggleRepeatButton.innerHTML = ""; - toggleRepeatButton.classList.remove('repeatButton-active'); + switch (repeatMode) { + case 'RepeatAll': + break; + case 'RepeatOne': + innHtml = ''; + break; + case 'RepeatNone': + default: + repeatOn = false; + break; + } + + for (const toggleRepeatButton of toggleRepeatButtons) { + toggleRepeatButton.classList.toggle(cssClass, repeatOn); + toggleRepeatButton.innerHTML = innHtml; } } function updatePlayerVolumeState(context, isMuted, volumeLevel) { var view = context; var supportedCommands = currentPlayerSupportedCommands; - var showMuteButton = true; - var showVolumeSlider = true; if (-1 === supportedCommands.indexOf('Mute')) { showMuteButton = false; @@ -362,24 +411,21 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL buttonMuteIcon.classList.add('volume_up'); } - if (showMuteButton) { - buttonMute.classList.remove('hide'); + if (!showMuteButton && !showVolumeSlider) { + context.querySelector('.volumecontrol').classList.add('hide'); } else { - buttonMute.classList.add('hide'); - } + buttonMute.classList.toggle('hide', !showMuteButton); - var nowPlayingVolumeSlider = context.querySelector('.nowPlayingVolumeSlider'); - var nowPlayingVolumeSliderContainer = context.querySelector('.nowPlayingVolumeSliderContainer'); + var nowPlayingVolumeSlider = context.querySelector('.nowPlayingVolumeSlider'); + var nowPlayingVolumeSliderContainer = context.querySelector('.nowPlayingVolumeSliderContainer'); - if (nowPlayingVolumeSlider) { - if (showVolumeSlider) { - nowPlayingVolumeSliderContainer.classList.remove('hide'); - } else { - nowPlayingVolumeSliderContainer.classList.add('hide'); - } + if (nowPlayingVolumeSlider) { - if (!nowPlayingVolumeSlider.dragging) { - nowPlayingVolumeSlider.value = volumeLevel || 0; + nowPlayingVolumeSliderContainer.classList.toggle('hide', !showVolumeSlider); + + if (!nowPlayingVolumeSlider.dragging) { + nowPlayingVolumeSlider.value = volumeLevel || 0; + } } } } @@ -420,11 +466,21 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL function loadPlaylist(context, player) { getPlaylistItems(player).then(function (items) { var html = ''; + let favoritesEnabled = true; + if (layoutManager.mobile) { + if (items.length > 0) { + context.querySelector('.btnTogglePlaylist').classList.remove('hide'); + } else { + context.querySelector('.btnTogglePlaylist').classList.add('hide'); + } + favoritesEnabled = false; + } + html += listView.getListViewHtml({ items: items, smallIcon: true, action: 'setplaylistindex', - enableUserDataButtons: false, + enableUserDataButtons: favoritesEnabled, rightButtons: [{ icon: 'remove_circle_outline', title: globalize.translate('ButtonRemove'), @@ -433,14 +489,17 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL dragHandle: true }); - if (items.length) { - context.querySelector('.btnTogglePlaylist').classList.remove('hide'); - } else { - context.querySelector('.btnTogglePlaylist').classList.add('hide'); + var itemsContainer = context.querySelector('.playlist'); + let focusedItemPlaylistId = itemsContainer.querySelector('button:focus'); + itemsContainer.innerHTML = html; + if (focusedItemPlaylistId !== null) { + focusedItemPlaylistId = focusedItemPlaylistId.getAttribute('data-playlistitemid'); + const newFocusedItem = itemsContainer.querySelector(`button[data-playlistitemid=${focusedItemPlaylistId}]`); + if (newFocusedItem !== null) { + newFocusedItem.focus(); + } } - var itemsContainer = context.querySelector('.playlist'); - itemsContainer.innerHTML = html; var playlistItemId = playbackManager.getCurrentPlaylistItemId(player); if (playlistItemId) { @@ -453,9 +512,6 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL } imageLoader.lazyChildren(itemsContainer); - context.querySelector('.playlist').classList.add('hide'); - context.querySelector('.contextMenu').classList.add('hide'); - context.querySelector('.btnSavePlaylist').classList.add('hide'); }); } @@ -465,9 +521,31 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL onStateChanged.call(player, e, state); } - function onRepeatModeChange(e) { - var player = this; - updateRepeatModeDisplay(playbackManager.getRepeatMode(player)); + function onRepeatModeChange() { + updateRepeatModeDisplay(playbackManager.getRepeatMode()); + } + + function onShuffleQueueModeChange(updateView = true) { + let shuffleMode = playbackManager.getQueueShuffleMode(this); + let context = dlg; + const cssClass = 'shuffleQueue-active'; + let shuffleButtons = context.querySelectorAll('.btnShuffleQueue'); + + for (let shuffleButton of shuffleButtons) { + switch (shuffleMode) { + case 'Shuffle': + shuffleButton.classList.add(cssClass); + break; + case 'Sorted': + default: + shuffleButton.classList.remove(cssClass); + break; + } + } + + if (updateView) { + onPlaylistUpdate(); + } } function onPlaylistUpdate(e) { @@ -476,14 +554,18 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL function onPlaylistItemRemoved(e, info) { var context = dlg; - var playlistItemIds = info.playlistItemIds; + if (info !== undefined) { + var playlistItemIds = info.playlistItemIds; - for (var i = 0, length = playlistItemIds.length; i < length; i++) { - var listItem = context.querySelector('.listItem[data-playlistItemId="' + playlistItemIds[i] + '"]'); + for (var i = 0, length = playlistItemIds.length; i < length; i++) { + var listItem = context.querySelector('.listItem[data-playlistItemId="' + playlistItemIds[i] + '"]'); - if (listItem) { - listItem.parentNode.removeChild(listItem); + if (listItem) { + listItem.parentNode.removeChild(listItem); + } } + } else { + onPlaylistUpdate(); } } @@ -493,7 +575,6 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL if (!state.NextMediaType) { updatePlayerState(player, dlg, {}); - loadPlaylist(dlg); Emby.Page.back(); } } @@ -505,7 +586,7 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL function onStateChanged(event, state) { var player = this; updatePlayerState(player, dlg, state); - loadPlaylist(dlg, player); + onPlaylistUpdate(); } function onTimeUpdate(e) { @@ -531,8 +612,10 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL events.off(player, 'playbackstart', onPlaybackStart); events.off(player, 'statechange', onStateChanged); events.off(player, 'repeatmodechange', onRepeatModeChange); - events.off(player, 'playlistitemremove', onPlaylistUpdate); + events.off(player, 'shufflequeuemodechange', onShuffleQueueModeChange); + events.off(player, 'playlistitemremove', onPlaylistItemRemoved); events.off(player, 'playlistitemmove', onPlaylistUpdate); + events.off(player, 'playlistitemadd', onPlaylistUpdate); events.off(player, 'playbackstop', onPlaybackStopped); events.off(player, 'volumechange', onVolumeChanged); events.off(player, 'pause', onPlayPauseStateChanged); @@ -551,8 +634,10 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL events.on(player, 'playbackstart', onPlaybackStart); events.on(player, 'statechange', onStateChanged); events.on(player, 'repeatmodechange', onRepeatModeChange); + events.on(player, 'shufflequeuemodechange', onShuffleQueueModeChange); events.on(player, 'playlistitemremove', onPlaylistItemRemoved); events.on(player, 'playlistitemmove', onPlaylistUpdate); + events.on(player, 'playlistitemadd', onPlaylistUpdate); events.on(player, 'playbackstop', onPlaybackStopped); events.on(player, 'volumechange', onVolumeChanged); events.on(player, 'pause', onPlayPauseStateChanged); @@ -568,7 +653,7 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL function onBtnCommandClick() { if (currentPlayer) { if (this.classList.contains('repeatToggleButton')) { - toggleRepeat(currentPlayer); + toggleRepeat(); } else { playbackManager.sendCommand({ Name: this.getAttribute('data-command') @@ -603,6 +688,7 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL function bindEvents(context) { var btnCommand = context.querySelectorAll('.btnCommand'); + var positionSlider = context.querySelector('.nowPlayingPositionSlider'); for (var i = 0, length = btnCommand.length; i < length; i++) { btnCommand[i].addEventListener('click', onBtnCommandClick); @@ -650,12 +736,37 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL playbackManager.fastForward(currentPlayer); } }); - context.querySelector('.btnPreviousTrack').addEventListener('click', function () { + for (const shuffleButton of context.querySelectorAll('.btnShuffleQueue')) { + shuffleButton.addEventListener('click', function () { + if (currentPlayer) { + playbackManager.toggleQueueShuffleMode(currentPlayer); + } + }); + } + + context.querySelector('.btnPreviousTrack').addEventListener('click', function (e) { + if (currentPlayer) { + if (lastPlayerState.NowPlayingItem.MediaType === 'Audio' && (currentPlayer._currentTime >= 5 || !playbackManager.previousTrack(currentPlayer))) { + // Cancel this event if doubleclick is fired + if (e.detail > 1 && playbackManager.previousTrack(currentPlayer)) { + return; + } + playbackManager.seekPercent(0, currentPlayer); + // This is done automatically by playbackManager. However, setting this here gives instant visual feedback. + // TODO: Check why seekPercentage doesn't reflect the changes inmmediately, so we can remove this workaround. + positionSlider.value = 0; + } else { + playbackManager.previousTrack(currentPlayer); + } + } + }); + + context.querySelector('.btnPreviousTrack').addEventListener('dblclick', function () { if (currentPlayer) { playbackManager.previousTrack(currentPlayer); } }); - context.querySelector('.nowPlayingPositionSlider').addEventListener('change', function () { + positionSlider.addEventListener('change', function () { var value = this.value; if (currentPlayer) { @@ -664,7 +775,7 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL } }); - context.querySelector('.nowPlayingPositionSlider').getBubbleText = function (value) { + positionSlider.getBubbleText = function (value) { var state = lastPlayerState; if (!state || !state.NowPlayingItem || !currentRuntimeTicks) { @@ -701,21 +812,19 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL if (context.querySelector('.playlist').classList.contains('hide')) { context.querySelector('.playlist').classList.remove('hide'); context.querySelector('.btnSavePlaylist').classList.remove('hide'); - context.querySelector('.contextMenu').classList.add('hide'); context.querySelector('.volumecontrol').classList.add('hide'); + if (layoutManager.mobile) { + context.querySelector('.playlistSectionButton').classList.remove('playlistSectionButtonTransparent'); + } } else { context.querySelector('.playlist').classList.add('hide'); context.querySelector('.btnSavePlaylist').classList.add('hide'); - context.querySelector('.volumecontrol').classList.remove('hide'); - } - }); - context.querySelector('.btnToggleContextMenu').addEventListener('click', function () { - if (context.querySelector('.contextMenu').classList.contains('hide')) { - context.querySelector('.contextMenu').classList.remove('hide'); - context.querySelector('.btnSavePlaylist').classList.add('hide'); - context.querySelector('.playlist').classList.add('hide'); - } else { - context.querySelector('.contextMenu').classList.add('hide'); + if (showMuteButton || showVolumeSlider) { + context.querySelector('.volumecontrol').classList.remove('hide'); + } + if (layoutManager.mobile) { + context.querySelector('.playlistSectionButton').classList.add('playlistSectionButtonTransparent'); + } } }); } @@ -764,16 +873,24 @@ define(['browser', 'datetime', 'backdrop', 'libraryBrowser', 'listView', 'imageL } function init(ownerView, context) { - let contextmenuHtml = ``; let volumecontrolHtml = '
'; volumecontrolHtml += ``; volumecontrolHtml += '
'; volumecontrolHtml += '
'; + let optionsSection = context.querySelector('.playlistSectionButton'); if (!layoutManager.mobile) { - context.querySelector('.nowPlayingSecondaryButtons').innerHTML += volumecontrolHtml; - context.querySelector('.playlistSectionButton').innerHTML += contextmenuHtml; + context.querySelector('.nowPlayingSecondaryButtons').insertAdjacentHTML('beforeend', volumecontrolHtml); + optionsSection.classList.remove('align-items-center', 'justify-content-center'); + optionsSection.classList.add('align-items-right', 'justify-content-flex-end'); + context.querySelector('.playlist').classList.remove('hide'); + context.querySelector('.btnSavePlaylist').classList.remove('hide'); + context.classList.add('padded-bottom'); } else { - context.querySelector('.playlistSectionButton').innerHTML += volumecontrolHtml + contextmenuHtml; + optionsSection.querySelector('.btnTogglePlaylist').insertAdjacentHTML('afterend', volumecontrolHtml); + optionsSection.classList.add('playlistSectionButtonTransparent'); + context.querySelector('.btnTogglePlaylist').classList.remove('hide'); + context.querySelector('.playlistSectionButton').classList.remove('justify-content-center'); + context.querySelector('.playlistSectionButton').classList.add('justify-content-space-between'); } bindEvents(context); diff --git a/src/nowplaying.html b/src/nowplaying.html index 5f235a562e..9460cb814b 100644 --- a/src/nowplaying.html +++ b/src/nowplaying.html @@ -26,9 +26,14 @@
-
+
+ + - + + +
@@ -72,7 +81,12 @@ - + + @@ -162,21 +176,18 @@
-
- - +
-
-
-
-
-
-
diff --git a/src/plugins/chromecastPlayer/plugin.js b/src/plugins/chromecastPlayer/plugin.js index b3f75f7a6d..6384ce7690 100644 --- a/src/plugins/chromecastPlayer/plugin.js +++ b/src/plugins/chromecastPlayer/plugin.js @@ -548,7 +548,7 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' events.trigger(instance, 'playbackstop', [state]); - var state = instance.lastPlayerData.PlayState || {}; + state = instance.lastPlayerData.PlayState || {}; var volume = state.VolumeLevel || 0.5; var mute = state.IsMuted || false; @@ -572,6 +572,7 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' bindEventForRelay(instance, 'unpause'); bindEventForRelay(instance, 'volumechange'); bindEventForRelay(instance, 'repeatmodechange'); + bindEventForRelay(instance, 'shufflequeuemodechange'); events.on(instance._castPlayer, 'playstatechange', function (e, data) { @@ -651,6 +652,7 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' 'SetSubtitleStreamIndex', 'DisplayContent', 'SetRepeatMode', + 'SetShuffleQueue', 'EndSession', 'PlayMediaSource', 'PlayTrailers' @@ -864,6 +866,12 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' return state.RepeatMode; }; + ChromecastPlayer.prototype.getQueueShuffleMode = function () { + var state = this.lastPlayerData || {}; + state = state.PlayState || {}; + return state.ShuffleMode; + }; + ChromecastPlayer.prototype.playTrailers = function (item) { this._castPlayer.sendMessage({ @@ -884,6 +892,15 @@ define(['appSettings', 'userSettings', 'playbackManager', 'connectionManager', ' }); }; + ChromecastPlayer.prototype.setQueueShuffleMode = function (value) { + this._castPlayer.sendMessage({ + options: { + ShuffleMode: value + }, + command: 'SetShuffleQueue' + }); + }; + ChromecastPlayer.prototype.toggleMute = function () { this._castPlayer.sendMessage({ diff --git a/src/plugins/sessionPlayer/plugin.js b/src/plugins/sessionPlayer/plugin.js index 489b57493d..084aa027cf 100644 --- a/src/plugins/sessionPlayer/plugin.js +++ b/src/plugins/sessionPlayer/plugin.js @@ -263,7 +263,7 @@ define(['playbackManager', 'events', 'serverNotifications', 'connectionManager'] appName: s.Client, playableMediaTypes: s.PlayableMediaTypes, isLocalPlayer: false, - supportedCommands: s.SupportedCommands, + supportedCommands: s.Capabilities.SupportedCommands, user: s.UserId ? { Id: s.UserId, @@ -506,6 +506,17 @@ define(['playbackManager', 'events', 'serverNotifications', 'connectionManager'] }); }; + SessionPlayer.prototype.setQueueShuffleMode = function (mode) { + + sendCommandByName(this, 'SetShuffleQueue', { + ShuffleMode: mode + }); + }; + + SessionPlayer.prototype.getQueueShuffleMode = function () { + + }; + SessionPlayer.prototype.displayContent = function (options) { sendCommandByName(this, 'DisplayContent', options); diff --git a/src/scripts/serverNotifications.js b/src/scripts/serverNotifications.js index 2553c284f0..cddd2cf794 100644 --- a/src/scripts/serverNotifications.js +++ b/src/scripts/serverNotifications.js @@ -65,6 +65,9 @@ define(['connectionManager', 'playbackManager', 'syncPlayManager', 'events', 'in case 'SetRepeatMode': playbackManager.setRepeatMode(cmd.Arguments.RepeatMode); break; + case 'SetShuffleQueue': + playbackManager.setQueueShuffleMode(cmd.Arguments.ShuffleMode); + break; case 'VolumeUp': inputManager.trigger('volumeup'); return; diff --git a/src/scripts/site.js b/src/scripts/site.js index ddadad4c80..106d81a8c7 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -196,7 +196,7 @@ var Dashboard = { capabilities: function (appHost) { var capabilities = { PlayableMediaTypes: ['Audio', 'Video'], - SupportedCommands: ['MoveUp', 'MoveDown', 'MoveLeft', 'MoveRight', 'PageUp', 'PageDown', 'PreviousLetter', 'NextLetter', 'ToggleOsd', 'ToggleContextMenu', 'Select', 'Back', 'SendKey', 'SendString', 'GoHome', 'GoToSettings', 'VolumeUp', 'VolumeDown', 'Mute', 'Unmute', 'ToggleMute', 'SetVolume', 'SetAudioStreamIndex', 'SetSubtitleStreamIndex', 'DisplayContent', 'GoToSearch', 'DisplayMessage', 'SetRepeatMode', 'ChannelUp', 'ChannelDown', 'PlayMediaSource', 'PlayTrailers'], + SupportedCommands: ['MoveUp', 'MoveDown', 'MoveLeft', 'MoveRight', 'PageUp', 'PageDown', 'PreviousLetter', 'NextLetter', 'ToggleOsd', 'ToggleContextMenu', 'Select', 'Back', 'SendKey', 'SendString', 'GoHome', 'GoToSettings', 'VolumeUp', 'VolumeDown', 'Mute', 'Unmute', 'ToggleMute', 'SetVolume', 'SetAudioStreamIndex', 'SetSubtitleStreamIndex', 'DisplayContent', 'GoToSearch', 'DisplayMessage', 'SetRepeatMode', 'SetShuffleQueue', 'ChannelUp', 'ChannelDown', 'PlayMediaSource', 'PlayTrailers'], SupportsPersistentIdentifier: 'cordova' === self.appMode || 'android' === self.appMode, SupportsMediaControl: true }; diff --git a/src/strings/en-us.json b/src/strings/en-us.json index acd038adb0..41dd4cd507 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1527,7 +1527,7 @@ "Vertical": "Vertical", "VideoRange": "Video range", "ViewAlbum": "View album", - "ViewArtist": "View artist", + "ViewAlbumArtist": "View album artist", "ViewPlaybackInfo": "View playback info", "Watched": "Watched", "Wednesday": "Wednesday", @@ -1563,5 +1563,7 @@ "EnableBlurhashHelp": "Images that are still being loaded will be displayed with a blurred placeholder", "ButtonSyncPlay": "SyncPlay", "ButtonCast": "Cast", - "ButtonPlayer": "Player" + "ButtonPlayer": "Player", + "StopPlayback": "Stop playback", + "ClearQueue": "Clear queue" } diff --git a/src/strings/es.json b/src/strings/es.json index 87e1f298cf..640a51d634 100644 --- a/src/strings/es.json +++ b/src/strings/es.json @@ -1205,7 +1205,7 @@ "ValueTimeLimitSingleHour": "Tiempo límite: 1 hora", "ValueVideoCodec": "Códec de video: {0}", "ViewAlbum": "Ver album", - "ViewArtist": "Ver artista", + "ViewAlbumArtist": "Ver artista del álbum", "ViewPlaybackInfo": "Ver información de la reproducción", "Watched": "Visto", "Wednesday": "Miércoles", @@ -1573,5 +1573,7 @@ "LabelRepositoryUrl": "URL del repositorio", "HeaderNewRepository": "Nuevo repositorio", "MessageNoRepositories": "Sin repositorios.", - "Writers": "Escritores" + "Writers": "Escritores", + "StopPlayback": "Detener la reproducción", + "ClearQueue": "Borrar la cola" } diff --git a/src/themes/appletv/theme.css b/src/themes/appletv/theme.css index 0c7a462f02..b92a09d14b 100644 --- a/src/themes/appletv/theme.css +++ b/src/themes/appletv/theme.css @@ -445,6 +445,10 @@ html { color: #4285f4; } +.shuffleQueue-active { + color: #4285f4 !important; +} + .card:focus .cardBox.visualCardBox, .card:focus .cardBox:not(.visualCardBox) .cardScalable { border-color: #00a4dc !important; diff --git a/src/themes/blueradiance/theme.css b/src/themes/blueradiance/theme.css index 0d0446157f..6ab2e11ab8 100644 --- a/src/themes/blueradiance/theme.css +++ b/src/themes/blueradiance/theme.css @@ -445,6 +445,10 @@ html { color: #4285f4; } +.shuffleQueue-active { + color: #4285f4 !important; +} + .cardBox:not(.visualCardBox) .cardPadder { background-color: rgba(0, 0, 0, 0.5); } diff --git a/src/themes/dark/theme.css b/src/themes/dark/theme.css index 5c12389a1f..cf35d0b696 100644 --- a/src/themes/dark/theme.css +++ b/src/themes/dark/theme.css @@ -416,6 +416,10 @@ html { color: #4285f4; } +.shuffleQueue-active { + color: #4285f4 !important; +} + .card:focus .cardBox.visualCardBox, .card:focus .cardBox:not(.visualCardBox) .cardScalable { border-color: #00a4dc !important; diff --git a/src/themes/light/theme.css b/src/themes/light/theme.css index c178537042..db273266e8 100644 --- a/src/themes/light/theme.css +++ b/src/themes/light/theme.css @@ -427,6 +427,10 @@ html { color: #4285f4; } +.shuffleQueue-active { + color: #4285f4 !important; +} + .cardBox:not(.visualCardBox) .cardPadder { background-color: #fff; } diff --git a/src/themes/purplehaze/theme.css b/src/themes/purplehaze/theme.css index 025e7901be..bda397da6f 100644 --- a/src/themes/purplehaze/theme.css +++ b/src/themes/purplehaze/theme.css @@ -542,6 +542,10 @@ a[data-role=button] { color: #4285f4; } +.shuffleQueue-active { + color: #4285f4 !important; +} + .personCard .cardScalable { border-radius: 50% !important; border: 1px solid rgb(255, 255, 255); diff --git a/src/themes/wmc/theme.css b/src/themes/wmc/theme.css index cd9e6bc16e..b66e7bd425 100644 --- a/src/themes/wmc/theme.css +++ b/src/themes/wmc/theme.css @@ -425,6 +425,10 @@ html { color: #4285f4; } +.shuffleQueue-active { + color: #4285f4 !important; +} + .cardBox:not(.visualCardBox) .cardPadder { background-color: #0f3562; }