diff --git a/dashboard-ui/css/images/favicon.ico b/dashboard-ui/css/images/favicon.ico index 0a4f2b8e26..4d7e2ff432 100644 Binary files a/dashboard-ui/css/images/favicon.ico and b/dashboard-ui/css/images/favicon.ico differ diff --git a/dashboard-ui/css/images/iossplash.png b/dashboard-ui/css/images/iossplash.png index fd625265ca..bd82c45e03 100644 Binary files a/dashboard-ui/css/images/iossplash.png and b/dashboard-ui/css/images/iossplash.png differ diff --git a/dashboard-ui/css/images/logo536.png b/dashboard-ui/css/images/logo536.png index 696b959afd..695d1b991b 100644 Binary files a/dashboard-ui/css/images/logo536.png and b/dashboard-ui/css/images/logo536.png differ diff --git a/dashboard-ui/css/images/mblogoicon.png b/dashboard-ui/css/images/mblogoicon.png index 40d150174b..4f3a92d5f7 100644 Binary files a/dashboard-ui/css/images/mblogoicon.png and b/dashboard-ui/css/images/mblogoicon.png differ diff --git a/dashboard-ui/css/images/touchicon.png b/dashboard-ui/css/images/touchicon.png index 7774a48342..d582ae2615 100644 Binary files a/dashboard-ui/css/images/touchicon.png and b/dashboard-ui/css/images/touchicon.png differ diff --git a/dashboard-ui/css/images/touchicon114.png b/dashboard-ui/css/images/touchicon114.png index 2c58b4b114..86fa84aeda 100644 Binary files a/dashboard-ui/css/images/touchicon114.png and b/dashboard-ui/css/images/touchicon114.png differ diff --git a/dashboard-ui/css/images/touchicon144.png b/dashboard-ui/css/images/touchicon144.png index 786c53857e..dac3348e20 100644 Binary files a/dashboard-ui/css/images/touchicon144.png and b/dashboard-ui/css/images/touchicon144.png differ diff --git a/dashboard-ui/css/images/touchicon72.png b/dashboard-ui/css/images/touchicon72.png index 30953f2cf1..cb673b46f1 100644 Binary files a/dashboard-ui/css/images/touchicon72.png and b/dashboard-ui/css/images/touchicon72.png differ diff --git a/dashboard-ui/favicon.ico b/dashboard-ui/favicon.ico index 4acd8f9219..0abd554f45 100644 Binary files a/dashboard-ui/favicon.ico and b/dashboard-ui/favicon.ico differ diff --git a/dashboard-ui/mypreferenceswebclient.html b/dashboard-ui/mypreferenceswebclient.html index 20e6e250d7..c89f135dd1 100644 --- a/dashboard-ui/mypreferenceswebclient.html +++ b/dashboard-ui/mypreferenceswebclient.html @@ -175,10 +175,8 @@
${LabelExternalPlayers} - - - + +
${LabelExternalPlayersHelp}
diff --git a/dashboard-ui/scripts/chromecast.js b/dashboard-ui/scripts/chromecast.js index 09d3e09ffd..b7e694b9b0 100644 --- a/dashboard-ui/scripts/chromecast.js +++ b/dashboard-ui/scripts/chromecast.js @@ -809,11 +809,9 @@ }); } - if ($.browser.chrome) { - requirejs(["thirdparty/cast_sender"], function () { + requirejs(["thirdparty/cast_sender"], function () { - initializeChromecast(); - }); - } + initializeChromecast(); + }); })(window, window.chrome, console); \ No newline at end of file diff --git a/dashboard-ui/scripts/dashboardpage.js b/dashboard-ui/scripts/dashboardpage.js index 6b9026c470..dddcee9ce1 100644 --- a/dashboard-ui/scripts/dashboardpage.js +++ b/dashboard-ui/scripts/dashboardpage.js @@ -1012,8 +1012,7 @@ } }; -$(document).on('pagebeforeshowready', "#dashboardPage", DashboardPage.onPageShow) - .on('pagehide', "#dashboardPage", DashboardPage.onPageHide); +$(document).on('pageshowready', "#dashboardPage", DashboardPage.onPageShow).on('pagehide', "#dashboardPage", DashboardPage.onPageHide); (function ($, document, window) { @@ -1308,7 +1307,7 @@ $(document).on('pagebeforeshowready', "#dashboardPage", DashboardPage.onPageShow result.CustomPrefs[welcomeTourKey] = welcomeDismissValue; ApiClient.updateDisplayPreferences('dashboard', result, userId, 'dashboard'); - $(page).off('pagebeforeshow.checktour'); + $(page).off('.checktour'); }); } @@ -1373,7 +1372,7 @@ $(document).on('pagebeforeshowready', "#dashboardPage", DashboardPage.onPageShow takeTour(page, Dashboard.getCurrentUserId()); }); - }).on('pagebeforeshowready.checktour', "#dashboardPage", function () { + }).on('pageshowready.checktour', "#dashboardPage", function () { var page = this; @@ -1389,7 +1388,7 @@ $(document).on('pagebeforeshowready', "#dashboardPage", DashboardPage.onPageShow (function () { - $(document).on('pagebeforeshowready', ".type-interior", function () { + $(document).on('pageshowready', ".type-interior", function () { var page = this; diff --git a/dashboard-ui/scripts/externalplayer.js b/dashboard-ui/scripts/externalplayer.js index cffa8bbe10..6e255f6315 100644 --- a/dashboard-ui/scripts/externalplayer.js +++ b/dashboard-ui/scripts/externalplayer.js @@ -1,315 +1,301 @@ (function (window, store) { - function getExternalPlayers() { - return JSON.parse(store.getItem('externalplayers') || '[]'); - } + function getDeviceProfile(serverAddress, deviceId, item, startPositionTicks, maxBitrate, mediaSourceId, audioStreamIndex, subtitleStreamIndex) { - function getUrl(player, item) { + var bitrateSetting = AppSettings.maxStreamingBitrate(); - return 'vlc://http://www.google.com'; + var profile = {}; - } + profile.MaxStreamingBitrate = bitrateSetting; + profile.MaxStaticBitrate = 40000000; + profile.MusicStreamingTranscodingBitrate = Math.min(bitrateSetting, 192000); - function getCodecLimits(maxBitrate) { + profile.DirectPlayProfiles = []; - var maxWidth; + profile.DirectPlayProfiles.push({ + Container: 'mkv,mov,mp4,m4v,wmv', + Type: 'Video' + }); - if (maxBitrate <= 1000000) { - maxWidth = 720; - } - else if (maxBitrate <= 5000000) { - maxWidth = 1280; - } else { - maxWidth = 1280; - } + profile.DirectPlayProfiles.push({ + Container: 'aac,mp3,flac,wma', + Type: 'Audio' + }); - return { + profile.TranscodingProfiles = []; - maxVideoAudioChannels: 6, - maxAudioChannels: 2, - maxVideoLevel: 50, - maxWidth: maxWidth, - maxSampleRate: 48000 + profile.TranscodingProfiles.push({ + Container: 'ts', + Type: 'Video', + AudioCodec: 'aac', + VideoCodec: 'h264', + Context: 'Streaming', + Protocol: 'hls' + }); - }; - } + profile.TranscodingProfiles.push({ + Container: 'aac', + Type: 'Audio', + AudioCodec: 'aac', + Context: 'Streaming', + Protocol: 'hls' + }); - function canDirectStream(mediaType, mediaSource, maxBitrate) { + profile.ContainerProfiles = []; - // If bitrate is unknown don't direct stream - if (!mediaSource.Bitrate || mediaSource.Bitrate > maxBitrate) { - return false; - } + var audioConditions = []; - var codecLimits = getCodecLimits(maxBitrate); + var maxAudioChannels = '6'; - if (mediaType == "Audio") { + audioConditions.push({ + Condition: 'LessThanEqual', + Property: 'AudioChannels', + Value: maxAudioChannels + }); - return true; - } - else if (mediaType == "Video") { + profile.CodecProfiles = []; + profile.CodecProfiles.push({ + Type: 'Audio', + Conditions: audioConditions + }); - var videoStream = mediaSource.MediaStreams.filter(function (s) { + profile.CodecProfiles.push({ + Type: 'VideoAudio', + Codec: 'mp3', + Conditions: [{ + Condition: 'LessThanEqual', + Property: 'AudioChannels', + Value: maxAudioChannels + }] + }); - return s.Type == 'Video'; - - })[0]; - - if (!videoStream) { - return false; - } - - if (videoStream.Width && videoStream.Width > codecLimits.maxWidth) { - return false; - } - - if (mediaSource.VideoType != 'VideoFile') { - return false; - } - - return mediaSource.Protocol == 'File'; - } - - throw new Error('Unrecognized MediaType'); - } - - function canPlayAudioStreamDirect(audioStream, isVideo, maxBitrate) { - - var audioCodec = (audioStream.Codec || '').toLowerCase().replace('-', ''); - - if (audioCodec.indexOf('aac') == -1 && - audioCodec.indexOf('mp3') == -1 && - audioCodec.indexOf('mpeg') == -1) { - - return false; - } - - var codecLimits = getCodecLimits(maxBitrate); - - var maxChannels = isVideo ? codecLimits.maxVideoAudioChannels : codecLimits.maxAudioChannels; - - if (!audioStream.Channels || audioStream.Channels > maxChannels) { - return false; - } - - if (!audioStream.SampleRate || audioStream.SampleRate > codecLimits.maxSampleRate) { - return false; - } - - var bitrate = audioStream.BitRate; - if (!bitrate) { - return false; - } - - if (isVideo) { - - if (audioCodec.indexOf('aac') != -1 && bitrate > 768000) { - return false; - } - if (audioCodec.indexOf('mp3') != -1 || audioCodec.indexOf('mpeg') != -1) { - if (bitrate > 320000) { - return false; + profile.CodecProfiles.push({ + Type: 'VideoAudio', + Codec: 'aac', + Conditions: [ + { + Condition: 'LessThanEqual', + Property: 'AudioChannels', + Value: maxAudioChannels } - } + ] + }); - } else { - if (bitrate > 320000) { - return false; - } + profile.CodecProfiles.push({ + Type: 'Video', + Codec: 'h264', + Conditions: [ + { + Condition: 'EqualsAny', + Property: 'VideoProfile', + Value: 'high|main|baseline|constrained baseline' + }, + { + Condition: 'LessThanEqual', + Property: 'VideoLevel', + Value: '41' + }] + }); + + // Subtitle profiles + profile.SubtitleProfiles = []; + profile.ResponseProfiles = []; + + return profile; + } + + function validatePlaybackInfoResult(result) { + + if (result.ErrorCode) { + + MediaController.showPlaybackInfoErrorMessage(result.ErrorCode); + return false; } return true; } - function isSupportedCodec(mediaType, mediaSource) { + function getOptimalMediaSource(mediaType, versions) { - if (mediaType == "Audio") { - return false; - } - else if (mediaType == "Video") { + var optimalVersion = versions.filter(function (v) { - return mediaSource.MediaStreams.filter(function (m) { + v.enableDirectPlay = MediaController.supportsDirectPlay(v); - return m.Type == "Video" && (m.Codec || '').toLowerCase() == 'h264'; + return v.enableDirectPlay; - }).length > 0; + })[0]; + + if (!optimalVersion) { + optimalVersion = versions.filter(function (v) { + + return v.SupportsDirectStream; + + })[0]; } - throw new Error('Unrecognized MediaType'); - } - - function getStreamByIndex(streams, type, index) { - return streams.filter(function (s) { - - return s.Type == type && s.Index == index; - + return optimalVersion || versions.filter(function (s) { + return s.SupportsTranscoding; })[0]; } - function getMediaSourceInfo(item, maxBitrate, mediaSourceId, audioStreamIndex, subtitleStreamIndex) { + var currentMediaSource; + var currentItem; + var basePlayerState; + var progressInterval; - var sources = item.MediaSources.filter(function (m) { + function getVideoStreamInfo(item) { - m.audioStream = mediaSourceId == m.Id && audioStreamIndex != null ? - getStreamByIndex(m.MediaStreams, 'Audio', audioStreamIndex) : - getStreamByIndex(m.MediaStreams, 'Audio', m.DefaultAudioStreamIndex); + var deferred = $.Deferred(); + Dashboard.showModalLoadingMsg(); - if (item.MediaType == "Audio" && !m.audioStream) { - m.audioStream = m.MediaStreams.filter(function (s) { - return s.Type == 'Audio'; - })[0]; + var deviceProfile = getDeviceProfile(); + var startPosition = 0; + + MediaController.getPlaybackInfo(item.Id, deviceProfile, startPosition).done(function (playbackInfoResult) { + + if (validatePlaybackInfoResult(playbackInfoResult)) { + + var mediaSource = getOptimalMediaSource(item.MediaType, playbackInfoResult.MediaSources); + + if (mediaSource) { + + if (mediaSource.RequiresOpening) { + + MediaController.getLiveStream(item.Id, playbackInfoResult.PlaySessionId, deviceProfile, startPosition, mediaSource, null, null).done(function (openLiveStreamResult) { + + openLiveStreamResult.MediaSource.enableDirectPlay = MediaController.supportsDirectPlay(openLiveStreamResult.MediaSource); + + playInternalPostMediaSourceSelection(item, openLiveStreamResult.MediaSource, startPosition, deferred); + }); + + } else { + playInternalPostMediaSourceSelection(item, mediaSource, startPosition, deferred); + } + } else { + Dashboard.hideModalLoadingMsg(); + MediaController.showPlaybackInfoErrorMessage('NoCompatibleStream'); + } } - m.subtitleStream = mediaSourceId == m.Id && subtitleStreamIndex != null ? - getStreamByIndex(m.MediaStreams, 'Subtitle', subtitleStreamIndex) : - getStreamByIndex(m.MediaStreams, 'Subtitle', m.DefaultSubtitleStreamIndex); - - return !mediaSourceId || m.Id == mediaSourceId; - }); - // Find first one that can be direct streamed - var source = sources.filter(function (m) { + return deferred.promise(); + } - var audioStream = m.audioStream; + function playInternalPostMediaSourceSelection(item, mediaSource, startPosition, deferred) { + + Dashboard.hideModalLoadingMsg(); + + currentItem = item; + currentMediaSource = mediaSource; + + basePlayerState = { + PlayState: { - if (!audioStream || !canPlayAudioStreamDirect(audioStream, item.MediaType == 'Video', maxBitrate)) { - return false; } - - if (m.subtitleStream && m.subtitleStream.IsExternal) { - return false; - } - - return canDirectStream(item.MediaType, m, maxBitrate, audioStream); - - })[0]; - - if (source) { - return { - mediaSource: source, - isStatic: true, - streamContainer: source.Container - }; - } - - // Find first one with supported codec - source = sources.filter(function (m) { - - return isSupportedCodec(item.MediaType, m); - - })[0]; - - source = source || sources[0]; - - var container = item.MediaType == 'Audio' ? 'mp3' : 'm3u8'; - - // Default to first one - return { - mediaSource: source, - isStatic: false, - streamContainer: container }; + + var streamInfo = MediaPlayer.createStreamInfo('Video', item, mediaSource, startPosition); + var currentSrc = streamInfo.url; + + var audioStreamIndex = getParameterByName('AudioStreamIndex', currentSrc); + + if (audioStreamIndex) { + basePlayerState.PlayState.AudioStreamIndex = parseInt(audioStreamIndex); + } + basePlayerState.PlayState.SubtitleStreamIndex = self.currentSubtitleStreamIndex; + + basePlayerState.PlayState.PlayMethod = getParameterByName('static', currentSrc) == 'true' ? + 'DirectStream' : + 'Transcode'; + + basePlayerState.PlayState.LiveStreamId = getParameterByName('LiveStreamId', currentSrc); + basePlayerState.PlayState.PlaySessionId = getParameterByName('PlaySessionId', currentSrc); + + basePlayerState.PlayState.MediaSourceId = mediaSource.Id; + basePlayerState.PlayState.CanSeek = false; + basePlayerState.NowPlayingItem = MediaPlayer.getNowPlayingItemForReporting(item, mediaSource); + + deferred.resolveWith(null, [streamInfo]); } - function getStreamInfo(serverAddress, deviceId, item, startPositionTicks, maxBitrate, mediaSourceId, audioStreamIndex, subtitleStreamIndex) { + function getPlayerState(positionTicks) { - var mediaSourceInfo = getMediaSourceInfo(item, maxBitrate, mediaSourceId, audioStreamIndex, subtitleStreamIndex); + var state = basePlayerState; - var url = getStreamUrl(serverAddress, deviceId, item.MediaType, item.Id, mediaSourceInfo, startPositionTicks, maxBitrate); + state.PlayState.PositionTicks = positionTicks; - if (mediaSourceInfo.subtitleStream && mediaSourceInfo.subtitleStream.IsExternal) { - url += "&SubtitleStreamIndex=" + mediaSourceInfo.Index; + return state; + } + + function onPlaybackStart() { + + closePlayMenu(); + + var state = getPlayerState(); + + var info = { + ItemId: state.NowPlayingItem.Id, + NowPlayingItem: state.NowPlayingItem + }; + + info = $.extend(info, state.PlayState); + + ApiClient.reportPlaybackStart(info); + + // This is really just a ping to let the server know we're still playing + progressInterval = setInterval(function () { + onPlaybackProgress(null); + + }, 10000); + + setTimeout(function () { + + showPostPlayMenu(currentItem); + }, 500); + } + + function onPlaybackProgress(positionTicks) { + + var state = getPlayerState(positionTicks); + + var info = { + ItemId: state.NowPlayingItem.Id, + NowPlayingItem: state.NowPlayingItem + }; + + info = $.extend(info, state.PlayState); + + ApiClient.reportPlaybackProgress(info); + } + + function onPlaybackStopped(positionTicks) { + + var state = getPlayerState(positionTicks); + + var stopInfo = { + itemId: state.NowPlayingItem.Id, + mediaSourceId: state.PlayState.MediaSourceId, + positionTicks: state.PlayState.PositionTicks + }; + + if (state.PlayState.LiveStreamId) { + stopInfo.LiveStreamId = state.PlayState.LiveStreamId; } - mediaSourceInfo.url = url; - - return mediaSourceInfo; - } - - function getStreamUrl(serverAddress, deviceId, mediaType, itemId, mediaSourceInfo, startPositionTicks, maxBitrate) { - - var url; - - var codecLimits = getCodecLimits(maxBitrate); - - if (mediaType == 'Audio') { - - url = serverAddress + '/audio/' + itemId + '/stream.' + mediaSourceInfo.streamContainer; - - url += '?mediasourceid=' + mediaSourceInfo.mediaSource.Id; - - if (mediaSourceInfo.isStatic) { - url += '&static=true'; - - } else { - - url += '&maxaudiochannels=' + codecLimits.maxAudioChannels; - - if (startPositionTicks) { - url += '&startTimeTicks=' + startPositionTicks.toString(); - } - - if (maxBitrate) { - url += '&audiobitrate=' + Math.min(maxBitrate, 320000).toString(); - } - - url += '&deviceId=' + deviceId; - } - - return url; - - } - else if (mediaType == 'Video') { - - if (mediaSourceInfo.isStatic) { - url = serverAddress + '/videos/' + itemId + '/stream.' + mediaSourceInfo.streamContainer + '?static=true'; - } - else { - url = serverAddress + '/videos/' + itemId + '/stream.' + mediaSourceInfo.streamContainer + '?static=false'; - } - - url += '&maxaudiochannels=' + codecLimits.maxVideoAudioChannels; - - if (maxBitrate) { - - var audioRate = 320000; - url += '&audiobitrate=' + audioRate.toString(); - url += '&videobitrate=' + (maxBitrate - audioRate).toString(); - } - - url += '&profile=high'; - url += '&level=41'; - - url += '&maxwidth=' + codecLimits.maxWidth; - - url += '&videoCodec=h264'; - url += '&audioCodec=aac'; - - url += '&mediasourceid=' + mediaSourceInfo.mediaSource.Id; - url += '&deviceId=' + deviceId; - - return url; + if (state.PlayState.PlaySessionId) { + stopInfo.PlaySessionId = state.PlayState.PlaySessionId; } - throw new Error('Unrecognized MediaType'); + ApiClient.reportPlaybackStopped(stopInfo); + + if (progressInterval) { + clearInterval(progressInterval); + progressInterval = null; + } } - function getVideoUrl(item) { - - var maxBitrate = AppSettings.maxStreamingBitrate(); - - var info = getStreamInfo(ApiClient.serverAddress(), ApiClient.deviceId(), item, null, maxBitrate); - - return info.url; - } - - function getPlayerUrl(item, player) { - - return player.scheme.replace('{0}', getVideoUrl(item)); - } - - function showPostPlayMenu(item, userId) { + function showPostPlayMenu(item) { $('.externalPlayerPostPlayFlyout').popup("close").remove(); @@ -379,23 +365,24 @@ $('.externalPlayerPostPlayFlyout').popup("close").remove(); - ApiClient.stopActiveEncodings(); + var position = 0; if ($('#radioMarkInProgress', elem).checked()) { var pct = $(".playstateSlider", elem).val(); var ticks = item.RunTimeTicks * (Number(pct) * .01); - ApiClient.markPlayed(userId, item.Id, new Date()); + position = ticks; } else if (autoMarkWatched || $('#radioMarkWatched', elem).checked()) { - ApiClient.markPlayed(userId, item.Id, new Date()); + position = currentMediaSource.RunTimeTicks; } else if ($('#radioMarkUnwatched', elem).checked()) { - ApiClient.markUnplayed(userId, item.Id); + position = 0; } + onPlaybackStopped(position); }); $(".playstateSlider", elem).on("change", function (e) { @@ -417,11 +404,11 @@ $('.externalPlayerFlyout').popup("close").remove(); } - function showMenuForItem(item, userId) { + function showMenuForItem(item, players) { closePlayMenu(); - var html = '
'; + var html = '
'; html += '
'; } - $(elem).html(html).lazyChildren(); - $(elem).createCardMenus(); + $(elem).html(html).lazyChildren().createCardMenus(); }); } @@ -316,8 +314,7 @@ html += '
'; } - $(elem).html(html).lazyChildren(); - $(elem).createCardMenus(); + $(elem).html(html).lazyChildren().createCardMenus(); }); } @@ -406,8 +403,7 @@ }); html += ''; - var elem = $('#channel' + channel.Id + '', page).html(html).lazyChildren().trigger('create'); - $(elem).createCardMenus(); + $('#channel' + channel.Id + '', page).html(html).lazyChildren().trigger('create').createCardMenus(); }); } diff --git a/dashboard-ui/scripts/librarybrowser.js b/dashboard-ui/scripts/librarybrowser.js index 7d0377f1c3..6f93e70d02 100644 --- a/dashboard-ui/scripts/librarybrowser.js +++ b/dashboard-ui/scripts/librarybrowser.js @@ -232,13 +232,20 @@ return html; }, + playInExternalPlayer: function(id) { + + Dashboard.loadExternalPlayer().done(function () { + ExternalPlayer.showMenu(id); + }); + }, + showPlayMenu: function (positionTo, itemId, itemType, isFolder, mediaType, resumePositionTicks, showAddToPlaylist) { - var externalPlayers = ExternalPlayer.getExternalPlayers(); + var externalPlayers = AppSettings.enableExternalPlayers(); if (!resumePositionTicks && mediaType != "Audio" && !isFolder) { - if (!externalPlayers.length || mediaType != "Video") { + if (!externalPlayers || mediaType != "Video") { MediaController.play(itemId); return; } @@ -253,8 +260,8 @@ html += '
  • ' + Globalize.translate('ButtonPlay') + '
  • '; - if (!isFolder && externalPlayers.length) { - html += '
  • ' + Globalize.translate('ButtonPlayExternalPlayer') + '
  • '; + if (!isFolder && externalPlayers) { + html += '
  • ' + Globalize.translate('ButtonPlayExternalPlayer') + '
  • '; } if (resumePositionTicks) { @@ -1105,7 +1112,7 @@ posterWidth = 320; thumbWidth = 320; } - + var dateText; for (var i = 0, length = items.length; i < length; i++) { diff --git a/dashboard-ui/scripts/librarylist.js b/dashboard-ui/scripts/librarylist.js index 5d97699954..3fdefe3d60 100644 --- a/dashboard-ui/scripts/librarylist.js +++ b/dashboard-ui/scripts/librarylist.js @@ -284,7 +284,7 @@ var id = this.getAttribute('data-itemid'); - ExternalPlayer.showMenu(id); + LibraryBrowser.playInExternalPlayer(id); return false; } @@ -420,7 +420,7 @@ } } - if (mediaType == 'Video' && ExternalPlayer.getExternalPlayers().length) { + if (mediaType == 'Video' && AppSettings.enableExternalPlayers()) { html += '
  • ' + Globalize.translate('ButtonPlayExternalPlayer') + '
  • '; } diff --git a/dashboard-ui/scripts/librarymenu.js b/dashboard-ui/scripts/librarymenu.js index 3b2e9194fb..c0ba7db776 100644 --- a/dashboard-ui/scripts/librarymenu.js +++ b/dashboard-ui/scripts/librarymenu.js @@ -30,13 +30,14 @@ html += ''; } - html += ''; - - html += '
    '; + html += ''; + html += '
    '; + html += ''; html += ''; html += ''; html += ''; - html += '
    '; + html += ''; + html += '
    '; html += '