From 2ea2132740238f79136e7e398d78a9d25329c9fa Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Fri, 29 May 2020 23:32:45 +0200 Subject: [PATCH 01/50] Add barebones comic book reader --- package.json | 2 + src/bundle.js | 6 + src/components/comicsPlayer/plugin.js | 215 +++++++++++++++++++++ src/components/playback/playbackmanager.js | 6 +- src/components/pluginManager.js | 2 +- src/scripts/site.js | 4 +- webpack.common.js | 18 +- yarn.lock | 5 + 8 files changed, 252 insertions(+), 6 deletions(-) create mode 100644 src/components/comicsPlayer/plugin.js diff --git a/package.json b/package.json index 749c62d39c..aefec20d7a 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto", "jquery": "^3.5.1", "jstree": "^3.3.7", + "libarchive.js": "^1.3.0", "libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv", "material-design-icons-iconfont": "^5.0.1", "native-promise-only": "^0.8.0-a", @@ -92,6 +93,7 @@ "src/components/autoFocuser.js", "src/components/cardbuilder/cardBuilder.js", "src/scripts/fileDownloader.js", + "src/components/comicsPlayer/plugin.js", "src/components/images/imageLoader.js", "src/components/lazyLoader/lazyLoaderIntersectionObserver.js", "src/components/playback/mediasession.js", diff --git a/src/bundle.js b/src/bundle.js index d7ba6c6a51..fd9099aaf3 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -176,3 +176,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/comicsPlayer/plugin.js b/src/components/comicsPlayer/plugin.js new file mode 100644 index 0000000000..43469bfed4 --- /dev/null +++ b/src/components/comicsPlayer/plugin.js @@ -0,0 +1,215 @@ +import connectionManager from 'connectionManager'; +import loading from 'loading'; +import dialogHelper from 'dialogHelper'; +import keyboardnavigation from 'keyboardnavigation'; +import appRouter from 'appRouter'; +import 'css!../slideshow/style'; +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; + } + + // Hide loader in case player was not fully loaded yet + 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 = 'bookPlayer'; + + 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 virtual slides option not supporting it. + loop: false, + zoom: { + minRatio: 1, + toggle: true, + containerClass: 'slider-zoom-container' + }, + autoplay: false, + keyboard: { + enabled: true + }, + preloadImages: true, + slidesPerView: 1, + slidesPerColumn: 1, + initialSlide: 0, + // Virtual slides reduce 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) { + 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/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 59108cf72e..053088ef2f 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -2187,7 +2187,7 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla // Only used internally self.getCurrentTicks = getCurrentTicks; - function playPhotos(items, options, user) { + function playOther(items, options, user) { var playStartIndex = options.startIndex || 0; var player = getPlayer(items[playStartIndex], options); @@ -2216,9 +2216,9 @@ define(['events', 'datetime', 'appSettings', 'itemHelper', 'pluginManager', 'pla return Promise.reject(); } - if (firstItem.MediaType === 'Photo') { + if (firstItem.MediaType === 'Photo' || firstItem.MediaType === 'Book') { - return playPhotos(items, options, user); + return playOther(items, options, user); } var apiClient = connectionManager.getApiClient(firstItem.ServerId); diff --git a/src/components/pluginManager.js b/src/components/pluginManager.js index 6cb56d767b..fd35d344bf 100644 --- a/src/components/pluginManager.js +++ b/src/components/pluginManager.js @@ -58,7 +58,7 @@ define(['events', 'globalize'], function (events, globalize) { return new Promise(function (resolve, reject) { require([pluginSpec], (pluginFactory) => { - var plugin = new pluginFactory(); + var plugin = pluginFactory.default ? new pluginFactory.default() : new pluginFactory(); // See if it's already installed var existing = instance.pluginsList.filter(function (p) { diff --git a/src/scripts/site.js b/src/scripts/site.js index aeb651d888..2e83928f97 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -490,6 +490,7 @@ var AppInfo = {}; 'components/playback/experimentalwarnings', 'components/htmlAudioPlayer/plugin', 'components/htmlVideoPlayer/plugin', + 'components/comicsPlayer/plugin', 'components/photoPlayer/plugin', 'components/youtubeplayer/plugin', 'components/backdropScreensaver/plugin', @@ -701,7 +702,8 @@ var AppInfo = {}; 'events', 'credentialprovider', 'connectionManagerFactory', - 'appStorage' + 'appStorage', + 'comicReader' ] }, urlArgs: urlArgs, diff --git a/webpack.common.js b/webpack.common.js index 03beb63a73..2cc8478d86 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', @@ -34,6 +41,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 20fdef5de1..5b8df85f45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6783,6 +6783,11 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +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" From 48899bead2a0c1e53eb518e45f6e41f53496b2c3 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Sat, 1 Aug 2020 14:48:48 +0200 Subject: [PATCH 02/50] Update Swiper to 6.1.1 --- package.json | 2 +- src/bundle.js | 4 ++-- yarn.lock | 30 +++++++++++++++--------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 17d175b7e7..b91d1004f6 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "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.2.0" }, diff --git a/src/bundle.js b/src/bundle.js index ae2a59f0d5..3c21ed66a6 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -60,8 +60,8 @@ _define('resize-observer-polyfill', function() { }); // swiper -var swiper = require('swiper/js/swiper'); -require('swiper/css/swiper.min.css'); +var swiper = require('swiper/swiper-bundle'); +require('swiper/swiper-bundle.css'); _define('swiper', function() { return swiper; }); diff --git a/yarn.lock b/yarn.lock index 09181bfc78..ef66f04419 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3510,12 +3510,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" @@ -10462,10 +10462,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" @@ -11038,13 +11038,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" From 9b1ed7ce4f837b8e0209cf18aefe60fe2a4f0679 Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Sat, 15 Aug 2020 11:30:36 -0400 Subject: [PATCH 03/50] Change all instances of currentTime to be in ms. --- src/components/nowPlayingBar/nowPlayingBar.js | 2 +- src/components/playback/playbackmanager.js | 14 +++----------- src/components/remotecontrol/remotecontrol.js | 2 +- src/components/syncPlay/syncPlayManager.js | 2 +- src/components/upnextdialog/upnextdialog.js | 2 +- src/controllers/playback/video/index.js | 2 +- src/plugins/chromecastPlayer/plugin.js | 4 ++-- src/plugins/sessionPlayer/plugin.js | 4 ++-- 8 files changed, 12 insertions(+), 20 deletions(-) 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 8502b551af..a5951e2784 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -1572,11 +1572,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); @@ -1585,11 +1581,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; @@ -3173,7 +3165,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.js b/src/components/remotecontrol/remotecontrol.js index 6048c918c7..ee02cfd45a 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -615,7 +615,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/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/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 73540cd636..c8d385b6d8 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/plugins/chromecastPlayer/plugin.js b/src/plugins/chromecastPlayer/plugin.js index b7e6d05969..d3faea2ce8 100644 --- a/src/plugins/chromecastPlayer/plugin.js +++ b/src/plugins/chromecastPlayer/plugin.js @@ -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/sessionPlayer/plugin.js b/src/plugins/sessionPlayer/plugin.js index c68e0d7a4a..fc795c2a6d 100644 --- a/src/plugins/sessionPlayer/plugin.js +++ b/src/plugins/sessionPlayer/plugin.js @@ -324,12 +324,12 @@ define(['playbackManager', 'events', 'serverNotifications', 'connectionManager'] SessionPlayer.prototype.currentTime = function (val) { if (val != null) { - return this.seek(val); + return this.seek(val * 10000); } var state = this.lastPlayerData || {}; state = state.PlayState || {}; - return state.PositionTicks; + return state.PositionTicks / 10000; }; SessionPlayer.prototype.duration = function () { From 7952b75ca24aaffeaac8834f24880e502f5e344e Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Sat, 15 Aug 2020 14:33:31 -0400 Subject: [PATCH 04/50] Show seek buttons when playing video on mobile. --- src/components/remotecontrol/remotecontrol.css | 15 ++++----------- src/components/remotecontrol/remotecontrol.js | 10 ++++++++-- 2 files changed, 12 insertions(+), 13 deletions(-) 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 6048c918c7..6a1355047c 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -332,8 +332,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); From dc162aca95711b119ddb1e331b86cd3666dd0467 Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Wed, 19 Aug 2020 19:19:23 -0400 Subject: [PATCH 05/50] Use strict equality check. Co-authored-by: Matjaz Zavski --- src/components/remotecontrol/remotecontrol.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index 6a1355047c..ed463f67fe 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -332,8 +332,8 @@ export default function () { buttonVisible(context.querySelector('.btnNextTrack'), item != null); buttonVisible(context.querySelector('.btnPreviousTrack'), item != null); if (layoutManager.mobile) { - const playingVideo = playbackManager.isPlayingVideo() && item != null; - const playingAudio = !playbackManager.isPlayingVideo() && item != null; + 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); From 49edf39d52f166ae1dc62922b86b2a22b79eb2ff Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Tue, 25 Aug 2020 02:27:24 -0400 Subject: [PATCH 06/50] Fix video osd hiding remote seek buttons. --- src/assets/css/videoosd.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/assets/css/videoosd.css b/src/assets/css/videoosd.css index 59a485468d..fe19dde605 100644 --- a/src/assets/css/videoosd.css +++ b/src/assets/css/videoosd.css @@ -248,8 +248,8 @@ } @media all and (max-width: 30em) { - .btnFastForward, - .btnRewind, + .osdControls .btnFastForward, + .osdControls .btnRewind, .osdMediaInfo, .osdPoster { display: none !important; @@ -281,3 +281,4 @@ display: none !important; } } + From 6523de60b1634b62311adbdb5948ade0282dd5f2 Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Tue, 25 Aug 2020 02:32:48 -0400 Subject: [PATCH 07/50] Remove empty line for stylelint. --- src/assets/css/videoosd.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/assets/css/videoosd.css b/src/assets/css/videoosd.css index fe19dde605..2a28e9cff8 100644 --- a/src/assets/css/videoosd.css +++ b/src/assets/css/videoosd.css @@ -281,4 +281,3 @@ display: none !important; } } - From b8cf026bad79439024409e7466338282f3c131c7 Mon Sep 17 00:00:00 2001 From: Cameron Date: Tue, 25 Aug 2020 15:44:01 +0100 Subject: [PATCH 08/50] Fix broken item details menu on TV Shows --- src/controllers/itemDetails/index.js | 3561 +++++++++++++------------- 1 file changed, 1779 insertions(+), 1782 deletions(-) diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index 63225f6793..74872f1261 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -29,2006 +29,2003 @@ 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('LabelVideo')); - 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('LabelVideo')); + 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 }); - - if (html && !parentNameLast) { - if (tvSeasonHtml) { - html += '

' + tvSeasonHtml + ' - ' + name + '

'; - } else { - html += '

' + name + '

'; - } - } 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.push('' + artist.Name + ''); } - 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 html.join(' / '); +} - function renderBackdrop(item) { - if (dom.getWindowSize().innerWidth >= 1000) { - backdrop.setBackdrops([item]); - } else { - backdrop.clearBackdrop(); - } - } +/** + * 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 renderDetailPageBackdrop(page, item, apiClient) { - let imgUrl; - let hasbackdrop = false; - const itemBackdropElement = page.querySelector('#itemBackdrop'); - - if (!layoutManager.mobile && !userSettings.detailsBanner()) { - return false; - } - - 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 = ''; - } - - return hasbackdrop; - } - - function reloadFromItem(instance, page, params, item, user) { - const apiClient = connectionManager.getApiClient(item.ServerId); - - Emby.Page.setTitle(''); - - // Start rendering the artwork first - renderImage(page, item); - renderLogo(page, item, apiClient); - renderBackdrop(item); - renderDetailPageBackdrop(page, item, apiClient); - - // 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); - - if ((item.LocalTrailerCount || item.RemoteTrailers && item.RemoteTrailers.length) && playbackManager.getSupportedCommands().indexOf('PlayTrailers') !== -1) { - hideAll(page, 'btnPlayTrailer', true); - } else { - hideAll(page, 'btnPlayTrailer'); - } - - setTrailerButtonVisibility(page, item); - - 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'; + 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 (user.Policy.IsAdministrator && groupedVersions.length) { - page.querySelector('.btnSplitVersions').classList.remove('hide'); - } else { - page.querySelector('.btnSplitVersions').classList.add('hide'); - } + 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); + } - if (itemContextMenu.getCommands(getContextMenuOptions(item, user)).length) { - hideAll(page, 'btnMoreCommands', true); - } else { - hideAll(page, 'btnMoreCommands'); - } + // 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]; - 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'); + 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 + }); + + if (html && !parentNameLast) { + if (tvSeasonHtml) { + html += '

' + tvSeasonHtml + ' - ' + name + '

'; + } else { + html += '

' + name + '

'; + } + } 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'); + } +} + +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'); + } +} + +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; + } + + 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 = ''; + } + + return hasbackdrop; +} + +function reloadFromItem(instance, page, params, item, user) { + const apiClient = connectionManager.getApiClient(item.ServerId); + + Emby.Page.setTitle(''); + + // Start rendering the artwork first + renderImage(page, item); + renderLogo(page, item, apiClient); + renderBackdrop(item); + renderDetailPageBackdrop(page, item, apiClient); + + // 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); + + if ((item.LocalTrailerCount || item.RemoteTrailers && item.RemoteTrailers.length) && playbackManager.getSupportedCommands().indexOf('PlayTrailers') !== -1) { + hideAll(page, 'btnPlayTrailer', true); + } else { + hideAll(page, 'btnPlayTrailer'); + } + + setTrailerButtonVisibility(page, item); + + 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'; + }); + + 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'); + } + + 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 { + 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); +} + +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 (item.Taglines && item.Taglines.length) { + taglineElement.classList.remove('hide'); + taglineElement.innerHTML = item.Taglines[0]; + } else { + taglineElement.classList.add('hide'); + } +} - if (miscInfo.innerHTML && item.Type !== 'SeriesTimer') { - miscInfo.classList.remove('hide'); - } else { - miscInfo.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); + renderLinks(page, item); + renderTags(page, item); + renderSeriesAirTime(page, item, isStatic); +} + +function enableScrollX() { + return browser.mobile && screen.availWidth <= 1000; +} + +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'); } - 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'); - } - } - } - - function renderTagline(page, item) { - const taglineElement = page.querySelector('.tagline'); - - 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); - renderLinks(page, item); - renderTags(page, item); - renderSeriesAirTime(page, item, isStatic); - } - - function enableScrollX() { - return browser.mobile && screen.availWidth <= 1000; - } - - 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) { + 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') { + 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 + }); + 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; + 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; + }); +} From d25850b6969c3c103fb3c595304fdc11eea176aa Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Tue, 25 Aug 2020 22:14:53 -0400 Subject: [PATCH 09/50] Remove un-needed styles. --- src/assets/css/videoosd.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/assets/css/videoosd.css b/src/assets/css/videoosd.css index 2a28e9cff8..808915e58b 100644 --- a/src/assets/css/videoosd.css +++ b/src/assets/css/videoosd.css @@ -248,8 +248,6 @@ } @media all and (max-width: 30em) { - .osdControls .btnFastForward, - .osdControls .btnRewind, .osdMediaInfo, .osdPoster { display: none !important; From dbf1d4f811a762681497640c4b49fdca53a99dbd Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Tue, 25 Aug 2020 22:21:31 -0400 Subject: [PATCH 10/50] Add mediaType to isPlaying for remote players. --- src/plugins/chromecastPlayer/plugin.js | 4 ++-- src/plugins/sessionPlayer/plugin.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plugins/chromecastPlayer/plugin.js b/src/plugins/chromecastPlayer/plugin.js index b7e6d05969..5ba8596089 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() { diff --git a/src/plugins/sessionPlayer/plugin.js b/src/plugins/sessionPlayer/plugin.js index cbeb6f34b4..7e5dc9f4f7 100644 --- a/src/plugins/sessionPlayer/plugin.js +++ b/src/plugins/sessionPlayer/plugin.js @@ -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() { From 7eff82d671d2df4c65988e229984af98a15189b8 Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Tue, 25 Aug 2020 22:27:28 -0400 Subject: [PATCH 11/50] Add null check to remotecontrol. --- src/components/remotecontrol/remotecontrol.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index 8e1eb55a1d..fcadadcf30 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 (typeof item !== 'undefined' && item !== null) { const nowPlayingServerId = (item.ServerId || serverId); if (item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') { const songName = item.Name; From 4e474325f3da7c6ac3ca48f9bfc8c92c75c459e9 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Wed, 26 Aug 2020 19:07:29 +0200 Subject: [PATCH 12/50] Make top bar non-floating on TV --- src/assets/css/librarybrowser.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/assets/css/librarybrowser.css b/src/assets/css/librarybrowser.css index 643fb9ca97..7f956174a7 100644 --- a/src/assets/css/librarybrowser.css +++ b/src/assets/css/librarybrowser.css @@ -163,6 +163,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; } From 186acfc3e6ef54350d9201263ce4ca8e4e375f47 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Wed, 26 Aug 2020 19:09:07 +0200 Subject: [PATCH 13/50] Fix inline CSS style for itemDetails --- src/controllers/itemDetails/index.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/controllers/itemDetails/index.html b/src/controllers/itemDetails/index.html index a69d917874..7806b454ac 100644 --- a/src/controllers/itemDetails/index.html +++ b/src/controllers/itemDetails/index.html @@ -7,8 +7,8 @@
-
-
+
+
@@ -124,7 +124,7 @@
-
+
@@ -139,14 +139,14 @@

-
- +
+
-
+

${Schedule}

From 45cd976b494596b1e5a3170ed436bbaba71b42c7 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Wed, 26 Aug 2020 19:30:01 +0200 Subject: [PATCH 14/50] Fix font sizes Makes the baseline 16px again (It was 14.88px before, for some reason) and sets the TV baseline to 20px. --- src/assets/css/fonts.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 { From 6336c50d3a253ffc9f0c8bf3101bee29d816b886 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Wed, 26 Aug 2020 19:30:23 +0200 Subject: [PATCH 15/50] Fix scollers cutting off images when focused on TV --- src/assets/css/librarybrowser.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/assets/css/librarybrowser.css b/src/assets/css/librarybrowser.css index 7f956174a7..52f99a9e31 100644 --- a/src/assets/css/librarybrowser.css +++ b/src/assets/css/librarybrowser.css @@ -1146,13 +1146,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) { From 3f7a84512ee7b262238acbc7a6e9be91792d94b6 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Wed, 26 Aug 2020 19:30:46 +0200 Subject: [PATCH 16/50] Fix icon posiition on cards in TV layout --- src/components/cardbuilder/card.css | 4 ++++ 1 file changed, 4 insertions(+) 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; } From 98f5bc3d9c6bb0454deb81d9c6a152938c16b466 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Wed, 26 Aug 2020 19:53:11 +0200 Subject: [PATCH 17/50] Don't constraing end position for scrollers if in TV layout --- src/libraries/scroller.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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); From 9518ffa456478ed1fd85128e1938cfcbf280444d Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Wed, 26 Aug 2020 19:58:21 +0200 Subject: [PATCH 18/50] Use emblem as logo in TV layout --- src/themes/appletv/theme.css | 4 ++++ src/themes/blueradiance/theme.css | 4 ++++ src/themes/dark/theme.css | 4 ++++ src/themes/light/theme.css | 4 ++++ src/themes/purplehaze/theme.css | 4 ++++ src/themes/wmc/theme.css | 4 ++++ 6 files changed, 24 insertions(+) diff --git a/src/themes/appletv/theme.css b/src/themes/appletv/theme.css index 41540ce0dd..8b91bda65a 100644 --- a/src/themes/appletv/theme.css +++ b/src/themes/appletv/theme.css @@ -38,6 +38,10 @@ html { background-image: url(../../assets/img/banner-dark.png); } +.layout-tv .pageTitleWithDefaultLogo { + background-image: url(../../assets/img/icon-transparent.png); +} + html { background: #d5e9f2; } diff --git a/src/themes/blueradiance/theme.css b/src/themes/blueradiance/theme.css index 7ecc4a74a5..e503ba8416 100644 --- a/src/themes/blueradiance/theme.css +++ b/src/themes/blueradiance/theme.css @@ -41,6 +41,10 @@ html { background-image: url(../../assets/img/banner-light.png); } +.layout-tv .pageTitleWithDefaultLogo { + background-image: url(../../assets/img/icon-transparent.png); +} + .dialog, .nowPlayingPlaylist, .nowPlayingContextMenu, diff --git a/src/themes/dark/theme.css b/src/themes/dark/theme.css index be2b9269af..3acc425ddf 100644 --- a/src/themes/dark/theme.css +++ b/src/themes/dark/theme.css @@ -34,6 +34,10 @@ html { background-image: url(../../assets/img/banner-light.png); } +.layout-tv .pageTitleWithDefaultLogo { + background-image: url(../../assets/img/icon-transparent.png); +} + .backgroundContainer, .dialog, .nowPlayingPlaylist, diff --git a/src/themes/light/theme.css b/src/themes/light/theme.css index c5161985cd..d5eb94e1dc 100644 --- a/src/themes/light/theme.css +++ b/src/themes/light/theme.css @@ -42,6 +42,10 @@ html { background-image: url(../../assets/img/banner-light.png); } +.layout-tv .pageTitleWithDefaultLogo { + background-image: url(../../assets/img/icon-transparent.png); +} + .backgroundContainer, html { background-color: #f2f2f2; diff --git a/src/themes/purplehaze/theme.css b/src/themes/purplehaze/theme.css index 1d82afc23a..7849c91658 100644 --- a/src/themes/purplehaze/theme.css +++ b/src/themes/purplehaze/theme.css @@ -36,6 +36,10 @@ html { background-image: url(../../assets/img/banner-light.png); } +.layout-tv .pageTitleWithDefaultLogo { + background-image: url(../../assets/img/icon-transparent.png); +} + .dialog, .nowPlayingPlaylist, .nowPlayingContextMenu, diff --git a/src/themes/wmc/theme.css b/src/themes/wmc/theme.css index 4a7375a129..01a6aa5efd 100644 --- a/src/themes/wmc/theme.css +++ b/src/themes/wmc/theme.css @@ -47,6 +47,10 @@ html { background-image: url(../../assets/img/banner-light.png); } +.layout-tv .pageTitleWithDefaultLogo { + background-image: url(../../assets/img/icon-transparent.png); +} + .backgroundContainer, .dialog, .nowPlayingPlaylist, From 350195b5bca9c741b81e6c65554e07785c1a398f Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Wed, 26 Aug 2020 20:06:48 +0200 Subject: [PATCH 19/50] Hide admin menus if in TV layout --- src/controllers/user/menu/index.html | 2 +- src/controllers/user/menu/index.js | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/controllers/user/menu/index.html b/src/controllers/user/menu/index.html index 4d91f1206f..1c131919fe 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); }); From ece0b39d7085656d5aa8e8160fd98be6134bf60a Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Wed, 26 Aug 2020 21:29:10 +0200 Subject: [PATCH 20/50] Rework details page for TV --- src/assets/css/librarybrowser.css | 46 +++++++++++++++----------- src/controllers/itemDetails/index.html | 12 +++---- src/controllers/itemDetails/index.js | 45 ++++++++++++++++--------- src/themes/appletv/theme.css | 15 +++++---- src/themes/blueradiance/theme.css | 14 ++++---- src/themes/dark/theme.css | 13 ++++++++ src/themes/light/theme.css | 8 +++++ src/themes/purplehaze/theme.css | 13 +++++--- src/themes/wmc/theme.css | 14 ++++---- 9 files changed, 115 insertions(+), 65 deletions(-) diff --git a/src/assets/css/librarybrowser.css b/src/assets/css/librarybrowser.css index 52f99a9e31..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; } @@ -453,8 +457,7 @@ height: 26.5vh; } -.layout-desktop .itemBackdrop::after, -.layout-tv .itemBackdrop::after { +.layout-desktop .itemBackdrop::after { content: ""; width: 100%; height: 100%; @@ -462,8 +465,8 @@ display: block; } -.layout-desktop .noBackdrop .itemBackdrop, -.layout-tv .noBackdrop .itemBackdrop { +.layout-tv .itemBackdrop, +.layout-desktop .noBackdrop .itemBackdrop { display: none; } @@ -630,6 +633,10 @@ z-index: 2; } +.layout-tv .detailPagePrimaryContainer { + display: block; +} + .layout-mobile .detailPagePrimaryContainer { display: block; position: relative; @@ -643,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; @@ -754,8 +765,7 @@ div.itemDetailGalleryLink.defaultCardBackground { position: relative; } - .layout-desktop .itemBackdrop, - .layout-tv .itemBackdrop { + .layout-desktop .itemBackdrop { height: 40vh; } @@ -781,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); } @@ -850,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) { @@ -919,10 +924,6 @@ div.itemDetailGalleryLink.defaultCardBackground { } @media all and (min-width: 100em) { - .detailFloatingButton { - display: none !important; - } - .personBackdrop { display: none !important; } @@ -931,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) { diff --git a/src/controllers/itemDetails/index.html b/src/controllers/itemDetails/index.html index 7806b454ac..3c808e0b7b 100644 --- a/src/controllers/itemDetails/index.html +++ b/src/controllers/itemDetails/index.html @@ -11,7 +11,7 @@
-
+
diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index f837d3fc1d..fe5f8419d6 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -383,7 +383,7 @@ import 'emby-select'; itemType: 'MusicArtist', serverId: serverId }); - html.push('
' + artist.Name + ''); + html.push('' + artist.Name + ''); } return html.join(' / '); @@ -416,7 +416,7 @@ import 'emby-select'; }, { context: context }); - parentNameHtml.push('' + item.SeriesName + ''); + parentNameHtml.push('' + item.SeriesName + ''); } else if (item.IsSeries || item.EpisodeTitle) { parentNameHtml.push(item.Name); } @@ -431,7 +431,7 @@ import 'emby-select'; }, { context: context }); - parentNameHtml.push('' + item.SeriesName + ''); + parentNameHtml.push('' + item.SeriesName + ''); } else if (item.ParentIndexNumber != null && item.Type === 'Episode') { parentRoute = appRouter.getRouteUrl({ Id: item.SeasonId, @@ -442,7 +442,7 @@ import 'emby-select'; }, { context: context }); - parentNameHtml.push('' + item.SeasonName + ''); + 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')) { @@ -455,7 +455,7 @@ import 'emby-select'; }, { context: context }); - parentNameHtml.push('' + item.Album + ''); + parentNameHtml.push('' + item.Album + ''); } else if (item.Album) { parentNameHtml.push(item.Album); } @@ -572,9 +572,13 @@ import 'emby-select'; // Start rendering the artwork first renderImage(page, item); - renderLogo(page, item, apiClient); + + // Same some screen real estate in TV mode + if (!layoutManager.tv) { + renderLogo(page, item, apiClient); + renderDetailPageBackdrop(page, item, apiClient); + } renderBackdrop(item); - renderDetailPageBackdrop(page, item, apiClient); // Render the main information for the item page.querySelector('.detailPagePrimaryContainer').classList.add('detailRibbon'); @@ -766,6 +770,9 @@ import 'emby-select'; 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) { @@ -1061,7 +1068,12 @@ import 'emby-select'; renderOverview(page, item); renderMiscInfo(page, item); reloadUserDataButtons(page, item); - renderLinks(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); } @@ -1672,12 +1684,6 @@ import 'emby-select'; 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) { @@ -1982,7 +1988,16 @@ import 'emby-select'; let currentItem; const self = this; const apiClient = params.serverId ? connectionManager.getApiClient(params.serverId) : ApiClient; - view.querySelectorAll('.btnPlay'); + + 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'); + } bindAll(view, '.btnPlay', 'click', onPlayClick); bindAll(view, '.btnResume', 'click', onPlayClick); bindAll(view, '.btnInstantMix', 'click', onInstantMixClick); diff --git a/src/themes/appletv/theme.css b/src/themes/appletv/theme.css index 8b91bda65a..ad18bf24c5 100644 --- a/src/themes/appletv/theme.css +++ b/src/themes/appletv/theme.css @@ -23,17 +23,17 @@ 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); } @@ -238,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 e503ba8416..85795c90c3 100644 --- a/src/themes/blueradiance/theme.css +++ b/src/themes/blueradiance/theme.css @@ -21,22 +21,20 @@ 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); } @@ -235,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 3acc425ddf..1024770c7c 100644 --- a/src/themes/dark/theme.css +++ b/src/themes/dark/theme.css @@ -30,6 +30,10 @@ html { background-color: rgba(0, 0, 0, 0.4); } +.layout-tv .skinHeader.semiTransparent { + background: none; +} + .pageTitleWithDefaultLogo { background-image: url(../../assets/img/banner-light.png); } @@ -211,6 +215,10 @@ html { background: rgba(32, 32, 32, 0.8); } +.layout-tv .detailRibbon { + background: none; +} + .noBackdrop .detailRibbon { background: #202020; } @@ -458,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 d5eb94e1dc..ec618138cd 100644 --- a/src/themes/light/theme.css +++ b/src/themes/light/theme.css @@ -38,6 +38,10 @@ html { background-color: rgba(0, 0, 0, 0.4); } +.layout-tv .skinHeader.semiTransparent { + background: none; +} + .pageTitleWithDefaultLogo { background-image: url(../../assets/img/banner-light.png); } @@ -237,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 7849c91658..4b2451cfe4 100644 --- a/src/themes/purplehaze/theme.css +++ b/src/themes/purplehaze/theme.css @@ -17,21 +17,20 @@ 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); } @@ -322,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 01a6aa5efd..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,13 +33,14 @@ 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); } @@ -217,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); From 9b6b79628f89bb6b7d87acf31187fef13f356c7a Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Wed, 26 Aug 2020 17:05:01 -0400 Subject: [PATCH 21/50] Switch to simple check for item. --- src/components/remotecontrol/remotecontrol.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index fcadadcf30..befedfe1ac 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' && item !== null) { + if (item) { const nowPlayingServerId = (item.ServerId || serverId); if (item.Type == 'Audio' || item.MediaStreams[0].Type == 'Audio') { const songName = item.Name; From c22e3b2cc6b9a7b9a76f4c33fc9a5523936c9c67 Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Wed, 26 Aug 2020 17:18:23 -0400 Subject: [PATCH 22/50] Fix seriesImageUrl linter issue. --- src/components/remotecontrol/remotecontrol.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index befedfe1ac..b6bc74e98c 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -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 From e7b52072940f6236249ccabe6320114398da1fdd Mon Sep 17 00:00:00 2001 From: Ian Walton Date: Wed, 26 Aug 2020 18:35:20 -0400 Subject: [PATCH 23/50] Fix clearing backdrop when no item is playing. --- src/components/remotecontrol/remotecontrol.js | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index b6bc74e98c..8d6c53cead 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -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 = ''; } } From 0ba71ebd4b9b05dc690b36bd33db1e34d3789cea Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2020 13:15:41 +0000 Subject: [PATCH 24/50] Bump date-fns from 2.15.0 to 2.16.0 Bumps [date-fns](https://github.com/date-fns/date-fns) from 2.15.0 to 2.16.0. - [Release notes](https://github.com/date-fns/date-fns/releases) - [Changelog](https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md) - [Commits](https://github.com/date-fns/date-fns/compare/v2.15.0...v2.16.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 203be0749d..8e08e10952 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "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", diff --git a/yarn.lock b/yarn.lock index 86c167aee6..cf709af8a4 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" From 0e45c25e403515cf5fdf243eccfcde863a4815de Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 28 Aug 2020 23:04:16 +0900 Subject: [PATCH 25/50] move comics player to plugin directory and update web config --- package.json | 2 +- src/config.template.json | 1 + src/{components => plugins}/comicsPlayer/plugin.js | 0 3 files changed, 2 insertions(+), 1 deletion(-) rename src/{components => plugins}/comicsPlayer/plugin.js (100%) diff --git a/package.json b/package.json index 775fc2f9cf..ee1907d4f3 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,6 @@ "src/components/channelMapper/channelMapper.js", "src/components/collectionEditor/collectionEditor.js", "src/components/confirm/confirm.js", - "src/components/comicsPlayer/plugin.js", "src/components/dialog/dialog.js", "src/components/dialogHelper/dialogHelper.js", "src/components/directorybrowser/directorybrowser.js", @@ -180,6 +179,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/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/components/comicsPlayer/plugin.js b/src/plugins/comicsPlayer/plugin.js similarity index 100% rename from src/components/comicsPlayer/plugin.js rename to src/plugins/comicsPlayer/plugin.js From 90babaca9f98d68b806ea1082bfd564cc1a4742f Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 28 Aug 2020 23:09:35 +0900 Subject: [PATCH 26/50] minor code tweaks for comics player --- src/plugins/comicsPlayer/plugin.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/plugins/comicsPlayer/plugin.js b/src/plugins/comicsPlayer/plugin.js index 43469bfed4..723dd20106 100644 --- a/src/plugins/comicsPlayer/plugin.js +++ b/src/plugins/comicsPlayer/plugin.js @@ -28,14 +28,12 @@ export class ComicsPlayer { stop() { this.unbindEvents(); - let elem = this._mediaElement; - + let elem = this.mediaElement; if (elem) { dialogHelper.close(elem); - this._mediaElement = null; + this.mediaElement = null; } - // Hide loader in case player was not fully loaded yet loading.hide(); } @@ -61,8 +59,7 @@ export class ComicsPlayer { } createMediaElement() { - let elem = this._mediaElement; - + let elem = this.mediaElement; if (elem) { return elem; } @@ -77,19 +74,17 @@ export class ComicsPlayer { exitAnimation: 'fadeout', removeOnClose: true }); - elem.id = 'bookPlayer'; + elem.id = 'bookPlayer'; elem.classList.add('slideshowDialog'); elem.innerHTML = '
'; this.bindEvents(); - dialogHelper.open(elem); } - this._mediaElement = elem; - + this.mediaElement = elem; return elem; } @@ -109,13 +104,14 @@ export class ComicsPlayer { 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 virtual slides option not supporting it. + // loop is disabled due to the lack of support in virtual slides loop: false, zoom: { minRatio: 1, @@ -130,7 +126,7 @@ export class ComicsPlayer { slidesPerView: 1, slidesPerColumn: 1, initialSlide: 0, - // Virtual slides reduce memory consumption for large libraries while allowing preloading of images; + // reduces memory consumption for large libraries while allowing preloading of images virtual: { slides: archiveSource.urls, cache: true, @@ -160,6 +156,7 @@ export class ComicsPlayer { if (item.Path && (item.Path.endsWith('cbz') || item.Path.endsWith('cbr'))) { return true; } + return false; } } @@ -178,6 +175,7 @@ class ArchiveSource { if (!res.ok) { return; } + let blob = await res.blob(); this.archive = await libarchive.Archive.open(blob); this.raw = await this.archive.getFilesArray(); @@ -186,10 +184,11 @@ class ArchiveSource { let files = await this.archive.getFilesArray(); files.sort((a, b) => { - if (a.file.name < b.file.name) + if (a.file.name < b.file.name) { return -1; - else + } else { return 1; + } }); for (let file of files) { From f75ad14c324cccb1ade24859c8def5357a021d81 Mon Sep 17 00:00:00 2001 From: dkanada Date: Fri, 28 Aug 2020 23:11:53 +0900 Subject: [PATCH 27/50] update dialog id and rename some variables --- src/plugins/comicsPlayer/plugin.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/comicsPlayer/plugin.js b/src/plugins/comicsPlayer/plugin.js index 723dd20106..33e8e8d251 100644 --- a/src/plugins/comicsPlayer/plugin.js +++ b/src/plugins/comicsPlayer/plugin.js @@ -19,7 +19,7 @@ export class ComicsPlayer { } play(options) { - this._progress = 0; + this.progress = 0; let elem = this.createMediaElement(); return this.setCurrentSrc(elem, options); @@ -75,7 +75,7 @@ export class ComicsPlayer { removeOnClose: true }); - elem.id = 'bookPlayer'; + elem.id = 'comicsPlayer'; elem.classList.add('slideshowDialog'); elem.innerHTML = '
'; @@ -90,7 +90,7 @@ export class ComicsPlayer { setCurrentSrc(elem, options) { let item = options.items[0]; - this._currentItem = item; + this.currentItem = item; loading.show(); From cf70659deb5d5f75086e3e2f89b91a4678903bde Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2020 14:36:12 +0000 Subject: [PATCH 28/50] Bump webpack-stream from 5.2.1 to 6.0.0 Bumps [webpack-stream](https://github.com/shama/webpack-stream) from 5.2.1 to 6.0.0. - [Release notes](https://github.com/shama/webpack-stream/releases) - [Commits](https://github.com/shama/webpack-stream/compare/v5.2.1...v6.0.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 8e08e10952..2bea3ad8e2 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "stylelint-order": "^4.1.0", "webpack": "^4.44.1", "webpack-merge": "^4.2.2", - "webpack-stream": "^5.2.1" + "webpack-stream": "^6.0.0" }, "dependencies": { "alameda": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index cf709af8a4..287380aef0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10918,7 +10918,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== @@ -11939,17 +11939,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" From 26b09014d02795a6d12c53170020f0771e8ca850 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Sat, 29 Aug 2020 16:34:21 +0200 Subject: [PATCH 29/50] Fix bad global restriction --- src/components/serviceworker/notifications.js | 3 ++- src/scripts/apploader.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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/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) { From 3a8e0394dccf0ebc23d5f46b2e09e484af134bd5 Mon Sep 17 00:00:00 2001 From: MrTimscampi Date: Sat, 29 Aug 2020 16:44:36 +0200 Subject: [PATCH 30/50] Remove withCredentials from xhrSetup in hls.js --- src/plugins/htmlAudioPlayer/plugin.js | 5 +---- src/plugins/htmlVideoPlayer/plugin.js | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) 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); From ac0fdd8059573045c954fcb26829f48ca4b40fbb Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Sat, 29 Aug 2020 19:43:05 +0300 Subject: [PATCH 31/50] Fix fake image zoom --- src/components/slideshow/slideshow.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From f915ecd400d680a71f2d66b3abb1de9910e3e72a Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Sat, 29 Aug 2020 19:44:05 +0300 Subject: [PATCH 32/50] Add vendor styles polyfill --- src/legacy/vendorStyles.js | 32 ++++++++++++++++++++++++++++++++ src/scripts/site.js | 2 ++ 2 files changed, 34 insertions(+) create mode 100644 src/legacy/vendorStyles.js diff --git a/src/legacy/vendorStyles.js b/src/legacy/vendorStyles.js new file mode 100644 index 0000000000..3b9dd9cf10 --- /dev/null +++ b/src/legacy/vendorStyles.js @@ -0,0 +1,32 @@ +// 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)) { + for (const vendorName of vendorProperties[name] || []) { + 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; } + }); + + break; + } + } + } + } + + if (elem.style instanceof CSSStyleDeclaration) { + polyfillProperty('transform'); + polyfillProperty('transition'); + } +})(); diff --git a/src/scripts/site.js b/src/scripts/site.js index 0e1e44251c..fda46b8dbe 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(); }); @@ -655,6 +656,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); From fad3b562ea3dc1fc9a11ce41039b9efa44bb0981 Mon Sep 17 00:00:00 2001 From: Dmitry Lyzo Date: Sat, 29 Aug 2020 20:10:23 +0300 Subject: [PATCH 33/50] Fix variable usage (SonarCloud) --- src/legacy/vendorStyles.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/legacy/vendorStyles.js b/src/legacy/vendorStyles.js index 3b9dd9cf10..74a90ba04d 100644 --- a/src/legacy/vendorStyles.js +++ b/src/legacy/vendorStyles.js @@ -10,7 +10,7 @@ function polyfillProperty(name) { if (!(name in elem.style)) { - for (const vendorName of vendorProperties[name] || []) { + (vendorProperties[name] || []).every((vendorName) => { if (vendorName in elem.style) { console.debug(`polyfill '${name}' with '${vendorName}'`); @@ -19,9 +19,11 @@ set: function (val) { this[vendorName] = val; } }); - break; + return false; } - } + + return true; + }); } } From dfc906e7d31ac06cd006ff4cb1a230437964e0f2 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 30 Aug 2020 14:57:36 +0900 Subject: [PATCH 34/50] fix build issues --- package.json | 3 ++- src/plugins/comicsPlayer/plugin.js | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ee1907d4f3..0154195c9e 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "stylelint-order": "^4.1.0", "webpack": "^4.44.1", "webpack-merge": "^4.2.2", - "webpack-stream": "^5.2.1" + "webpack-stream": "^5.2.1", + "worker-plugin": "^5.0.0" }, "dependencies": { "alameda": "^1.4.0", diff --git a/src/plugins/comicsPlayer/plugin.js b/src/plugins/comicsPlayer/plugin.js index 33e8e8d251..7881a6d060 100644 --- a/src/plugins/comicsPlayer/plugin.js +++ b/src/plugins/comicsPlayer/plugin.js @@ -3,7 +3,6 @@ import loading from 'loading'; import dialogHelper from 'dialogHelper'; import keyboardnavigation from 'keyboardnavigation'; import appRouter from 'appRouter'; -import 'css!../slideshow/style'; import * as libarchive from 'libarchive'; export class ComicsPlayer { From 924928fa0f9918616b95088ed3ccc24c749ebf42 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 30 Aug 2020 15:01:10 +0900 Subject: [PATCH 35/50] update yarn.lock --- yarn.lock | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/yarn.lock b/yarn.lock index 4f8dd29c45..f1c6f1d581 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12041,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" From 6b776aac44919a35fe88228f85b322e6699ffab5 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 30 Aug 2020 15:11:20 +0900 Subject: [PATCH 36/50] fix linting error with url global --- src/plugins/comicsPlayer/plugin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/comicsPlayer/plugin.js b/src/plugins/comicsPlayer/plugin.js index 7881a6d060..5f9546b9fe 100644 --- a/src/plugins/comicsPlayer/plugin.js +++ b/src/plugins/comicsPlayer/plugin.js @@ -191,6 +191,7 @@ class ArchiveSource { }); for (let file of files) { + /* eslint-disable-next-line compat/compat */ let url = URL.createObjectURL(file.file); this.urls.push(url); } From 3aa8304efc0c05115e5224d864344e1f5eb7b860 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2020 12:41:59 +0000 Subject: [PATCH 37/50] Bump html-webpack-plugin from 4.3.0 to 4.4.0 Bumps [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) from 4.3.0 to 4.4.0. - [Release notes](https://github.com/jantimon/html-webpack-plugin/releases) - [Changelog](https://github.com/jantimon/html-webpack-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/jantimon/html-webpack-plugin/compare/v4.3.0...v4.4.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 39c7937bd7..6dfe8fe5a1 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.0", "lazypipe": "^1.0.2", "node-sass": "^4.13.1", "postcss-loader": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index d3aee5fe79..3e61547551 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.4.0.tgz#ed6ab2b5e4e476ffa3c5ce52505aa31a42075029" + integrity sha512-FHeg2JN9ar1kaR0SLgbF07w46o/n1nGszyByYlPxqEymSpl82vA8EX0leE67kZr3GJnOBh8BbBzmCLO6O1YTIQ== dependencies: "@types/html-minifier-terser" "^5.0.0" "@types/tapable" "^1.0.5" From 5ca7342e3eb27bc90632eba6f95af53cf2bf18f0 Mon Sep 17 00:00:00 2001 From: millallo Date: Sun, 30 Aug 2020 10:41:51 +0000 Subject: [PATCH 38/50] Translated using Weblate (Italian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/it/ --- src/strings/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strings/it.json b/src/strings/it.json index f89c8b3a65..df11607245 100644 --- a/src/strings/it.json +++ b/src/strings/it.json @@ -292,7 +292,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", From 9e4e8a911f107ad2e7b146e1a150dc7dc87471d3 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sun, 30 Aug 2020 23:14:39 +0900 Subject: [PATCH 39/50] fix spelling error --- src/controllers/itemDetails/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index fe5f8419d6..cc970c856c 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -573,7 +573,7 @@ import 'emby-select'; // Start rendering the artwork first renderImage(page, item); - // Same some screen real estate in TV mode + // Save some screen real estate in TV mode if (!layoutManager.tv) { renderLogo(page, item, apiClient); renderDetailPageBackdrop(page, item, apiClient); From 24b1f67005044352f044e9f5a23989b935bc44ca Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2020 16:27:13 +0000 Subject: [PATCH 40/50] Bump html-webpack-plugin from 4.4.0 to 4.4.1 Bumps [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) from 4.4.0 to 4.4.1. - [Release notes](https://github.com/jantimon/html-webpack-plugin/releases) - [Changelog](https://github.com/jantimon/html-webpack-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/jantimon/html-webpack-plugin/compare/v4.4.0...v4.4.1) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6dfe8fe5a1..a0b84400e9 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.4.0", + "html-webpack-plugin": "^4.4.1", "lazypipe": "^1.0.2", "node-sass": "^4.13.1", "postcss-loader": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 3e61547551..8f9fc1aa40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.4.0.tgz#ed6ab2b5e4e476ffa3c5ce52505aa31a42075029" - integrity sha512-FHeg2JN9ar1kaR0SLgbF07w46o/n1nGszyByYlPxqEymSpl82vA8EX0leE67kZr3GJnOBh8BbBzmCLO6O1YTIQ== +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" From e6c2f2ee0838d702c1cc93e87ed1600e40c707c5 Mon Sep 17 00:00:00 2001 From: SaddFox Date: Sun, 30 Aug 2020 17:00:23 +0000 Subject: [PATCH 41/50] Translated using Weblate (Slovenian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sl/ --- src/strings/sl-si.json | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/strings/sl-si.json b/src/strings/sl-si.json index 641d21e8bc..3937f7fc95 100644 --- a/src/strings/sl-si.json +++ b/src/strings/sl-si.json @@ -1355,5 +1355,27 @@ "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" } From 734b3dd38236f2021895856728737b419e7261c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Dardenne?= Date: Tue, 26 May 2020 22:58:27 +0200 Subject: [PATCH 42/50] Show track artists in the list view, except if the artists for all tracks match the album artists --- src/components/listview/listview.js | 14 ++------------ src/controllers/itemDetails/index.js | 13 +++++++++++-- 2 files changed, 13 insertions(+), 14 deletions(-) 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/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index b1781e75f0..e5154dcd88 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -1337,16 +1337,25 @@ function renderChildren(page, item) { 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: true, + showIndex: new Set(discNumbers).size > 1 || (discNumbers.length >= 1 && discNumbers[0] > 1), index: 'disc', showIndexNumberLeft: true, playFromHere: true, action: 'playallfromhere', image: false, - artist: 'auto', + artist: showArtist, containerAlbumArtists: item.AlbumArtists }); isList = true; From afcbe8adbfb342872042e987488379c9b541b470 Mon Sep 17 00:00:00 2001 From: SaddFox Date: Sun, 30 Aug 2020 17:16:07 +0000 Subject: [PATCH 43/50] Translated using Weblate (Slovenian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/sl/ --- src/strings/sl-si.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/strings/sl-si.json b/src/strings/sl-si.json index 3937f7fc95..8c1cec86a9 100644 --- a/src/strings/sl-si.json +++ b/src/strings/sl-si.json @@ -1377,5 +1377,11 @@ "Unmute": "Obnovi zvok", "Uniform": "Obroba", "Transcoding": "Prekodiranje", - "TrackCount": "{0} skladb" + "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." } From 2ff95140bb33f0573f3f2491dea3f8334c4bfa10 Mon Sep 17 00:00:00 2001 From: "Joshua M. Boniface" Date: Sun, 30 Aug 2020 16:05:29 -0400 Subject: [PATCH 44/50] Merge pull request #1849 from brianjmurrell/patch-1 Add BuildRequires: git to Fedora specfile (cherry picked from commit 5f1af65c2e5694bb2b53e1eca1fd48c50bafb2cb) Signed-off-by: Joshua M. Boniface --- fedora/jellyfin-web.spec | 2 ++ 1 file changed, 2 insertions(+) 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 From d7ac4b6701518e7c279f18eaec431771119440df Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 01:54:33 +0000 Subject: [PATCH 45/50] Bump material-design-icons-iconfont from 5.0.1 to 6.0.1 Bumps [material-design-icons-iconfont](https://github.com/jossef/material-design-icons-iconfont) from 5.0.1 to 6.0.1. - [Release notes](https://github.com/jossef/material-design-icons-iconfont/releases) - [Commits](https://github.com/jossef/material-design-icons-iconfont/compare/5.0.1...6.0.1) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b696ed582f..9862ef56ba 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "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", diff --git a/yarn.lock b/yarn.lock index 106811a0ab..57044c8c7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7041,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" From dde44155ae51e3f64b790b03d1333d68bfb84e32 Mon Sep 17 00:00:00 2001 From: TheGoose Date: Mon, 31 Aug 2020 04:21:12 +0000 Subject: [PATCH 46/50] Translated using Weblate (English (United Kingdom)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en_GB/ --- src/strings/en-gb.json | 125 ++++++++++++++++++++++------------------- 1 file changed, 67 insertions(+), 58 deletions(-) diff --git a/src/strings/en-gb.json b/src/strings/en-gb.json index d44183a4a8..f37ae4f4c4 100644 --- a/src/strings/en-gb.json +++ b/src/strings/en-gb.json @@ -22,7 +22,7 @@ "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.", "OptionFavorite": "Favourites", - "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", @@ -68,7 +68,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", @@ -81,7 +81,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", @@ -155,7 +155,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?", @@ -185,8 +185,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", @@ -229,10 +229,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.", @@ -284,7 +284,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", @@ -353,7 +353,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", @@ -460,9 +460,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.", @@ -495,7 +495,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.", @@ -516,9 +516,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", @@ -553,7 +553,7 @@ "OptionThumb": "Thumb", "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", "OptionRuntime": "Runtime", "OptionResumable": "Resumable", @@ -567,9 +567,9 @@ "OptionProfileAudio": "Audio", "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", "OptionNone": "None", @@ -585,7 +585,7 @@ "OptionBlockMovies": "Movies", "OptionBlockChannelContent": "Internet Channel Content", "OptionBanner": "Banner", - "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", "OptionArtist": "Artist", "OptionAllowVideoPlaybackTranscoding": "Allow video playback that requires transcoding", @@ -595,7 +595,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", @@ -623,9 +623,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.", @@ -645,8 +645,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?", @@ -681,7 +681,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.", @@ -743,7 +743,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:", @@ -830,7 +830,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:", @@ -885,13 +885,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:", @@ -917,10 +917,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:", @@ -937,7 +937,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:", @@ -958,7 +958,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:", @@ -971,7 +971,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:", @@ -987,32 +987,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", @@ -1028,7 +1028,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:", @@ -1053,7 +1053,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:", @@ -1108,7 +1108,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", "OptionPosterCard": "Poster card", @@ -1123,7 +1123,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:", @@ -1134,7 +1134,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", @@ -1151,7 +1151,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", "OptionContinuing": "Continuing", @@ -1188,7 +1188,7 @@ "OptionDownloadPrimaryImage": "Primary", "OptionDownloadMenuImage": "Menu", "OptionDownloadLogoImage": "Logo", - "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", "OptionDownloadDiscImage": "Disc", "OptionDownloadBoxImage": "Box", @@ -1196,7 +1196,7 @@ "OptionDownloadBackImage": "Back", "OptionDownloadArtImage": "Art", "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:", @@ -1228,7 +1228,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", @@ -1258,7 +1258,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.", @@ -1289,7 +1289,7 @@ "HeaderSelectServerCachePath": "Select Server Cache Path", "HeaderSelectServer": "Select Server", "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", @@ -1305,7 +1305,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", "HeaderProfile": "Profile", "HeaderPreferredMetadataLanguage": "Preferred Metadata Language", @@ -1346,7 +1346,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", @@ -1418,7 +1418,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", @@ -1433,5 +1433,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" } From 77ce9f1088f4b6237f9871299870e3d2be59ffa6 Mon Sep 17 00:00:00 2001 From: TheGoose Date: Mon, 31 Aug 2020 04:27:29 +0000 Subject: [PATCH 47/50] Translated using Weblate (English) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/en/ --- src/strings/en-us.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strings/en-us.json b/src/strings/en-us.json index f87c1a326e..8470ca840f 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -512,7 +512,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:", From 0be0faa0acb5975d945790dcc90c6bcc2791964a Mon Sep 17 00:00:00 2001 From: dkanada Date: Mon, 31 Aug 2020 18:06:53 +0900 Subject: [PATCH 48/50] linting issue --- src/controllers/itemDetails/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/itemDetails/index.js b/src/controllers/itemDetails/index.js index e5154dcd88..c1ec022e5a 100644 --- a/src/controllers/itemDetails/index.js +++ b/src/controllers/itemDetails/index.js @@ -1340,7 +1340,7 @@ function renderChildren(page, item) { 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) )) { + if (!equalSet(track.ArtistItems.map(x => x.Id), track.AlbumArtists.map(x => x.Id))) { showArtist = true; break; } From 8dcbacb196e363b1e184f21a1d7c19ca709bdaca Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Mon, 31 Aug 2020 15:48:24 +0200 Subject: [PATCH 49/50] Add application/json as content type for JSON requests --- src/components/channelMapper/channelMapper.js | 1 + src/components/directorybrowser/directorybrowser.js | 3 ++- src/controllers/dashboard/encodingsettings.js | 3 ++- src/controllers/session/resetPassword/index.js | 3 ++- src/controllers/wizard/remote/index.js | 3 ++- src/controllers/wizard/settings/index.js | 3 ++- src/controllers/wizard/start/index.js | 3 ++- src/controllers/wizard/user/index.js | 3 ++- 8 files changed, 15 insertions(+), 7 deletions(-) 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/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/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/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..12a81ccc33 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); } From c094247988df3db7eed92e5dfaa0434eb45a3095 Mon Sep 17 00:00:00 2001 From: Claus Vium Date: Mon, 31 Aug 2020 15:58:54 +0200 Subject: [PATCH 50/50] Fix lint error --- src/controllers/wizard/settings/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/wizard/settings/index.js b/src/controllers/wizard/settings/index.js index 12a81ccc33..6e3a82cd9b 100644 --- a/src/controllers/wizard/settings/index.js +++ b/src/controllers/wizard/settings/index.js @@ -13,7 +13,7 @@ function save(page) { type: 'POST', data: JSON.stringify(config), url: apiClient.getUrl('Startup/Configuration'), - contentType: 'application/json', + contentType: 'application/json' }).then(function () { loading.hide(); navigateToNextPage();