diff --git a/dist/plex-meets-homeassistant.js b/dist/plex-meets-homeassistant.js index 2a2d4dc..61cd92d 100644 --- a/dist/plex-meets-homeassistant.js +++ b/dist/plex-meets-homeassistant.js @@ -17205,8 +17205,8 @@ const CSS_STYLE = { }; const supported = { kodi: ['movie', 'episode'], - androidtv: ['movie', 'show', 'season', 'episode'], - plexPlayer: ['movie', 'show', 'season', 'episode'] + androidtv: ['movie', 'show', 'season', 'episode', 'clip'], + plexPlayer: ['movie', 'show', 'season', 'episode', 'clip'] }; var bind = function bind(fn, thisArg) { @@ -19041,6 +19041,71 @@ const getOffset = (el) => { } return { top: y, left: x }; }; +const createEpisodesView = (playController, plexProtocol, ip, port, token, data) => { + const episodeContainer = document.createElement('div'); + episodeContainer.className = 'episodeContainer'; + episodeContainer.style.width = `${CSS_STYLE.episodeWidth}px`; + const episodeThumbURL = `${plexProtocol}://${ip}:${port}/photo/:/transcode?width=${CSS_STYLE.episodeWidth}&height=${CSS_STYLE.episodeHeight}&minSize=1&upscale=1&url=${data.thumb}&X-Plex-Token=${token}`; + const episodeElem = document.createElement('div'); + episodeElem.className = 'episodeElem'; + episodeElem.style.width = `${CSS_STYLE.episodeWidth}px`; + episodeElem.style.height = `${CSS_STYLE.episodeHeight}px`; + episodeElem.style.backgroundImage = `url('${episodeThumbURL}')`; + episodeElem.dataset.clicked = 'false'; + if (typeof data.lastViewedAt === 'undefined') { + const toViewElem = document.createElement('div'); + toViewElem.className = 'toViewEpisode'; + episodeElem.appendChild(toViewElem); + } + if (playController.isPlaySupported(data)) { + const episodeInteractiveArea = document.createElement('div'); + episodeInteractiveArea.className = 'interactiveArea'; + const episodePlayButton = document.createElement('button'); + episodePlayButton.name = 'playButton'; + episodePlayButton.addEventListener('click', episodeEvent => { + episodeEvent.stopPropagation(); + playController.play(data, true); + }); + episodeInteractiveArea.append(episodePlayButton); + episodeElem.append(episodeInteractiveArea); + } + episodeContainer.append(episodeElem); + const episodeTitleElem = document.createElement('div'); + episodeTitleElem.className = 'episodeTitleElem'; + episodeTitleElem.innerHTML = escapeHtml(data.title); + episodeContainer.append(episodeTitleElem); + const episodeNumber = document.createElement('div'); + episodeNumber.className = 'episodeNumber'; + if (data.type === 'episode') { + episodeNumber.innerHTML = escapeHtml(`Episode ${escapeHtml(data.index)}`); + } + else if (data.type === 'clip') { + let text = ''; + switch (data.subtype) { + case 'behindTheScenes': + text = 'Behind the Scenes'; + break; + case 'trailer': + text = 'Trailer'; + break; + case 'scene': + text = 'Scene'; + break; + case 'sceneOrSample': + text = 'Scene'; + break; + default: + text = data.subtype; + break; + } + episodeNumber.innerHTML = escapeHtml(text); + } + episodeContainer.append(episodeNumber); + episodeContainer.addEventListener('click', episodeEvent => { + episodeEvent.stopPropagation(); + }); + return episodeContainer; +}; const isScrolledIntoView = (elem) => { const rect = elem.getBoundingClientRect(); const elemTop = rect.top; @@ -19558,6 +19623,14 @@ style.textContent = css ` margin-right: 10px; transition: 0.5s; } + .movieExtras { + z-index: 4; + position: absolute; + top: 340px; + width: calc(100% - 32px); + left: 0; + padding: 16px; + } .interactiveArea { position: relative; width: 100%; @@ -20134,49 +20207,8 @@ class PlexMeetsHomeAssistant extends HTMLElement { this.episodesElem.style.transition = `0s`; this.episodesElem.style.top = `${top + 2000}px`; lodash.forEach(episodesData, episodeData => { - if (this.episodesElem) { - const episodeContainer = document.createElement('div'); - episodeContainer.className = 'episodeContainer'; - episodeContainer.style.width = `${CSS_STYLE.episodeWidth}px`; - const episodeThumbURL = `${this.plexProtocol}://${this.config.ip}:${this.config.port}/photo/:/transcode?width=${CSS_STYLE.episodeWidth}&height=${CSS_STYLE.episodeHeight}&minSize=1&upscale=1&url=${episodeData.thumb}&X-Plex-Token=${this.config.token}`; - const episodeElem = document.createElement('div'); - episodeElem.className = 'episodeElem'; - episodeElem.style.width = `${CSS_STYLE.episodeWidth}px`; - episodeElem.style.height = `${CSS_STYLE.episodeHeight}px`; - episodeElem.style.backgroundImage = `url('${episodeThumbURL}')`; - episodeElem.dataset.clicked = 'false'; - if (typeof episodeData.lastViewedAt === 'undefined') { - const toViewElem = document.createElement('div'); - toViewElem.className = 'toViewEpisode'; - episodeElem.appendChild(toViewElem); - } - if (this.playController && this.playController.isPlaySupported(episodeData)) { - const episodeInteractiveArea = document.createElement('div'); - episodeInteractiveArea.className = 'interactiveArea'; - const episodePlayButton = document.createElement('button'); - episodePlayButton.name = 'playButton'; - episodePlayButton.addEventListener('click', episodeEvent => { - episodeEvent.stopPropagation(); - if (this.plex && this.playController) { - this.playController.play(episodeData, true); - } - }); - episodeInteractiveArea.append(episodePlayButton); - episodeElem.append(episodeInteractiveArea); - } - episodeContainer.append(episodeElem); - const episodeTitleElem = document.createElement('div'); - episodeTitleElem.className = 'episodeTitleElem'; - episodeTitleElem.innerHTML = escapeHtml(episodeData.title); - episodeContainer.append(episodeTitleElem); - const episodeNumber = document.createElement('div'); - episodeNumber.className = 'episodeNumber'; - episodeNumber.innerHTML = escapeHtml(`Episode ${escapeHtml(episodeData.index)}`); - episodeContainer.append(episodeNumber); - episodeContainer.addEventListener('click', episodeEvent => { - episodeEvent.stopPropagation(); - }); - this.episodesElem.append(episodeContainer); + if (this.episodesElem && this.playController) { + this.episodesElem.append(createEpisodesView(this.playController, this.plexProtocol, this.config.ip, this.config.port, this.config.token, episodeData)); } }); clearInterval(this.episodesLoadTimeout); @@ -20245,7 +20277,33 @@ class PlexMeetsHomeAssistant extends HTMLElement { }, 200); } else { - console.log(await this.plex.getDetails(data.key.split('/')[3])); + const movieDetails = await this.plex.getDetails(data.key.split('/')[3]); + const extras = movieDetails.Extras.Metadata; + this.episodesElemFreshlyLoaded = true; + if (this.episodesElem) { + this.episodesElemHidden = false; + this.episodesElem.style.display = 'block'; + this.episodesElem.innerHTML = ''; + this.episodesElem.style.transition = `0s`; + this.episodesElem.style.top = `${top + 2000}px`; + lodash.forEach(extras, extrasData => { + if (this.episodesElem && this.playController) { + this.episodesElem.append(createEpisodesView(this.playController, this.plexProtocol, this.config.ip, this.config.port, this.config.token, extrasData)); + } + }); + clearInterval(this.episodesLoadTimeout); + this.episodesLoadTimeout = setTimeout(() => { + if (this.episodesElem) { + this.episodesElem.style.transition = `0.7s`; + this.episodesElem.style.top = `${top + CSS_STYLE.expandedHeight + 16}px`; + this.resizeBackground(); + } + }, 200); + clearInterval(this.episodesElemFreshlyLoadedTimeout); + this.episodesElemFreshlyLoadedTimeout = setTimeout(() => { + this.episodesElemFreshlyLoaded = false; + }, 700); + } } } }; diff --git a/src/const.ts b/src/const.ts index 2288ea8..8d45489 100644 --- a/src/const.ts +++ b/src/const.ts @@ -10,8 +10,8 @@ const CSS_STYLE = { const supported: any = { kodi: ['movie', 'episode'], - androidtv: ['movie', 'show', 'season', 'episode'], - plexPlayer: ['movie', 'show', 'season', 'episode'] + androidtv: ['movie', 'show', 'season', 'episode', 'clip'], + plexPlayer: ['movie', 'show', 'season', 'episode', 'clip'] }; const LOREM_IPSUM = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec semper risus vitae aliquet interdum. Nulla facilisi. Pellentesque viverra sagittis lorem eget aliquet. Cras vehicula, purus vel consectetur mattis, ipsum arcu ullamcorper mi, id viverra purus ex eu dolor. Integer vehicula lacinia sem convallis iaculis. Nulla fermentum erat interdum, efficitur felis in, mollis neque. Vivamus luctus metus eget nisl pellentesque, placerat elementum magna eleifend. diff --git a/src/modules/style.ts b/src/modules/style.ts index b8bace5..87b8ab8 100644 --- a/src/modules/style.ts +++ b/src/modules/style.ts @@ -272,6 +272,14 @@ style.textContent = css` margin-right: 10px; transition: 0.5s; } + .movieExtras { + z-index: 4; + position: absolute; + top: 340px; + width: calc(100% - 32px); + left: 0; + padding: 16px; + } .interactiveArea { position: relative; width: 100%; diff --git a/src/modules/utils.ts b/src/modules/utils.ts index 3c7bf2d..3c7249b 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -1,6 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-env browser */ import _ from 'lodash'; +import { CSS_STYLE } from '../const'; +import PlayController from './PlayController'; +import Plex from './Plex'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const escapeHtml = (unsafe: any): string => { if (unsafe) { @@ -40,6 +43,86 @@ const getOffset = (el: Element): Record => { return { top: y, left: x }; }; +const createEpisodesView = ( + playController: PlayController, + plexProtocol: string, + ip: string, + port: string, + token: string, + data: Record +): HTMLElement => { + const episodeContainer = document.createElement('div'); + episodeContainer.className = 'episodeContainer'; + episodeContainer.style.width = `${CSS_STYLE.episodeWidth}px`; + const episodeThumbURL = `${plexProtocol}://${ip}:${port}/photo/:/transcode?width=${CSS_STYLE.episodeWidth}&height=${CSS_STYLE.episodeHeight}&minSize=1&upscale=1&url=${data.thumb}&X-Plex-Token=${token}`; + + const episodeElem = document.createElement('div'); + episodeElem.className = 'episodeElem'; + episodeElem.style.width = `${CSS_STYLE.episodeWidth}px`; + episodeElem.style.height = `${CSS_STYLE.episodeHeight}px`; + episodeElem.style.backgroundImage = `url('${episodeThumbURL}')`; + episodeElem.dataset.clicked = 'false'; + + if (typeof data.lastViewedAt === 'undefined') { + const toViewElem = document.createElement('div'); + toViewElem.className = 'toViewEpisode'; + episodeElem.appendChild(toViewElem); + } + + if (playController.isPlaySupported(data)) { + const episodeInteractiveArea = document.createElement('div'); + episodeInteractiveArea.className = 'interactiveArea'; + + const episodePlayButton = document.createElement('button'); + episodePlayButton.name = 'playButton'; + episodePlayButton.addEventListener('click', episodeEvent => { + episodeEvent.stopPropagation(); + playController.play(data, true); + }); + + episodeInteractiveArea.append(episodePlayButton); + episodeElem.append(episodeInteractiveArea); + } + episodeContainer.append(episodeElem); + + const episodeTitleElem = document.createElement('div'); + episodeTitleElem.className = 'episodeTitleElem'; + episodeTitleElem.innerHTML = escapeHtml(data.title); + episodeContainer.append(episodeTitleElem); + + const episodeNumber = document.createElement('div'); + episodeNumber.className = 'episodeNumber'; + if (data.type === 'episode') { + episodeNumber.innerHTML = escapeHtml(`Episode ${escapeHtml(data.index)}`); + } else if (data.type === 'clip') { + let text = ''; + switch (data.subtype) { + case 'behindTheScenes': + text = 'Behind the Scenes'; + break; + case 'trailer': + text = 'Trailer'; + break; + case 'scene': + text = 'Scene'; + break; + case 'sceneOrSample': + text = 'Scene'; + break; + default: + text = data.subtype; + break; + } + episodeNumber.innerHTML = escapeHtml(text); + } + episodeContainer.append(episodeNumber); + + episodeContainer.addEventListener('click', episodeEvent => { + episodeEvent.stopPropagation(); + }); + return episodeContainer; +}; + const isScrolledIntoView = (elem: HTMLElement): boolean => { const rect = elem.getBoundingClientRect(); const elemTop = rect.top; @@ -53,4 +136,4 @@ const isScrolledIntoView = (elem: HTMLElement): boolean => { }; // eslint-disable-next-line import/prefer-default-export -export { escapeHtml, getOffset, isScrolledIntoView, getHeight }; +export { escapeHtml, getOffset, isScrolledIntoView, getHeight, createEpisodesView }; diff --git a/src/plex-meets-homeassistant.ts b/src/plex-meets-homeassistant.ts index 9915e04..36e7a6e 100644 --- a/src/plex-meets-homeassistant.ts +++ b/src/plex-meets-homeassistant.ts @@ -5,7 +5,7 @@ import _ from 'lodash'; import { supported, CSS_STYLE } from './const'; import Plex from './modules/Plex'; import PlayController from './modules/PlayController'; -import { escapeHtml, getOffset, isScrolledIntoView, getHeight } from './modules/utils'; +import { escapeHtml, getOffset, isScrolledIntoView, getHeight, createEpisodesView } from './modules/utils'; import style from './modules/style'; class PlexMeetsHomeAssistant extends HTMLElement { @@ -637,58 +637,17 @@ class PlexMeetsHomeAssistant extends HTMLElement { this.episodesElem.style.transition = `0s`; this.episodesElem.style.top = `${top + 2000}px`; _.forEach(episodesData, episodeData => { - if (this.episodesElem) { - const episodeContainer = document.createElement('div'); - episodeContainer.className = 'episodeContainer'; - episodeContainer.style.width = `${CSS_STYLE.episodeWidth}px`; - const episodeThumbURL = `${this.plexProtocol}://${this.config.ip}:${this.config.port}/photo/:/transcode?width=${CSS_STYLE.episodeWidth}&height=${CSS_STYLE.episodeHeight}&minSize=1&upscale=1&url=${episodeData.thumb}&X-Plex-Token=${this.config.token}`; - - const episodeElem = document.createElement('div'); - episodeElem.className = 'episodeElem'; - episodeElem.style.width = `${CSS_STYLE.episodeWidth}px`; - episodeElem.style.height = `${CSS_STYLE.episodeHeight}px`; - episodeElem.style.backgroundImage = `url('${episodeThumbURL}')`; - episodeElem.dataset.clicked = 'false'; - - if (typeof episodeData.lastViewedAt === 'undefined') { - const toViewElem = document.createElement('div'); - toViewElem.className = 'toViewEpisode'; - episodeElem.appendChild(toViewElem); - } - - if (this.playController && this.playController.isPlaySupported(episodeData)) { - const episodeInteractiveArea = document.createElement('div'); - episodeInteractiveArea.className = 'interactiveArea'; - - const episodePlayButton = document.createElement('button'); - episodePlayButton.name = 'playButton'; - episodePlayButton.addEventListener('click', episodeEvent => { - episodeEvent.stopPropagation(); - if (this.plex && this.playController) { - this.playController.play(episodeData, true); - } - }); - - episodeInteractiveArea.append(episodePlayButton); - episodeElem.append(episodeInteractiveArea); - } - episodeContainer.append(episodeElem); - - const episodeTitleElem = document.createElement('div'); - episodeTitleElem.className = 'episodeTitleElem'; - episodeTitleElem.innerHTML = escapeHtml(episodeData.title); - episodeContainer.append(episodeTitleElem); - - const episodeNumber = document.createElement('div'); - episodeNumber.className = 'episodeNumber'; - episodeNumber.innerHTML = escapeHtml(`Episode ${escapeHtml(episodeData.index)}`); - episodeContainer.append(episodeNumber); - - episodeContainer.addEventListener('click', episodeEvent => { - episodeEvent.stopPropagation(); - }); - - this.episodesElem.append(episodeContainer); + if (this.episodesElem && this.playController) { + this.episodesElem.append( + createEpisodesView( + this.playController, + this.plexProtocol, + this.config.ip, + this.config.port, + this.config.token, + episodeData + ) + ); } }); clearInterval(this.episodesLoadTimeout); @@ -762,7 +721,47 @@ class PlexMeetsHomeAssistant extends HTMLElement { } }, 200); } else { - console.log(await this.plex.getDetails(data.key.split('/')[3])); + const movieDetails = await this.plex.getDetails(data.key.split('/')[3]); + + const extras = movieDetails.Extras.Metadata; + + this.episodesElemFreshlyLoaded = true; + if (this.episodesElem) { + this.episodesElemHidden = false; + this.episodesElem.style.display = 'block'; + this.episodesElem.innerHTML = ''; + this.episodesElem.style.transition = `0s`; + this.episodesElem.style.top = `${top + 2000}px`; + + _.forEach(extras, extrasData => { + if (this.episodesElem && this.playController) { + this.episodesElem.append( + createEpisodesView( + this.playController, + this.plexProtocol, + this.config.ip, + this.config.port, + this.config.token, + extrasData + ) + ); + } + }); + + clearInterval(this.episodesLoadTimeout); + this.episodesLoadTimeout = setTimeout(() => { + if (this.episodesElem) { + this.episodesElem.style.transition = `0.7s`; + this.episodesElem.style.top = `${top + CSS_STYLE.expandedHeight + 16}px`; + + this.resizeBackground(); + } + }, 200); + clearInterval(this.episodesElemFreshlyLoadedTimeout); + this.episodesElemFreshlyLoadedTimeout = setTimeout(() => { + this.episodesElemFreshlyLoaded = false; + }, 700); + } } } };