diff --git a/fedora/jellyfin-web.spec b/fedora/jellyfin-web.spec index b8c77f2a1f..1d85e5ae6b 100644 --- a/fedora/jellyfin-web.spec +++ b/fedora/jellyfin-web.spec @@ -11,6 +11,8 @@ Source0: jellyfin-web-%{version}.tar.gz %if 0%{?centos} BuildRequires: yarn +# sadly the yarn RPM at https://dl.yarnpkg.com/rpm/ uses git but doesn't Requires: it +BuildRequires: git %else BuildRequires: nodejs-yarn %endif diff --git a/package.json b/package.json index 203be0749d..5f092d6630 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "gulp-sass": "^4.0.2", "gulp-sourcemaps": "^2.6.5", "gulp-terser": "^1.4.0", - "html-webpack-plugin": "^4.3.0", + "html-webpack-plugin": "^4.4.1", "lazypipe": "^1.0.2", "node-sass": "^4.13.1", "postcss-loader": "^3.0.0", @@ -52,14 +52,15 @@ "stylelint-order": "^4.1.0", "webpack": "^4.44.1", "webpack-merge": "^4.2.2", - "webpack-stream": "^5.2.1" + "webpack-stream": "^6.0.0", + "worker-plugin": "^5.0.0" }, "dependencies": { "alameda": "^1.4.0", "blurhash": "^1.1.3", "classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz", "core-js": "^3.6.5", - "date-fns": "^2.15.0", + "date-fns": "^2.16.0", "epubjs": "^0.3.85", "fast-text-encoding": "^1.0.3", "flv.js": "^1.5.0", @@ -71,15 +72,16 @@ "jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto", "jquery": "^3.5.1", "jstree": "^3.3.10", + "libarchive.js": "^1.3.0", "libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv", - "material-design-icons-iconfont": "^5.0.1", + "material-design-icons-iconfont": "^6.0.1", "native-promise-only": "^0.8.0-a", "page": "^1.11.6", "query-string": "^6.13.1", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.2", "sortablejs": "^1.10.2", - "swiper": "^5.4.5", + "swiper": "^6.1.1", "webcomponents.js": "^0.7.24", "whatwg-fetch": "^3.4.0" }, @@ -178,6 +180,7 @@ "src/plugins/experimentalWarnings/plugin.js", "src/plugins/sessionPlayer/plugin.js", "src/plugins/htmlAudioPlayer/plugin.js", + "src/plugins/comicsPlayer/plugin.js", "src/plugins/chromecastPlayer/plugin.js", "src/components/slideshow/slideshow.js", "src/components/sortmenu/sortmenu.js", diff --git a/src/assets/css/fonts.css b/src/assets/css/fonts.css index cb0da0f80f..6e87f11d9d 100644 --- a/src/assets/css/fonts.css +++ b/src/assets/css/fonts.css @@ -1,7 +1,5 @@ html { font-family: "Noto Sans", sans-serif; - font-size: 93%; - -webkit-text-size-adjust: 100%; text-size-adjust: 100%; -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; @@ -29,7 +27,9 @@ h3 { } .layout-tv { - font-size: 130%; + /* Per WebOS and Tizen guidelines, fonts must be 20px minimum. + This takes the 16px baseline and multiplies it by 1.25 to get 20px. */ + font-size: 125%; } .layout-mobile { diff --git a/src/assets/css/librarybrowser.css b/src/assets/css/librarybrowser.css index 643fb9ca97..c9ee82c8a0 100644 --- a/src/assets/css/librarybrowser.css +++ b/src/assets/css/librarybrowser.css @@ -28,6 +28,10 @@ padding-top: 0 !important; } +.layout-tv .itemDetailPage { + padding-top: 4.2em !important; +} + .standalonePage { padding-top: 4.5em !important; } @@ -163,6 +167,12 @@ transition: background ease-in-out 0.5s; } +.layout-tv .skinHeader { + /* In TV layout, it makes more sense to keep the top bar at the top of the page + Having it follow the view only makes us lose vertical space, while not being focusable */ + position: relative; +} + .hiddenViewMenuBar .skinHeader { display: none; } @@ -447,8 +457,7 @@ height: 26.5vh; } -.layout-desktop .itemBackdrop::after, -.layout-tv .itemBackdrop::after { +.layout-desktop .itemBackdrop::after { content: ""; width: 100%; height: 100%; @@ -456,8 +465,8 @@ display: block; } -.layout-desktop .noBackdrop .itemBackdrop, -.layout-tv .noBackdrop .itemBackdrop { +.layout-tv .itemBackdrop, +.layout-desktop .noBackdrop .itemBackdrop { display: none; } @@ -624,6 +633,10 @@ z-index: 2; } +.layout-tv .detailPagePrimaryContainer { + display: block; +} + .layout-mobile .detailPagePrimaryContainer { display: block; position: relative; @@ -637,12 +650,16 @@ padding-left: 32.45vw; } -.layout-desktop .detailRibbon, -.layout-tv .detailRibbon { +.layout-desktop .detailRibbon { margin-top: -7.2em; height: 7.2em; } +.layout-tv .detailRibbon { + margin-top: 0; + height: inherit; +} + .layout-desktop .noBackdrop .detailRibbon, .layout-tv .noBackdrop .detailRibbon { margin-top: 0; @@ -748,8 +765,7 @@ div.itemDetailGalleryLink.defaultCardBackground { position: relative; } - .layout-desktop .itemBackdrop, - .layout-tv .itemBackdrop { + .layout-desktop .itemBackdrop { height: 40vh; } @@ -775,13 +791,8 @@ div.itemDetailGalleryLink.defaultCardBackground { } .emby-button.detailFloatingButton { - position: absolute; - background-color: rgba(0, 0, 0, 0.5); - z-index: 3; - top: 100%; - left: 90%; - margin: -2.2em 0 0 -2.2em; - padding: 0.4em; + font-size: 1.4em; + margin-right: 0.5em !important; color: rgba(255, 255, 255, 0.76); } @@ -844,7 +855,7 @@ div.itemDetailGalleryLink.defaultCardBackground { -webkit-align-items: center; align-items: center; margin: 0 !important; - padding: 0.5em 0.7em !important; + padding: 0.7em 0.7em !important; } @media all and (min-width: 29em) { @@ -913,10 +924,6 @@ div.itemDetailGalleryLink.defaultCardBackground { } @media all and (min-width: 100em) { - .detailFloatingButton { - display: none !important; - } - .personBackdrop { display: none !important; } @@ -925,6 +932,11 @@ div.itemDetailGalleryLink.defaultCardBackground { font-size: 108%; margin: 1.25em 0; } + + .layout-tv .mainDetailButtons { + font-size: 108%; + margin: 1em 0 1.25em; + } } @media all and (max-width: 50em) { @@ -1140,13 +1152,13 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards { } .layout-tv .padded-top-focusscale { - padding-top: 1em; - margin-top: -1em; + padding-top: 1.5em; + margin-top: -1.5em; } .layout-tv .padded-bottom-focusscale { - padding-bottom: 1em; - margin-bottom: -1em; + padding-bottom: 1.5em; + margin-bottom: -1.5em; } @media all and (min-height: 31.25em) { diff --git a/src/assets/css/videoosd.css b/src/assets/css/videoosd.css index 59a485468d..808915e58b 100644 --- a/src/assets/css/videoosd.css +++ b/src/assets/css/videoosd.css @@ -248,8 +248,6 @@ } @media all and (max-width: 30em) { - .btnFastForward, - .btnRewind, .osdMediaInfo, .osdPoster { display: none !important; diff --git a/src/bundle.js b/src/bundle.js index 5a7ffed075..25810f58ee 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -60,8 +60,8 @@ _define('resize-observer-polyfill', function() { }); // swiper -const swiper = require('swiper/js/swiper'); -require('swiper/css/swiper.min.css'); +const swiper = require('swiper/swiper-bundle'); +require('swiper/swiper-bundle.css'); _define('swiper', function() { return swiper; }); @@ -175,3 +175,9 @@ _define('connectionManagerFactory', function () { _define('appStorage', function () { return apiclient.AppStorage; }); + +// libarchive.js +var libarchive = require('libarchive.js'); +_define('libarchive', function () { + return libarchive; +}); diff --git a/src/components/cardbuilder/card.css b/src/components/cardbuilder/card.css index 74c376e85b..4c046ce984 100644 --- a/src/components/cardbuilder/card.css +++ b/src/components/cardbuilder/card.css @@ -209,6 +209,10 @@ button::-moz-focus-inner { contain: strict; } +.defaultCardBackground { + display: flex; +} + .cardContent:not(.defaultCardBackground) { background-color: transparent; } diff --git a/src/components/channelMapper/channelMapper.js b/src/components/channelMapper/channelMapper.js index e747279693..2cf65e647c 100644 --- a/src/components/channelMapper/channelMapper.js +++ b/src/components/channelMapper/channelMapper.js @@ -24,6 +24,7 @@ export default class channelMapper { tunerChannelId: channelId, providerChannelId: providerChannelId }), + contentType: 'application/json', dataType: 'json' }).then(mapping => { const listItem = dom.parentWithClass(button, 'listItem'); diff --git a/src/components/directorybrowser/directorybrowser.js b/src/components/directorybrowser/directorybrowser.js index 4205e04a4f..3dd3302b28 100644 --- a/src/components/directorybrowser/directorybrowser.js +++ b/src/components/directorybrowser/directorybrowser.js @@ -169,7 +169,8 @@ import 'emby-button'; data: JSON.stringify({ ValidateWriteable: validateWriteable, Path: path - }) + }), + contentType: 'application/json' }).catch(response => { if (response) { if (response.status === 404) { diff --git a/src/components/listview/listview.js b/src/components/listview/listview.js index 11e8e953ae..8fb96f664d 100644 --- a/src/components/listview/listview.js +++ b/src/components/listview/listview.js @@ -370,18 +370,8 @@ import 'emby-playstatebutton'; } } } else { - let showArtist = options.artist === true; - const artistItems = item.ArtistItems; - - if (!showArtist && options.artist !== false) { - if (!artistItems || !artistItems.length) { - showArtist = true; - } else if (artistItems.length > 1 || !containerAlbumArtistIds.includes(artistItems[0].Id)) { - showArtist = true; - } - } - - if (showArtist) { + if (options.artist) { + const artistItems = item.ArtistItems; if (artistItems && item.Type !== 'MusicAlbum') { textlines.push(artistItems.map(a => { return a.Name; diff --git a/src/components/nowPlayingBar/nowPlayingBar.js b/src/components/nowPlayingBar/nowPlayingBar.js index 7aa8c623b3..d2de2f60f2 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.js +++ b/src/components/nowPlayingBar/nowPlayingBar.js @@ -701,7 +701,7 @@ import 'emby-ratingbutton'; const player = this; currentRuntimeTicks = playbackManager.duration(player); - updateTimeDisplay(playbackManager.currentTime(player), currentRuntimeTicks, playbackManager.getBufferedRanges(player)); + updateTimeDisplay(playbackManager.currentTime(player) * 10000, currentRuntimeTicks, playbackManager.getBufferedRanges(player)); } function releaseCurrentPlayer() { diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 3b4099d540..cf21ef53a8 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -1618,11 +1618,7 @@ class PlaybackManager { player = player || self._currentPlayer; if (player && !enableLocalPlaylistManagement(player)) { - if (player.isLocalPlayer) { - return player.seek((ticks || 0) / 10000); - } else { - return player.seek(ticks); - } + return player.seek(ticks); } changeStream(player, ticks); @@ -1631,11 +1627,7 @@ class PlaybackManager { self.seekRelative = function (offsetTicks, player) { player = player || self._currentPlayer; if (player && !enableLocalPlaylistManagement(player) && player.seekRelative) { - if (player.isLocalPlayer) { - return player.seekRelative((ticks || 0) / 10000); - } else { - return player.seekRelative(ticks); - } + return player.seekRelative(ticks); } const ticks = getCurrentTicks(player) + offsetTicks; @@ -3219,7 +3211,7 @@ class PlaybackManager { return player.currentTime(); } - return this.getCurrentTicks(player); + return this.getCurrentTicks(player) / 10000; } nextItem(player = this._currentPlayer) { diff --git a/src/components/remotecontrol/remotecontrol.css b/src/components/remotecontrol/remotecontrol.css index c260799585..1c31b4382b 100644 --- a/src/components/remotecontrol/remotecontrol.css +++ b/src/components/remotecontrol/remotecontrol.css @@ -222,18 +222,10 @@ 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; @@ -362,7 +354,8 @@ border-radius: 0; } - .nowPlayingInfoButtons .btnRepeat { + .nowPlayingInfoButtons .btnRepeat, + .nowPlayingInfoButtons .btnRewind { position: absolute; left: 0; margin-left: 0; @@ -370,7 +363,8 @@ font-size: smaller; } - .nowPlayingInfoButtons .btnShuffleQueue { + .nowPlayingInfoButtons .btnShuffleQueue, + .nowPlayingInfoButtons .btnFastForward { position: absolute; right: 0; margin-right: 0; @@ -468,7 +462,6 @@ } @media all and (max-width: 63em) { - .nowPlayingSecondaryButtons .repeatToggleButton, .nowPlayingInfoButtons .playlist .listItemMediaInfo, .nowPlayingInfoButtons .btnStop { display: none !important; diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index 3216241e7a..a0d158e396 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -134,7 +134,7 @@ function imageUrl(item, options) { function updateNowPlayingInfo(context, state, serverId) { const item = state.NowPlayingItem; const displayName = item ? getNowPlayingNameHtml(item).replace('
', ' - ') : ''; - if (typeof item !== 'undefined') { + if (item) { const nowPlayingServerId = (item.ServerId || serverId); if (item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') { const songName = item.Name; @@ -192,11 +192,11 @@ function updateNowPlayingInfo(context, state, serverId) { context.querySelector('.nowPlayingPageTitle').classList.add('hide'); } - const url = item ? seriesImageUrl(item, { + const url = seriesImageUrl(item, { maxHeight: 300 }) || imageUrl(item, { maxHeight: 300 - }) : null; + }); let contextButton = context.querySelector('.btnToggleContextMenu'); // We remove the previous event listener by replacing the item in each update event @@ -228,18 +228,16 @@ function updateNowPlayingInfo(context, state, serverId) { }); }); setImageUrl(context, state, url); - if (item) { - backdrop.setBackdrops([item]); - apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) { - const userData = fullItem.UserData || {}; - const likes = userData.Likes == null ? '' : userData.Likes; - context.querySelector('.nowPlayingPageUserDataButtonsTitle').innerHTML = ''; - context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = ''; - }); - } else { - backdrop.clearBackdrop(); - context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = ''; - } + backdrop.setBackdrops([item]); + apiClient.getItem(apiClient.getCurrentUserId(), item.Id).then(function (fullItem) { + const userData = fullItem.UserData || {}; + const likes = userData.Likes == null ? '' : userData.Likes; + context.querySelector('.nowPlayingPageUserDataButtonsTitle').innerHTML = ''; + context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = ''; + }); + } else { + backdrop.clearBackdrop(); + context.querySelector('.nowPlayingPageUserDataButtons').innerHTML = ''; } } @@ -332,8 +330,14 @@ export default function () { buttonVisible(context.querySelector('.btnNextTrack'), item != null); buttonVisible(context.querySelector('.btnPreviousTrack'), item != null); if (layoutManager.mobile) { - buttonVisible(context.querySelector('.btnRewind'), false); - buttonVisible(context.querySelector('.btnFastForward'), false); + const playingVideo = playbackManager.isPlayingVideo() && item !== null; + const playingAudio = !playbackManager.isPlayingVideo() && item !== null; + buttonVisible(context.querySelector('.btnRepeat'), playingAudio); + buttonVisible(context.querySelector('.btnShuffleQueue'), playingAudio); + buttonVisible(context.querySelector('.btnRewind'), playingVideo); + buttonVisible(context.querySelector('.btnFastForward'), playingVideo); + buttonVisible(context.querySelector('.nowPlayingSecondaryButtons .btnShuffleQueue'), playingVideo); + buttonVisible(context.querySelector('.nowPlayingSecondaryButtons .btnRepeat'), playingVideo); } else { buttonVisible(context.querySelector('.btnRewind'), item != null); buttonVisible(context.querySelector('.btnFastForward'), item != null); @@ -615,7 +619,7 @@ export default function () { lastUpdateTime = now; const player = this; currentRuntimeTicks = playbackManager.duration(player); - updateTimeDisplay(playbackManager.currentTime(player), currentRuntimeTicks); + updateTimeDisplay(playbackManager.currentTime(player) * 10000, currentRuntimeTicks); } } diff --git a/src/components/serviceworker/notifications.js b/src/components/serviceworker/notifications.js index 339d521bbc..9b50553244 100644 --- a/src/components/serviceworker/notifications.js +++ b/src/components/serviceworker/notifications.js @@ -26,7 +26,8 @@ }); } - window.addEventListener('notificationclick', function (event) { + /* eslint-disable-next-line no-restricted-globals -- self is valid in a serviceworker environment */ + self.addEventListener('notificationclick', function (event) { var notification = event.notification; notification.close(); diff --git a/src/components/slideshow/slideshow.js b/src/components/slideshow/slideshow.js index 82f541a116..6e2d5c379c 100644 --- a/src/components/slideshow/slideshow.js +++ b/src/components/slideshow/slideshow.js @@ -256,7 +256,7 @@ export default function (options) { /** * Handles zoom changes. */ - function onZoomChange(scale, imageEl, slideEl) { + function onZoomChange(swiper, scale, imageEl, slideEl) { const zoomImage = slideEl.querySelector('.swiper-zoom-fakeimg'); if (zoomImage) { diff --git a/src/components/syncPlay/syncPlayManager.js b/src/components/syncPlay/syncPlayManager.js index 2366172a79..26d6cdbad1 100644 --- a/src/components/syncPlay/syncPlayManager.js +++ b/src/components/syncPlay/syncPlayManager.js @@ -741,7 +741,7 @@ class SyncPlayManager { const playAtTime = this.lastCommand.When; - const currentPositionTicks = playbackManager.currentTime(); + const currentPositionTicks = playbackManager.currentTime() * 10000; // Estimate PositionTicks on server const serverPositionTicks = this.lastCommand.PositionTicks + ((currentTime - playAtTime) + this.timeOffsetWithServer) * 10000; // Measure delay that needs to be recovered diff --git a/src/components/upnextdialog/upnextdialog.js b/src/components/upnextdialog/upnextdialog.js index e28bb03abe..69cc6512fe 100644 --- a/src/components/upnextdialog/upnextdialog.js +++ b/src/components/upnextdialog/upnextdialog.js @@ -256,7 +256,7 @@ import 'flexStyles'; const runtimeTicks = playbackManager.duration(options.player); if (runtimeTicks) { - const timeRemainingTicks = runtimeTicks - playbackManager.currentTime(options.player); + const timeRemainingTicks = runtimeTicks - playbackManager.currentTime(options.player) * 10000; return Math.round(timeRemainingTicks / 10000); } diff --git a/src/config.template.json b/src/config.template.json index 9d98e8b6f7..0f308ccc11 100644 --- a/src/config.template.json +++ b/src/config.template.json @@ -28,6 +28,7 @@ "plugins/htmlAudioPlayer/plugin", "plugins/htmlVideoPlayer/plugin", "plugins/photoPlayer/plugin", + "plugins/comicsPlayer/plugin", "plugins/bookPlayer/plugin", "plugins/youtubePlayer/plugin", "plugins/backdropScreensaver/plugin", diff --git a/src/controllers/dashboard/encodingsettings.js b/src/controllers/dashboard/encodingsettings.js index dfad07618c..df36ffdc6f 100644 --- a/src/controllers/dashboard/encodingsettings.js +++ b/src/controllers/dashboard/encodingsettings.js @@ -48,7 +48,8 @@ import libraryMenu from 'libraryMenu'; data: JSON.stringify({ Path: form.querySelector('.txtEncoderPath').value, PathType: 'Custom' - }) + }), + contentType: 'application/json' }).then(Dashboard.processServerConfigurationUpdateResult, onSaveEncodingPathFailure); }); } diff --git a/src/controllers/itemDetails/index.html b/src/controllers/itemDetails/index.html index 3a27f61710..b2604e0341 100644 --- a/src/controllers/itemDetails/index.html +++ b/src/controllers/itemDetails/index.html @@ -7,11 +7,11 @@
-
-
+
+
-
+
-
+

${Schedule}

diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index 5cb027881b..45f0f94d36 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -29,2006 +29,2034 @@ import 'emby-ratingbutton'; import 'emby-scroller'; import 'emby-select'; -/* eslint-disable indent */ +function getPromise(apiClient, params) { + const id = params.id; - function getPromise(apiClient, params) { - const id = params.id; - - if (id) { - return apiClient.getItem(apiClient.getCurrentUserId(), id); - } - - if (params.seriesTimerId) { - return apiClient.getLiveTvSeriesTimer(params.seriesTimerId); - } - - if (params.genre) { - return apiClient.getGenre(params.genre, apiClient.getCurrentUserId()); - } - - if (params.musicgenre) { - return apiClient.getMusicGenre(params.musicgenre, apiClient.getCurrentUserId()); - } - - if (params.musicartist) { - return apiClient.getArtist(params.musicartist, apiClient.getCurrentUserId()); - } - - throw new Error('Invalid request'); + if (id) { + return apiClient.getItem(apiClient.getCurrentUserId(), id); } - function hideAll(page, className, show) { - for (const elem of page.querySelectorAll('.' + className)) { - if (show) { - elem.classList.remove('hide'); - } else { - elem.classList.add('hide'); - } + if (params.seriesTimerId) { + return apiClient.getLiveTvSeriesTimer(params.seriesTimerId); + } + + if (params.genre) { + return apiClient.getGenre(params.genre, apiClient.getCurrentUserId()); + } + + if (params.musicgenre) { + return apiClient.getMusicGenre(params.musicgenre, apiClient.getCurrentUserId()); + } + + if (params.musicartist) { + return apiClient.getArtist(params.musicartist, apiClient.getCurrentUserId()); + } + + throw new Error('Invalid request'); +} + +function hideAll(page, className, show) { + for (const elem of page.querySelectorAll('.' + className)) { + if (show) { + elem.classList.remove('hide'); + } else { + elem.classList.add('hide'); } } +} - function getContextMenuOptions(item, user, button) { - return { - item: item, - open: false, - play: false, - playAllFromHere: false, - queueAllFromHere: false, - positionTo: button, - cancelTimer: false, - record: false, - deleteItem: item.CanDelete === true, - shuffle: false, - instantMix: false, - user: user, - share: true - }; - } +function getContextMenuOptions(item, user, button) { + return { + item: item, + open: false, + play: false, + playAllFromHere: false, + queueAllFromHere: false, + positionTo: button, + cancelTimer: false, + record: false, + deleteItem: item.CanDelete === true, + shuffle: false, + instantMix: false, + user: user, + share: true + }; +} - function getProgramScheduleHtml(items) { - let html = ''; +function getProgramScheduleHtml(items) { + let html = ''; - html += '
'; - html += listView.getListViewHtml({ - items: items, - enableUserDataButtons: false, - image: true, - imageSource: 'channel', - showProgramDateTime: true, - showChannel: false, - mediaInfo: false, - action: 'none', - moreButton: false, - recordButton: false - }); + html += '
'; + html += listView.getListViewHtml({ + items: items, + enableUserDataButtons: false, + image: true, + imageSource: 'channel', + showProgramDateTime: true, + showChannel: false, + mediaInfo: false, + action: 'none', + moreButton: false, + recordButton: false + }); - html += '
'; + html += '
'; - return html; - } + return html; +} - function renderSeriesTimerSchedule(page, apiClient, seriesTimerId) { - apiClient.getLiveTvTimers({ - UserId: apiClient.getCurrentUserId(), - ImageTypeLimit: 1, - EnableImageTypes: 'Primary,Backdrop,Thumb', - SortBy: 'StartDate', - EnableTotalRecordCount: false, - EnableUserData: false, - SeriesTimerId: seriesTimerId, - Fields: 'ChannelInfo,ChannelImage' - }).then(function (result) { - if (result.Items.length && result.Items[0].SeriesTimerId != seriesTimerId) { - result.Items = []; - } - - const html = getProgramScheduleHtml(result.Items); - const scheduleTab = page.querySelector('.seriesTimerSchedule'); - scheduleTab.innerHTML = html; - imageLoader.lazyChildren(scheduleTab); - }); - } - - function renderTimerEditor(page, item, apiClient, user) { - if (item.Type !== 'Recording' || !user.Policy.EnableLiveTvManagement || !item.TimerId || item.Status !== 'InProgress') { - return void hideAll(page, 'btnCancelTimer'); +function renderSeriesTimerSchedule(page, apiClient, seriesTimerId) { + apiClient.getLiveTvTimers({ + UserId: apiClient.getCurrentUserId(), + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Thumb', + SortBy: 'StartDate', + EnableTotalRecordCount: false, + EnableUserData: false, + SeriesTimerId: seriesTimerId, + Fields: 'ChannelInfo,ChannelImage' + }).then(function (result) { + if (result.Items.length && result.Items[0].SeriesTimerId != seriesTimerId) { + result.Items = []; } - hideAll(page, 'btnCancelTimer', true); + const html = getProgramScheduleHtml(result.Items); + const scheduleTab = page.querySelector('.seriesTimerSchedule'); + scheduleTab.innerHTML = html; + imageLoader.lazyChildren(scheduleTab); + }); +} + +function renderTimerEditor(page, item, apiClient, user) { + if (item.Type !== 'Recording' || !user.Policy.EnableLiveTvManagement || !item.TimerId || item.Status !== 'InProgress') { + return void hideAll(page, 'btnCancelTimer'); } - function renderSeriesTimerEditor(page, item, apiClient, user) { - if (item.Type !== 'SeriesTimer') { - return void hideAll(page, 'btnCancelSeriesTimer'); - } + hideAll(page, 'btnCancelTimer', true); +} - if (user.Policy.EnableLiveTvManagement) { - import('seriesRecordingEditor').then(({default: seriesRecordingEditor}) => { - seriesRecordingEditor.embed(item, apiClient.serverId(), { - context: page.querySelector('.seriesRecordingEditor') - }); - }); - - page.querySelector('.seriesTimerScheduleSection').classList.remove('hide'); - hideAll(page, 'btnCancelSeriesTimer', true); - return void renderSeriesTimerSchedule(page, apiClient, item.Id); - } - - page.querySelector('.seriesTimerScheduleSection').classList.add('hide'); +function renderSeriesTimerEditor(page, item, apiClient, user) { + if (item.Type !== 'SeriesTimer') { return void hideAll(page, 'btnCancelSeriesTimer'); } - function renderTrackSelections(page, instance, item, forceReload) { - const select = page.querySelector('.selectSource'); - - if (!item.MediaSources || !itemHelper.supportsMediaSourceSelection(item) || playbackManager.getSupportedCommands().indexOf('PlayMediaSource') === -1 || !playbackManager.canPlay(item)) { - page.querySelector('.trackSelections').classList.add('hide'); - select.innerHTML = ''; - page.querySelector('.selectVideo').innerHTML = ''; - page.querySelector('.selectAudio').innerHTML = ''; - page.querySelector('.selectSubtitles').innerHTML = ''; - return; - } - - const mediaSources = item.MediaSources; - instance._currentPlaybackMediaSources = mediaSources; - - page.querySelector('.trackSelections').classList.remove('hide'); - select.setLabel(globalize.translate('LabelVersion')); - - const currentValue = select.value; - - const selectedId = mediaSources[0].Id; - select.innerHTML = mediaSources.map(function (v) { - const selected = v.Id === selectedId ? ' selected' : ''; - return ''; - }).join(''); - - if (mediaSources.length > 1) { - page.querySelector('.selectSourceContainer').classList.remove('hide'); - } else { - page.querySelector('.selectSourceContainer').classList.add('hide'); - } - - if (select.value !== currentValue || forceReload) { - renderVideoSelections(page, mediaSources); - renderAudioSelections(page, mediaSources); - renderSubtitleSelections(page, mediaSources); - } - } - - function renderVideoSelections(page, mediaSources) { - const mediaSourceId = page.querySelector('.selectSource').value; - const mediaSource = mediaSources.filter(function (m) { - return m.Id === mediaSourceId; - })[0]; - - const tracks = mediaSource.MediaStreams.filter(function (m) { - return m.Type === 'Video'; + if (user.Policy.EnableLiveTvManagement) { + import('seriesRecordingEditor').then(({ default: seriesRecordingEditor }) => { + seriesRecordingEditor.embed(item, apiClient.serverId(), { + context: page.querySelector('.seriesRecordingEditor') + }); }); - const select = page.querySelector('.selectVideo'); - select.setLabel(globalize.translate('Video')); - const selectedId = tracks.length ? tracks[0].Index : -1; - select.innerHTML = tracks.map(function (v) { - const selected = v.Index === selectedId ? ' selected' : ''; - const titleParts = []; - const resolutionText = mediaInfo.getResolutionText(v); + page.querySelector('.seriesTimerScheduleSection').classList.remove('hide'); + hideAll(page, 'btnCancelSeriesTimer', true); + return void renderSeriesTimerSchedule(page, apiClient, item.Id); + } - if (resolutionText) { - titleParts.push(resolutionText); - } + page.querySelector('.seriesTimerScheduleSection').classList.add('hide'); + return void hideAll(page, 'btnCancelSeriesTimer'); +} - if (v.Codec) { - titleParts.push(v.Codec.toUpperCase()); - } +function renderTrackSelections(page, instance, item, forceReload) { + const select = page.querySelector('.selectSource'); - return ''; - }).join(''); + if (!item.MediaSources || !itemHelper.supportsMediaSourceSelection(item) || playbackManager.getSupportedCommands().indexOf('PlayMediaSource') === -1 || !playbackManager.canPlay(item)) { + page.querySelector('.trackSelections').classList.add('hide'); + select.innerHTML = ''; + page.querySelector('.selectVideo').innerHTML = ''; + page.querySelector('.selectAudio').innerHTML = ''; + page.querySelector('.selectSubtitles').innerHTML = ''; + return; + } + + const mediaSources = item.MediaSources; + instance._currentPlaybackMediaSources = mediaSources; + + page.querySelector('.trackSelections').classList.remove('hide'); + select.setLabel(globalize.translate('LabelVersion')); + + const currentValue = select.value; + + const selectedId = mediaSources[0].Id; + select.innerHTML = mediaSources.map(function (v) { + const selected = v.Id === selectedId ? ' selected' : ''; + return ''; + }).join(''); + + if (mediaSources.length > 1) { + page.querySelector('.selectSourceContainer').classList.remove('hide'); + } else { + page.querySelector('.selectSourceContainer').classList.add('hide'); + } + + if (select.value !== currentValue || forceReload) { + renderVideoSelections(page, mediaSources); + renderAudioSelections(page, mediaSources); + renderSubtitleSelections(page, mediaSources); + } +} + +function renderVideoSelections(page, mediaSources) { + const mediaSourceId = page.querySelector('.selectSource').value; + const mediaSource = mediaSources.filter(function (m) { + return m.Id === mediaSourceId; + })[0]; + + const tracks = mediaSource.MediaStreams.filter(function (m) { + return m.Type === 'Video'; + }); + + const select = page.querySelector('.selectVideo'); + select.setLabel(globalize.translate('Video')); + const selectedId = tracks.length ? tracks[0].Index : -1; + select.innerHTML = tracks.map(function (v) { + const selected = v.Index === selectedId ? ' selected' : ''; + const titleParts = []; + const resolutionText = mediaInfo.getResolutionText(v); + + if (resolutionText) { + titleParts.push(resolutionText); + } + + if (v.Codec) { + titleParts.push(v.Codec.toUpperCase()); + } + + return ''; + }).join(''); + select.setAttribute('disabled', 'disabled'); + + if (tracks.length) { + page.querySelector('.selectVideoContainer').classList.remove('hide'); + } else { + page.querySelector('.selectVideoContainer').classList.add('hide'); + } +} + +function renderAudioSelections(page, mediaSources) { + const mediaSourceId = page.querySelector('.selectSource').value; + const mediaSource = mediaSources.filter(function (m) { + return m.Id === mediaSourceId; + })[0]; + const tracks = mediaSource.MediaStreams.filter(function (m) { + return m.Type === 'Audio'; + }); + const select = page.querySelector('.selectAudio'); + select.setLabel(globalize.translate('Audio')); + const selectedId = mediaSource.DefaultAudioStreamIndex; + select.innerHTML = tracks.map(function (v) { + const selected = v.Index === selectedId ? ' selected' : ''; + return ''; + }).join(''); + + if (tracks.length > 1) { + select.removeAttribute('disabled'); + } else { select.setAttribute('disabled', 'disabled'); - - if (tracks.length) { - page.querySelector('.selectVideoContainer').classList.remove('hide'); - } else { - page.querySelector('.selectVideoContainer').classList.add('hide'); - } } - function renderAudioSelections(page, mediaSources) { - const mediaSourceId = page.querySelector('.selectSource').value; - const mediaSource = mediaSources.filter(function (m) { - return m.Id === mediaSourceId; - })[0]; - const tracks = mediaSource.MediaStreams.filter(function (m) { - return m.Type === 'Audio'; - }); - const select = page.querySelector('.selectAudio'); - select.setLabel(globalize.translate('Audio')); - const selectedId = mediaSource.DefaultAudioStreamIndex; - select.innerHTML = tracks.map(function (v) { - const selected = v.Index === selectedId ? ' selected' : ''; + if (tracks.length) { + page.querySelector('.selectAudioContainer').classList.remove('hide'); + } else { + page.querySelector('.selectAudioContainer').classList.add('hide'); + } +} + +function renderSubtitleSelections(page, mediaSources) { + const mediaSourceId = page.querySelector('.selectSource').value; + const mediaSource = mediaSources.filter(function (m) { + return m.Id === mediaSourceId; + })[0]; + const tracks = mediaSource.MediaStreams.filter(function (m) { + return m.Type === 'Subtitle'; + }); + const select = page.querySelector('.selectSubtitles'); + select.setLabel(globalize.translate('Subtitles')); + const selectedId = mediaSource.DefaultSubtitleStreamIndex == null ? -1 : mediaSource.DefaultSubtitleStreamIndex; + + const videoTracks = mediaSource.MediaStreams.filter(function (m) { + return m.Type === 'Video'; + }); + + // This only makes sense on Video items + if (videoTracks.length) { + let selected = selectedId === -1 ? ' selected' : ''; + select.innerHTML = '' + tracks.map(function (v) { + selected = v.Index === selectedId ? ' selected' : ''; return ''; }).join(''); - if (tracks.length > 1) { + if (tracks.length > 0) { select.removeAttribute('disabled'); } else { select.setAttribute('disabled', 'disabled'); } - if (tracks.length) { - page.querySelector('.selectAudioContainer').classList.remove('hide'); - } else { - page.querySelector('.selectAudioContainer').classList.add('hide'); - } + page.querySelector('.selectSubtitlesContainer').classList.remove('hide'); + } else { + select.innerHTML = ''; + page.querySelector('.selectSubtitlesContainer').classList.add('hide'); } +} - function renderSubtitleSelections(page, mediaSources) { - const mediaSourceId = page.querySelector('.selectSource').value; - const mediaSource = mediaSources.filter(function (m) { - return m.Id === mediaSourceId; - })[0]; - const tracks = mediaSource.MediaStreams.filter(function (m) { - return m.Type === 'Subtitle'; - }); - const select = page.querySelector('.selectSubtitles'); - select.setLabel(globalize.translate('Subtitles')); - const selectedId = mediaSource.DefaultSubtitleStreamIndex == null ? -1 : mediaSource.DefaultSubtitleStreamIndex; +function reloadPlayButtons(page, item) { + let canPlay = false; - const videoTracks = mediaSource.MediaStreams.filter(function (m) { - return m.Type === 'Video'; - }); + if (item.Type == 'Program') { + const now = new Date(); - // This only makes sense on Video items - if (videoTracks.length) { - let selected = selectedId === -1 ? ' selected' : ''; - select.innerHTML = '' + tracks.map(function (v) { - selected = v.Index === selectedId ? ' selected' : ''; - return ''; - }).join(''); - - if (tracks.length > 0) { - select.removeAttribute('disabled'); - } else { - select.setAttribute('disabled', 'disabled'); - } - - page.querySelector('.selectSubtitlesContainer').classList.remove('hide'); - } else { - select.innerHTML = ''; - page.querySelector('.selectSubtitlesContainer').classList.add('hide'); - } - } - - function reloadPlayButtons(page, item) { - let canPlay = false; - - if (item.Type == 'Program') { - const now = new Date(); - - if (now >= datetime.parseISO8601Date(item.StartDate, true) && now < datetime.parseISO8601Date(item.EndDate, true)) { - hideAll(page, 'btnPlay', true); - canPlay = true; - } else { - hideAll(page, 'btnPlay'); - } - - hideAll(page, 'btnResume'); - hideAll(page, 'btnInstantMix'); - hideAll(page, 'btnShuffle'); - } else if (playbackManager.canPlay(item)) { + if (now >= datetime.parseISO8601Date(item.StartDate, true) && now < datetime.parseISO8601Date(item.EndDate, true)) { hideAll(page, 'btnPlay', true); - const enableInstantMix = ['Audio', 'MusicAlbum', 'MusicGenre', 'MusicArtist'].indexOf(item.Type) !== -1; - hideAll(page, 'btnInstantMix', enableInstantMix); - const enableShuffle = item.IsFolder || ['MusicAlbum', 'MusicGenre', 'MusicArtist'].indexOf(item.Type) !== -1; - hideAll(page, 'btnShuffle', enableShuffle); canPlay = true; - - const isResumable = item.UserData && item.UserData.PlaybackPositionTicks > 0; - hideAll(page, 'btnResume', isResumable); - - if (isResumable) { - for (const elem of page.querySelectorAll('.btnPlay')) { - elem.querySelector('.detailButton-icon').classList.replace('play_arrow', 'replay'); - } - } } else { hideAll(page, 'btnPlay'); - hideAll(page, 'btnResume'); - hideAll(page, 'btnInstantMix'); - hideAll(page, 'btnShuffle'); } - return canPlay; + hideAll(page, 'btnResume'); + hideAll(page, 'btnInstantMix'); + hideAll(page, 'btnShuffle'); + } else if (playbackManager.canPlay(item)) { + hideAll(page, 'btnPlay', true); + const enableInstantMix = ['Audio', 'MusicAlbum', 'MusicGenre', 'MusicArtist'].indexOf(item.Type) !== -1; + hideAll(page, 'btnInstantMix', enableInstantMix); + const enableShuffle = item.IsFolder || ['MusicAlbum', 'MusicGenre', 'MusicArtist'].indexOf(item.Type) !== -1; + hideAll(page, 'btnShuffle', enableShuffle); + canPlay = true; + + const isResumable = item.UserData && item.UserData.PlaybackPositionTicks > 0; + hideAll(page, 'btnResume', isResumable); + + if (isResumable) { + for (const elem of page.querySelectorAll('.btnPlay')) { + elem.querySelector('.detailButton-icon').classList.replace('play_arrow', 'replay'); + } + } + } else { + hideAll(page, 'btnPlay'); + hideAll(page, 'btnResume'); + hideAll(page, 'btnInstantMix'); + hideAll(page, 'btnShuffle'); } - function reloadUserDataButtons(page, item) { - let i; - let length; - const btnPlaystates = page.querySelectorAll('.btnPlaystate'); + return canPlay; +} - for (i = 0, length = btnPlaystates.length; i < length; i++) { - const btnPlaystate = btnPlaystates[i]; +function reloadUserDataButtons(page, item) { + let i; + let length; + const btnPlaystates = page.querySelectorAll('.btnPlaystate'); - if (itemHelper.canMarkPlayed(item)) { - btnPlaystate.classList.remove('hide'); - btnPlaystate.setItem(item); - } else { - btnPlaystate.classList.add('hide'); - btnPlaystate.setItem(null); - } - } + for (i = 0, length = btnPlaystates.length; i < length; i++) { + const btnPlaystate = btnPlaystates[i]; - const btnUserRatings = page.querySelectorAll('.btnUserRating'); - - for (i = 0, length = btnUserRatings.length; i < length; i++) { - const btnUserRating = btnUserRatings[i]; - - if (itemHelper.canRate(item)) { - btnUserRating.classList.remove('hide'); - btnUserRating.setItem(item); - } else { - btnUserRating.classList.add('hide'); - btnUserRating.setItem(null); - } + if (itemHelper.canMarkPlayed(item)) { + btnPlaystate.classList.remove('hide'); + btnPlaystate.setItem(item); + } else { + btnPlaystate.classList.add('hide'); + btnPlaystate.setItem(null); } } - function getArtistLinksHtml(artists, serverId, context) { - const html = []; + const btnUserRatings = page.querySelectorAll('.btnUserRating'); - for (const artist of artists) { - const href = appRouter.getRouteUrl(artist, { - context: context, - itemType: 'MusicArtist', - serverId: serverId - }); - html.push('' + artist.Name + ''); + for (i = 0, length = btnUserRatings.length; i < length; i++) { + const btnUserRating = btnUserRatings[i]; + + if (itemHelper.canRate(item)) { + btnUserRating.classList.remove('hide'); + btnUserRating.setItem(item); + } else { + btnUserRating.classList.add('hide'); + btnUserRating.setItem(null); } - - return html.join(' / '); } +} - /** - * Renders the item's name block - * @param {Object} item - Item used to render the name. - * @param {HTMLDivElement} container - Container to render the information into. - * @param {Object} context - Application context. - */ - function renderName(item, container, context) { - let parentRoute; - const parentNameHtml = []; - let parentNameLast = false; +function getArtistLinksHtml(artists, serverId, context) { + const html = []; - if (item.AlbumArtists) { - parentNameHtml.push(getArtistLinksHtml(item.AlbumArtists, item.ServerId, context)); - parentNameLast = true; - } else if (item.ArtistItems && item.ArtistItems.length && item.Type === 'MusicVideo') { - parentNameHtml.push(getArtistLinksHtml(item.ArtistItems, item.ServerId, context)); - parentNameLast = true; - } else if (item.SeriesName && item.Type === 'Episode') { - parentRoute = appRouter.getRouteUrl({ - Id: item.SeriesId, - Name: item.SeriesName, - Type: 'Series', - IsFolder: true, - ServerId: item.ServerId - }, { - context: context - }); - parentNameHtml.push('' + item.SeriesName + ''); - } else if (item.IsSeries || item.EpisodeTitle) { - parentNameHtml.push(item.Name); - } - - if (item.SeriesName && item.Type === 'Season') { - parentRoute = appRouter.getRouteUrl({ - Id: item.SeriesId, - Name: item.SeriesName, - Type: 'Series', - IsFolder: true, - ServerId: item.ServerId - }, { - context: context - }); - parentNameHtml.push('' + item.SeriesName + ''); - } else if (item.ParentIndexNumber != null && item.Type === 'Episode') { - parentRoute = appRouter.getRouteUrl({ - Id: item.SeasonId, - Name: item.SeasonName, - Type: 'Season', - IsFolder: true, - ServerId: item.ServerId - }, { - context: context - }); - parentNameHtml.push('' + item.SeasonName + ''); - } else if (item.ParentIndexNumber != null && item.IsSeries) { - parentNameHtml.push(item.SeasonName || 'S' + item.ParentIndexNumber); - } else if (item.Album && item.AlbumId && (item.Type === 'MusicVideo' || item.Type === 'Audio')) { - parentRoute = appRouter.getRouteUrl({ - Id: item.AlbumId, - Name: item.Album, - Type: 'MusicAlbum', - IsFolder: true, - ServerId: item.ServerId - }, { - context: context - }); - parentNameHtml.push('' + item.Album + ''); - } else if (item.Album) { - parentNameHtml.push(item.Album); - } - - // FIXME: This whole section needs some refactoring, so it becames easier to scale across all form factors. See GH #1022 - let html = ''; - const tvShowHtml = parentNameHtml[0]; - const tvSeasonHtml = parentNameHtml[1]; - - if (parentNameHtml.length) { - if (parentNameLast) { - // Music - if (layoutManager.mobile) { - html = '

' + parentNameHtml.join('
') + '

'; - } else { - html = '

' + parentNameHtml.join(' - ') + '

'; - } - } else { - html = '

' + tvShowHtml + '

'; - } - } - - const name = itemHelper.getDisplayName(item, { - includeParentInfo: false + for (const artist of artists) { + const href = appRouter.getRouteUrl(artist, { + context: context, + itemType: 'MusicArtist', + serverId: serverId }); + html.push('' + artist.Name + ''); + } - if (html && !parentNameLast) { - if (tvSeasonHtml) { - html += '

' + tvSeasonHtml + ' - ' + name + '

'; + return html.join(' / '); +} + +/** + * Renders the item's name block + * @param {Object} item - Item used to render the name. + * @param {HTMLDivElement} container - Container to render the information into. + * @param {Object} context - Application context. + */ +function renderName(item, container, context) { + let parentRoute; + const parentNameHtml = []; + let parentNameLast = false; + + if (item.AlbumArtists) { + parentNameHtml.push(getArtistLinksHtml(item.AlbumArtists, item.ServerId, context)); + parentNameLast = true; + } else if (item.ArtistItems && item.ArtistItems.length && item.Type === 'MusicVideo') { + parentNameHtml.push(getArtistLinksHtml(item.ArtistItems, item.ServerId, context)); + parentNameLast = true; + } else if (item.SeriesName && item.Type === 'Episode') { + parentRoute = appRouter.getRouteUrl({ + Id: item.SeriesId, + Name: item.SeriesName, + Type: 'Series', + IsFolder: true, + ServerId: item.ServerId + }, { + context: context + }); + parentNameHtml.push('' + item.SeriesName + ''); + } else if (item.IsSeries || item.EpisodeTitle) { + parentNameHtml.push(item.Name); + } + + if (item.SeriesName && item.Type === 'Season') { + parentRoute = appRouter.getRouteUrl({ + Id: item.SeriesId, + Name: item.SeriesName, + Type: 'Series', + IsFolder: true, + ServerId: item.ServerId + }, { + context: context + }); + parentNameHtml.push('' + item.SeriesName + ''); + } else if (item.ParentIndexNumber != null && item.Type === 'Episode') { + parentRoute = appRouter.getRouteUrl({ + Id: item.SeasonId, + Name: item.SeasonName, + Type: 'Season', + IsFolder: true, + ServerId: item.ServerId + }, { + context: context + }); + parentNameHtml.push('' + item.SeasonName + ''); + } else if (item.ParentIndexNumber != null && item.IsSeries) { + parentNameHtml.push(item.SeasonName || 'S' + item.ParentIndexNumber); + } else if (item.Album && item.AlbumId && (item.Type === 'MusicVideo' || item.Type === 'Audio')) { + parentRoute = appRouter.getRouteUrl({ + Id: item.AlbumId, + Name: item.Album, + Type: 'MusicAlbum', + IsFolder: true, + ServerId: item.ServerId + }, { + context: context + }); + parentNameHtml.push('' + item.Album + ''); + } else if (item.Album) { + parentNameHtml.push(item.Album); + } + + // FIXME: This whole section needs some refactoring, so it becames easier to scale across all form factors. See GH #1022 + let html = ''; + const tvShowHtml = parentNameHtml[0]; + const tvSeasonHtml = parentNameHtml[1]; + + if (parentNameHtml.length) { + if (parentNameLast) { + // Music + if (layoutManager.mobile) { + html = '

' + parentNameHtml.join('
') + '

'; } else { - html += '

' + name + '

'; + html = '

' + parentNameHtml.join(' - ') + '

'; } - } else if (item.OriginalTitle && item.OriginalTitle != item.Name) { - html = '

' + name + '

' + html; } else { - html = '

' + name + '

' + html; - } - - if (item.OriginalTitle && item.OriginalTitle != item.Name) { - html += '

' + item.OriginalTitle + '

'; - } - - container.innerHTML = html; - - if (html.length) { - container.classList.remove('hide'); - } else { - container.classList.add('hide'); + html = '

' + tvShowHtml + '

'; } } - function setTrailerButtonVisibility(page, item) { - if ((item.LocalTrailerCount || item.RemoteTrailers && item.RemoteTrailers.length) && playbackManager.getSupportedCommands().indexOf('PlayTrailers') !== -1) { - hideAll(page, 'btnPlayTrailer', true); + const name = itemHelper.getDisplayName(item, { + includeParentInfo: false + }); + + if (html && !parentNameLast) { + if (tvSeasonHtml) { + html += '

' + tvSeasonHtml + ' - ' + name + '

'; } else { - hideAll(page, 'btnPlayTrailer'); + html += '

' + name + '

'; } + } else if (item.OriginalTitle && item.OriginalTitle != item.Name) { + html = '

' + name + '

' + html; + } else { + html = '

' + name + '

' + html; } - function renderBackdrop(item) { - if (dom.getWindowSize().innerWidth >= 1000) { - backdrop.setBackdrops([item]); - } else { - backdrop.clearBackdrop(); - } + if (item.OriginalTitle && item.OriginalTitle != item.Name) { + html += '

' + item.OriginalTitle + '

'; } - function renderDetailPageBackdrop(page, item, apiClient) { - let imgUrl; - let hasbackdrop = false; - const itemBackdropElement = page.querySelector('#itemBackdrop'); + container.innerHTML = html; - if (!layoutManager.mobile && !userSettings.detailsBanner()) { - return false; - } + if (html.length) { + container.classList.remove('hide'); + } else { + container.classList.add('hide'); + } +} - if (item.BackdropImageTags && item.BackdropImageTags.length) { - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Backdrop', - maxWidth: dom.getScreenWidth(), - index: 0, - tag: item.BackdropImageTags[0] - }); - imageLoader.lazyImage(itemBackdropElement, imgUrl); - hasbackdrop = true; - } else if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) { - imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, { - type: 'Backdrop', - maxWidth: dom.getScreenWidth(), - index: 0, - tag: item.ParentBackdropImageTags[0] - }); - imageLoader.lazyImage(itemBackdropElement, imgUrl); - hasbackdrop = true; - } else if (item.ImageTags && item.ImageTags.Primary) { - imgUrl = apiClient.getScaledImageUrl(item.Id, { - type: 'Primary', - maxWidth: dom.getScreenWidth(), - tag: item.ImageTags.Primary - }); - imageLoader.lazyImage(itemBackdropElement, imgUrl); - hasbackdrop = true; - } else { - itemBackdropElement.style.backgroundImage = ''; - } +function setTrailerButtonVisibility(page, item) { + if ((item.LocalTrailerCount || item.RemoteTrailers && item.RemoteTrailers.length) && playbackManager.getSupportedCommands().indexOf('PlayTrailers') !== -1) { + hideAll(page, 'btnPlayTrailer', true); + } else { + hideAll(page, 'btnPlayTrailer'); + } +} - return hasbackdrop; +function renderBackdrop(item) { + if (dom.getWindowSize().innerWidth >= 1000) { + backdrop.setBackdrops([item]); + } else { + backdrop.clearBackdrop(); + } +} + +function renderDetailPageBackdrop(page, item, apiClient) { + let imgUrl; + let hasbackdrop = false; + const itemBackdropElement = page.querySelector('#itemBackdrop'); + + if (!layoutManager.mobile && !userSettings.detailsBanner()) { + return false; } - function reloadFromItem(instance, page, params, item, user) { - const apiClient = connectionManager.getApiClient(item.ServerId); + if (item.BackdropImageTags && item.BackdropImageTags.length) { + imgUrl = apiClient.getScaledImageUrl(item.Id, { + type: 'Backdrop', + maxWidth: dom.getScreenWidth(), + index: 0, + tag: item.BackdropImageTags[0] + }); + imageLoader.lazyImage(itemBackdropElement, imgUrl); + hasbackdrop = true; + } else if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) { + imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, { + type: 'Backdrop', + maxWidth: dom.getScreenWidth(), + index: 0, + tag: item.ParentBackdropImageTags[0] + }); + imageLoader.lazyImage(itemBackdropElement, imgUrl); + hasbackdrop = true; + } else if (item.ImageTags && item.ImageTags.Primary) { + imgUrl = apiClient.getScaledImageUrl(item.Id, { + type: 'Primary', + maxWidth: dom.getScreenWidth(), + tag: item.ImageTags.Primary + }); + imageLoader.lazyImage(itemBackdropElement, imgUrl); + hasbackdrop = true; + } else { + itemBackdropElement.style.backgroundImage = ''; + } - Emby.Page.setTitle(''); + return hasbackdrop; +} - // Start rendering the artwork first - renderImage(page, item); +function reloadFromItem(instance, page, params, item, user) { + const apiClient = connectionManager.getApiClient(item.ServerId); + + Emby.Page.setTitle(''); + + // Start rendering the artwork first + renderImage(page, item); + // Save some screen real estate in TV mode + if (!layoutManager.tv) { renderLogo(page, item, apiClient); - renderBackdrop(item); renderDetailPageBackdrop(page, item, apiClient); + } + renderBackdrop(item); - // Render the main information for the item - page.querySelector('.detailPagePrimaryContainer').classList.add('detailRibbon'); - renderName(item, page.querySelector('.nameContainer'), params.context); - renderDetails(page, item, apiClient, params.context); - renderTrackSelections(page, instance, item); + // Render the main information for the item + page.querySelector('.detailPagePrimaryContainer').classList.add('detailRibbon'); + renderName(item, page.querySelector('.nameContainer'), params.context); + renderDetails(page, item, apiClient, params.context); + renderTrackSelections(page, instance, item); - renderSeriesTimerEditor(page, item, apiClient, user); - renderTimerEditor(page, item, apiClient, user); - setInitialCollapsibleState(page, item, apiClient, params.context, user); - const canPlay = reloadPlayButtons(page, item); + renderSeriesTimerEditor(page, item, apiClient, user); + renderTimerEditor(page, item, apiClient, user); + setInitialCollapsibleState(page, item, apiClient, params.context, user); + const canPlay = reloadPlayButtons(page, item); - if ((item.LocalTrailerCount || item.RemoteTrailers && item.RemoteTrailers.length) && playbackManager.getSupportedCommands().indexOf('PlayTrailers') !== -1) { - hideAll(page, 'btnPlayTrailer', true); - } else { - hideAll(page, 'btnPlayTrailer'); - } + if ((item.LocalTrailerCount || item.RemoteTrailers && item.RemoteTrailers.length) && playbackManager.getSupportedCommands().indexOf('PlayTrailers') !== -1) { + hideAll(page, 'btnPlayTrailer', true); + } else { + hideAll(page, 'btnPlayTrailer'); + } - setTrailerButtonVisibility(page, item); + setTrailerButtonVisibility(page, item); - if (item.Type !== 'Program' || canPlay) { - hideAll(page, 'mainDetailButtons', true); - } else { - hideAll(page, 'mainDetailButtons'); - } + if (item.Type !== 'Program' || canPlay) { + hideAll(page, 'mainDetailButtons', true); + } else { + hideAll(page, 'mainDetailButtons'); + } - showRecordingFields(instance, page, item, user); - const groupedVersions = (item.MediaSources || []).filter(function (g) { - return g.Type == 'Grouping'; - }); + showRecordingFields(instance, page, item, user); + const groupedVersions = (item.MediaSources || []).filter(function (g) { + return g.Type == 'Grouping'; + }); - if (user.Policy.IsAdministrator && groupedVersions.length) { - page.querySelector('.btnSplitVersions').classList.remove('hide'); - } else { - page.querySelector('.btnSplitVersions').classList.add('hide'); - } + if (user.Policy.IsAdministrator && groupedVersions.length) { + page.querySelector('.btnSplitVersions').classList.remove('hide'); + } else { + page.querySelector('.btnSplitVersions').classList.add('hide'); + } - if (itemContextMenu.getCommands(getContextMenuOptions(item, user)).length) { - hideAll(page, 'btnMoreCommands', true); - } else { - hideAll(page, 'btnMoreCommands'); - } + if (itemContextMenu.getCommands(getContextMenuOptions(item, user)).length) { + hideAll(page, 'btnMoreCommands', true); + } else { + hideAll(page, 'btnMoreCommands'); + } - const itemBirthday = page.querySelector('#itemBirthday'); + const itemBirthday = page.querySelector('#itemBirthday'); - if (item.Type == 'Person' && item.PremiereDate) { - try { - const birthday = datetime.parseISO8601Date(item.PremiereDate, true).toDateString(); - itemBirthday.classList.remove('hide'); - itemBirthday.innerHTML = globalize.translate('BirthDateValue', birthday); - } catch (err) { - itemBirthday.classList.add('hide'); - } - } else { + if (item.Type == 'Person' && item.PremiereDate) { + try { + const birthday = datetime.parseISO8601Date(item.PremiereDate, true).toDateString(); + itemBirthday.classList.remove('hide'); + itemBirthday.innerHTML = globalize.translate('BirthDateValue', birthday); + } catch (err) { itemBirthday.classList.add('hide'); } + } else { + itemBirthday.classList.add('hide'); + } - const itemDeathDate = page.querySelector('#itemDeathDate'); + const itemDeathDate = page.querySelector('#itemDeathDate'); - if (item.Type == 'Person' && item.EndDate) { - try { - const deathday = datetime.parseISO8601Date(item.EndDate, true).toDateString(); - itemDeathDate.classList.remove('hide'); - itemDeathDate.innerHTML = globalize.translate('DeathDateValue', deathday); - } catch (err) { - itemDeathDate.classList.add('hide'); - } - } else { + if (item.Type == 'Person' && item.EndDate) { + try { + const deathday = datetime.parseISO8601Date(item.EndDate, true).toDateString(); + itemDeathDate.classList.remove('hide'); + itemDeathDate.innerHTML = globalize.translate('DeathDateValue', deathday); + } catch (err) { itemDeathDate.classList.add('hide'); } - - const itemBirthLocation = page.querySelector('#itemBirthLocation'); - - if (item.Type == 'Person' && item.ProductionLocations && item.ProductionLocations.length) { - const gmap = '' + item.ProductionLocations[0] + ''; - itemBirthLocation.classList.remove('hide'); - itemBirthLocation.innerHTML = globalize.translate('BirthPlaceValue', gmap); - } else { - itemBirthLocation.classList.add('hide'); - } - - setPeopleHeader(page, item); - loading.hide(); - - if (item.Type === 'Book' && item.CanDownload && appHost.supports('filedownload')) { - hideAll(page, 'btnDownload', true); - } - - import('autoFocuser').then(({default: autoFocuser}) => { - autoFocuser.autoFocus(page); - }); + } else { + itemDeathDate.classList.add('hide'); } - function logoImageUrl(item, apiClient, options) { - options = options || {}; - options.type = 'Logo'; + const itemBirthLocation = page.querySelector('#itemBirthLocation'); - if (item.ImageTags && item.ImageTags.Logo) { - options.tag = item.ImageTags.Logo; - return apiClient.getScaledImageUrl(item.Id, options); - } - - if (item.ParentLogoImageTag) { - options.tag = item.ParentLogoImageTag; - return apiClient.getScaledImageUrl(item.ParentLogoItemId, options); - } - - return null; + if (item.Type == 'Person' && item.ProductionLocations && item.ProductionLocations.length) { + const gmap = '' + item.ProductionLocations[0] + ''; + itemBirthLocation.classList.remove('hide'); + itemBirthLocation.innerHTML = globalize.translate('BirthPlaceValue', gmap); + } else { + itemBirthLocation.classList.add('hide'); } - function renderLogo(page, item, apiClient) { - const detailLogo = page.querySelector('.detailLogo'); + setPeopleHeader(page, item); + loading.hide(); - const url = logoImageUrl(item, apiClient, {}); - - if (!layoutManager.mobile && !userSettings.enableBackdrops()) { - detailLogo.classList.add('hide'); - } else if (url) { - detailLogo.classList.remove('hide'); - imageLoader.setLazyImage(detailLogo, url); - } else { - detailLogo.classList.add('hide'); - } + if (item.Type === 'Book' && item.CanDownload && appHost.supports('filedownload')) { + hideAll(page, 'btnDownload', true); } - function showRecordingFields(instance, page, item, user) { - if (!instance.currentRecordingFields) { - const recordingFieldsElement = page.querySelector('.recordingFields'); + import('autoFocuser').then(({ default: autoFocuser }) => { + autoFocuser.autoFocus(page); + }); +} - if (item.Type == 'Program' && user.Policy.EnableLiveTvManagement) { - import('recordingFields').then(({default: recordingFields}) => { - instance.currentRecordingFields = new recordingFields({ - parent: recordingFieldsElement, - programId: item.Id, - serverId: item.ServerId - }); - recordingFieldsElement.classList.remove('hide'); +function logoImageUrl(item, apiClient, options) { + options = options || {}; + options.type = 'Logo'; + + if (item.ImageTags && item.ImageTags.Logo) { + options.tag = item.ImageTags.Logo; + return apiClient.getScaledImageUrl(item.Id, options); + } + + if (item.ParentLogoImageTag) { + options.tag = item.ParentLogoImageTag; + return apiClient.getScaledImageUrl(item.ParentLogoItemId, options); + } + + return null; +} + +function renderLogo(page, item, apiClient) { + const detailLogo = page.querySelector('.detailLogo'); + + const url = logoImageUrl(item, apiClient, {}); + + if (!layoutManager.mobile && !userSettings.enableBackdrops()) { + detailLogo.classList.add('hide'); + } else if (url) { + detailLogo.classList.remove('hide'); + imageLoader.setLazyImage(detailLogo, url); + } else { + detailLogo.classList.add('hide'); + } +} + +function showRecordingFields(instance, page, item, user) { + if (!instance.currentRecordingFields) { + const recordingFieldsElement = page.querySelector('.recordingFields'); + + if (item.Type == 'Program' && user.Policy.EnableLiveTvManagement) { + import('recordingFields').then(({ default: recordingFields }) => { + instance.currentRecordingFields = new recordingFields({ + parent: recordingFieldsElement, + programId: item.Id, + serverId: item.ServerId }); - } else { - recordingFieldsElement.classList.add('hide'); - recordingFieldsElement.innerHTML = ''; - } - } - } - - function renderLinks(page, item) { - const externalLinksElem = page.querySelector('.itemExternalLinks'); - - const links = []; - - if (!layoutManager.tv && item.HomePageUrl) { - links.push(`${globalize.translate('ButtonWebsite')}`); - } - - if (item.ExternalUrls) { - for (const url of item.ExternalUrls) { - links.push(`${url.Name}`); - } - } - - const html = []; - if (links.length) { - html.push(links.join(', ')); - } - - externalLinksElem.innerHTML = html.join(', '); - - if (html.length) { - externalLinksElem.classList.remove('hide'); - } else { - externalLinksElem.classList.add('hide'); - } - } - - function renderDetailImage(elem, item, imageLoader) { - const itemArray = []; - itemArray.push(item); - const cardHtml = cardBuilder.getCardsHtml(itemArray, { - shape: 'auto', - showTitle: false, - centerText: true, - overlayText: false, - transition: false, - disableIndicators: true, - overlayPlayButton: true, - action: 'play', - width: dom.getWindowSize().innerWidth * 0.25 - }); - - elem.innerHTML = cardHtml; - imageLoader.lazyChildren(elem); - } - - function renderImage(page, item) { - renderDetailImage( - page.querySelector('.detailImageContainer'), - item, - imageLoader - ); - } - - function refreshDetailImageUserData(elem, item) { - elem.querySelector('.detailImageProgressContainer').innerHTML = indicators.getProgressBarHtml(item); - } - - function refreshImage(page, item) { - refreshDetailImageUserData(page.querySelector('.detailImageContainer'), item); - } - - function setPeopleHeader(page, item) { - if (item.MediaType == 'Audio' || item.Type == 'MusicAlbum' || item.MediaType == 'Book' || item.MediaType == 'Photo') { - page.querySelector('#peopleHeader').innerHTML = globalize.translate('People'); - } else { - page.querySelector('#peopleHeader').innerHTML = globalize.translate('HeaderCastAndCrew'); - } - } - - function renderNextUp(page, item, user) { - const section = page.querySelector('.nextUpSection'); - - if (item.Type != 'Series') { - return void section.classList.add('hide'); - } - - connectionManager.getApiClient(item.ServerId).getNextUpEpisodes({ - SeriesId: item.Id, - UserId: user.Id - }).then(function (result) { - if (result.Items.length) { - section.classList.remove('hide'); - } else { - section.classList.add('hide'); - } - - const html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'overflowBackdrop', - showTitle: true, - displayAsSpecial: item.Type == 'Season' && item.IndexNumber, - overlayText: false, - centerText: true, - overlayPlayButton: true + recordingFieldsElement.classList.remove('hide'); }); - const itemsContainer = section.querySelector('.nextUpItems'); - itemsContainer.innerHTML = html; - imageLoader.lazyChildren(itemsContainer); - }); + } else { + recordingFieldsElement.classList.add('hide'); + recordingFieldsElement.innerHTML = ''; + } + } +} + +function renderLinks(page, item) { + const externalLinksElem = page.querySelector('.itemExternalLinks'); + + const links = []; + + if (!layoutManager.tv && item.HomePageUrl) { + links.push(`${globalize.translate('ButtonWebsite')}`); } - function setInitialCollapsibleState(page, item, apiClient, context, user) { - page.querySelector('.collectionItems').innerHTML = ''; + if (item.ExternalUrls) { + for (const url of item.ExternalUrls) { + links.push(`${url.Name}`); + } + } - if (item.Type == 'Playlist') { - page.querySelector('#childrenCollapsible').classList.remove('hide'); - renderPlaylistItems(page, item); - } else if (item.Type == 'Studio' || item.Type == 'Person' || item.Type == 'Genre' || item.Type == 'MusicGenre' || item.Type == 'MusicArtist') { - page.querySelector('#childrenCollapsible').classList.remove('hide'); - renderItemsByName(page, item); - } else if (item.IsFolder) { - if (item.Type == 'BoxSet') { - page.querySelector('#childrenCollapsible').classList.add('hide'); - } + const html = []; + if (links.length) { + html.push(links.join(', ')); + } - renderChildren(page, item); + externalLinksElem.innerHTML = html.join(', '); + + if (html.length) { + externalLinksElem.classList.remove('hide'); + } else { + externalLinksElem.classList.add('hide'); + } +} + +function renderDetailImage(elem, item, imageLoader) { + const itemArray = []; + itemArray.push(item); + const cardHtml = cardBuilder.getCardsHtml(itemArray, { + shape: 'auto', + showTitle: false, + centerText: true, + overlayText: false, + transition: false, + disableIndicators: true, + overlayPlayButton: true, + action: 'play', + width: dom.getWindowSize().innerWidth * 0.25 + }); + + elem.innerHTML = cardHtml; + imageLoader.lazyChildren(elem); + + // Avoid breaking the design by preventing focus of the poster using the keyboard. + elem.querySelector('button').tabIndex = -1; +} + +function renderImage(page, item) { + renderDetailImage( + page.querySelector('.detailImageContainer'), + item, + imageLoader + ); +} + +function refreshDetailImageUserData(elem, item) { + elem.querySelector('.detailImageProgressContainer').innerHTML = indicators.getProgressBarHtml(item); +} + +function refreshImage(page, item) { + refreshDetailImageUserData(page.querySelector('.detailImageContainer'), item); +} + +function setPeopleHeader(page, item) { + if (item.MediaType == 'Audio' || item.Type == 'MusicAlbum' || item.MediaType == 'Book' || item.MediaType == 'Photo') { + page.querySelector('#peopleHeader').innerHTML = globalize.translate('People'); + } else { + page.querySelector('#peopleHeader').innerHTML = globalize.translate('HeaderCastAndCrew'); + } +} + +function renderNextUp(page, item, user) { + const section = page.querySelector('.nextUpSection'); + + if (item.Type != 'Series') { + return void section.classList.add('hide'); + } + + connectionManager.getApiClient(item.ServerId).getNextUpEpisodes({ + SeriesId: item.Id, + UserId: user.Id + }).then(function (result) { + if (result.Items.length) { + section.classList.remove('hide'); } else { + section.classList.add('hide'); + } + + const html = cardBuilder.getCardsHtml({ + items: result.Items, + shape: 'overflowBackdrop', + showTitle: true, + displayAsSpecial: item.Type == 'Season' && item.IndexNumber, + overlayText: false, + centerText: true, + overlayPlayButton: true + }); + const itemsContainer = section.querySelector('.nextUpItems'); + itemsContainer.innerHTML = html; + imageLoader.lazyChildren(itemsContainer); + }); +} + +function setInitialCollapsibleState(page, item, apiClient, context, user) { + page.querySelector('.collectionItems').innerHTML = ''; + + if (item.Type == 'Playlist') { + page.querySelector('#childrenCollapsible').classList.remove('hide'); + renderPlaylistItems(page, item); + } else if (item.Type == 'Studio' || item.Type == 'Person' || item.Type == 'Genre' || item.Type == 'MusicGenre' || item.Type == 'MusicArtist') { + page.querySelector('#childrenCollapsible').classList.remove('hide'); + renderItemsByName(page, item); + } else if (item.IsFolder) { + if (item.Type == 'BoxSet') { page.querySelector('#childrenCollapsible').classList.add('hide'); } - if (item.Type == 'Series') { - renderSeriesSchedule(page, item); - renderNextUp(page, item, user); - } else { - page.querySelector('.nextUpSection').classList.add('hide'); - } - - renderScenes(page, item); - - if (item.SpecialFeatureCount && item.SpecialFeatureCount != 0 && item.Type != 'Series') { - page.querySelector('#specialsCollapsible').classList.remove('hide'); - renderSpecials(page, item, user); - } else { - page.querySelector('#specialsCollapsible').classList.add('hide'); - } - - renderCast(page, item); - - if (item.PartCount && item.PartCount > 1) { - page.querySelector('#additionalPartsCollapsible').classList.remove('hide'); - renderAdditionalParts(page, item, user); - } else { - page.querySelector('#additionalPartsCollapsible').classList.add('hide'); - } - - if (item.Type == 'MusicAlbum') { - renderMusicVideos(page, item, user); - } else { - page.querySelector('#musicVideosCollapsible').classList.add('hide'); - } + renderChildren(page, item); + } else { + page.querySelector('#childrenCollapsible').classList.add('hide'); } - function toggleLineClamp(clampTarget, e) { - const expandButton = e.target; - const clampClassName = 'detail-clamp-text'; - - if (clampTarget.classList.contains(clampClassName)) { - clampTarget.classList.remove(clampClassName); - expandButton.innerHTML = globalize.translate('ShowLess'); - } else { - clampTarget.classList.add(clampClassName); - expandButton.innerHTML = globalize.translate('ShowMore'); - } + if (item.Type == 'Series') { + renderSeriesSchedule(page, item); + renderNextUp(page, item, user); + } else { + page.querySelector('.nextUpSection').classList.add('hide'); } - function renderOverview(page, item) { - for (const overviewElemnt of page.querySelectorAll('.overview')) { - const overview = item.Overview || ''; + renderScenes(page, item); - if (overview) { - overviewElemnt.innerHTML = overview; - overviewElemnt.classList.remove('hide'); - overviewElemnt.classList.add('detail-clamp-text'); + if (item.SpecialFeatureCount && item.SpecialFeatureCount != 0 && item.Type != 'Series') { + page.querySelector('#specialsCollapsible').classList.remove('hide'); + renderSpecials(page, item, user); + } else { + page.querySelector('#specialsCollapsible').classList.add('hide'); + } - // Grab the sibling element to control the expand state - const expandButton = overviewElemnt.parentElement.querySelector('.overview-expand'); + renderCast(page, item); - // Detect if we have overflow of text. Based on this StackOverflow answer - // https://stackoverflow.com/a/35157976 - if (Math.abs(overviewElemnt.scrollHeight - overviewElemnt.offsetHeight) > 2) { - expandButton.classList.remove('hide'); - } else { - expandButton.classList.add('hide'); - } + if (item.PartCount && item.PartCount > 1) { + page.querySelector('#additionalPartsCollapsible').classList.remove('hide'); + renderAdditionalParts(page, item, user); + } else { + page.querySelector('#additionalPartsCollapsible').classList.add('hide'); + } - expandButton.addEventListener('click', toggleLineClamp.bind(null, overviewElemnt)); + if (item.Type == 'MusicAlbum') { + renderMusicVideos(page, item, user); + } else { + page.querySelector('#musicVideosCollapsible').classList.add('hide'); + } +} - for (const anchor of overviewElemnt.querySelectorAll('a')) { - anchor.setAttribute('target', '_blank'); - } +function toggleLineClamp(clampTarget, e) { + const expandButton = e.target; + const clampClassName = 'detail-clamp-text'; + + if (clampTarget.classList.contains(clampClassName)) { + clampTarget.classList.remove(clampClassName); + expandButton.innerHTML = globalize.translate('ShowLess'); + } else { + clampTarget.classList.add(clampClassName); + expandButton.innerHTML = globalize.translate('ShowMore'); + } +} + +function renderOverview(page, item) { + for (const overviewElemnt of page.querySelectorAll('.overview')) { + const overview = item.Overview || ''; + + if (overview) { + overviewElemnt.innerHTML = overview; + overviewElemnt.classList.remove('hide'); + overviewElemnt.classList.add('detail-clamp-text'); + + // Grab the sibling element to control the expand state + const expandButton = overviewElemnt.parentElement.querySelector('.overview-expand'); + + // Detect if we have overflow of text. Based on this StackOverflow answer + // https://stackoverflow.com/a/35157976 + if (Math.abs(overviewElemnt.scrollHeight - overviewElemnt.offsetHeight) > 2) { + expandButton.classList.remove('hide'); } else { - overviewElemnt.innerHTML = ''; - overviewElemnt.classList.add('hide'); + expandButton.classList.add('hide'); } - } - } - function renderGenres(page, item, context = inferContext(item)) { - const genres = item.GenreItems || []; - const type = context === 'music' ? 'MusicGenre' : 'Genre'; + expandButton.addEventListener('click', toggleLineClamp.bind(null, overviewElemnt)); - const html = genres.map(function (p) { - return '' + p.Name + ''; - }).join(', '); - - const genresLabel = page.querySelector('.genresLabel'); - genresLabel.innerHTML = globalize.translate(genres.length > 1 ? 'Genres' : 'Genre'); - const genresValue = page.querySelector('.genres'); - genresValue.innerHTML = html; - - const genresGroup = page.querySelector('.genresGroup'); - if (genres.length) { - genresGroup.classList.remove('hide'); + for (const anchor of overviewElemnt.querySelectorAll('a')) { + anchor.setAttribute('target', '_blank'); + } } else { - genresGroup.classList.add('hide'); + overviewElemnt.innerHTML = ''; + overviewElemnt.classList.add('hide'); } } +} - function renderWriter(page, item, context) { - const writers = (item.People || []).filter(function (person) { - return person.Type === 'Writer'; +function renderGenres(page, item, context = inferContext(item)) { + const genres = item.GenreItems || []; + const type = context === 'music' ? 'MusicGenre' : 'Genre'; + + const html = genres.map(function (p) { + return '' + p.Name + ''; + }).join(', '); + + const genresLabel = page.querySelector('.genresLabel'); + genresLabel.innerHTML = globalize.translate(genres.length > 1 ? 'Genres' : 'Genre'); + const genresValue = page.querySelector('.genres'); + genresValue.innerHTML = html; + + const genresGroup = page.querySelector('.genresGroup'); + if (genres.length) { + genresGroup.classList.remove('hide'); + } else { + genresGroup.classList.add('hide'); + } +} + +function renderWriter(page, item, context) { + const writers = (item.People || []).filter(function (person) { + return person.Type === 'Writer'; + }); + + const html = writers.map(function (person) { + return '' + person.Name + ''; + }).join(', '); + + const writersLabel = page.querySelector('.writersLabel'); + writersLabel.innerHTML = globalize.translate(writers.length > 1 ? 'Writers' : 'Writer'); + const writersValue = page.querySelector('.writers'); + writersValue.innerHTML = html; + + const writersGroup = page.querySelector('.writersGroup'); + if (writers.length) { + writersGroup.classList.remove('hide'); + } else { + writersGroup.classList.add('hide'); + } +} + +function renderDirector(page, item, context) { + const directors = (item.People || []).filter(function (person) { + return person.Type === 'Director'; + }); + + const html = directors.map(function (person) { + return '' + person.Name + ''; + }).join(', '); + + const directorsLabel = page.querySelector('.directorsLabel'); + directorsLabel.innerHTML = globalize.translate(directors.length > 1 ? 'Directors' : 'Director'); + const directorsValue = page.querySelector('.directors'); + directorsValue.innerHTML = html; + + const directorsGroup = page.querySelector('.directorsGroup'); + if (directors.length) { + directorsGroup.classList.remove('hide'); + } else { + directorsGroup.classList.add('hide'); + } +} + +function renderMiscInfo(page, item) { + const primaryItemMiscInfo = page.querySelectorAll('.itemMiscInfo-primary'); + + for (const miscInfo of primaryItemMiscInfo) { + mediaInfo.fillPrimaryMediaInfo(miscInfo, item, { + interactive: true, + episodeTitle: false, + subtitles: false }); - const html = writers.map(function (person) { - return '' + person.Name + ''; - }).join(', '); - - const writersLabel = page.querySelector('.writersLabel'); - writersLabel.innerHTML = globalize.translate(writers.length > 1 ? 'Writers' : 'Writer'); - const writersValue = page.querySelector('.writers'); - writersValue.innerHTML = html; - - const writersGroup = page.querySelector('.writersGroup'); - if (writers.length) { - writersGroup.classList.remove('hide'); + if (miscInfo.innerHTML && item.Type !== 'SeriesTimer') { + miscInfo.classList.remove('hide'); } else { - writersGroup.classList.add('hide'); + miscInfo.classList.add('hide'); } } - function renderDirector(page, item, context) { - const directors = (item.People || []).filter(function (person) { - return person.Type === 'Director'; + const secondaryItemMiscInfo = page.querySelectorAll('.itemMiscInfo-secondary'); + + for (const miscInfo of secondaryItemMiscInfo) { + mediaInfo.fillSecondaryMediaInfo(miscInfo, item, { + interactive: true }); - const html = directors.map(function (person) { - return '' + person.Name + ''; - }).join(', '); - - const directorsLabel = page.querySelector('.directorsLabel'); - directorsLabel.innerHTML = globalize.translate(directors.length > 1 ? 'Directors' : 'Director'); - const directorsValue = page.querySelector('.directors'); - directorsValue.innerHTML = html; - - const directorsGroup = page.querySelector('.directorsGroup'); - if (directors.length) { - directorsGroup.classList.remove('hide'); + if (miscInfo.innerHTML && item.Type !== 'SeriesTimer') { + miscInfo.classList.remove('hide'); } else { - directorsGroup.classList.add('hide'); + miscInfo.classList.add('hide'); } } +} - function renderMiscInfo(page, item) { - const primaryItemMiscInfo = page.querySelectorAll('.itemMiscInfo-primary'); +function renderTagline(page, item) { + const taglineElement = page.querySelector('.tagline'); - for (const miscInfo of primaryItemMiscInfo) { - mediaInfo.fillPrimaryMediaInfo(miscInfo, item, { - interactive: true, - episodeTitle: false, - subtitles: false - }); - - if (miscInfo.innerHTML && item.Type !== 'SeriesTimer') { - miscInfo.classList.remove('hide'); - } else { - miscInfo.classList.add('hide'); - } - } - - const secondaryItemMiscInfo = page.querySelectorAll('.itemMiscInfo-secondary'); - - for (const miscInfo of secondaryItemMiscInfo) { - mediaInfo.fillSecondaryMediaInfo(miscInfo, item, { - interactive: true - }); - - if (miscInfo.innerHTML && item.Type !== 'SeriesTimer') { - miscInfo.classList.remove('hide'); - } else { - miscInfo.classList.add('hide'); - } - } + if (item.Taglines && item.Taglines.length) { + taglineElement.classList.remove('hide'); + taglineElement.innerHTML = item.Taglines[0]; + } else { + taglineElement.classList.add('hide'); } +} - function renderTagline(page, item) { - const taglineElement = page.querySelector('.tagline'); +function renderDetails(page, item, apiClient, context, isStatic) { + renderSimilarItems(page, item, context); + renderMoreFromSeason(page, item, apiClient); + renderMoreFromArtist(page, item, apiClient); + renderDirector(page, item, context); + renderWriter(page, item, context); + renderGenres(page, item, context); + renderChannelGuide(page, apiClient, item); + renderTagline(page, item); + renderOverview(page, item); + renderMiscInfo(page, item); + reloadUserDataButtons(page, item); - if (item.Taglines && item.Taglines.length) { - taglineElement.classList.remove('hide'); - taglineElement.innerHTML = item.Taglines[0]; - } else { - taglineElement.classList.add('hide'); - } - } - - function renderDetails(page, item, apiClient, context, isStatic) { - renderSimilarItems(page, item, context); - renderMoreFromSeason(page, item, apiClient); - renderMoreFromArtist(page, item, apiClient); - renderDirector(page, item, context); - renderWriter(page, item, context); - renderGenres(page, item, context); - renderChannelGuide(page, apiClient, item); - renderTagline(page, item); - renderOverview(page, item); - renderMiscInfo(page, item); - reloadUserDataButtons(page, item); + // Don't allow redirection to other websites from the TV layout + if (!layoutManager.tv) { renderLinks(page, item); - renderTags(page, item); - renderSeriesAirTime(page, item, isStatic); } - function enableScrollX() { - return browser.mobile && window.screen.availWidth <= 1000; + renderTags(page, item); + renderSeriesAirTime(page, item, isStatic); +} + +function enableScrollX() { + return browser.mobile && window.screen.availWidth <= 1000; +} + +function getPortraitShape(scrollX) { + if (scrollX == null) { + scrollX = enableScrollX(); } - function getPortraitShape(scrollX) { - if (scrollX == null) { - scrollX = enableScrollX(); + return scrollX ? 'overflowPortrait' : 'portrait'; +} + +function getSquareShape(scrollX) { + if (scrollX == null) { + scrollX = enableScrollX(); + } + + return scrollX ? 'overflowSquare' : 'square'; +} + +function renderMoreFromSeason(view, item, apiClient) { + const section = view.querySelector('.moreFromSeasonSection'); + + if (section) { + if (item.Type !== 'Episode' || !item.SeasonId || !item.SeriesId) { + return void section.classList.add('hide'); } - return scrollX ? 'overflowPortrait' : 'portrait'; - } - - function getSquareShape(scrollX) { - if (scrollX == null) { - scrollX = enableScrollX(); - } - - return scrollX ? 'overflowSquare' : 'square'; - } - - function renderMoreFromSeason(view, item, apiClient) { - const section = view.querySelector('.moreFromSeasonSection'); - - if (section) { - if (item.Type !== 'Episode' || !item.SeasonId || !item.SeriesId) { + const userId = apiClient.getCurrentUserId(); + apiClient.getEpisodes(item.SeriesId, { + SeasonId: item.SeasonId, + UserId: userId, + Fields: 'ItemCounts,PrimaryImageAspectRatio,BasicSyncInfo,CanDelete,MediaSourceCount' + }).then(function (result) { + if (result.Items.length < 2) { return void section.classList.add('hide'); } - const userId = apiClient.getCurrentUserId(); - apiClient.getEpisodes(item.SeriesId, { - SeasonId: item.SeasonId, - UserId: userId, - Fields: 'ItemCounts,PrimaryImageAspectRatio,BasicSyncInfo,CanDelete,MediaSourceCount' - }).then(function (result) { - if (result.Items.length < 2) { - return void section.classList.add('hide'); - } - - section.classList.remove('hide'); - section.querySelector('h2').innerHTML = globalize.translate('MoreFromValue', item.SeasonName); - const itemsContainer = section.querySelector('.itemsContainer'); - cardBuilder.buildCards(result.Items, { - parentContainer: section, - itemsContainer: itemsContainer, - shape: 'autooverflow', - sectionTitleTagName: 'h2', - scalable: true, - showTitle: true, - overlayText: false, - centerText: true, - includeParentInfoInTitle: false, - allowBottomPadding: false - }); - const card = itemsContainer.querySelector('.card[data-id="' + item.Id + '"]'); - - if (card) { - setTimeout(function () { - section.querySelector('.emby-scroller').toStart(card.previousSibling || card, true); - }, 100); - } + section.classList.remove('hide'); + section.querySelector('h2').innerHTML = globalize.translate('MoreFromValue', item.SeasonName); + const itemsContainer = section.querySelector('.itemsContainer'); + cardBuilder.buildCards(result.Items, { + parentContainer: section, + itemsContainer: itemsContainer, + shape: 'autooverflow', + sectionTitleTagName: 'h2', + scalable: true, + showTitle: true, + overlayText: false, + centerText: true, + includeParentInfoInTitle: false, + allowBottomPadding: false }); - } + const card = itemsContainer.querySelector('.card[data-id="' + item.Id + '"]'); + + if (card) { + setTimeout(function () { + section.querySelector('.emby-scroller').toStart(card.previousSibling || card, true); + }, 100); + } + }); } +} - function renderMoreFromArtist(view, item, apiClient) { - const section = view.querySelector('.moreFromArtistSection'); +function renderMoreFromArtist(view, item, apiClient) { + const section = view.querySelector('.moreFromArtistSection'); - if (section) { - if (item.Type === 'MusicArtist') { - if (!apiClient.isMinServerVersion('3.4.1.19')) { - return void section.classList.add('hide'); - } - } else if (item.Type !== 'MusicAlbum' || !item.AlbumArtists || !item.AlbumArtists.length) { + if (section) { + if (item.Type === 'MusicArtist') { + if (!apiClient.isMinServerVersion('3.4.1.19')) { + return void section.classList.add('hide'); + } + } else if (item.Type !== 'MusicAlbum' || !item.AlbumArtists || !item.AlbumArtists.length) { + return void section.classList.add('hide'); + } + + const query = { + IncludeItemTypes: 'MusicAlbum', + Recursive: true, + ExcludeItemIds: item.Id, + SortBy: 'ProductionYear,SortName', + SortOrder: 'Descending' + }; + + if (item.Type === 'MusicArtist') { + query.ContributingArtistIds = item.Id; + } else if (apiClient.isMinServerVersion('3.4.1.18')) { + query.AlbumArtistIds = item.AlbumArtists[0].Id; + } else { + query.ArtistIds = item.AlbumArtists[0].Id; + } + + apiClient.getItems(apiClient.getCurrentUserId(), query).then(function (result) { + if (!result.Items.length) { return void section.classList.add('hide'); } - const query = { - IncludeItemTypes: 'MusicAlbum', - Recursive: true, - ExcludeItemIds: item.Id, - SortBy: 'ProductionYear,SortName', - SortOrder: 'Descending' - }; + section.classList.remove('hide'); if (item.Type === 'MusicArtist') { - query.ContributingArtistIds = item.Id; - } else if (apiClient.isMinServerVersion('3.4.1.18')) { - query.AlbumArtistIds = item.AlbumArtists[0].Id; + section.querySelector('h2').innerHTML = globalize.translate('HeaderAppearsOn'); } else { - query.ArtistIds = item.AlbumArtists[0].Id; + section.querySelector('h2').innerHTML = globalize.translate('MoreFromValue', item.AlbumArtists[0].Name); } - apiClient.getItems(apiClient.getCurrentUserId(), query).then(function (result) { - if (!result.Items.length) { - return void section.classList.add('hide'); - } - - section.classList.remove('hide'); - - if (item.Type === 'MusicArtist') { - section.querySelector('h2').innerHTML = globalize.translate('HeaderAppearsOn'); - } else { - section.querySelector('h2').innerHTML = globalize.translate('MoreFromValue', item.AlbumArtists[0].Name); - } - - cardBuilder.buildCards(result.Items, { - parentContainer: section, - itemsContainer: section.querySelector('.itemsContainer'), - shape: 'autooverflow', - sectionTitleTagName: 'h2', - scalable: true, - coverImage: item.Type === 'MusicArtist' || item.Type === 'MusicAlbum', - showTitle: true, - showParentTitle: false, - centerText: true, - overlayText: false, - overlayPlayButton: true, - showYear: true - }); + cardBuilder.buildCards(result.Items, { + parentContainer: section, + itemsContainer: section.querySelector('.itemsContainer'), + shape: 'autooverflow', + sectionTitleTagName: 'h2', + scalable: true, + coverImage: item.Type === 'MusicArtist' || item.Type === 'MusicAlbum', + showTitle: true, + showParentTitle: false, + centerText: true, + overlayText: false, + overlayPlayButton: true, + showYear: true }); - } + }); } +} - function renderSimilarItems(page, item, context) { - const similarCollapsible = page.querySelector('#similarCollapsible'); +function renderSimilarItems(page, item, context) { + const similarCollapsible = page.querySelector('#similarCollapsible'); - if (similarCollapsible) { - if (item.Type != 'Movie' && item.Type != 'Trailer' && item.Type != 'Series' && item.Type != 'Program' && item.Type != 'Recording' && item.Type != 'MusicAlbum' && item.Type != 'MusicArtist' && item.Type != 'Playlist') { + if (similarCollapsible) { + if (item.Type != 'Movie' && item.Type != 'Trailer' && item.Type != 'Series' && item.Type != 'Program' && item.Type != 'Recording' && item.Type != 'MusicAlbum' && item.Type != 'MusicArtist' && item.Type != 'Playlist') { + return void similarCollapsible.classList.add('hide'); + } + + similarCollapsible.classList.remove('hide'); + const apiClient = connectionManager.getApiClient(item.ServerId); + const options = { + userId: apiClient.getCurrentUserId(), + limit: 12, + fields: 'PrimaryImageAspectRatio,UserData,CanDelete' + }; + + if (item.Type == 'MusicAlbum' && item.AlbumArtists && item.AlbumArtists.length) { + options.ExcludeArtistIds = item.AlbumArtists[0].Id; + } + + apiClient.getSimilarItems(item.Id, options).then(function (result) { + if (!result.Items.length) { return void similarCollapsible.classList.add('hide'); } similarCollapsible.classList.remove('hide'); - const apiClient = connectionManager.getApiClient(item.ServerId); - const options = { - userId: apiClient.getCurrentUserId(), - limit: 12, - fields: 'PrimaryImageAspectRatio,UserData,CanDelete' - }; - - if (item.Type == 'MusicAlbum' && item.AlbumArtists && item.AlbumArtists.length) { - options.ExcludeArtistIds = item.AlbumArtists[0].Id; - } - - apiClient.getSimilarItems(item.Id, options).then(function (result) { - if (!result.Items.length) { - return void similarCollapsible.classList.add('hide'); - } - - similarCollapsible.classList.remove('hide'); - let html = ''; - html += cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'autooverflow', - showParentTitle: item.Type == 'MusicAlbum', - centerText: true, - showTitle: true, - context: context, - lazy: true, - showDetailsMenu: true, - coverImage: item.Type == 'MusicAlbum' || item.Type == 'MusicArtist', - overlayPlayButton: true, - overlayText: false, - showYear: item.Type === 'Movie' || item.Type === 'Trailer' || item.Type === 'Series' - }); - const similarContent = similarCollapsible.querySelector('.similarContent'); - similarContent.innerHTML = html; - imageLoader.lazyChildren(similarContent); - }); - } - } - - function renderSeriesAirTime(page, item, isStatic) { - const seriesAirTime = page.querySelector('#seriesAirTime'); - if (item.Type != 'Series') { - seriesAirTime.classList.add('hide'); - return; - } - let html = ''; - if (item.AirDays && item.AirDays.length) { - if (item.AirDays.length == 7) { - html += 'daily'; - } else { - html += item.AirDays.map(function (a) { - return a + 's'; - }).join(','); - } - } - if (item.AirTime) { - html += ' at ' + item.AirTime; - } - if (item.Studios.length) { - if (isStatic) { - html += ' on ' + item.Studios[0].Name; - } else { - const context = inferContext(item); - const href = appRouter.getRouteUrl(item.Studios[0], { - context: context, - itemType: 'Studio', - serverId: item.ServerId - }); - html += ' on ' + item.Studios[0].Name + ''; - } - } - if (html) { - html = (item.Status == 'Ended' ? 'Aired ' : 'Airs ') + html; - seriesAirTime.innerHTML = html; - seriesAirTime.classList.remove('hide'); - } else { - seriesAirTime.classList.add('hide'); - } - } - - function renderTags(page, item) { - const itemTags = page.querySelector('.itemTags'); - const tagElements = []; - let tags = item.Tags || []; - - if (item.Type === 'Program') { - tags = []; - } - - for (let i = 0, length = tags.length; i < length; i++) { - tagElements.push(tags[i]); - } - - if (tagElements.length) { - itemTags.innerHTML = globalize.translate('TagsValue', tagElements.join(', ')); - itemTags.classList.remove('hide'); - } else { - itemTags.innerHTML = ''; - itemTags.classList.add('hide'); - } - } - - function renderChildren(page, item) { - let fields = 'ItemCounts,PrimaryImageAspectRatio,BasicSyncInfo,CanDelete,MediaSourceCount'; - const query = { - ParentId: item.Id, - Fields: fields - }; - - if (item.Type !== 'BoxSet') { - query.SortBy = 'SortName'; - } - - let promise; - const apiClient = connectionManager.getApiClient(item.ServerId); - const userId = apiClient.getCurrentUserId(); - - if (item.Type == 'Series') { - promise = apiClient.getSeasons(item.Id, { - userId: userId, - Fields: fields - }); - } else if (item.Type == 'Season') { - fields += ',Overview'; - promise = apiClient.getEpisodes(item.SeriesId, { - seasonId: item.Id, - userId: userId, - Fields: fields - }); - } else if (item.Type == 'MusicArtist') { - query.SortBy = 'ProductionYear,SortName'; - } - - promise = promise || apiClient.getItems(apiClient.getCurrentUserId(), query); - promise.then(function (result) { let html = ''; - let scrollX = false; - let isList = false; - const childrenItemsContainer = page.querySelector('.childrenItemsContainer'); + html += cardBuilder.getCardsHtml({ + items: result.Items, + shape: 'autooverflow', + showParentTitle: item.Type == 'MusicAlbum', + centerText: true, + showTitle: true, + context: context, + lazy: true, + showDetailsMenu: true, + coverImage: item.Type == 'MusicAlbum' || item.Type == 'MusicArtist', + overlayPlayButton: true, + overlayText: false, + showYear: item.Type === 'Movie' || item.Type === 'Trailer' || item.Type === 'Series' + }); + const similarContent = similarCollapsible.querySelector('.similarContent'); + similarContent.innerHTML = html; + imageLoader.lazyChildren(similarContent); + }); + } +} - if (item.Type == 'MusicAlbum') { - html = listView.getListViewHtml({ - items: result.Items, - smallIcon: true, - showIndex: true, - index: 'disc', - showIndexNumberLeft: true, - playFromHere: true, - action: 'playallfromhere', - image: false, - artist: 'auto', - containerAlbumArtists: item.AlbumArtists - }); +function renderSeriesAirTime(page, item, isStatic) { + const seriesAirTime = page.querySelector('#seriesAirTime'); + if (item.Type != 'Series') { + seriesAirTime.classList.add('hide'); + return; + } + let html = ''; + if (item.AirDays && item.AirDays.length) { + if (item.AirDays.length == 7) { + html += 'daily'; + } else { + html += item.AirDays.map(function (a) { + return a + 's'; + }).join(','); + } + } + if (item.AirTime) { + html += ' at ' + item.AirTime; + } + if (item.Studios.length) { + if (isStatic) { + html += ' on ' + item.Studios[0].Name; + } else { + const context = inferContext(item); + const href = appRouter.getRouteUrl(item.Studios[0], { + context: context, + itemType: 'Studio', + serverId: item.ServerId + }); + html += ' on ' + item.Studios[0].Name + ''; + } + } + if (html) { + html = (item.Status == 'Ended' ? 'Aired ' : 'Airs ') + html; + seriesAirTime.innerHTML = html; + seriesAirTime.classList.remove('hide'); + } else { + seriesAirTime.classList.add('hide'); + } +} + +function renderTags(page, item) { + const itemTags = page.querySelector('.itemTags'); + const tagElements = []; + let tags = item.Tags || []; + + if (item.Type === 'Program') { + tags = []; + } + + for (let i = 0, length = tags.length; i < length; i++) { + tagElements.push(tags[i]); + } + + if (tagElements.length) { + itemTags.innerHTML = globalize.translate('TagsValue', tagElements.join(', ')); + itemTags.classList.remove('hide'); + } else { + itemTags.innerHTML = ''; + itemTags.classList.add('hide'); + } +} + +function renderChildren(page, item) { + let fields = 'ItemCounts,PrimaryImageAspectRatio,BasicSyncInfo,CanDelete,MediaSourceCount'; + const query = { + ParentId: item.Id, + Fields: fields + }; + + if (item.Type !== 'BoxSet') { + query.SortBy = 'SortName'; + } + + let promise; + const apiClient = connectionManager.getApiClient(item.ServerId); + const userId = apiClient.getCurrentUserId(); + + if (item.Type == 'Series') { + promise = apiClient.getSeasons(item.Id, { + userId: userId, + Fields: fields + }); + } else if (item.Type == 'Season') { + fields += ',Overview'; + promise = apiClient.getEpisodes(item.SeriesId, { + seasonId: item.Id, + userId: userId, + Fields: fields + }); + } else if (item.Type == 'MusicArtist') { + query.SortBy = 'ProductionYear,SortName'; + } + + promise = promise || apiClient.getItems(apiClient.getCurrentUserId(), query); + promise.then(function (result) { + let html = ''; + let scrollX = false; + let isList = false; + const childrenItemsContainer = page.querySelector('.childrenItemsContainer'); + + if (item.Type == 'MusicAlbum') { + const equalSet = (arr1, arr2) => arr1.every(x => arr2.indexOf(x) !== -1) && arr1.length === arr2.length; + let showArtist = false; + for (const track of result.Items) { + if (!equalSet(track.ArtistItems.map(x => x.Id), track.AlbumArtists.map(x => x.Id))) { + showArtist = true; + break; + } + } + const discNumbers = result.Items.map(x => x.ParentIndexNumber); + html = listView.getListViewHtml({ + items: result.Items, + smallIcon: true, + showIndex: new Set(discNumbers).size > 1 || (discNumbers.length >= 1 && discNumbers[0] > 1), + index: 'disc', + showIndexNumberLeft: true, + playFromHere: true, + action: 'playallfromhere', + image: false, + artist: showArtist, + containerAlbumArtists: item.AlbumArtists + }); + isList = true; + } else if (item.Type == 'Series') { + scrollX = enableScrollX(); + html = cardBuilder.getCardsHtml({ + items: result.Items, + shape: 'overflowPortrait', + showTitle: true, + centerText: true, + lazy: true, + overlayPlayButton: true, + allowBottomPadding: !scrollX + }); + } else if (item.Type == 'Season' || item.Type == 'Episode') { + if (item.Type !== 'Episode') { isList = true; - } else if (item.Type == 'Series') { - scrollX = enableScrollX(); + } + scrollX = item.Type == 'Episode'; + if (result.Items.length < 2 && item.Type === 'Episode') { + return; + } + + if (item.Type === 'Episode') { html = cardBuilder.getCardsHtml({ items: result.Items, - shape: 'overflowPortrait', + shape: 'overflowBackdrop', showTitle: true, - centerText: true, + displayAsSpecial: item.Type == 'Season' && item.IndexNumber, + playFromHere: true, + overlayText: true, lazy: true, + showDetailsMenu: true, overlayPlayButton: true, - allowBottomPadding: !scrollX + allowBottomPadding: !scrollX, + includeParentInfoInTitle: false + }); + } else if (item.Type === 'Season') { + html = listView.getListViewHtml({ + items: result.Items, + showIndexNumber: false, + enableOverview: true, + enablePlayedButton: layoutManager.mobile ? false : true, + infoButton: layoutManager.mobile ? false : true, + imageSize: 'large', + enableSideMediaInfo: false, + highlight: false, + action: !layoutManager.desktop ? 'link' : 'none', + imagePlayButton: true, + includeParentInfoInTitle: false }); - } else if (item.Type == 'Season' || item.Type == 'Episode') { - if (item.Type !== 'Episode') { - isList = true; - } - scrollX = item.Type == 'Episode'; - if (result.Items.length < 2 && item.Type === 'Episode') { - return; - } - - if (item.Type === 'Episode') { - html = cardBuilder.getCardsHtml({ - items: result.Items, - shape: 'overflowBackdrop', - showTitle: true, - displayAsSpecial: item.Type == 'Season' && item.IndexNumber, - playFromHere: true, - overlayText: true, - lazy: true, - showDetailsMenu: true, - overlayPlayButton: true, - allowBottomPadding: !scrollX, - includeParentInfoInTitle: false - }); - } else if (item.Type === 'Season') { - html = listView.getListViewHtml({ - items: result.Items, - showIndexNumber: false, - enableOverview: true, - enablePlayedButton: layoutManager.mobile ? false : true, - infoButton: layoutManager.mobile ? false : true, - imageSize: 'large', - enableSideMediaInfo: false, - highlight: false, - action: !layoutManager.desktop ? 'link' : 'none', - imagePlayButton: true, - includeParentInfoInTitle: false - }); - } } + } - if (item.Type !== 'BoxSet') { - page.querySelector('#childrenCollapsible').classList.remove('hide'); - } - if (scrollX) { - childrenItemsContainer.classList.add('scrollX'); - childrenItemsContainer.classList.add('hiddenScrollX'); + if (item.Type !== 'BoxSet') { + page.querySelector('#childrenCollapsible').classList.remove('hide'); + } + if (scrollX) { + childrenItemsContainer.classList.add('scrollX'); + childrenItemsContainer.classList.add('hiddenScrollX'); + childrenItemsContainer.classList.remove('vertical-wrap'); + childrenItemsContainer.classList.remove('vertical-list'); + } else { + childrenItemsContainer.classList.remove('scrollX'); + childrenItemsContainer.classList.remove('hiddenScrollX'); + childrenItemsContainer.classList.remove('smoothScrollX'); + if (isList) { + childrenItemsContainer.classList.add('vertical-list'); childrenItemsContainer.classList.remove('vertical-wrap'); - childrenItemsContainer.classList.remove('vertical-list'); } else { - childrenItemsContainer.classList.remove('scrollX'); - childrenItemsContainer.classList.remove('hiddenScrollX'); - childrenItemsContainer.classList.remove('smoothScrollX'); - if (isList) { - childrenItemsContainer.classList.add('vertical-list'); - childrenItemsContainer.classList.remove('vertical-wrap'); - } else { - childrenItemsContainer.classList.add('vertical-wrap'); - childrenItemsContainer.classList.remove('vertical-list'); - } + childrenItemsContainer.classList.add('vertical-wrap'); + childrenItemsContainer.classList.remove('vertical-list'); } - if (layoutManager.mobile) { - childrenItemsContainer.classList.remove('padded-right'); - } - childrenItemsContainer.innerHTML = html; - imageLoader.lazyChildren(childrenItemsContainer); - if (item.Type == 'BoxSet') { - const collectionItemTypes = [{ - name: globalize.translate('HeaderVideos'), - mediaType: 'Video' - }, { - name: globalize.translate('Series'), - type: 'Series' - }, { - name: globalize.translate('Albums'), - type: 'MusicAlbum' - }, { - name: globalize.translate('Books'), - type: 'Book' - }]; - renderCollectionItems(page, item, collectionItemTypes, result.Items); - } - }); - - if (item.Type == 'Season') { - page.querySelector('#childrenTitle').innerHTML = globalize.translate('Episodes'); - } else if (item.Type == 'Series') { - page.querySelector('#childrenTitle').innerHTML = globalize.translate('HeaderSeasons'); - } else if (item.Type == 'MusicAlbum') { - page.querySelector('#childrenTitle').innerHTML = globalize.translate('HeaderTracks'); - } else { - page.querySelector('#childrenTitle').innerHTML = globalize.translate('Items'); } - - if (item.Type == 'MusicAlbum' || item.Type == 'Season') { - page.querySelector('.childrenSectionHeader').classList.add('hide'); - page.querySelector('#childrenCollapsible').classList.add('verticalSection-extrabottompadding'); - } else { - page.querySelector('.childrenSectionHeader').classList.remove('hide'); + if (layoutManager.mobile) { + childrenItemsContainer.classList.remove('padded-right'); } + childrenItemsContainer.innerHTML = html; + imageLoader.lazyChildren(childrenItemsContainer); + if (item.Type == 'BoxSet') { + const collectionItemTypes = [{ + name: globalize.translate('HeaderVideos'), + mediaType: 'Video' + }, { + name: globalize.translate('Series'), + type: 'Series' + }, { + name: globalize.translate('Albums'), + type: 'MusicAlbum' + }, { + name: globalize.translate('Books'), + type: 'Book' + }]; + renderCollectionItems(page, item, collectionItemTypes, result.Items); + } + }); + + if (item.Type == 'Season') { + page.querySelector('#childrenTitle').innerHTML = globalize.translate('Episodes'); + } else if (item.Type == 'Series') { + page.querySelector('#childrenTitle').innerHTML = globalize.translate('HeaderSeasons'); + } else if (item.Type == 'MusicAlbum') { + page.querySelector('#childrenTitle').innerHTML = globalize.translate('HeaderTracks'); + } else { + page.querySelector('#childrenTitle').innerHTML = globalize.translate('Items'); } - function renderItemsByName(page, item) { - import('scripts/itembynamedetailpage').then(() => { - window.ItemsByName.renderItems(page, item); - }); + if (item.Type == 'MusicAlbum' || item.Type == 'Season') { + page.querySelector('.childrenSectionHeader').classList.add('hide'); + page.querySelector('#childrenCollapsible').classList.add('verticalSection-extrabottompadding'); + } else { + page.querySelector('.childrenSectionHeader').classList.remove('hide'); } +} - function renderPlaylistItems(page, item) { - import('scripts/playlistedit').then(() => { - PlaylistViewer.render(page, item); - }); - } +function renderItemsByName(page, item) { + import('scripts/itembynamedetailpage').then(() => { + window.ItemsByName.renderItems(page, item); + }); +} - function renderProgramsForChannel(page, result) { - let html = ''; - let currentItems = []; - let currentStartDate = null; +function renderPlaylistItems(page, item) { + import('scripts/playlistedit').then(() => { + PlaylistViewer.render(page, item); + }); +} - for (let i = 0, length = result.Items.length; i < length; i++) { - const item = result.Items[i]; - const itemStartDate = datetime.parseISO8601Date(item.StartDate); +function renderProgramsForChannel(page, result) { + let html = ''; + let currentItems = []; + let currentStartDate = null; - if (!(currentStartDate && currentStartDate.toDateString() === itemStartDate.toDateString())) { - if (currentItems.length) { - html += '
'; - html += '

' + datetime.toLocaleDateString(currentStartDate, { - weekday: 'long', - month: 'long', - day: 'numeric' - }) + '

'; - html += '
' + listView.getListViewHtml({ - items: currentItems, - enableUserDataButtons: false, - showParentTitle: true, - image: false, - showProgramTime: true, - mediaInfo: false, - parentTitleWithTitle: true - }) + '
'; - } + for (let i = 0, length = result.Items.length; i < length; i++) { + const item = result.Items[i]; + const itemStartDate = datetime.parseISO8601Date(item.StartDate); - currentStartDate = itemStartDate; - currentItems = []; + if (!(currentStartDate && currentStartDate.toDateString() === itemStartDate.toDateString())) { + if (currentItems.length) { + html += '
'; + html += '

' + datetime.toLocaleDateString(currentStartDate, { + weekday: 'long', + month: 'long', + day: 'numeric' + }) + '

'; + html += '
' + listView.getListViewHtml({ + items: currentItems, + enableUserDataButtons: false, + showParentTitle: true, + image: false, + showProgramTime: true, + mediaInfo: false, + parentTitleWithTitle: true + }) + '
'; } - currentItems.push(item); + currentStartDate = itemStartDate; + currentItems = []; } - if (currentItems.length) { - html += '
'; - html += '

' + datetime.toLocaleDateString(currentStartDate, { - weekday: 'long', - month: 'long', - day: 'numeric' - }) + '

'; - html += '
' + listView.getListViewHtml({ - items: currentItems, - enableUserDataButtons: false, - showParentTitle: true, - image: false, - showProgramTime: true, - mediaInfo: false, - parentTitleWithTitle: true - }) + '
'; - } - - page.querySelector('.programGuide').innerHTML = html; + currentItems.push(item); } - function renderChannelGuide(page, apiClient, item) { - if (item.Type === 'TvChannel') { - page.querySelector('.programGuideSection').classList.remove('hide'); - apiClient.getLiveTvPrograms({ - ChannelIds: item.Id, - UserId: apiClient.getCurrentUserId(), - HasAired: false, - SortBy: 'StartDate', - EnableTotalRecordCount: false, - EnableImages: false, - ImageTypeLimit: 0, - EnableUserData: false - }).then(function (result) { - renderProgramsForChannel(page, result); - }); - } + if (currentItems.length) { + html += '
'; + html += '

' + datetime.toLocaleDateString(currentStartDate, { + weekday: 'long', + month: 'long', + day: 'numeric' + }) + '

'; + html += '
' + listView.getListViewHtml({ + items: currentItems, + enableUserDataButtons: false, + showParentTitle: true, + image: false, + showProgramTime: true, + mediaInfo: false, + parentTitleWithTitle: true + }) + '
'; } - function renderSeriesSchedule(page, item) { - const apiClient = connectionManager.getApiClient(item.ServerId); + page.querySelector('.programGuide').innerHTML = html; +} + +function renderChannelGuide(page, apiClient, item) { + if (item.Type === 'TvChannel') { + page.querySelector('.programGuideSection').classList.remove('hide'); apiClient.getLiveTvPrograms({ + ChannelIds: item.Id, UserId: apiClient.getCurrentUserId(), HasAired: false, SortBy: 'StartDate', EnableTotalRecordCount: false, EnableImages: false, ImageTypeLimit: 0, - Limit: 50, - EnableUserData: false, - LibrarySeriesId: item.Id + EnableUserData: false }).then(function (result) { - if (result.Items.length) { - page.querySelector('#seriesScheduleSection').classList.remove('hide'); - } else { - page.querySelector('#seriesScheduleSection').classList.add('hide'); - } - - page.querySelector('#seriesScheduleList').innerHTML = listView.getListViewHtml({ - items: result.Items, - enableUserDataButtons: false, - showParentTitle: false, - image: false, - showProgramDateTime: true, - mediaInfo: false, - showTitle: true, - moreButton: false, - action: 'programdialog' - }); - loading.hide(); + renderProgramsForChannel(page, result); }); } +} - function inferContext(item) { - if (item.Type === 'Movie' || item.Type === 'BoxSet') { - return 'movies'; - } - - if (item.Type === 'Series' || item.Type === 'Season' || item.Type === 'Episode') { - return 'tvshows'; - } - - if (item.Type === 'MusicArtist' || item.Type === 'MusicAlbum' || item.Type === 'Audio' || item.Type === 'AudioBook') { - return 'music'; - } - - if (item.Type === 'Program') { - return 'livetv'; - } - - return null; - } - - function filterItemsByCollectionItemType(items, typeInfo) { - return items.filter(function (item) { - if (typeInfo.mediaType) { - return item.MediaType == typeInfo.mediaType; - } - - return item.Type == typeInfo.type; - }); - } - - function canPlaySomeItemInCollection(items) { - let i = 0; - - for (let length = items.length; i < length; i++) { - if (playbackManager.canPlay(items[i])) { - return true; - } - } - - return false; - } - - function renderCollectionItems(page, parentItem, types, items) { - page.querySelector('.collectionItems').classList.remove('hide'); - page.querySelector('.collectionItems').innerHTML = ''; - - for (const type of types) { - const typeItems = filterItemsByCollectionItemType(items, type); - - if (typeItems.length) { - renderCollectionItemType(page, parentItem, type, typeItems); - } - } - - const otherType = { - name: globalize.translate('HeaderOtherItems') - }; - const otherTypeItems = items.filter(function (curr) { - return !types.filter(function (t) { - return filterItemsByCollectionItemType([curr], t).length > 0; - }).length; - }); - - if (otherTypeItems.length) { - renderCollectionItemType(page, parentItem, otherType, otherTypeItems); - } - - if (!items.length) { - renderCollectionItemType(page, parentItem, { - name: globalize.translate('Items') - }, items); - } - - const containers = page.querySelectorAll('.collectionItemsContainer'); - - const notifyRefreshNeeded = function () { - renderChildren(page, parentItem); - }; - - for (const container of containers) { - container.notifyRefreshNeeded = notifyRefreshNeeded; - } - - // if nothing in the collection can be played hide play and shuffle buttons - if (!canPlaySomeItemInCollection(items)) { - hideAll(page, 'btnPlay', false); - hideAll(page, 'btnShuffle', false); - } - - // HACK: Call autoFocuser again because btnPlay may be hidden, but focused by reloadFromItem - // FIXME: Sometimes focus does not move until all (?) sections are loaded - import('autoFocuser').then(({default: autoFocuser}) => { - autoFocuser.autoFocus(page); - }); - } - - function renderCollectionItemType(page, parentItem, type, items) { - let html = ''; - html += '
'; - html += '
'; - html += '

'; - html += '' + type.name + ''; - html += '

'; - html += '
'; - html += '
'; - const shape = type.type == 'MusicAlbum' ? getSquareShape(false) : getPortraitShape(false); - html += cardBuilder.getCardsHtml({ - items: items, - shape: shape, - showTitle: true, - showYear: type.mediaType === 'Video' || type.type === 'Series', - centerText: true, - lazy: true, - showDetailsMenu: true, - overlayMoreButton: true, - showAddToCollection: false, - showRemoveFromCollection: true, - collectionId: parentItem.Id - }); - html += '
'; - html += '
'; - const collectionItems = page.querySelector('.collectionItems'); - collectionItems.insertAdjacentHTML('beforeend', html); - imageLoader.lazyChildren(collectionItems); - } - - function renderMusicVideos(page, item, user) { - connectionManager.getApiClient(item.ServerId).getItems(user.Id, { - SortBy: 'SortName', - SortOrder: 'Ascending', - IncludeItemTypes: 'MusicVideo', - Recursive: true, - Fields: 'PrimaryImageAspectRatio,BasicSyncInfo,CanDelete,MediaSourceCount', - AlbumIds: item.Id - }).then(function (result) { - if (result.Items.length) { - page.querySelector('#musicVideosCollapsible').classList.remove('hide'); - const musicVideosContent = page.querySelector('.musicVideosContent'); - musicVideosContent.innerHTML = getVideosHtml(result.Items); - imageLoader.lazyChildren(musicVideosContent); - } else { - page.querySelector('#musicVideosCollapsible').classList.add('hide'); - } - }); - } - - function renderAdditionalParts(page, item, user) { - connectionManager.getApiClient(item.ServerId).getAdditionalVideoParts(user.Id, item.Id).then(function (result) { - if (result.Items.length) { - page.querySelector('#additionalPartsCollapsible').classList.remove('hide'); - const additionalPartsContent = page.querySelector('#additionalPartsContent'); - additionalPartsContent.innerHTML = getVideosHtml(result.Items); - imageLoader.lazyChildren(additionalPartsContent); - } else { - page.querySelector('#additionalPartsCollapsible').classList.add('hide'); - } - }); - } - - function renderScenes(page, item) { - let chapters = item.Chapters || []; - - if (chapters.length && !chapters[0].ImageTag && (chapters = []), chapters.length) { - page.querySelector('#scenesCollapsible').classList.remove('hide'); - const scenesContent = page.querySelector('#scenesContent'); - - import('chaptercardbuilder').then(({default: chaptercardbuilder}) => { - chaptercardbuilder.buildChapterCards(item, chapters, { - itemsContainer: scenesContent, - backdropShape: 'overflowBackdrop', - squareShape: 'overflowSquare', - imageBlurhashes: item.ImageBlurHashes - }); - }); +function renderSeriesSchedule(page, item) { + const apiClient = connectionManager.getApiClient(item.ServerId); + apiClient.getLiveTvPrograms({ + UserId: apiClient.getCurrentUserId(), + HasAired: false, + SortBy: 'StartDate', + EnableTotalRecordCount: false, + EnableImages: false, + ImageTypeLimit: 0, + Limit: 50, + EnableUserData: false, + LibrarySeriesId: item.Id + }).then(function (result) { + if (result.Items.length) { + page.querySelector('#seriesScheduleSection').classList.remove('hide'); } else { - page.querySelector('#scenesCollapsible').classList.add('hide'); + page.querySelector('#seriesScheduleSection').classList.add('hide'); } - } - function getVideosHtml(items) { - return cardBuilder.getCardsHtml({ - items: items, - shape: 'autooverflow', + page.querySelector('#seriesScheduleList').innerHTML = listView.getListViewHtml({ + items: result.Items, + enableUserDataButtons: false, + showParentTitle: false, + image: false, + showProgramDateTime: true, + mediaInfo: false, showTitle: true, - action: 'play', - overlayText: false, - centerText: true, - showRuntime: true + moreButton: false, + action: 'programdialog' }); + loading.hide(); + }); +} + +function inferContext(item) { + if (item.Type === 'Movie' || item.Type === 'BoxSet') { + return 'movies'; } - function renderSpecials(page, item, user) { - connectionManager.getApiClient(item.ServerId).getSpecialFeatures(user.Id, item.Id).then(function (specials) { - const specialsContent = page.querySelector('#specialsContent'); - specialsContent.innerHTML = getVideosHtml(specials); - imageLoader.lazyChildren(specialsContent); - }); + if (item.Type === 'Series' || item.Type === 'Season' || item.Type === 'Episode') { + return 'tvshows'; } - function renderCast(page, item) { - const people = (item.People || []).filter(function (p) { - return p.Type === 'Actor'; - }); + if (item.Type === 'MusicArtist' || item.Type === 'MusicAlbum' || item.Type === 'Audio' || item.Type === 'AudioBook') { + return 'music'; + } - if (!people.length) { - return void page.querySelector('#castCollapsible').classList.add('hide'); + if (item.Type === 'Program') { + return 'livetv'; + } + + return null; +} + +function filterItemsByCollectionItemType(items, typeInfo) { + return items.filter(function (item) { + if (typeInfo.mediaType) { + return item.MediaType == typeInfo.mediaType; } - page.querySelector('#castCollapsible').classList.remove('hide'); - const castContent = page.querySelector('#castContent'); + return item.Type == typeInfo.type; + }); +} - import('peoplecardbuilder').then(({default: peoplecardbuilder}) => { - peoplecardbuilder.buildPeopleCards(people, { - itemsContainer: castContent, - coverImage: true, - serverId: item.ServerId, - shape: 'overflowPortrait', +function canPlaySomeItemInCollection(items) { + let i = 0; + + for (let length = items.length; i < length; i++) { + if (playbackManager.canPlay(items[i])) { + return true; + } + } + + return false; +} + +function renderCollectionItems(page, parentItem, types, items) { + page.querySelector('.collectionItems').classList.remove('hide'); + page.querySelector('.collectionItems').innerHTML = ''; + + for (const type of types) { + const typeItems = filterItemsByCollectionItemType(items, type); + + if (typeItems.length) { + renderCollectionItemType(page, parentItem, type, typeItems); + } + } + + const otherType = { + name: globalize.translate('HeaderOtherItems') + }; + const otherTypeItems = items.filter(function (curr) { + return !types.filter(function (t) { + return filterItemsByCollectionItemType([curr], t).length > 0; + }).length; + }); + + if (otherTypeItems.length) { + renderCollectionItemType(page, parentItem, otherType, otherTypeItems); + } + + if (!items.length) { + renderCollectionItemType(page, parentItem, { + name: globalize.translate('Items') + }, items); + } + + const containers = page.querySelectorAll('.collectionItemsContainer'); + + const notifyRefreshNeeded = function () { + renderChildren(page, parentItem); + }; + + for (const container of containers) { + container.notifyRefreshNeeded = notifyRefreshNeeded; + } + + // if nothing in the collection can be played hide play and shuffle buttons + if (!canPlaySomeItemInCollection(items)) { + hideAll(page, 'btnPlay', false); + hideAll(page, 'btnShuffle', false); + } + + // HACK: Call autoFocuser again because btnPlay may be hidden, but focused by reloadFromItem + // FIXME: Sometimes focus does not move until all (?) sections are loaded + import('autoFocuser').then(({ default: autoFocuser }) => { + autoFocuser.autoFocus(page); + }); +} + +function renderCollectionItemType(page, parentItem, type, items) { + let html = ''; + html += '
'; + html += '
'; + html += '

'; + html += '' + type.name + ''; + html += '

'; + html += '
'; + html += '
'; + const shape = type.type == 'MusicAlbum' ? getSquareShape(false) : getPortraitShape(false); + html += cardBuilder.getCardsHtml({ + items: items, + shape: shape, + showTitle: true, + showYear: type.mediaType === 'Video' || type.type === 'Series', + centerText: true, + lazy: true, + showDetailsMenu: true, + overlayMoreButton: true, + showAddToCollection: false, + showRemoveFromCollection: true, + collectionId: parentItem.Id + }); + html += '
'; + html += '
'; + const collectionItems = page.querySelector('.collectionItems'); + collectionItems.insertAdjacentHTML('beforeend', html); + imageLoader.lazyChildren(collectionItems); +} + +function renderMusicVideos(page, item, user) { + connectionManager.getApiClient(item.ServerId).getItems(user.Id, { + SortBy: 'SortName', + SortOrder: 'Ascending', + IncludeItemTypes: 'MusicVideo', + Recursive: true, + Fields: 'PrimaryImageAspectRatio,BasicSyncInfo,CanDelete,MediaSourceCount', + AlbumIds: item.Id + }).then(function (result) { + if (result.Items.length) { + page.querySelector('#musicVideosCollapsible').classList.remove('hide'); + const musicVideosContent = page.querySelector('.musicVideosContent'); + musicVideosContent.innerHTML = getVideosHtml(result.Items); + imageLoader.lazyChildren(musicVideosContent); + } else { + page.querySelector('#musicVideosCollapsible').classList.add('hide'); + } + }); +} + +function renderAdditionalParts(page, item, user) { + connectionManager.getApiClient(item.ServerId).getAdditionalVideoParts(user.Id, item.Id).then(function (result) { + if (result.Items.length) { + page.querySelector('#additionalPartsCollapsible').classList.remove('hide'); + const additionalPartsContent = page.querySelector('#additionalPartsContent'); + additionalPartsContent.innerHTML = getVideosHtml(result.Items); + imageLoader.lazyChildren(additionalPartsContent); + } else { + page.querySelector('#additionalPartsCollapsible').classList.add('hide'); + } + }); +} + +function renderScenes(page, item) { + let chapters = item.Chapters || []; + + if (chapters.length && !chapters[0].ImageTag && (chapters = []), chapters.length) { + page.querySelector('#scenesCollapsible').classList.remove('hide'); + const scenesContent = page.querySelector('#scenesContent'); + + import('chaptercardbuilder').then(({ default: chaptercardbuilder }) => { + chaptercardbuilder.buildChapterCards(item, chapters, { + itemsContainer: scenesContent, + backdropShape: 'overflowBackdrop', + squareShape: 'overflowSquare', imageBlurhashes: item.ImageBlurHashes }); }); + } else { + page.querySelector('#scenesCollapsible').classList.add('hide'); + } +} + +function getVideosHtml(items) { + return cardBuilder.getCardsHtml({ + items: items, + shape: 'autooverflow', + showTitle: true, + action: 'play', + overlayText: false, + centerText: true, + showRuntime: true + }); +} + +function renderSpecials(page, item, user) { + connectionManager.getApiClient(item.ServerId).getSpecialFeatures(user.Id, item.Id).then(function (specials) { + const specialsContent = page.querySelector('#specialsContent'); + specialsContent.innerHTML = getVideosHtml(specials); + imageLoader.lazyChildren(specialsContent); + }); +} + +function renderCast(page, item) { + const people = (item.People || []).filter(function (p) { + return p.Type === 'Actor'; + }); + + if (!people.length) { + return void page.querySelector('#castCollapsible').classList.add('hide'); } - function itemDetailPage() { - const self = this; - self.setInitialCollapsibleState = setInitialCollapsibleState; - self.renderDetails = renderDetails; - self.renderCast = renderCast; + page.querySelector('#castCollapsible').classList.remove('hide'); + const castContent = page.querySelector('#castContent'); + + import('peoplecardbuilder').then(({ default: peoplecardbuilder }) => { + peoplecardbuilder.buildPeopleCards(people, { + itemsContainer: castContent, + coverImage: true, + serverId: item.ServerId, + shape: 'overflowPortrait', + imageBlurhashes: item.ImageBlurHashes + }); + }); +} + +function itemDetailPage() { + const self = this; + self.setInitialCollapsibleState = setInitialCollapsibleState; + self.renderDetails = renderDetails; + self.renderCast = renderCast; +} + +function bindAll(view, selector, eventName, fn) { + const elems = view.querySelectorAll(selector); + + for (const elem of elems) { + elem.addEventListener(eventName, fn); } +} - function bindAll(view, selector, eventName, fn) { - const elems = view.querySelectorAll(selector); +function onTrackSelectionsSubmit(e) { + e.preventDefault(); + return false; +} - for (const elem of elems) { - elem.addEventListener(eventName, fn); - } - } +window.ItemDetailPage = new itemDetailPage(); - function onTrackSelectionsSubmit(e) { - e.preventDefault(); - return false; - } +export default function (view, params) { + function reload(instance, page, params) { + loading.show(); - window.ItemDetailPage = new itemDetailPage(); - - export default function (view, params) { - function reload(instance, page, params) { - loading.show(); - - const apiClient = params.serverId ? connectionManager.getApiClient(params.serverId) : ApiClient; - - Promise.all([getPromise(apiClient, params), apiClient.getCurrentUser()]).then(([item, user]) => { - currentItem = item; - reloadFromItem(instance, page, params, item, user); - }).catch((error) => { - console.error('failed to get item or current user: ', error); - }); - } - - function splitVersions(instance, page, apiClient, params) { - import('confirm').then(({default: confirm}) => { - confirm('Are you sure you wish to split the media sources into separate items?', 'Split Media Apart').then(function () { - loading.show(); - apiClient.ajax({ - type: 'DELETE', - url: apiClient.getUrl('Videos/' + params.id + '/AlternateSources') - }).then(function () { - loading.hide(); - reload(instance, page, params); - }); - }); - }); - } - - function getPlayOptions(startPosition) { - const audioStreamIndex = view.querySelector('.selectAudio').value || null; - return { - startPositionTicks: startPosition, - mediaSourceId: view.querySelector('.selectSource').value, - audioStreamIndex: audioStreamIndex, - subtitleStreamIndex: view.querySelector('.selectSubtitles').value - }; - } - - function playItem(item, startPosition) { - const playOptions = getPlayOptions(startPosition); - playOptions.items = [item]; - playbackManager.play(playOptions); - } - - function playTrailer() { - playbackManager.playTrailers(currentItem); - } - - function playCurrentItem(button, mode) { - const item = currentItem; - - if (item.Type === 'Program') { - const apiClient = connectionManager.getApiClient(item.ServerId); - return void apiClient.getLiveTvChannel(item.ChannelId, apiClient.getCurrentUserId()).then(function (channel) { - playbackManager.play({ - items: [channel] - }); - }); - } - - playItem(item, item.UserData && mode === 'resume' ? item.UserData.PlaybackPositionTicks : 0); - } - - function onPlayClick() { - playCurrentItem(this, this.getAttribute('data-mode')); - } - - function onPosterClick(e) { - itemShortcuts.onClick.call(view.querySelector('.detailImageContainer'), e); - } - - function onInstantMixClick() { - playbackManager.instantMix(currentItem); - } - - function onShuffleClick() { - playbackManager.shuffle(currentItem); - } - - function onCancelSeriesTimerClick() { - import('recordingHelper').then(({default: recordingHelper}) => { - recordingHelper.cancelSeriesTimerWithConfirmation(currentItem.Id, currentItem.ServerId).then(function () { - Dashboard.navigate('livetv.html'); - }); - }); - } - - function onCancelTimerClick() { - import('recordingHelper').then(({default: recordingHelper}) => { - recordingHelper.cancelTimer(connectionManager.getApiClient(currentItem.ServerId), currentItem.TimerId).then(function () { - reload(self, view, params); - }); - }); - } - - function onPlayTrailerClick() { - playTrailer(); - } - - function onDownloadClick() { - import('fileDownloader').then(({default: fileDownloader}) => { - const downloadHref = apiClient.getItemDownloadUrl(currentItem.Id); - fileDownloader.download([{ - url: downloadHref, - itemId: currentItem.Id, - serverId: currentItem.serverId - }]); - }); - } - - function onMoreCommandsClick() { - var button = this; - var selectedItem = currentItem; - apiClient.getItem(apiClient.getCurrentUserId(), view.querySelector('.selectSource').value).then(function (item) { - selectedItem = item; - - apiClient.getCurrentUser().then(function (user) { - itemContextMenu.show(getContextMenuOptions(selectedItem, user, button)).then(function (result) { - if (result.deleted) { - appRouter.goHome(); - } else if (result.updated) { - reload(self, view, params); - } - }); - }); - }); - } - - function onPlayerChange() { - renderTrackSelections(view, self, currentItem); - setTrailerButtonVisibility(view, currentItem); - } - - function onWebSocketMessage(e, data) { - const msg = data; - - if (msg.MessageType === 'UserDataChanged' && currentItem && msg.Data.UserId == apiClient.getCurrentUserId()) { - const key = currentItem.UserData.Key; - const userData = msg.Data.UserDataList.filter(function (u) { - return u.Key == key; - })[0]; - - if (userData) { - currentItem.UserData = userData; - reloadPlayButtons(view, currentItem); - refreshImage(view, currentItem); - } - } - } - - let currentItem; - const self = this; const apiClient = params.serverId ? connectionManager.getApiClient(params.serverId) : ApiClient; - view.querySelectorAll('.btnPlay'); - bindAll(view, '.btnPlay', 'click', onPlayClick); - bindAll(view, '.btnResume', 'click', onPlayClick); - bindAll(view, '.btnInstantMix', 'click', onInstantMixClick); - bindAll(view, '.btnShuffle', 'click', onShuffleClick); - bindAll(view, '.btnPlayTrailer', 'click', onPlayTrailerClick); - bindAll(view, '.btnCancelSeriesTimer', 'click', onCancelSeriesTimerClick); - bindAll(view, '.btnCancelTimer', 'click', onCancelTimerClick); - bindAll(view, '.btnDownload', 'click', onDownloadClick); - view.querySelector('.detailImageContainer').addEventListener('click', onPosterClick); - view.querySelector('.trackSelections').addEventListener('submit', onTrackSelectionsSubmit); - view.querySelector('.btnSplitVersions').addEventListener('click', function () { - splitVersions(self, view, apiClient, params); - }); - bindAll(view, '.btnMoreCommands', 'click', onMoreCommandsClick); - view.querySelector('.selectSource').addEventListener('change', function () { - renderVideoSelections(view, self._currentPlaybackMediaSources); - renderAudioSelections(view, self._currentPlaybackMediaSources); - renderSubtitleSelections(view, self._currentPlaybackMediaSources); - }); - view.addEventListener('viewshow', function (e) { - const page = this; - libraryMenu.setTransparentMenu(true); - - if (e.detail.isRestored) { - if (currentItem) { - Emby.Page.setTitle(''); - renderTrackSelections(page, self, currentItem, true); - } - } else { - reload(self, page, params); - } - - events.on(apiClient, 'message', onWebSocketMessage); - events.on(playbackManager, 'playerchange', onPlayerChange); - }); - view.addEventListener('viewbeforehide', function () { - events.off(apiClient, 'message', onWebSocketMessage); - events.off(playbackManager, 'playerchange', onPlayerChange); - libraryMenu.setTransparentMenu(false); - }); - view.addEventListener('viewdestroy', function () { - currentItem = null; - self._currentPlaybackMediaSources = null; - self.currentRecordingFields = null; + Promise.all([getPromise(apiClient, params), apiClient.getCurrentUser()]).then(([item, user]) => { + currentItem = item; + reloadFromItem(instance, page, params, item, user); + }).catch((error) => { + console.error('failed to get item or current user: ', error); }); } -/* eslint-enable indent */ + function splitVersions(instance, page, apiClient, params) { + import('confirm').then(({ default: confirm }) => { + confirm('Are you sure you wish to split the media sources into separate items?', 'Split Media Apart').then(function () { + loading.show(); + apiClient.ajax({ + type: 'DELETE', + url: apiClient.getUrl('Videos/' + params.id + '/AlternateSources') + }).then(function () { + loading.hide(); + reload(instance, page, params); + }); + }); + }); + } + + function getPlayOptions(startPosition) { + const audioStreamIndex = view.querySelector('.selectAudio').value || null; + return { + startPositionTicks: startPosition, + mediaSourceId: view.querySelector('.selectSource').value, + audioStreamIndex: audioStreamIndex, + subtitleStreamIndex: view.querySelector('.selectSubtitles').value + }; + } + + function playItem(item, startPosition) { + const playOptions = getPlayOptions(startPosition); + playOptions.items = [item]; + playbackManager.play(playOptions); + } + + function playTrailer() { + playbackManager.playTrailers(currentItem); + } + + function playCurrentItem(button, mode) { + const item = currentItem; + + if (item.Type === 'Program') { + const apiClient = connectionManager.getApiClient(item.ServerId); + return void apiClient.getLiveTvChannel(item.ChannelId, apiClient.getCurrentUserId()).then(function (channel) { + playbackManager.play({ + items: [channel] + }); + }); + } + + playItem(item, item.UserData && mode === 'resume' ? item.UserData.PlaybackPositionTicks : 0); + } + + function onPlayClick() { + playCurrentItem(this, this.getAttribute('data-mode')); + } + + function onPosterClick(e) { + itemShortcuts.onClick.call(view.querySelector('.detailImageContainer'), e); + } + + function onInstantMixClick() { + playbackManager.instantMix(currentItem); + } + + function onShuffleClick() { + playbackManager.shuffle(currentItem); + } + + function onCancelSeriesTimerClick() { + import('recordingHelper').then(({ default: recordingHelper }) => { + recordingHelper.cancelSeriesTimerWithConfirmation(currentItem.Id, currentItem.ServerId).then(function () { + Dashboard.navigate('livetv.html'); + }); + }); + } + + function onCancelTimerClick() { + import('recordingHelper').then(({ default: recordingHelper }) => { + recordingHelper.cancelTimer(connectionManager.getApiClient(currentItem.ServerId), currentItem.TimerId).then(function () { + reload(self, view, params); + }); + }); + } + + function onPlayTrailerClick() { + playTrailer(); + } + + function onDownloadClick() { + import('fileDownloader').then(({ default: fileDownloader }) => { + const downloadHref = apiClient.getItemDownloadUrl(currentItem.Id); + fileDownloader.download([{ + url: downloadHref, + itemId: currentItem.Id, + serverId: currentItem.serverId + }]); + }); + } + + function onMoreCommandsClick() { + const button = this; + let selectedItem = view.querySelector('.selectSource').value || currentItem.Id; + + apiClient.getItem(apiClient.getCurrentUserId(), selectedItem).then(function (item) { + selectedItem = item; + + apiClient.getCurrentUser().then(function (user) { + itemContextMenu.show(getContextMenuOptions(selectedItem, user, button)).then(function (result) { + if (result.deleted) { + appRouter.goHome(); + } else if (result.updated) { + reload(self, view, params); + } + }); + }); + }); + } + + function onPlayerChange() { + renderTrackSelections(view, self, currentItem); + setTrailerButtonVisibility(view, currentItem); + } + + function onWebSocketMessage(e, data) { + const msg = data; + + if (msg.MessageType === 'UserDataChanged' && currentItem && msg.Data.UserId == apiClient.getCurrentUserId()) { + const key = currentItem.UserData.Key; + const userData = msg.Data.UserDataList.filter(function (u) { + return u.Key == key; + })[0]; + + if (userData) { + currentItem.UserData = userData; + reloadPlayButtons(view, currentItem); + refreshImage(view, currentItem); + } + } + } + + let currentItem; + const self = this; + const apiClient = params.serverId ? connectionManager.getApiClient(params.serverId) : ApiClient; + + const btnResume = view.querySelector('.mainDetailButtons .btnResume'); + const btnPlay = view.querySelector('.mainDetailButtons .btnPlay'); + if (layoutManager.tv && !btnResume.classList.contains('hide')) { + btnResume.classList.add('fab'); + btnResume.classList.add('detailFloatingButton'); + } else if (layoutManager.tv && btnResume.classList.contains('hide')) { + btnPlay.classList.add('fab'); + btnPlay.classList.add('detailFloatingButton'); + } + + view.querySelectorAll('.btnPlay'); + bindAll(view, '.btnPlay', 'click', onPlayClick); + bindAll(view, '.btnResume', 'click', onPlayClick); + bindAll(view, '.btnInstantMix', 'click', onInstantMixClick); + bindAll(view, '.btnShuffle', 'click', onShuffleClick); + bindAll(view, '.btnPlayTrailer', 'click', onPlayTrailerClick); + bindAll(view, '.btnCancelSeriesTimer', 'click', onCancelSeriesTimerClick); + bindAll(view, '.btnCancelTimer', 'click', onCancelTimerClick); + bindAll(view, '.btnDownload', 'click', onDownloadClick); + view.querySelector('.detailImageContainer').addEventListener('click', onPosterClick); + view.querySelector('.trackSelections').addEventListener('submit', onTrackSelectionsSubmit); + view.querySelector('.btnSplitVersions').addEventListener('click', function () { + splitVersions(self, view, apiClient, params); + }); + bindAll(view, '.btnMoreCommands', 'click', onMoreCommandsClick); + view.querySelector('.selectSource').addEventListener('change', function () { + renderVideoSelections(view, self._currentPlaybackMediaSources); + renderAudioSelections(view, self._currentPlaybackMediaSources); + renderSubtitleSelections(view, self._currentPlaybackMediaSources); + }); + view.addEventListener('viewshow', function (e) { + const page = this; + + libraryMenu.setTransparentMenu(true); + + if (e.detail.isRestored) { + if (currentItem) { + Emby.Page.setTitle(''); + renderTrackSelections(page, self, currentItem, true); + } + } else { + reload(self, page, params); + } + + events.on(apiClient, 'message', onWebSocketMessage); + events.on(playbackManager, 'playerchange', onPlayerChange); + }); + view.addEventListener('viewbeforehide', function () { + events.off(apiClient, 'message', onWebSocketMessage); + events.off(playbackManager, 'playerchange', onPlayerChange); + libraryMenu.setTransparentMenu(false); + }); + view.addEventListener('viewdestroy', function () { + currentItem = null; + self._currentPlaybackMediaSources = null; + self.currentRecordingFields = null; + }); +} diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 420268ea75..a6e17a8063 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -693,7 +693,7 @@ import 'css!assets/css/videoosd'; lastUpdateTime = now; const player = this; currentRuntimeTicks = playbackManager.duration(player); - const currentTime = playbackManager.currentTime(player); + const currentTime = playbackManager.currentTime(player) * 10000; updateTimeDisplay(currentTime, currentRuntimeTicks, playbackManager.playbackStartTime(player), playbackManager.getBufferedRanges(player)); const item = currentItem; refreshProgramInfoIfNeeded(player, item); diff --git a/src/controllers/session/resetPassword/index.js b/src/controllers/session/resetPassword/index.js index 3cb8d4a9e2..d4f7df5bbd 100644 --- a/src/controllers/session/resetPassword/index.js +++ b/src/controllers/session/resetPassword/index.js @@ -31,7 +31,8 @@ import globalize from 'globalize'; dataType: 'json', data: JSON.stringify({ Pin: view.querySelector('#txtPin').value - }) + }), + contentType: 'application/json' }).then(processForgotPasswordResult); e.preventDefault(); return false; diff --git a/src/controllers/user/menu/index.html b/src/controllers/user/menu/index.html index b2ecd41d28..1655f75c5e 100644 --- a/src/controllers/user/menu/index.html +++ b/src/controllers/user/menu/index.html @@ -57,7 +57,7 @@
-
+

${HeaderAdmin}

diff --git a/src/controllers/user/menu/index.js b/src/controllers/user/menu/index.js index f44a193a52..7a8d619bb0 100644 --- a/src/controllers/user/menu/index.js +++ b/src/controllers/user/menu/index.js @@ -1,4 +1,5 @@ import appHost from 'apphost'; +import layoutManager from 'layoutManager'; import 'listViewStyle'; import 'emby-button'; @@ -38,19 +39,19 @@ export default function (view, params) { page.querySelector('.selectServer').classList.add('hide'); } - // hide the actions if user preferences are being edited for a different user + ApiClient.getUser(userId).then(function (user) { + page.querySelector('.headerUsername').innerHTML = user.Name; + if (user.Policy.IsAdministrator && !layoutManager.tv) { + page.querySelector('.adminSection').classList.remove('hide'); + } + }); + + // Hide the actions if user preferences are being edited for a different user if (params.userId && params.userId !== Dashboard.getCurrentUserId) { page.querySelector('.userSection').classList.add('hide'); page.querySelector('.adminSection').classList.add('hide'); } - ApiClient.getUser(userId).then(function (user) { - page.querySelector('.headerUsername').innerHTML = user.Name; - if (!user.Policy.IsAdministrator) { - page.querySelector('.adminSection').classList.add('hide'); - } - }); - import('autoFocuser').then(({default: autoFocuser}) => { autoFocuser.autoFocus(view); }); diff --git a/src/controllers/wizard/remote/index.js b/src/controllers/wizard/remote/index.js index 74c795658d..b967d668ad 100644 --- a/src/controllers/wizard/remote/index.js +++ b/src/controllers/wizard/remote/index.js @@ -12,7 +12,8 @@ function save(page) { apiClient.ajax({ type: 'POST', data: JSON.stringify(config), - url: apiClient.getUrl('Startup/RemoteAccess') + url: apiClient.getUrl('Startup/RemoteAccess'), + contentType: 'application/json' }).then(function () { loading.hide(); navigateToNextPage(); diff --git a/src/controllers/wizard/settings/index.js b/src/controllers/wizard/settings/index.js index 2eaef4fab6..6e3a82cd9b 100644 --- a/src/controllers/wizard/settings/index.js +++ b/src/controllers/wizard/settings/index.js @@ -12,7 +12,8 @@ function save(page) { apiClient.ajax({ type: 'POST', data: JSON.stringify(config), - url: apiClient.getUrl('Startup/Configuration') + url: apiClient.getUrl('Startup/Configuration'), + contentType: 'application/json' }).then(function () { loading.hide(); navigateToNextPage(); diff --git a/src/controllers/wizard/start/index.js b/src/controllers/wizard/start/index.js index 8c147fc75e..3cd53b4ceb 100644 --- a/src/controllers/wizard/start/index.js +++ b/src/controllers/wizard/start/index.js @@ -18,7 +18,8 @@ function save(page) { apiClient.ajax({ type: 'POST', data: JSON.stringify(config), - url: apiClient.getUrl('Startup/Configuration') + url: apiClient.getUrl('Startup/Configuration'), + contentType: 'application/json' }).then(function () { Dashboard.navigate('wizarduser.html'); }); diff --git a/src/controllers/wizard/user/index.js b/src/controllers/wizard/user/index.js index fc9a27855f..ec587fec8e 100644 --- a/src/controllers/wizard/user/index.js +++ b/src/controllers/wizard/user/index.js @@ -27,7 +27,8 @@ function submit(form) { Name: form.querySelector('#txtUsername').value, Password: form.querySelector('#txtManualPassword').value }), - url: apiClient.getUrl('Startup/User') + url: apiClient.getUrl('Startup/User'), + contentType: 'application/json' }).then(onUpdateUserComplete); } diff --git a/src/legacy/vendorStyles.js b/src/legacy/vendorStyles.js new file mode 100644 index 0000000000..74a90ba04d --- /dev/null +++ b/src/legacy/vendorStyles.js @@ -0,0 +1,34 @@ +// Polyfill for vendor prefixed style properties + +(function () { + const vendorProperties = { + 'transform': ['webkitTransform'], + 'transition': ['webkitTransition'] + }; + + const elem = document.createElement('div'); + + function polyfillProperty(name) { + if (!(name in elem.style)) { + (vendorProperties[name] || []).every((vendorName) => { + if (vendorName in elem.style) { + console.debug(`polyfill '${name}' with '${vendorName}'`); + + Object.defineProperty(CSSStyleDeclaration.prototype, name, { + get: function () { return this[vendorName]; }, + set: function (val) { this[vendorName] = val; } + }); + + return false; + } + + return true; + }); + } + } + + if (elem.style instanceof CSSStyleDeclaration) { + polyfillProperty('transform'); + polyfillProperty('transition'); + } +})(); diff --git a/src/libraries/scroller.js b/src/libraries/scroller.js index c460ec5b2c..dbb3de16e3 100644 --- a/src/libraries/scroller.js +++ b/src/libraries/scroller.js @@ -256,7 +256,11 @@ var scrollerFactory = function (frame, options) { ensureSizeInfo(); var pos = self._pos; - newPos = within(newPos, pos.start, pos.end); + if (layoutManager.tv) { + newPos = within(newPos, pos.start); + } else { + newPos = within(newPos, pos.start, pos.end); + } if (!transform) { nativeScrollTo(nativeScrollElement, newPos, immediate); diff --git a/src/plugins/chromecastPlayer/plugin.js b/src/plugins/chromecastPlayer/plugin.js index b7e6d05969..31d9568898 100644 --- a/src/plugins/chromecastPlayer/plugin.js +++ b/src/plugins/chromecastPlayer/plugin.js @@ -931,9 +931,9 @@ class ChromecastPlayer { return state.VolumeLevel == null ? 100 : state.VolumeLevel; } - isPlaying() { + isPlaying(mediaType) { const state = this.lastPlayerData || {}; - return state.NowPlayingItem != null; + return state.NowPlayingItem != null && (state.NowPlayingItem.MediaType === mediaType || !mediaType); } isPlayingVideo() { @@ -950,12 +950,12 @@ class ChromecastPlayer { currentTime(val) { if (val != null) { - return this.seek(val); + return this.seek(val * 10000); } let state = this.lastPlayerData || {}; state = state.PlayState || {}; - return state.PositionTicks; + return state.PositionTicks / 10000; } duration() { diff --git a/src/plugins/comicsPlayer/plugin.js b/src/plugins/comicsPlayer/plugin.js new file mode 100644 index 0000000000..5f9546b9fe --- /dev/null +++ b/src/plugins/comicsPlayer/plugin.js @@ -0,0 +1,214 @@ +import connectionManager from 'connectionManager'; +import loading from 'loading'; +import dialogHelper from 'dialogHelper'; +import keyboardnavigation from 'keyboardnavigation'; +import appRouter from 'appRouter'; +import * as libarchive from 'libarchive'; + +export class ComicsPlayer { + constructor() { + this.name = 'Comics Player'; + this.type = 'mediaplayer'; + this.id = 'comicsplayer'; + this.priority = 1; + this.imageMap = new Map(); + + this.onDialogClosed = this.onDialogClosed.bind(this); + this.onWindowKeyUp = this.onWindowKeyUp.bind(this); + } + + play(options) { + this.progress = 0; + + let elem = this.createMediaElement(); + return this.setCurrentSrc(elem, options); + } + + stop() { + this.unbindEvents(); + + let elem = this.mediaElement; + if (elem) { + dialogHelper.close(elem); + this.mediaElement = null; + } + + loading.hide(); + } + + onDialogClosed() { + this.stop(); + } + + onWindowKeyUp(e) { + let key = keyboardnavigation.getKeyName(e); + switch (key) { + case 'Escape': + this.stop(); + break; + } + } + + bindEvents() { + document.addEventListener('keyup', this.onWindowKeyUp); + } + + unbindEvents() { + document.removeEventListener('keyup', this.onWindowKeyUp); + } + + createMediaElement() { + let elem = this.mediaElement; + if (elem) { + return elem; + } + + elem = document.getElementById('comicsPlayer'); + if (!elem) { + elem = dialogHelper.createDialog({ + exitAnimationDuration: 400, + size: 'fullscreen', + autoFocus: false, + scrollY: false, + exitAnimation: 'fadeout', + removeOnClose: true + }); + + elem.id = 'comicsPlayer'; + elem.classList.add('slideshowDialog'); + + elem.innerHTML = '
'; + + this.bindEvents(); + dialogHelper.open(elem); + } + + this.mediaElement = elem; + return elem; + } + + setCurrentSrc(elem, options) { + let item = options.items[0]; + this.currentItem = item; + + loading.show(); + + let serverId = item.ServerId; + let apiClient = connectionManager.getApiClient(serverId); + + libarchive.Archive.init({ + workerUrl: appRouter.baseUrl() + '/libraries/worker-bundle.js' + }); + + return new Promise((resolve, reject) => { + let downloadUrl = apiClient.getItemDownloadUrl(item.Id); + const archiveSource = new ArchiveSource(downloadUrl); + + var instance = this; + import('swiper').then(({default: Swiper}) => { + archiveSource.load().then(() => { + loading.hide(); + this.swiperInstance = new Swiper(elem.querySelector('.slideshowSwiperContainer'), { + direction: 'horizontal', + // loop is disabled due to the lack of support in virtual slides + loop: false, + zoom: { + minRatio: 1, + toggle: true, + containerClass: 'slider-zoom-container' + }, + autoplay: false, + keyboard: { + enabled: true + }, + preloadImages: true, + slidesPerView: 1, + slidesPerColumn: 1, + initialSlide: 0, + // reduces memory consumption for large libraries while allowing preloading of images + virtual: { + slides: archiveSource.urls, + cache: true, + renderSlide: instance.getImgFromUrl, + addSlidesBefore: 1, + addSlidesAfter: 1 + } + }); + }); + }); + }); + } + + getImgFromUrl(url) { + return `
+
+ +
+
`; + } + + canPlayMediaType(mediaType) { + return (mediaType || '').toLowerCase() === 'book'; + } + + canPlayItem(item) { + if (item.Path && (item.Path.endsWith('cbz') || item.Path.endsWith('cbr'))) { + return true; + } + + return false; + } +} + +class ArchiveSource { + constructor(url) { + this.url = url; + this.files = []; + this.urls = []; + this.loadPromise = this.load(); + this.itemsLoaded = 0; + } + + async load() { + let res = await fetch(this.url); + if (!res.ok) { + return; + } + + let blob = await res.blob(); + this.archive = await libarchive.Archive.open(blob); + this.raw = await this.archive.getFilesArray(); + this.numberOfFiles = this.raw.length; + await this.archive.extractFiles(); + + let files = await this.archive.getFilesArray(); + files.sort((a, b) => { + if (a.file.name < b.file.name) { + return -1; + } else { + return 1; + } + }); + + for (let file of files) { + /* eslint-disable-next-line compat/compat */ + let url = URL.createObjectURL(file.file); + this.urls.push(url); + } + } + + getLength() { + return this.raw.length; + } + + async item(index) { + if (this.urls[index]) { + return this.urls[index]; + } + + await this.loadPromise; + return this.urls[index]; + } +} + +export default ComicsPlayer; diff --git a/src/plugins/htmlAudioPlayer/plugin.js b/src/plugins/htmlAudioPlayer/plugin.js index acce15df88..6f413fac50 100644 --- a/src/plugins/htmlAudioPlayer/plugin.js +++ b/src/plugins/htmlAudioPlayer/plugin.js @@ -132,10 +132,7 @@ class HtmlAudioPlayer { return new Promise(function (resolve, reject) { requireHlsPlayer(function () { const hls = new Hls({ - manifestLoadingTimeOut: 20000, - xhrSetup: function (xhr, url) { - xhr.withCredentials = true; - } + manifestLoadingTimeOut: 20000 }); hls.loadSource(val); hls.attachMedia(elem); diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index 58c8624e34..88329fecff 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -393,10 +393,7 @@ function tryRemoveElement(elem) { return new Promise((resolve, reject) => { requireHlsPlayer(() => { const hls = new Hls({ - manifestLoadingTimeOut: 20000, - xhrSetup(xhr) { - xhr.withCredentials = true; - } + manifestLoadingTimeOut: 20000 }); hls.loadSource(url); hls.attachMedia(elem); diff --git a/src/plugins/sessionPlayer/plugin.js b/src/plugins/sessionPlayer/plugin.js index cbeb6f34b4..6ae5c1c712 100644 --- a/src/plugins/sessionPlayer/plugin.js +++ b/src/plugins/sessionPlayer/plugin.js @@ -322,12 +322,12 @@ class SessionPlayer { currentTime(val) { if (val != null) { - return this.seek(val); + return this.seek(val * 10000); } let state = this.lastPlayerData || {}; state = state.PlayState || {}; - return state.PositionTicks; + return state.PositionTicks / 10000; } duration() { @@ -466,9 +466,9 @@ class SessionPlayer { sendCommandByName(this, 'DisplayContent', options); } - isPlaying() { + isPlaying(mediaType) { const state = this.lastPlayerData || {}; - return state.NowPlayingItem != null; + return state.NowPlayingItem != null && (state.NowPlayingItem.MediaType === mediaType || !mediaType); } isPlayingVideo() { diff --git a/src/scripts/apploader.js b/src/scripts/apploader.js index f5f6850c40..fdf52fd699 100644 --- a/src/scripts/apploader.js +++ b/src/scripts/apploader.js @@ -35,7 +35,8 @@ // Promise() being missing on some legacy browser, and a funky one // is Promise() present but buggy on WebOS 2 window.Promise = undefined; - window.Promise = undefined; + /* eslint-disable-next-line no-restricted-globals -- Explicit check on self needed */ + self.Promise = undefined; } if (!window.Promise) { diff --git a/src/scripts/site.js b/src/scripts/site.js index 0e1e44251c..b6bd7b4793 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -222,6 +222,7 @@ function initClient() { }); require(['mouseManager']); require(['focusPreventScroll']); + require(['vendorStyles']); require(['autoFocuser'], function(autoFocuser) { autoFocuser.enable(); }); @@ -525,7 +526,8 @@ function initClient() { 'events', 'credentialprovider', 'connectionManagerFactory', - 'appStorage' + 'appStorage', + 'comicReader' ] }, urlArgs: urlArgs, @@ -655,6 +657,7 @@ function initClient() { }); define('slideshow', [componentsPath + '/slideshow/slideshow'], returnFirstDependency); define('focusPreventScroll', ['legacy/focusPreventScroll'], returnFirstDependency); + define('vendorStyles', ['legacy/vendorStyles'], returnFirstDependency); define('userdataButtons', [componentsPath + '/userdatabuttons/userdatabuttons'], returnFirstDependency); define('listView', [componentsPath + '/listview/listview'], returnFirstDependency); define('indicators', [componentsPath + '/indicators/indicators'], returnFirstDependency); diff --git a/src/strings/en-gb.json b/src/strings/en-gb.json index 8a0f893cfd..4ef5c732b4 100644 --- a/src/strings/en-gb.json +++ b/src/strings/en-gb.json @@ -21,7 +21,7 @@ "NewCollectionHelp": "Collections allow you to create personalized groupings of movies and other library content.", "NoNewDevicesFound": "No new devices found. To add a new tuner, close this dialogueand enter the device information manually.", "OptionEnableExternalContentInSuggestionsHelp": "Allow internet trailers and live TV programs to be included within suggested content.", - "OptionIgnoreTranscodeByteRangeRequestsHelp": "If enabled, these requests will be honoured but will ignore the byte range header.", + "OptionIgnoreTranscodeByteRangeRequestsHelp": "These requests will be honored but will ignore the byte range header.", "PlaceFavoriteChannelsAtBeginning": "Place favourite channels at the beginning", "Programs": "Programs", "TabCatalog": "Catalogue", @@ -67,7 +67,7 @@ "AllowMediaConversionHelp": "Grant or deny access to the convert media feature.", "AllowOnTheFlySubtitleExtraction": "Allow subtitle extraction on the fly", "AllowOnTheFlySubtitleExtractionHelp": "Embedded subtitles can be extracted from videos and delivered to clients in plain text, in order to help prevent video transcoding. On some systems this can take a long time and cause video playback to stall during the extraction process. Disable this to have embedded subtitles burned in with video transcoding when they are not natively supported by the client device.", - "AllowRemoteAccess": "Allow remote connections to this Jellyfin Server.", + "AllowRemoteAccess": "Allow remote connections to this server.", "AllowRemoteAccessHelp": "If unchecked, all remote connections will be blocked.", "AllowedRemoteAddressesHelp": "Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely. If left blank, all remote addresses will be allowed.", "AlwaysPlaySubtitles": "Always Play", @@ -80,7 +80,7 @@ "Ascending": "Ascending", "AspectRatio": "Aspect Ratio", "Audio": "Audio", - "AuthProviderHelp": "Select an Authentication Provider to be used to authenticate this user's password.", + "AuthProviderHelp": "Select an authentication provider to be used to authenticate this user's password.", "Auto": "Auto", "Backdrop": "Backdrop", "Backdrops": "Backdrops", @@ -148,7 +148,7 @@ "ChannelNumber": "Channel number", "CommunityRating": "Community rating", "Composer": "Composer", - "ConfigureDateAdded": "Configure how date added is determined in the Jellyfin Server dashboard under Library settings", + "ConfigureDateAdded": "Configure how date added is determined in the dashboard under the library settings", "ConfirmDeleteImage": "Delete image?", "ConfirmDeleteItem": "Deleting this item will delete it from both the file system and your media library. Are you sure you wish to continue?", "ConfirmDeleteItems": "Deleting these items will delete them from both the file system and your media library. Are you sure you wish to continue?", @@ -178,8 +178,8 @@ "DetectingDevices": "Detecting devices", "DeviceAccessHelp": "This only applies to devices that can be uniquely identified and will not prevent browser access. Filtering user device access will prevent them from using new devices until they've been approved here.", "DirectPlaying": "Direct playing", - "DirectStreamHelp1": "The media is compatible with the device regarding resolution and media type (H.264, AC3, etc), but is in an incompatible file container (mkv, avi, wmv, etc). The video will be re-packaged on the fly before streaming it to the device.", - "DirectStreamHelp2": "Direct Streaming a file uses very little processing power without any loss in video quality.", + "DirectStreamHelp1": "The media is compatible with the device regarding resolution and media type (H.264, AC3, etc), but in an incompatible file container (mkv, avi, wmv, etc). The video will be re-packaged on the fly before being sent to the device.", + "DirectStreamHelp2": "Direct stream uses very little processing power with a minimal loss in video quality.", "DirectStreaming": "Direct streaming", "Director": "Director", "Directors": "Directors", @@ -220,10 +220,10 @@ "EndsAtValue": "Ends at {0}", "Episodes": "Episodes", "ErrorAddingListingsToSchedulesDirect": "There was an error adding the lineup to your Schedules Direct account. Schedules Direct only allows a limited number of lineups per account. You may need to log into the Schedules Direct website and remove others listings from your account before proceeding.", - "ErrorAddingMediaPathToVirtualFolder": "There was an error adding the media path. Please ensure the path is valid and the Jellyfin Server process has access to that location.", + "ErrorAddingMediaPathToVirtualFolder": "There was an error adding the media path. Please ensure the path is valid and Jellyfin has access to that location.", "ErrorAddingTunerDevice": "There was an error adding the tuner device. Please ensure it is accessible and try again.", "ErrorAddingXmlTvFile": "There was an error accessing the XMLTV file. Please ensure the file exists and try again.", - "ErrorDeletingItem": "There was an error deleting the item from Jellyfin Server. Please check that Jellyfin Server has write access to the media folder and try again.", + "ErrorDeletingItem": "There was an error deleting the item from the server. Please check that Jellyfin has write access to the media folder and try again.", "ErrorGettingTvLineups": "There was an error downloading TV lineups. Please ensure your information is correct and try again.", "ErrorStartHourGreaterThanEnd": "End time must be greater than the start time.", "ErrorPleaseSelectLineup": "Please select a lineup and try again. If no lineups are available, then please check that your username, password, and postal code is correct.", @@ -275,7 +275,7 @@ "HeaderAllowMediaDeletionFrom": "Allow Media Deletion From", "HeaderApiKey": "API Key", "HeaderApiKeys": "API Keys", - "HeaderApiKeysHelp": "External applications are required to have an API key in order to communicate with Jellyfin Server. Keys are issued by logging in with a Jellyfin account, or by manually granting the application a key.", + "HeaderApiKeysHelp": "External applications are required to have an API key in order to communicate with the server. Keys are issued by logging in with a normal user account or manually granting the application a key.", "HeaderApp": "App", "HeaderAppearsOn": "Appears On", "HeaderAudioBooks": "Audio Books", @@ -335,7 +335,7 @@ "HeaderInstantMix": "Instant Mix", "HeaderKeepRecording": "Keep Recording", "HeaderKeepSeries": "Keep Series", - "HeaderKodiMetadataHelp": "To enable or disable NFO metadata, edit a library in Jellyfin library setup and locate the metadata savers section.", + "HeaderKodiMetadataHelp": "To enable or disable NFO metadata, edit a library and locate the metadata savers section.", "HeaderLatestEpisodes": "Latest Episodes", "HeaderLatestMedia": "Latest Media", "HeaderLatestMovies": "Latest Movies", @@ -440,9 +440,9 @@ "SettingsWarning": "Changing these values may cause instability or connectivity failures. If you experience any problems, we recommend changing them back to default.", "SettingsSaved": "Settings saved.", "Settings": "Settings", - "ServerUpdateNeeded": "This Jellyfin Server needs to be updated. To download the latest version, please visit {0}", - "ServerRestartNeededAfterPluginInstall": "Jellyfin Server will need to be restarted after installing a plugin.", - "ServerNameIsShuttingDown": "Jellyfin Server - {0} is shutting down.", + "ServerUpdateNeeded": "This server needs to be updated. To download the latest version, please visit {0}", + "ServerRestartNeededAfterPluginInstall": "Jellyfin will need to be restarted after installing a plugin.", + "ServerNameIsShuttingDown": "The server at {0} is shutting down.", "SeriesYearToPresent": "{0} - Present", "SeriesSettings": "Series settings", "SeriesRecordingScheduled": "Series recording scheduled.", @@ -475,7 +475,7 @@ "RememberMe": "Remember Me", "ReleaseDate": "Release date", "RefreshMetadata": "Refresh metadata", - "RefreshDialogHelp": "Metadata is refreshed based on settings and internet services that are enabled in the Jellyfin Server dashboard.", + "RefreshDialogHelp": "Metadata is refreshed based on settings and internet services that are enabled in the dashboard.", "Refresh": "Refresh", "Recordings": "Recordings", "RecordingScheduled": "Recording scheduled.", @@ -496,9 +496,9 @@ "Premiere": "Premiere", "PreferEmbeddedTitlesOverFileNamesHelp": "This determines the default display title when no internet metadata or local metadata is available.", "PreferEmbeddedTitlesOverFileNames": "Prefer embedded titles over filenames", - "MessagePluginInstalled": "The plugin has been successfully installed. Jellyfin Server will need to be restarted for changes to take effect.", + "MessagePluginInstalled": "The plugin has been successfully installed. The server will need to be restarted for changes to take effect.", "PleaseSelectTwoItems": "Please select at least two items.", - "PleaseRestartServerName": "Please restart Jellyfin Server - {0}.", + "PleaseRestartServerName": "Please restart Jellyfin on {0}.", "PleaseConfirmPluginInstallation": "Please click OK to confirm you've read the above and wish to proceed with the plugin installation.", "PleaseAddAtLeastOneFolder": "Please add at least one folder to this library by clicking the Add button.", "Played": "Played", @@ -529,7 +529,7 @@ "OptionTrackName": "Track Name", "OptionSubstring": "Substring", "OptionSpecialEpisode": "Specials", - "OptionSaveMetadataAsHiddenHelp": "Changing this will apply to new metadata saved going forward. Existing metadata files will be updated the next time they are saved by Jellyfin Server.", + "OptionSaveMetadataAsHiddenHelp": "Changing this will apply to new metadata saved going forward. Existing metadata files will be updated the next time they are saved by the server.", "OptionSaveMetadataAsHidden": "Save metadata and images as hidden files", "OptionResumable": "Resumable", "OptionResElement": "res element", @@ -541,9 +541,9 @@ "OptionProfileVideo": "Video", "OptionPremiereDate": "Premiere Date", "OptionPlayCount": "Play Count", - "OptionPlainVideoItemsHelp": "If enabled, all videos are represented in DIDL as \"object.item.videoItem\" instead of a more specific type, such as \"object.item.videoItem.movie\".", + "OptionPlainVideoItemsHelp": "All videos are represented in DIDL as \"object.item.videoItem\" instead of a more specific type, such as \"object.item.videoItem.movie\".", "OptionPlainVideoItems": "Display all videos as plain video items", - "OptionPlainStorageFoldersHelp": "If enabled, all folders are represented in DIDL as \"object.container.storageFolder\" instead of a more specific type, such as \"object.container.person.musicArtist\".", + "OptionPlainStorageFoldersHelp": "All folders are represented in DIDL as \"object.container.storageFolder\" instead of a more specific type, such as \"object.container.person.musicArtist\".", "OptionParentalRating": "Parental Rating", "OptionOnInterval": "On an interval", "OptionNew": "New…", @@ -552,7 +552,7 @@ "OptionLoginAttemptsBeforeLockoutHelp": "A value of zero means inheriting the default of three attempts for normal users and five for administrators. Setting this to -1 will disable the feature.", "OptionLoginAttemptsBeforeLockout": "Determines how many incorrect login attempts can be made before lockout occurs.", "OptionDatePlayed": "Date Played", - "OptionAutomaticallyGroupSeriesHelp": "If enabled, series that are spread across multiple folders within this library will be automatically merged into a single series.", + "OptionAutomaticallyGroupSeriesHelp": "Series that are spread across multiple folders within this library will be automatically merged into a single series.", "OptionAutomaticallyGroupSeries": "Automatically merge series that are spread across multiple folders", "OptionAllowVideoPlaybackTranscoding": "Allow video playback that requires transcoding", "OptionAllowVideoPlaybackRemuxing": "Allow video playback that requires conversion without re-encoding", @@ -561,7 +561,7 @@ "OptionAllowRemoteSharedDevicesHelp": "DLNA devices are considered shared until a user begins controlling them.", "OptionAllowRemoteSharedDevices": "Allow remote control of shared devices", "OptionAllowRemoteControlOthers": "Allow remote control of other users", - "OptionAllowMediaPlaybackTranscodingHelp": "Restricting access to transcoding may cause playback failures in Jellyfin apps due to unsupported media formats.", + "OptionAllowMediaPlaybackTranscodingHelp": "Restricting access to transcoding may cause playback failures in clients due to unsupported media formats.", "OptionAllowManageLiveTv": "Allow Live TV recording management", "OptionAllowLinkSharingHelp": "Only web pages containing media information are shared. Media files are never shared publicly. Shares are time-limited and will expire after {0} days.", "OptionAllowLinkSharing": "Allow social media sharing", @@ -587,9 +587,9 @@ "MusicLibraryHelp": "Review the {0}music naming guide{1}.", "Monday": "Monday", "MinutesBefore": "minutes before", - "MetadataSettingChangeHelp": "Changing metadata settings will affect new content that is added going forward. To refresh existing content, open the detail screen and click the refresh button, or perform bulk refreshes using the metadata manager.", + "MetadataSettingChangeHelp": "Changing metadata settings will affect new content added going forward. To refresh existing content, open the detail screen and click the refresh button, or perform bulk refreshes using the metadata manager.", "MetadataManager": "Metadata Manager", - "MessagePluginInstallDisclaimer": "Plugins built by Jellyfin community members are a great way to enhance your Jellyfin experience with additional features and benefits. Before installing, please be aware of the effects they may have on your Jellyfin Server, such as longer library scans, additional background processing, and decreased system stability.", + "MessagePluginInstallDisclaimer": "Plugins built by community members are a great way to enhance your experience with additional features and benefits. Before installing, please be aware of the effects they may have on your server, such as longer library scans, additional background processing, and decreased system stability.", "MessagePluginConfigurationRequiresLocalAccess": "To configure this plugin please sign in to your local server directly.", "MessagePleaseWait": "Please wait. This may take a minute.", "MessagePleaseEnsureInternetMetadata": "Please ensure downloading of internet metadata is enabled.", @@ -609,8 +609,8 @@ "MessageDeleteTaskTrigger": "Are you sure you wish to delete this task trigger?", "MessageCreateAccountAt": "Create an account at {0}", "MessageContactAdminToResetPassword": "Please contact your system administrator to reset your password.", - "MessageConfirmRevokeApiKey": "Are you sure you wish to revoke this api key? The application's connection to Jellyfin Server will be abruptly terminated.", - "MessageConfirmRestart": "Are you sure you wish to restart Jellyfin Server?", + "MessageConfirmRevokeApiKey": "Are you sure you wish to revoke this API key? The application's connection to this server will be abruptly terminated.", + "MessageConfirmRestart": "Are you sure you wish to restart Jellyfin?", "MessageConfirmRemoveMediaLocation": "Are you sure you wish to remove this location?", "MessageConfirmDeleteTunerDevice": "Are you sure you wish to delete this device?", "MessageConfirmDeleteGuideProvider": "Are you sure you wish to delete this guide provider?", @@ -642,7 +642,7 @@ "LeaveBlankToNotSetAPassword": "You can leave this field blank to set no password.", "LearnHowYouCanContribute": "Learn how you can contribute.", "LanNetworksHelp": "Comma separated list of IP addresses or IP/netmask entries for networks that will be considered on local network when enforcing bandwidth restrictions. If set, all other IP addresses will be considered to be on the external network and will be subject to the external bandwidth restrictions. If left blank, only the server's subnet is considered to be on the local network.", - "LabelffmpegPathHelp": "The path to the ffmpeg application file, or folder containing ffmpeg.", + "LabelffmpegPathHelp": "The path to the ffmpeg application file or folder containing ffmpeg.", "LabelffmpegPath": "FFmpeg path:", "LabelYear": "Year:", "LabelXDlnaDocHelp": "Determines the content of the X_DLNADOC element in the urn:schemas-dlna-org:device-1-0 namespace.", @@ -704,7 +704,7 @@ "EnableFasterAnimationsHelp": "Use faster animations and transitions", "LabelScheduledTaskLastRan": "Last ran {0}, taking {1}.", "LabelSaveLocalMetadataHelp": "Saving artwork into media folders will put them in a place where they can be easily edited.", - "LabelRuntimeMinutes": "Run time (minutes):", + "LabelRuntimeMinutes": "Runtime:", "LabelRemoteClientBitrateLimitHelp": "An optional per-stream bitrate limit for all out of network devices. This is useful to prevent devices from requesting a higher bitrate than your internet connection can handle. This may result in increased CPU load on your server in order to transcode videos on the fly to a lower bitrate.", "LabelRemoteClientBitrateLimit": "Internet streaming bitrate limit (Mbps):", "LabelReleaseDate": "Release date:", @@ -789,7 +789,7 @@ "LabelPlaceOfBirth": "Place of birth:", "LabelOverview": "Overview:", "LabelOriginalAspectRatio": "Original aspect ratio:", - "LabelMusicStreamingTranscodingBitrateHelp": "Specify a max bitrate when streaming music.", + "LabelMusicStreamingTranscodingBitrateHelp": "Specify a maximum bitrate when streaming music.", "LabelMetadataDownloadLanguage": "Preferred download language:", "LabelMetadata": "Metadata:", "LabelKeepUpTo": "Keep up to:", @@ -843,13 +843,13 @@ "LabelTunerType": "Tuner type:", "LabelServerName": "Server name:", "LabelServerHostHelp": "192.168.1.100:8096 or https://myserver.com", - "LabelSeriesRecordingPath": "Series recording path (optional):", + "LabelSeriesRecordingPath": "Series recording path:", "LabelRecordingPathHelp": "Specify the default location to save recordings. If left empty, the server's program data folder will be used.", "LabelRecordingPath": "Default recording path:", "LabelAlbumArtMaxWidth": "Album art max width:", - "LabelCustomCssHelp": "Apply your own custom styling to the web interface.", + "LabelCustomCssHelp": "Apply your own custom styles on the web interface.", "LabelBlastMessageIntervalHelp": "Determines the duration in seconds between blast alive messages.", - "LabelBlastMessageInterval": "Alive message interval (seconds)", + "LabelBlastMessageInterval": "Alive message interval", "LabelBitrate": "Bitrate:", "LabelAudioSampleRate": "Audio sample rate:", "LabelAlbumArtMaxHeight": "Album art max height:", @@ -875,10 +875,10 @@ "LabelEnableHardwareDecodingFor": "Enable hardware decoding for:", "LabelEnableDlnaServerHelp": "Allows UPnP devices on your network to browse and play content.", "LabelEnableDlnaDebugLoggingHelp": "Create large log files and should only be used as needed for troubleshooting purposes.", - "LabelEnableDlnaClientDiscoveryIntervalHelp": "Determines the duration in seconds between SSDP searches performed by Jellyfin.", + "LabelEnableDlnaClientDiscoveryIntervalHelp": "Determines the duration in seconds between SSDP searches.", "LabelEnableAutomaticPortMapHelp": "Automatically forward public ports on your router to local ports on your server via UPnP. This may not work with some router models or network configurations. Changes will not apply until after a server restart.", "InstallingPackage": "Installing {0} (version {1})", - "ImportMissingEpisodesHelp": "If enabled, information about missing episodes will be imported into your Jellyfin database and displayed within seasons and series. This may cause significantly longer library scans.", + "ImportMissingEpisodesHelp": "Information about missing episodes will be imported into your database and displayed within seasons and series. This may cause significantly longer library scans.", "HeaderSubtitleAppearance": "Subtitle Appearance", "LabelProtocol": "Protocol:", "LabelProfileVideoCodecs": "Video codecs:", @@ -895,7 +895,7 @@ "LabelPassword": "Password:", "LabelParentalRating": "Parental rating:", "LabelParentNumber": "Parent number:", - "LabelOptionalNetworkPath": "(Optional) Shared network folder:", + "LabelOptionalNetworkPath": "Shared network folder:", "LabelNewsCategories": "News categories:", "LabelNewPasswordConfirm": "New password confirm:", "LabelNewPassword": "New password:", @@ -916,7 +916,7 @@ "LabelMinResumeDuration": "Minimum resume duration:", "LabelMinBackdropDownloadWidth": "Minimum backdrop download width:", "LabelMethod": "Method:", - "LabelMetadataSaversHelp": "Choose the file formats to save your metadata to.", + "LabelMetadataSaversHelp": "Choose the file formats to use when saving your metadata.", "LabelMetadataSavers": "Metadata savers:", "LabelMetadataReadersHelp": "Rank your preferred local metadata sources in order of priority. The first file found will be read.", "LabelMetadataReaders": "Metadata readers:", @@ -929,7 +929,7 @@ "LabelManufacturerUrl": "Manufacturer URL", "LabelLoginDisclaimerHelp": "A message that will be displayed at the bottom of the login page.", "LabelLockItemToPreventChanges": "Lock this item to prevent future changes", - "LabelLocalHttpServerPortNumberHelp": "The TCP port number that Jellyfin's HTTP server should bind to.", + "LabelLocalHttpServerPortNumberHelp": "The TCP port number for the HTTP server.", "LabelLineup": "Lineup:", "LabelLanguage": "Language:", "LabelLanNetworks": "LAN networks:", @@ -945,32 +945,32 @@ "LabelIconMaxWidth": "Icon maximum width:", "LabelIconMaxHeightHelp": "Maximum resolution of icons exposed via upnp:icon.", "LabelIconMaxHeight": "Icon maximum height:", - "LabelHttpsPortHelp": "The TCP port number that Jellyfin's HTTPS server should bind to.", + "LabelHttpsPortHelp": "The TCP port number for the HTTPS server.", "LabelHttpsPort": "Local HTTPS port number:", "LabelHomeScreenSectionValue": "Home screen section {0}:", "LabelHomeNetworkQuality": "Home network quality:", "LabelHardwareAccelerationType": "Hardware acceleration:", "LabelEncoderPreset": "H264 and H265 encoding preset:", "LabelH264Crf": "H264 encoding CRF:", - "LabelGroupMoviesIntoCollectionsHelp": "When displaying movie lists, movies belonging to a collection will be displayed as one grouped item.", + "LabelGroupMoviesIntoCollectionsHelp": "When displaying movie lists, movies in a collection will be displayed as one grouped item.", "LabelGroupMoviesIntoCollections": "Group movies into collections", - "LabelServerNameHelp": "This name will be used to identify the server and will default to the server's computer name.", + "LabelServerNameHelp": "This name will be used to identify the server and will default to the server's hostname.", "LabelFriendlyName": "Friendly name:", "LabelFormat": "Format:", "LabelForgotPasswordUsernameHelp": "Enter your username, if you remember it.", "LabelFont": "Font:", "LabelExtractChaptersDuringLibraryScanHelp": "Generate chapter images when videos are imported during the library scan. Otherwise, they will be extracted during the chapter images scheduled task, allowing the regular library scan to complete faster.", - "LabelBaseUrlHelp": "Adds a custom subdirectory to the server URL. For example: http://example.com/<baseurl>", + "LabelBaseUrlHelp": "Add a custom subdirectory to the server URL. For example: http://example.com/<baseurl>", "LabelEveryXMinutes": "Every:", "LabelEvent": "Event:", "LabelEpisodeNumber": "Episode number:", "LabelEnableSingleImageInDidlLimitHelp": "Some devices will not render properly if multiple images are embedded within Didl.", - "LabelEnableRealtimeMonitorHelp": "Changes to files will be processed immediately, on supported file systems.", + "LabelEnableRealtimeMonitorHelp": "Changes to files will be processed immediately on supported file systems.", "LabelEnableRealtimeMonitor": "Enable real time monitoring", "LabelEnableDlnaServer": "Enable DLNA server", - "LabelEnableDlnaPlayToHelp": "Detect devices within your network and offer the ability to remote control them.", + "LabelEnableDlnaPlayToHelp": "Detect devices within your network and offer the ability to control them remotely.", "LabelEnableDlnaPlayTo": "Enable DLNA Play To", - "LabelEnableDlnaClientDiscoveryInterval": "Client discovery interval (seconds)", + "LabelEnableDlnaClientDiscoveryInterval": "Client discovery interval", "LabelEnableBlastAliveMessagesHelp": "Enable this if the server is not detected reliably by other UPnP devices on your network.", "LabelEnableBlastAliveMessages": "Blast alive messages", "LabelEnableAutomaticPortMap": "Enable automatic port mapping", @@ -986,7 +986,7 @@ "LabelDisplaySpecialsWithinSeasons": "Display specials within seasons they aired in", "LabelDisplayOrder": "Display order:", "LabelDisplayName": "Display name:", - "LabelDateAddedBehaviorHelp": "If a metadata value is present it will always be used before either of these options.", + "LabelDateAddedBehaviorHelp": "If a metadata value is present, it will always be used before either of these options.", "LabelCustomCss": "Custom CSS:", "LabelCustomCertificatePathHelp": "Path to a PKCS #12 file containing a certificate and private key to enable TLS support on a custom domain.", "LabelCurrentPassword": "Current password:", @@ -1011,7 +1011,7 @@ "LabelAudioChannels": "Audio channels:", "LabelAudioBitrate": "Audio bitrate:", "LabelAudioBitDepth": "Audio bit depth:", - "LabelArtistsHelp": "Separate multiple using ;", + "LabelArtistsHelp": "Separate multiple artists with a semicolon.", "LabelArtists": "Artists:", "LabelAppName": "App name", "LabelAllowedRemoteAddressesMode": "Remote IP address filter mode:", @@ -1066,7 +1066,7 @@ "Overview": "Overview", "LabelLogs": "Logs:", "Whitelist": "Whitelist", - "ServerNameIsRestarting": "Jellyfin Server - {0} is restarting.", + "ServerNameIsRestarting": "The server at {0} is restarting.", "OptionProtocolHls": "HTTP Live Streaming", "OptionProfileVideoAudio": "Video Audio", "OneChannel": "One channel", @@ -1078,7 +1078,7 @@ "LabelPasswordResetProvider": "Password Reset Provider:", "LabelPasswordConfirm": "Password (confirm):", "LabelOriginalTitle": "Original title:", - "LabelOptionalNetworkPathHelp": "If this folder is shared on your network, supplying the network share path can allow Jellyfin apps on other devices to access media files directly. For example, {0} or {1}.", + "LabelOptionalNetworkPathHelp": "If this folder is shared on your network, supplying the network share path can allow clients on other devices to access media files directly. For example, {0} or {1}.", "LabelNumberOfGuideDaysHelp": "Downloading more days worth of guide data provides the ability to schedule out further in advance and view more listings, but it will also take longer to download. Auto will choose based on the number of channels.", "LabelNumberOfGuideDays": "Number of days of guide data to download:", "LabelNumber": "Number:", @@ -1089,7 +1089,7 @@ "OptionDvd": "DVD", "MessageDirectoryPickerLinuxInstruction": "For Linux on Arch Linux, CentOS, Debian, Fedora, openSUSE, or Ubuntu, you must grant the service user at least read access to your storage locations.", "LabelCustomCertificatePath": "Custom SSL certificate path:", - "LabelBindToLocalNetworkAddressHelp": "Optional. Override the local IP address to bind the http server to. If left empty, the server will bind to all availabile addresses. Changing this value requires restarting Jellyfin Server.", + "LabelBindToLocalNetworkAddressHelp": "Override the local IP address for the HTTP server. If left empty, the server will bind to all available addresses. Changing this value requires a restart.", "LabelAppNameExample": "Example: Sickbeard, Sonarr", "HttpsRequiresCert": "To enable secure connections, you will need to supply a trusted SSL certificate, such as Let's Encrypt. Please either supply a certificate, or disable secure connections.", "Yesterday": "Yesterday", @@ -1106,7 +1106,7 @@ "Sports": "Sports", "OptionProfilePhoto": "Photo", "OptionPlainStorageFolders": "Display all folders as plain storage folders", - "OptionDisableUserHelp": "If disabled the server will not allow any connections from this user. Existing connections will be abruptly terminated.", + "OptionDisableUserHelp": "The server will not allow any connections from this user. Existing connections will be abruptly terminated.", "OptionDateAdded": "Date Added", "OptionDaily": "Daily", "OptionCommunityRating": "Community Rating", @@ -1133,10 +1133,10 @@ "OptionEnableAccessToAllChannels": "Enable access to all channels", "OptionEnableAccessFromAllDevices": "Enable access from all devices", "OptionEmbedSubtitles": "Embed within container", - "OptionDownloadImagesInAdvanceHelp": "By default, most images are only downloaded when requested by a Jellyfin app. Enable this option to download all images in advance, as new media is imported. This may cause significantly longer library scans.", + "OptionDownloadImagesInAdvanceHelp": "By default, most images are only downloaded when requested by a client. Enable this option to download all images in advance, as new media is imported. This may cause significantly longer library scans.", "OptionDownloadImagesInAdvance": "Download images in advance", "OptionDisplayFolderView": "Display a folder view to show plain media folders", - "LabelMovieRecordingPath": "Movie recording path (optional):", + "LabelMovieRecordingPath": "Movie recording path:", "LabelMoviePrefixHelp": "If a prefix is applied to movie titles, enter it here so the server can handle it properly.", "LabelMessageTitle": "Message title:", "LabelMessageText": "Message text:", @@ -1166,7 +1166,7 @@ "SkipEpisodesAlreadyInMyLibraryHelp": "Episodes will be compared using season and episode numbers, when available.", "RefreshQueued": "Refresh queued.", "Play": "Play", - "PasswordResetProviderHelp": "Choose a Password Reset Provider to be used when this user requests a password reset", + "PasswordResetProviderHelp": "Choose a password reset provider to be used when this user requests a password reset.", "OptionReleaseDate": "Release Date", "OptionDisplayFolderViewHelp": "Display folders alongside your other media libraries. This can be useful if you'd like to have a plain folder view.", "OptionDislikes": "Dislikes", @@ -1190,7 +1190,7 @@ "MessageLeaveEmptyToInherit": "Leave empty to inherit settings from a parent item or the global default value.", "MessageItemsAdded": "Items added.", "MessageItemSaved": "Item saved.", - "MessageDirectoryPickerBSDInstruction": "For BSD, you may need to configure storage within your FreeNAS Jail in order to allow Jellyfin to access it.", + "MessageDirectoryPickerBSDInstruction": "For BSD, you may need to configure storage within your FreeNAS Jail so Jellyfin can access your media.", "MessageConfirmShutdown": "Are you sure you wish to shutdown the server?", "LabelSaveLocalMetadata": "Save artwork into media folders", "LabelPleaseRestart": "Changes will take effect after manually reloading the web client.", @@ -1217,7 +1217,7 @@ "HeaderSelectServerCachePathHelp": "Browse or enter the path to use for server cache files. The folder must be writeable.", "HeaderSelectServerCachePath": "Select Server Cache Path", "HeaderSelectPath": "Select Path", - "HeaderSelectMetadataPathHelp": "Browse or enter the path you'd like to store metadata within. The folder must be writeable.", + "HeaderSelectMetadataPathHelp": "Browse or enter the path you'd like to use for metadata. The folder must be writeable.", "HeaderSelectMetadataPath": "Select Metadata Path", "HeaderSelectCertificatePath": "Select Certificate Path", "HeaderSecondsValue": "{0} Seconds", @@ -1232,7 +1232,7 @@ "HeaderRecordingPostProcessing": "Recording Post Processing", "HeaderRecordingOptions": "Recording Options", "HeaderRecentlyPlayed": "Recently Played", - "HeaderProfileServerSettingsHelp": "These values control how Jellyfin Server will present itself to the device.", + "HeaderProfileServerSettingsHelp": "These values control how the server will present itself to clients.", "HeaderProfileInformation": "Profile Information", "HeaderPreferredMetadataLanguage": "Preferred Metadata Language", "HeaderPluginInstallation": "Plugin Installation", @@ -1271,7 +1271,7 @@ "LastSeen": "Last seen {0}", "PersonRole": "as {0}", "ListPaging": "{0}-{1} of {2}", - "WriteAccessRequired": "Jellyfin Server requires write access to this folder. Please ensure write access and try again.", + "WriteAccessRequired": "Jellyfin requires write access to this folder. Please ensure write access and try again.", "PathNotFound": "The path could not be found. Please ensure the path is valid and try again.", "YadifBob": "YADIF Bob", "Yadif": "YADIF", @@ -1342,7 +1342,7 @@ "LabelRequireHttps": "Require HTTPS", "LabelStable": "Stable", "LabelChromecastVersion": "Chromecast Version", - "LabelEnableHttpsHelp": "Enables the server to listen on the configured HTTPS port. A valid certificate must also be configured in order for this to take effect.", + "LabelEnableHttpsHelp": "Listen on the configured HTTPS port. A valid certificate must also be supplied for this to take effect.", "LabelEnableHttps": "Enable HTTPS", "HeaderSyncPlayEnabled": "SyncPlay enabled", "HeaderSyncPlaySelectGroup": "Join a group", @@ -1357,5 +1357,14 @@ "ClearQueue": "Clear queue", "StopPlayback": "Stop playback", "ButtonPlayer": "Player", - "Writers": "Writers" + "Writers": "Writers", + "Preview": "Preview", + "SubtitleVerticalPositionHelp": "Line number where text appears. Positive numbers indicate top down. Negative numbers indicate bottom up.", + "LabelSubtitleVerticalPosition": "Vertical position:", + "PreviousTrack": "Skip to previous", + "MessageGetInstalledPluginsError": "An error occurred while getting the list of currently installed plugins.", + "MessagePluginInstallError": "An error occurred while installing the plugin.", + "PlaybackRate": "Playback Rate", + "NextTrack": "Skip to next", + "LabelUnstable": "Unstable" } diff --git a/src/strings/en-us.json b/src/strings/en-us.json index eedaef7170..894a8aaefb 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -490,7 +490,7 @@ "LabelAuthProvider": "Authentication Provider:", "LabelAutomaticallyRefreshInternetMetadataEvery": "Automatically refresh metadata from the internet:", "LabelBindToLocalNetworkAddress": "Bind to local network address:", - "LabelBindToLocalNetworkAddressHelp": "Override the local IP address for the HTTP server. If left empty, the server will bind to all availabile addresses. Changing this value requires a restart.", + "LabelBindToLocalNetworkAddressHelp": "Override the local IP address for the HTTP server. If left empty, the server will bind to all available addresses. Changing this value requires a restart.", "LabelBirthDate": "Birth date:", "LabelBirthYear": "Birth year:", "LabelBitrate": "Bitrate:", diff --git a/src/strings/it.json b/src/strings/it.json index 9a81ed45dd..0d71e75b05 100644 --- a/src/strings/it.json +++ b/src/strings/it.json @@ -284,7 +284,7 @@ "HeaderInstantMix": "Mix Istantaneo", "HeaderKeepRecording": "Mantieni la registrazione", "HeaderKeepSeries": "Mantieni Serie TV", - "HeaderKodiMetadataHelp": "Jellyfin include il supporto nativo per i file metadati NFO. Per attivare o disattivare i metadati NFO, utilizzare la scheda Metadati per configurare le opzioni per i tipi di supporto.", + "HeaderKodiMetadataHelp": "Per abilitare o disabilitare i metadati NFO, editare la libreria e configurare l'opzione nella sezione Metadati.", "HeaderLatestEpisodes": "Ultimi Episodi Aggiunti", "HeaderLatestMedia": "Ultimi Media", "HeaderLatestMovies": "Ultimi Film Aggiunti", diff --git a/src/strings/sl-si.json b/src/strings/sl-si.json index fc821e53ff..1930c194b1 100644 --- a/src/strings/sl-si.json +++ b/src/strings/sl-si.json @@ -1283,5 +1283,33 @@ "MessageGetInstalledPluginsError": "Pri pridobivanju seznama trenutno nameščenih dodatkov je prišlo do napake.", "MessagePluginInstallError": "Pri nameščanju dodatka je prišlo do napake.", "PlaybackErrorNoCompatibleStream": "Ta odjemalec ni kompatibilen s predstavnostjo in strežnik ne pošilja kompatibilnega formata predstavnosti.", - "PlaybackRate": "Hitrost predvajanja" + "PlaybackRate": "Hitrost predvajanja", + "PathNotFound": "Poti ni bilo mogoče najti. Prosimo preverite pravilnost poti in poskusite znova.", + "YadifBob": "YADIF Bob", + "Yadif": "YADIF", + "XmlTvSportsCategoriesHelp": "Programi s temi kategorijami bodo prikazani kot športni programi. Ločite več z '|'.", + "XmlTvNewsCategoriesHelp": "Programi s temi kategorijami bodo prikazani kot novice. Ločite več z '|'.", + "XmlTvMovieCategoriesHelp": "Programi s temi kategorijami bodo prikazani kot filmi. Ločite več z '|'.", + "XmlTvKidsCategoriesHelp": "Programi s temi kategorijami bodo prikazani kot programi za otroke. Ločite več z '|'.", + "XmlTvPathHelp": "Pot do XMLTV datoteke. Jellyfin bo periodično preveril to datoteko za posodobitve. Posodabljanje te datoteke je vaša odgovornost.", + "Writer": "Scenarij", + "Writers": "Scenarij", + "ViewAlbumArtist": "Pokaži izvajalca albuma", + "ViewAlbum": "Pokaži album", + "Vertical": "Navpično", + "ValueDiscNumber": "Disk {0}", + "ValueAlbumCount": "{0} albumov", + "UserAgentHelp": "Uporabite HTTP user-agent zaglavje po meri.", + "Upload": "Naloži", + "Up": "Gor", + "Unmute": "Obnovi zvok", + "Uniform": "Obroba", + "Transcoding": "Prekodiranje", + "TrackCount": "{0} skladb", + "Preview": "Predogled", + "LabelSubtitleVerticalPosition": "Navpična lokacija:", + "LastSeen": "Nazadnje viden {0}", + "PersonRole": "kot {0}", + "ListPaging": "{0}-{1} od {2}", + "WriteAccessRequired": "Jellyfin potrebuje dostop za pisanje v to mapo. Prosimo zagotovite dostop za pisanje in poskusite znova." } diff --git a/src/themes/appletv/theme.css b/src/themes/appletv/theme.css index 41540ce0dd..ad18bf24c5 100644 --- a/src/themes/appletv/theme.css +++ b/src/themes/appletv/theme.css @@ -23,21 +23,25 @@ html { .skinHeader-withBackground { color: rgba(0, 0, 0, 0.7); background: #303030; - background: -webkit-gradient(linear, left top, right top, from(#bcbcbc), color-stop(#a7b4b7), color-stop(#beb5a5), color-stop(#adbec2), to(#b9c7cb)); - background: -webkit-linear-gradient(left, #bcbcbc, #a7b4b7, #beb5a5, #adbec2, #b9c7cb); - background: -o-linear-gradient(left, #bcbcbc, #a7b4b7, #beb5a5, #adbec2, #b9c7cb); background: linear-gradient(to right, #bcbcbc, #a7b4b7, #beb5a5, #adbec2, #b9c7cb); } .skinHeader.semiTransparent { - -webkit-backdrop-filter: none !important; backdrop-filter: none !important; } +.layout-tv .skinHeader.semiTransparent { + background: none; +} + .pageTitleWithDefaultLogo { background-image: url(../../assets/img/banner-dark.png); } +.layout-tv .pageTitleWithDefaultLogo { + background-image: url(../../assets/img/icon-transparent.png); +} + html { background: #d5e9f2; } @@ -234,12 +238,13 @@ html { .detailRibbon { background: #303030; - background: -webkit-gradient(linear, left top, right top, from(#bcbcbc), color-stop(#a7b4b7), color-stop(#beb5a5), color-stop(#adbec2), to(#b9c7cb)); - background: -webkit-linear-gradient(left, #bcbcbc, #a7b4b7, #beb5a5, #adbec2, #b9c7cb); - background: -o-linear-gradient(left, #bcbcbc, #a7b4b7, #beb5a5, #adbec2, #b9c7cb); background: linear-gradient(to right, #bcbcbc, #a7b4b7, #beb5a5, #adbec2, #b9c7cb); } +.layout-tv .detailRibbon { + background: none; +} + .detailTableBodyRow-shaded:nth-child(even) { background: #f8f8f8; background: rgba(0, 0, 0, 0.1); diff --git a/src/themes/blueradiance/theme.css b/src/themes/blueradiance/theme.css index 7ecc4a74a5..85795c90c3 100644 --- a/src/themes/blueradiance/theme.css +++ b/src/themes/blueradiance/theme.css @@ -21,26 +21,28 @@ html { .skinHeader-withBackground { background: #303030; - background: -webkit-gradient(linear, left top, right top, from(#291a31), color-stop(#033664), color-stop(#011432), color-stop(#141a3a), to(#291a31)); - background: -webkit-linear-gradient(left, #291a31, #033664, #011432, #141a3a, #291a31); - background: -o-linear-gradient(left, #291a31, #033664, #011432, #141a3a, #291a31); background: linear-gradient(to right, #291a31, #033664, #011432, #141a3a, #291a31); } .skinHeader.semiTransparent { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; - background: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.6)), to(rgba(0, 0, 0, 0))); - background: -webkit-linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)); - background: -o-linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)); background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)); background-color: rgba(0, 0, 0, 0.3); } +.layout-tv .skinHeader.semiTransparent { + background: none; +} + .pageTitleWithDefaultLogo { background-image: url(../../assets/img/banner-light.png); } +.layout-tv .pageTitleWithDefaultLogo { + background-image: url(../../assets/img/icon-transparent.png); +} + .dialog, .nowPlayingPlaylist, .nowPlayingContextMenu, @@ -231,6 +233,10 @@ html { background: linear-gradient(to right, #291a31, #033664, #011432, #141a3a, #291a31); } +.layout-tv .detailRibbon { + background: none; +} + .detailTableBodyRow-shaded:nth-child(even) { background: #1c1c1c; background: rgba(30, 30, 30, 0.9); diff --git a/src/themes/dark/theme.css b/src/themes/dark/theme.css index be2b9269af..1024770c7c 100644 --- a/src/themes/dark/theme.css +++ b/src/themes/dark/theme.css @@ -30,10 +30,18 @@ html { background-color: rgba(0, 0, 0, 0.4); } +.layout-tv .skinHeader.semiTransparent { + background: none; +} + .pageTitleWithDefaultLogo { background-image: url(../../assets/img/banner-light.png); } +.layout-tv .pageTitleWithDefaultLogo { + background-image: url(../../assets/img/icon-transparent.png); +} + .backgroundContainer, .dialog, .nowPlayingPlaylist, @@ -207,6 +215,10 @@ html { background: rgba(32, 32, 32, 0.8); } +.layout-tv .detailRibbon { + background: none; +} + .noBackdrop .detailRibbon { background: #202020; } @@ -454,3 +466,8 @@ html { background-color: #00a4dc; color: #fff; } + +.layout-tv .emby-button.detailFloatingButton:focus { + background-color: #f2f2f2; + color: #00a4dc; +} diff --git a/src/themes/light/theme.css b/src/themes/light/theme.css index c5161985cd..ec618138cd 100644 --- a/src/themes/light/theme.css +++ b/src/themes/light/theme.css @@ -38,10 +38,18 @@ html { background-color: rgba(0, 0, 0, 0.4); } +.layout-tv .skinHeader.semiTransparent { + background: none; +} + .pageTitleWithDefaultLogo { background-image: url(../../assets/img/banner-light.png); } +.layout-tv .pageTitleWithDefaultLogo { + background-image: url(../../assets/img/icon-transparent.png); +} + .backgroundContainer, html { background-color: #f2f2f2; @@ -233,6 +241,10 @@ html { box-shadow: 0 0.0725em 0.29em 0 rgba(0, 0, 0, 0.37); } +.layout-tv .detailRibbon { + background: none; +} + .detailTableBodyRow-shaded:nth-child(even) { background: #f8f8f8; } diff --git a/src/themes/purplehaze/theme.css b/src/themes/purplehaze/theme.css index 1d82afc23a..4b2451cfe4 100644 --- a/src/themes/purplehaze/theme.css +++ b/src/themes/purplehaze/theme.css @@ -17,25 +17,28 @@ html { .skinHeader-withBackground { background: #000420; - background: -moz-linear-gradient(left, #000420 0%, #06256f 18%, #2b052b 38%, #2b052b 68%, #06256f 81%, #000420 100%); - background: -webkit-linear-gradient(left, #000420 0%, #06256f 18%, #2b052b 38%, #2b052b 68%, #06256f 81%, #000420 100%); background: linear-gradient(to right, #000420 0%, #06256f 18%, #2b052b 38%, #2b052b 68%, #06256f 81%, #000420 100%); } .skinHeader.semiTransparent { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; - background: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.6)), to(rgba(0, 0, 0, 0))); - background: -webkit-linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)); - background: -o-linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)); background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)); background-color: rgba(0, 0, 0, 0.3); } +.layout-tv .skinHeader.semiTransparent { + background: none; +} + .pageTitleWithDefaultLogo { background-image: url(../../assets/img/banner-light.png); } +.layout-tv .pageTitleWithDefaultLogo { + background-image: url(../../assets/img/icon-transparent.png); +} + .dialog, .nowPlayingPlaylist, .nowPlayingContextMenu, @@ -318,6 +321,10 @@ a[data-role=button] { background: linear-gradient(to right, #000420 0%, #06256f 18%, #2b052b 38%, #2b052b 68%, #06256f 81%, #000420 100%); } +.layout-tv .detailRibbon { + background: none; +} + .detailTableBodyRow-shaded:nth-child(even) { background: #1c1c1c; background: rgba(30, 30, 30, 0.9); diff --git a/src/themes/wmc/theme.css b/src/themes/wmc/theme.css index 4a7375a129..a0db089625 100644 --- a/src/themes/wmc/theme.css +++ b/src/themes/wmc/theme.css @@ -26,9 +26,6 @@ html { .formDialogHeader:not(.formDialogHeader-clear), .skinHeader-withBackground { - background: -webkit-gradient(linear, left top, left bottom, from(#0c2450), to(#081b3b)); - background: -webkit-linear-gradient(top, #0c2450, #081b3b); - background: -o-linear-gradient(top, #0c2450, #081b3b); background: linear-gradient(to bottom, #0c2450, #081b3b); background-color: #0c2450; } @@ -36,17 +33,22 @@ html { .skinHeader.semiTransparent { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; - background: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.6)), to(rgba(0, 0, 0, 0))); - background: -webkit-linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)); - background: -o-linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)); background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0)); background-color: rgba(0, 0, 0, 0.3); } +.layout-tv .skinHeader.semiTransparent { + background: none; +} + .pageTitleWithDefaultLogo { background-image: url(../../assets/img/banner-light.png); } +.layout-tv .pageTitleWithDefaultLogo { + background-image: url(../../assets/img/icon-transparent.png); +} + .backgroundContainer, .dialog, .nowPlayingPlaylist, @@ -213,6 +215,10 @@ html { background-color: #081b3b; } +.layout-tv .detailRibbon { + background: none; +} + .detailTableBodyRow-shaded:nth-child(even) { background: #1c1c1c; background: rgba(0, 0, 0, 0.3); diff --git a/webpack.common.js b/webpack.common.js index d870b1046e..fb3a1edc34 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -1,10 +1,12 @@ const path = require('path'); const CopyPlugin = require('copy-webpack-plugin'); +const WorkerPlugin = require('worker-plugin'); const Assets = [ 'alameda/alameda.js', 'native-promise-only/npo.js', + 'libarchive.js/dist/worker-bundle.js', 'libass-wasm/dist/js/subtitles-octopus-worker.js', 'libass-wasm/dist/js/subtitles-octopus-worker.data', 'libass-wasm/dist/js/subtitles-octopus-worker.wasm', @@ -13,6 +15,11 @@ const Assets = [ 'libass-wasm/dist/js/subtitles-octopus-worker-legacy.js.mem' ]; +const LibarchiveWasm = [ + 'libarchive.js/dist/wasm-gen/libarchive.js', + 'libarchive.js/dist/wasm-gen/libarchive.wasm' +]; + module.exports = { context: path.resolve(__dirname, 'src'), entry: './bundle.js', @@ -35,6 +42,15 @@ module.exports = { to: path.resolve(__dirname, './dist/libraries') }; }) - ) + ), + new CopyPlugin( + LibarchiveWasm.map(asset => { + return { + from: path.resolve(__dirname, `./node_modules/${asset}`), + to: path.resolve(__dirname, './dist/libraries/wasm-gen/') + }; + }) + ), + new WorkerPlugin() ] }; diff --git a/yarn.lock b/yarn.lock index 86c167aee6..013c06e045 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3218,10 +3218,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -date-fns@^2.15.0: - version "2.15.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.15.0.tgz#424de6b3778e4e69d3ff27046ec136af58ae5d5f" - integrity sha512-ZCPzAMJZn3rNUvvQIMlXhDr4A+Ar07eLeGsGREoWU19a3Pqf5oYa+ccd+B3F6XVtQY6HANMFdOQ8A+ipFnvJdQ== +date-fns@^2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.16.0.tgz#d34f0f5f2fd498c984513042e8f7247ea86c4cb7" + integrity sha512-DWTRyfOA85sZ4IiXPHhiRIOs3fW5U6Msrp+gElXARa6EpoQTXPyHQmh7hr+ssw2nx9FtOQWnAMJKgL5vaJqILw== dateformat@^2.0.0: version "2.2.0" @@ -3499,12 +3499,12 @@ dom-serializer@0: domelementtype "^2.0.1" entities "^2.0.0" -dom7@^2.1.5: - version "2.1.5" - resolved "https://registry.yarnpkg.com/dom7/-/dom7-2.1.5.tgz#a79411017800b31d8400070cdaebbfc92c1f6377" - integrity sha512-xnhwVgyOh3eD++/XGtH+5qBwYTgCm0aW91GFgPJ3XG+jlsRLyJivnbP0QmUBFhI+Oaz9FV0s7cxgXHezwOEBYA== +dom7@^3.0.0-alpha.7: + version "3.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/dom7/-/dom7-3.0.0-alpha.7.tgz#3b4ba156a83fa37fb3fa34b8ab40a1a41a56feb1" + integrity sha512-3epkQPsKsbk2Dixqqgm2DT/KzhiAPByjDK7emu6owwFLbM5UoiqWKgdsH+6PpMEgoeR6Ex/bW1UbOe0FWZU0zg== dependencies: - ssr-window "^2.0.0" + ssr-window "^3.0.0-alpha.1" domain-browser@^1.1.1: version "1.2.0" @@ -5544,10 +5544,10 @@ html-tags@^3.1.0: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg== -html-webpack-plugin@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.3.0.tgz#53bf8f6d696c4637d5b656d3d9863d89ce8174fd" - integrity sha512-C0fzKN8yQoVLTelcJxZfJCE+aAvQiY2VUf3UuKrR4a9k5UMWYOtpDLsaXwATbcVCnI05hUS7L9ULQHWLZhyi3w== +html-webpack-plugin@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.4.1.tgz#61ab85aa1a84ba181443345ebaead51abbb84149" + integrity sha512-nEtdEIsIGXdXGG7MjTTZlmhqhpHU9pJFc1OYxcP36c5/ZKP6b0BJMww2QTvJGQYA9aMxUnjDujpZdYcVOXiBCQ== dependencies: "@types/html-minifier-terser" "^5.0.0" "@types/tapable" "^1.0.5" @@ -6560,6 +6560,11 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +libarchive.js@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/libarchive.js/-/libarchive.js-1.3.0.tgz#18c42c6b4ce727a02359c90769e4e454cf3743cd" + integrity sha512-EkQfRXt9DhWwj6BnEA2TNpOf4jTnzSTUPGgE+iFxcdNqjktY8GitbDeHnx8qZA0/IukNyyBUR3oQKRdYkO+HFg== + "libass-wasm@https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv": version "4.0.0" resolved "https://github.com/jellyfin/JavascriptSubtitlesOctopus#58e9a3f1a7f7883556ee002545f445a430120639" @@ -7036,10 +7041,10 @@ matchdep@^2.0.0: resolve "^1.4.0" stack-trace "0.0.10" -material-design-icons-iconfont@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/material-design-icons-iconfont/-/material-design-icons-iconfont-5.0.1.tgz#371875ed7fe9c8c520bc7123c3231feeab731c31" - integrity sha512-Xg6rIdGrfySTqiTZ6d+nQbcFepS6R4uKbJP0oAqyeZXJY/bX6mZDnOmmUJusqLXfhIwirs0c++a6JpqVa8RFvA== +material-design-icons-iconfont@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/material-design-icons-iconfont/-/material-design-icons-iconfont-6.0.1.tgz#e5834ad566105013c68e1348bec7615481645434" + integrity sha512-NDC8gQD0ORRum5dkwwF8hiDzW+y4cROyu213HJa1c7ediok9lsi8TMmmSvbA51WApYbgcdE9X5u0TzgL8n4zTw== mathml-tag-names@^2.0.1, mathml-tag-names@^2.1.3: version "2.1.3" @@ -10395,10 +10400,10 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -ssr-window@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ssr-window/-/ssr-window-2.0.0.tgz#98c301aef99523317f8d69618f0010791096efc4" - integrity sha512-NXzN+/HPObKAx191H3zKlYomE5WrVIkoCB5IaSdvKokxTpjBdWfr0RaP+1Z5KOfDT0ZVz+2tdtiBkhsEQ9p+0A== +ssr-window@^3.0.0-alpha.1, ssr-window@^3.0.0-alpha.4: + version "3.0.0-alpha.4" + resolved "https://registry.yarnpkg.com/ssr-window/-/ssr-window-3.0.0-alpha.4.tgz#0c69a18c4305ecccdd8e11596155ca07b635f345" + integrity sha512-+dBRP/pZ+VyITxTzD0lMDzDwN/BmfUl8xi2e6t5Nz4+FqUphfcBLB1OOUSYCRNFB25rD3c8AJRYpY5rHTbL+kg== ssri@^6.0.1: version "6.0.1" @@ -10918,7 +10923,7 @@ supports-color@^3.2.3: dependencies: has-flag "^1.0.0" -supports-color@^5.3.0, supports-color@^5.5.0: +supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -10971,13 +10976,13 @@ svgo@^1.0.0, svgo@^1.3.2: unquote "~1.1.1" util.promisify "~1.0.0" -swiper@^5.4.5: - version "5.4.5" - resolved "https://registry.yarnpkg.com/swiper/-/swiper-5.4.5.tgz#a350f654bf68426dbb651793824925512d223c0f" - integrity sha512-7QjA0XpdOmiMoClfaZ2lYN6ICHcMm72LXiY+NF4fQLFidigameaofvpjEEiTQuw3xm5eksG5hzkaRsjQX57vtA== +swiper@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/swiper/-/swiper-6.1.1.tgz#1246f28557dd33968dc43e926bc6e9e9a7b3850d" + integrity sha512-w6rmEUnpuSWvzuIDJ+nTi7YQ4+pvr++zUnBO2VxkzOZbzQzcMNKNw1yj0RFEok682IHDPCs3LXSl8zSQ+zDEdw== dependencies: - dom7 "^2.1.5" - ssr-window "^2.0.0" + dom7 "^3.0.0-alpha.7" + ssr-window "^3.0.0-alpha.4" symbol-observable@1.0.1: version "1.0.1" @@ -11939,17 +11944,17 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1: source-list-map "^2.0.0" source-map "~0.6.1" -webpack-stream@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/webpack-stream/-/webpack-stream-5.2.1.tgz#35c992161399fe8cad9c10d4a5c258f022629b39" - integrity sha512-WvyVU0K1/VB1NZ7JfsaemVdG0PXAQUqbjUNW4A58th4pULvKMQxG+y33HXTL02JvD56ko2Cub+E2NyPwrLBT/A== +webpack-stream@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/webpack-stream/-/webpack-stream-6.0.0.tgz#0524b739a3c3a487362ee2e97522d0b8d366a510" + integrity sha512-bYv7apmGB1j0JBpa5Fgyky/1mWyzHOnUPXv+RmkgpK4FXPWCMBspgnYFmhE7Ly68JSp77eonFzm6WArWy4afpQ== dependencies: fancy-log "^1.3.3" lodash.clone "^4.3.2" lodash.some "^4.2.2" - memory-fs "^0.4.1" + memory-fs "^0.5.0" plugin-error "^1.0.1" - supports-color "^5.5.0" + supports-color "^7.1.0" through "^2.3.8" vinyl "^2.1.0" webpack "^4.26.1" @@ -12036,6 +12041,13 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" +worker-plugin@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/worker-plugin/-/worker-plugin-5.0.0.tgz#113b5fe1f4a5d6a957cecd29915bedafd70bb537" + integrity sha512-AXMUstURCxDD6yGam2r4E34aJg6kW85IiaeX72hi+I1cxyaMUtrvVY6sbfpGKAj5e7f68Acl62BjQF5aOOx2IQ== + dependencies: + loader-utils "^1.1.0" + wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"