From 29cec753f039aebb0b012e1b7ee11684b387fef3 Mon Sep 17 00:00:00 2001 From: hawken Date: Thu, 10 Dec 2020 01:09:35 +0100 Subject: [PATCH 01/15] Introduce DocumentManager for DOM stuff, and fix the jsdoc a little --- src/components/commandHandler.ts | 12 +- src/components/deviceprofileBuilder.ts | 10 +- src/components/documentManager.ts | 611 +++++++++++++++++++++++++ src/components/jellyfinActions.ts | 176 +------ src/components/maincontroller.ts | 4 +- src/components/playbackManager.ts | 43 +- src/helpers.ts | 393 +--------------- 7 files changed, 656 insertions(+), 593 deletions(-) create mode 100644 src/components/documentManager.ts diff --git a/src/components/commandHandler.ts b/src/components/commandHandler.ts index e5fad20d..cb307787 100644 --- a/src/components/commandHandler.ts +++ b/src/components/commandHandler.ts @@ -17,14 +17,12 @@ import { seek } from './maincontroller'; -import { - displayItem, - reportPlaybackProgress, - startBackdropInterval -} from './jellyfinActions'; +import { reportPlaybackProgress } from './jellyfinActions'; import { playbackManager } from './playbackManager'; +import { DocumentManager } from './documentManager'; + export abstract class CommandHandler { private static playerManager: cast.framework.PlayerManager; private static playbackManager: playbackManager; @@ -92,7 +90,7 @@ export abstract class CommandHandler { static displayContentHandler(data: DataMessage): void { if (!this.playbackManager.isPlaying()) { - displayItem((data.options).ItemId); + DocumentManager.showItemId((data.options).ItemId); } } @@ -141,7 +139,7 @@ export abstract class CommandHandler { static IdentifyHandler(): void { if (!this.playbackManager.isPlaying()) { - startBackdropInterval(); + DocumentManager.startBackdropInterval(); } else { // When a client connects send back the initial device state (volume etc) via a playbackstop message reportPlaybackProgress( diff --git a/src/components/deviceprofileBuilder.ts b/src/components/deviceprofileBuilder.ts index 225b4764..4b1087c1 100644 --- a/src/components/deviceprofileBuilder.ts +++ b/src/components/deviceprofileBuilder.ts @@ -41,11 +41,11 @@ let profileOptions: ProfileOptions; let currentDeviceId: number; /** - * @param Property What property the condition should test. - * @param Condition The condition to test the values for. - * @param Value The value to compare against. - * @param [IsRequired=false] - * @returns A profile condition created from the parameters. + * @param {ProfileConditionValue} Property What property the condition should test. + * @param {ProfileConditionType} Condition The condition to test the values for. + * @param {string} Value The value to compare against. + * @param {boolean} [IsRequired=false] Don't permit unknown values + * @returns {ProfileCondition} A profile condition created from the parameters. */ function createProfileCondition( Property: ProfileConditionValue, diff --git a/src/components/documentManager.ts b/src/components/documentManager.ts new file mode 100644 index 00000000..d764ba40 --- /dev/null +++ b/src/components/documentManager.ts @@ -0,0 +1,611 @@ +import { parseISO8601Date } from '../helpers'; +import { JellyfinApi } from './jellyfinApi'; +import { BaseItemDto } from '~/api/generated/models/base-item-dto'; + +export abstract class DocumentManager { + // Duration between each backdrop switch in ms + private static backdropPeriodMs: number | null = 30000; + // Timer state - so that we don't start the interval more than necessary + private static backdropTimer = 0; + + // TODO make enum + private static status = ''; + + /** + * Get url for primary image for a given item + * + * @param {BaseItemDto} item to look up + * @returns {string | null} url to primary image + */ + private static getPrimaryImageUrl(item: BaseItemDto): string | null { + if (item.AlbumPrimaryImageTag) + return JellyfinApi.createUrl( + `Items/${item.AlbumId}/Images/Primary?tag=${item.AlbumPrimaryImageTag}` + ); + else if (item.ImageTags?.Primary) + return JellyfinApi.createUrl( + `Items/${item.Id}/Images/Primary?tag=${item.ImageTags.Primary}` + ); + else return null; + } + + /** + * Get url for logo image for a given item + * + * @param {BaseItemDto} item to look up + * @returns {string | null} url to logo image + */ + private static getLogoUrl(item: BaseItemDto): string | null { + if (item.ImageTags?.Logo) + return JellyfinApi.createUrl( + `Items/${item.Id}/Images/Logo/0?tag=${item.ImageTags.Logo}` + ); + else if (item.ParentLogoItemId && item.ParentLogoImageTag) + return JellyfinApi.createUrl( + `Items/${item.ParentLogoItemId}/Images/Logo/0?tag=${item.ParentLogoImageTag}` + ); + else return null; + } + + /** + * This fucntion takes an item and shows details about it + * on the details page. This happens when no media is playing, + * and the connected client is browsing the library. + * + * @param {BaseItemDto} item to show information about + */ + public static showItem(item: BaseItemDto): void { + // stop cycling backdrops + this.clearBackdropInterval(); + + this.setAppStatus('details'); + this.setWaitingBackdrop(item); + + this.setLogo(this.getLogoUrl(item)); + this.setOverview(item.Overview ?? null); + this.setGenres(item?.Genres?.join(' / ') ?? null); + this.setDisplayName(item); + + this.setMiscInfo(item); + + const detailRating = document.getElementById('detailRating'); + if (detailRating) { + detailRating.innerHTML = this.getRatingHtml(item); + } + + const playedIndicator = document.getElementById('playedIndicator'); + + if (playedIndicator) { + if (item?.UserData?.Played) { + playedIndicator.style.display = 'block'; + playedIndicator.innerHTML = + ''; + } else if (item?.UserData?.UnplayedItemCount) { + playedIndicator.style.display = 'block'; + playedIndicator.innerHTML = item.UserData.UnplayedItemCount.toString(); + } else { + playedIndicator.style.display = 'none'; + } + } + + let detailImageUrl = this.getPrimaryImageUrl(item); + + if ( + item?.UserData?.PlayedPercentage && + item?.UserData?.PlayedPercentage < 100 && + !item.IsFolder + ) { + this.setHasPlayedPercentage(false); + this.setPlayedPercentage(item.UserData.PlayedPercentage); + + if (detailImageUrl != null) + detailImageUrl += + '&PercentPlayed=' + + item.UserData.PlayedPercentage.toString(); + } else { + this.setHasPlayedPercentage(false); + this.setPlayedPercentage(0); + } + + this.setDetailImage(detailImageUrl); + } + + /** + * Show item, but from just the id number, not an actual item. + * Looks up the item and then calls showItem + * + * @param {string} itemId id of item to look up + * @returns {Promise} promise that resolves when the item is shown + */ + public static showItemId(itemId: string): Promise { + return JellyfinApi.authAjaxUser('Items/' + itemId, { + dataType: 'json', + type: 'GET' + }).then((item: BaseItemDto) => DocumentManager.showItem(item)); + } + + /** + * Get HTML content used to display the rating of an item + * + * @param {BaseItemDto} item to look up + * @returns {string} html to put in document + */ + private static getRatingHtml(item: BaseItemDto): string { + let html = ''; + if (item.CommunityRating != null) { + html += + `
` + + '
' + + item.CommunityRating.toFixed(1) + + '
'; + } + + if (item.CriticRating != null) { + const verdict = item.CriticRating >= 60 ? 'fresh' : 'rotten'; + html += + `
` + + `
${item.CriticRating}%
`; + } + + return html; + } + + /** + * Set the status of the app, and switch the visible view + * to the corresponding one. + * + * @param {string} status to set + */ + public static setAppStatus(status: string): void { + this.status = status; + document.body.className = status; + } + + /** + * Get the status of the app + * + * @returns {string} app status + */ + public static getAppStatus(): string { + return this.status; + } + + /** + * BACKDROP LOGIC + * + * Backdrops are set on the waiting container. + * They are switched around every 30 seconds by default + * (governed by startBackdropInterval) + * + * @param {BaseItemDto | null} item Item to use for waiting backdrop, null to remove it. + */ + public static setWaitingBackdrop(item: BaseItemDto | null): void { + // no backdrop as a fallback + let src: string | null = null; + + if (item != null) { + if (item.BackdropImageTags && item.BackdropImageTags.length) { + // get first backdrop of image if applicable + src = JellyfinApi.createUrl( + `Items/${item.Id}/Images/Backdrop/0?tag=${item.BackdropImageTags[0]}` + ); + } else if ( + item.ParentBackdropItemId && + item.ParentBackdropImageTags && + item.ParentBackdropImageTags.length + ) { + // otherwise get first backdrop from parent + src = JellyfinApi.createUrl( + `Items/${item.ParentBackdropItemId}/Images/Backdrop/0?tag=${item.ParentBackdropImageTags[0]}` + ); + } + } + + const element: HTMLElement | null = document.querySelector( + '#waiting-container-backdrop' + ); + + if (element === null) { + console.error( + 'documentManager: Cannot find #waiting-container-backdrop' + ); + } else { + (element).style.backgroundImage = + src != null ? `url(${src})` : ''; + } + } + + /** + * Set a random backdrop on the waiting container + * + * @returns {Promise} promise waiting for the backdrop to be set + */ + private static setRandomUserBackdrop(): Promise { + return JellyfinApi.authAjaxUser('Items', { + dataType: 'json', + type: 'GET', + query: { + SortBy: 'Random', + IncludeItemTypes: 'Movie,Series', + ImageTypes: 'Backdrop', + Recursive: true, + Limit: 1, + // Although we're limiting to what the user has access to, + // not everyone will want to see adult backdrops rotating on their TV. + MaxOfficialRating: 'PG-13' + } + }).then((result) => { + if (result.Items && result.Items[0]) + return DocumentManager.setWaitingBackdrop(result.Items[0]); + else return DocumentManager.setWaitingBackdrop(null); + }); + } + + /** + * Stop the backdrop rotation + */ + public static clearBackdropInterval(): void { + if (this.backdropTimer !== 0) { + clearInterval(this.backdropTimer); + this.backdropTimer = 0; + } + } + + /** + * Start the backdrop rotation, restart if running, stop if disabled + * + * @returns {Promise} promise for the first backdrop to be set + */ + public static startBackdropInterval(): Promise { + // avoid running it multiple times + this.clearBackdropInterval(); + + // skip out if it's disabled + if (!this.backdropPeriodMs) { + this.setWaitingBackdrop(null); + return Promise.resolve(); + } + + this.backdropTimer = ( + setInterval( + () => DocumentManager.setRandomUserBackdrop(), + this.backdropPeriodMs + ) + ); + + return this.setRandomUserBackdrop(); + } + + /** + * Set interval between backdrop changes, null to disable + * + * @param {number | null} period in milliseconds or null + */ + public static setBackdropPeriodMs(period: number | null): void { + if (period !== this.backdropPeriodMs) { + this.backdropPeriodMs = period; + + // If the timer was running, restart it + if (this.backdropTimer !== 0) { + // startBackdropInterval will also clear the previous one + this.startBackdropInterval(); + } + + if (period === null) { + // No backdrop is wanted, and the timer has been cleared. + // This call will remove any present backdrop. + this.setWaitingBackdrop(null); + } + } + } + + /** + * Set background behind the media player, + * this is shown while the media is loading. + * + * @param {BaseItemDto} item to get backdrop from + */ + public static setPlayerBackdrop(item: BaseItemDto): void { + let backdropUrl: string | null = null; + + if (item.BackdropImageTags && item.BackdropImageTags.length) { + backdropUrl = JellyfinApi.createUrl( + `Items/${item.Id}/Images/Backdrop/0?tag=${item.BackdropImageTags[0]}` + ); + } else if ( + item.ParentBackdropItemId && + item.ParentBackdropImageTags && + item.ParentBackdropImageTags.length + ) { + backdropUrl = JellyfinApi.createUrl( + `Items/${item.ParentBackdropItemId}/Images/Backdrop/0?tag=${item.ParentBackdropImageTags[0]}` + ); + } + + if (backdropUrl != null) { + window.mediaElement?.style.setProperty( + '--background-image', + `url("${backdropUrl}")` + ); + } else { + window.mediaElement?.style.removeProperty('--background-image'); + } + } + /* /BACKDROP LOGIC */ + + /** + * Set the URL to the item logo, or null to remove it + * + * @param {string | null} src Source url or null + */ + public static setLogo(src: string | null): void { + const element: HTMLElement | null = document.querySelector( + '.detailLogo' + ); + if (element === null) { + console.error('documentManager: Cannot find .detailLogo'); + } else { + (element).style.backgroundImage = + src != null ? `url(${src})` : ''; + } + } + + /** + * Set the URL to the item banner image (I think?), + * or null to remove it + * + * @param {string | null} src Source url or null + */ + public static setDetailImage(src: string | null): void { + const element: HTMLElement | null = document.querySelector( + '.detailImage' + ); + if (element === null) { + console.error('documentManager: Cannot find .detailImage'); + } else { + (element).style.backgroundImage = + src != null ? `url(${src})` : ''; + } + } + + /** + * Set the human readable name for an item + * + * This combines the old statement setDisplayName(getDisplayName(item)) + * into setDisplayName(item). + * + * @param {BaseItemDto} item source for the displayed name + */ + private static setDisplayName(item: BaseItemDto): void { + const name: string = item.EpisodeTitle ?? item.Name; + + let displayName: string = name; + + if (item.Type == 'TvChannel') { + if (item.Number) displayName = `${item.Number} ${name}`; + } else if ( + item.Type == 'Episode' && + item.IndexNumber != null && + item.ParentIndexNumber != null + ) { + let episode = `S${item.ParentIndexNumber}, E${item.IndexNumber}`; + + if (item.IndexNumberEnd) { + episode += `-${item.IndexNumberEnd}`; + } + + displayName = `${episode} - ${name}`; + } + + const element: HTMLElement | null = document.querySelector( + '.displayName' + ); + if (element === null) { + console.error('documentManager: Cannot find .displayName'); + } else { + (element).innerHTML = displayName || ''; + } + } + + /** + * Set the html of the genres container + * + * @param {string | null} name String/html for genres box, null to empty + */ + private static setGenres(name: string | null): void { + const element: HTMLElement | null = document.querySelector('.genres'); + if (element === null) { + console.error('documentManager: Cannot find .genres'); + } else { + (element).innerHTML = name || ''; + } + } + + /** + * Set the html of the overview container + * + * @param {string | null} name string or html to insert + */ + private static setOverview(name: string | null): void { + const element: HTMLElement | null = document.querySelector('.overview'); + if (element === null) { + console.error('documentManager: Cannot find .overview'); + } else { + (element).innerHTML = name || ''; + } + } + + /** + * Set the progress of the progress bar in the + * item details page. (Not the same as the playback ui) + * + * @param {number} value Percentage to set + */ + private static setPlayedPercentage(value = 0): void { + const element: HTMLInputElement | null = ( + document.querySelector('.itemProgressBar') + ); + if (element === null) { + console.error('documentManager: Cannot find .itemProgressBar'); + } else { + (element).value = value.toString(); + } + } + + /** + * Set the visibility of the item progress bar in the + * item details page + * + * @param {boolean} value If true, show progress on details page + */ + private static setHasPlayedPercentage(value: boolean): void { + const element: HTMLElement | null = document.querySelector( + '.detailImageProgressContainer' + ); + if (element === null) { + console.error( + 'documentManager: Cannot find .detailImageProgressContainer' + ); + } else { + if (value) (element).classList.remove('hide'); + else (element).classList.add('hide'); + } + } + + /** + * Get a human readable representation of the current position + * in ticks + * + * @param {number} ticks tick position + * @returns {string} human readable position + */ + private static formatRunningTime(ticks: number): string { + const ticksPerHour = 36000000000; + const ticksPerMinute = 600000000; + const ticksPerSecond = 10000000; + + const parts: string[] = []; + + const hours: number = Math.floor(ticks / ticksPerHour); + + if (hours) { + parts.push(hours.toString()); + } + + ticks -= hours * ticksPerHour; + + const minutes: number = Math.floor(ticks / ticksPerMinute); + + ticks -= minutes * ticksPerMinute; + + if (minutes < 10 && hours) { + parts.push('0' + minutes.toString()); + } else { + parts.push(minutes.toString()); + } + + const seconds: number = Math.floor(ticks / ticksPerSecond); + + if (seconds < 10) { + parts.push('0' + seconds.toString()); + } else { + parts.push(seconds.toString()); + } + + return parts.join(':'); + } + + /** + * Set information about mostly episodes or series + * on the item details page + * + * @param {BaseItemDto} item to look up + */ + private static setMiscInfo(item: BaseItemDto): void { + const info: Array = []; + if (item.Type == 'Episode') { + if (item.PremiereDate) { + try { + info.push( + parseISO8601Date(item.PremiereDate).toLocaleDateString() + ); + } catch (e) { + console.log('Error parsing date: ' + item.PremiereDate); + } + } + } + if (item.StartDate) { + try { + info.push( + parseISO8601Date(item.StartDate).toLocaleDateString() + ); + } catch (e) { + console.log('Error parsing date: ' + item.PremiereDate); + } + } + if (item.ProductionYear && item.Type == 'Series') { + if (item.Status == 'Continuing') { + info.push(`${item.ProductionYear}-Present`); + } else if (item.ProductionYear) { + let text: string = item.ProductionYear.toString(); + if (item.EndDate) { + try { + const endYear = parseISO8601Date( + item.EndDate + ).getFullYear(); + if (endYear != item.ProductionYear) { + text += + '-' + + parseISO8601Date(item.EndDate).getFullYear(); + } + } catch (e) { + console.log('Error parsing date: ' + item.EndDate); + } + } + info.push(text); + } + } + if (item.Type != 'Series' && item.Type != 'Episode') { + if (item.ProductionYear) { + info.push(item.ProductionYear.toString()); + } else if (item.PremiereDate) { + try { + info.push( + parseISO8601Date(item.PremiereDate) + .getFullYear() + .toString() + ); + } catch (e) { + console.log('Error parsing date: ' + item.PremiereDate); + } + } + } + let minutes; + if (item.RunTimeTicks && item.Type != 'Series') { + if (item.Type == 'Audio') { + info.push(this.formatRunningTime(item.RunTimeTicks)); + } else { + minutes = item.RunTimeTicks / 600000000; + minutes = minutes || 1; + info.push(Math.round(minutes) + 'min'); + } + } + if ( + item.OfficialRating && + item.Type !== 'Season' && + item.Type !== 'Episode' + ) { + info.push(item.OfficialRating); + } + if (item.Video3DFormat) { + info.push('3D'); + } + + const element = document.getElementById('miscInfo'); + if (element === null) { + console.error('documentManager: Cannot find element miscInfo'); + } else { + element.innerHTML = info.join('    '); + } + } +} diff --git a/src/components/jellyfinActions.ts b/src/components/jellyfinActions.ts index ab9fe0e8..326a3470 100644 --- a/src/components/jellyfinActions.ts +++ b/src/components/jellyfinActions.ts @@ -1,21 +1,6 @@ import { getSenderReportingData, resetPlaybackScope, - getBackdropUrl, - getLogoUrl, - getPrimaryImageUrl, - getDisplayName, - getRatingHtml, - getMiscInfoHtml, - setAppStatus, - setDisplayName, - setGenres, - setOverview, - setPlayedPercentage, - setWaitingBackdrop, - setHasPlayedPercentage, - setLogo, - setDetailImage, extend, broadcastToMessageBus } from '../helpers'; @@ -28,7 +13,7 @@ import { MediaSourceInfo } from '../api/generated/models/media-source-info'; import { PlayRequest } from '../api/generated/models/play-request'; import { LiveStreamResponse } from '../api/generated/models/live-stream-response'; import { JellyfinApi } from './jellyfinApi'; -import { BaseItemDtoQueryResult } from '~/api/generated/models/base-item-dto-query-result'; +import { DocumentManager } from './documentManager'; interface PlayRequestQuery extends PlayRequest { UserId?: string; @@ -40,7 +25,6 @@ interface PlayRequestQuery extends PlayRequest { } let pingInterval: number; -let backdropInterval: number; let lastTranscoderPing = 0; /** @@ -87,7 +71,11 @@ export function reportPlaybackStart( $scope: GlobalScope, reportingParams: PlaybackProgressInfo ): Promise { - clearBackdropInterval(); + // it's just "reporting" that the playback is starting + // but it's also disabling the rotating backdrops + // in the line below. + // TODO move the responsibility to the caller. + DocumentManager.clearBackdropInterval(); broadcastToMessageBus({ //TODO: convert these to use a defined type in the type field @@ -111,6 +99,7 @@ export function reportPlaybackStart( * @param reportingParams parameters for jellyfin * @param reportToServer if jellyfin should be informed * @param broadcastEventName name of event to send to the cast sender + * @returns {Promise} Promise for the http request */ export function reportPlaybackProgress( $scope: GlobalScope, @@ -200,133 +189,6 @@ export function pingTranscoder( ); } -/** - * Stop the backdrop rotation - */ -function clearBackdropInterval(): void { - if (backdropInterval !== 0) { - clearInterval(backdropInterval); - backdropInterval = 0; - } -} - -/** - * Start the backdrop rotation - */ -export function startBackdropInterval(): void { - clearBackdropInterval(); - - setRandomUserBackdrop(); - - backdropInterval = setInterval(function () { - setRandomUserBackdrop(); - }, 30000); -} - -/** - * Get a random backdrop to set on the waiting container - * - * @returns promise to wait for the request - */ -function setRandomUserBackdrop(): Promise { - return JellyfinApi.authAjaxUser('Items', { - dataType: 'json', - type: 'GET', - query: { - SortBy: 'Random', - IncludeItemTypes: 'Movie,Series', - ImageTypes: 'Backdrop', - Recursive: true, - Limit: 1, - // Although we're limiting to what the user has access to, - // not everyone will want to see adult backdrops rotating on their TV. - MaxOfficialRating: 'PG-13' - } - }).then(function (result: BaseItemDtoQueryResult) { - let url = ''; - if (result.Items && result.Items[0]) { - url = getBackdropUrl(result.Items[0]) || ''; - } - setWaitingBackdrop(url); - }); -} - -/** - * This function takes an item and shows details about it. - * This function is responsible for the details page that is shown while browsing jellyfin - * - * @param item item to show information about - */ -function showItem(item: BaseItemDto): void { - clearBackdropInterval(); - - const backdropUrl = getBackdropUrl(item) || ''; - let detailImageUrl = getPrimaryImageUrl(item) || ''; - - setAppStatus('details'); - setWaitingBackdrop(backdropUrl); - - setLogo(getLogoUrl(item) || ''); - setOverview(item.Overview || ''); - setGenres(item?.Genres?.join(' / ')); - setDisplayName(getDisplayName(item)); - - const detailRating = document.getElementById('detailRating'); - const miscInfo = document.getElementById('miscInfo'); - if (miscInfo) { - miscInfo.innerHTML = getMiscInfoHtml(item) || ''; - } - - if (detailRating) { - detailRating.innerHTML = getRatingHtml(item); - } - - const playedIndicator = document.getElementById('playedIndicator'); - - if (playedIndicator) { - if (item?.UserData?.Played) { - playedIndicator.style.display = 'block'; - playedIndicator.innerHTML = - ''; - } else if (item?.UserData?.UnplayedItemCount) { - playedIndicator.style.display = 'block'; - playedIndicator.innerHTML = item.UserData.UnplayedItemCount.toString(); - } else { - playedIndicator.style.display = 'none'; - } - } - - if ( - item?.UserData?.PlayedPercentage && - item?.UserData?.PlayedPercentage < 100 && - !item.IsFolder - ) { - setHasPlayedPercentage(false); - setPlayedPercentage(item.UserData.PlayedPercentage); - - detailImageUrl += - '&PercentPlayed=' + item.UserData.PlayedPercentage.toString(); - } else { - setHasPlayedPercentage(false); - setPlayedPercentage(0); - } - - setDetailImage(detailImageUrl); -} - -/** - * Show item, but resolve it from an id number first - * - * @param itemId id to look up - * @returns promise to wait for the request - */ -export function displayItem(itemId: string): Promise { - return JellyfinApi.authAjaxUser('Items/' + itemId, { - dataType: 'json', - type: 'GET' - }).then((item: BaseItemDto) => showItem(item)); -} - /** * Update the context about the item we are playing. * @@ -345,7 +207,7 @@ export function load( $scope.item = serverItem; - setAppStatus('backdrop'); + DocumentManager.setAppStatus('backdrop'); $scope.mediaType = serverItem?.MediaType; } @@ -361,17 +223,18 @@ export function load( */ export function play($scope: GlobalScope): void { if ( - $scope.status == 'backdrop' || - $scope.status == 'playing-with-controls' || - $scope.status == 'playing' || - $scope.status == 'audio' + DocumentManager.getAppStatus() == 'backdrop' || + DocumentManager.getAppStatus() == 'playing-with-controls' || + DocumentManager.getAppStatus() == 'playing' || + DocumentManager.getAppStatus() == 'audio' ) { setTimeout(function () { window.mediaManager.play(); - setAppStatus('playing-with-controls'); if ($scope.mediaType == 'Audio') { - setAppStatus('audio'); + DocumentManager.setAppStatus('audio'); + } else { + DocumentManager.setAppStatus('playing-with-controls'); } }, 20); } @@ -382,7 +245,7 @@ export function play($scope: GlobalScope): void { */ export function stop(): void { setTimeout(function () { - setAppStatus('waiting'); + DocumentManager.setAppStatus('waiting'); }, 20); } @@ -471,7 +334,7 @@ export function getLiveStream( /** * Get download speed based on the jellyfin bitratetest api. * - * FYI this API has a 10MB limit. + * The API has a 10MB limit. * * @param byteSize number of bytes to request * @returns the bitrate in bits/s @@ -502,7 +365,7 @@ export function getDownloadSpeed(byteSize: number): Promise { * Function to detect the bitrate. * It first tries 1MB and if bitrate is above 1Mbit/s it tries again with 2.4MB. * - * @returns bitrate in bits/s + * @returns {Promise} bitrate in bits/s */ export function detectBitrate(): Promise { // First try a small amount so that we don't hang up their mobile connection @@ -521,7 +384,8 @@ export function detectBitrate(): Promise { /** * Tell Jellyfin to kill off our active transcoding session * - * @param $scope + * @param {GlobalScope} $scope Global scope variable + * @returns {Promise} Promise for the http request to go through */ export function stopActiveEncodings($scope: GlobalScope): Promise { const options = { diff --git a/src/components/maincontroller.ts b/src/components/maincontroller.ts index ea0d2291..500cc7b4 100644 --- a/src/components/maincontroller.ts +++ b/src/components/maincontroller.ts @@ -17,7 +17,6 @@ import { reportPlaybackProgress, reportPlaybackStopped, play, - startBackdropInterval, getPlaybackInfo, stopActiveEncodings, detectBitrate @@ -27,6 +26,7 @@ import { JellyfinApi } from './jellyfinApi'; import { playbackManager } from './playbackManager'; import { CommandHandler } from './commandHandler'; import { getMaxBitrateSupport } from './codecSupportHelper'; +import { DocumentManager } from './documentManager'; import { BaseItemDtoQueryResult } from '~/api/generated/models/base-item-dto-query-result'; import { BaseItemDto } from '~/api/generated/models/base-item-dto'; @@ -167,7 +167,7 @@ mgr.addEventListener(cast.framework.events.EventType.ENDED, function () { if (!playbackMgr.playNextItem()) { window.playlist = []; window.currentPlaylistIndex = -1; - startBackdropInterval(); + DocumentManager.startBackdropInterval(); } }); diff --git a/src/components/playbackManager.ts b/src/components/playbackManager.ts index 43ec4760..19b7663a 100644 --- a/src/components/playbackManager.ts +++ b/src/components/playbackManager.ts @@ -1,13 +1,11 @@ import { getNextPlaybackItemInfo, getIntros, - setAppStatus, broadcastConnectionErrorMessage, getReportingParams, createStreamInfo } from '../helpers'; -import { JellyfinApi } from './jellyfinApi'; import { getPlaybackInfo, getLiveStream, @@ -15,8 +13,7 @@ import { reportPlaybackStart, stop, stopPingInterval, - reportPlaybackStopped, - startBackdropInterval + reportPlaybackStopped } from './jellyfinActions'; import { getDeviceProfile } from './deviceprofileBuilder'; @@ -29,6 +26,7 @@ import { createMediaInformation } from './maincontroller'; +import { DocumentManager } from './documentManager'; import { BaseItemDto } from '~/api/generated/models/base-item-dto'; import { MediaSourceInfo } from '~/api/generated/models/media-source-info'; @@ -126,7 +124,7 @@ export class playbackManager { async playItemInternal(item: BaseItemDto, options: any): Promise { $scope.isChangingStream = false; - setAppStatus('loading'); + DocumentManager.setAppStatus('loading'); const maxBitrate = await getMaxBitrate(); const deviceProfile = getDeviceProfile({ @@ -187,7 +185,7 @@ export class playbackManager { mediaSource: MediaSourceInfo, options: any ): void { - setAppStatus('loading'); + DocumentManager.setAppStatus('loading'); const streamInfo = createStreamInfo( item, @@ -214,36 +212,7 @@ export class playbackManager { console.log('setting src to ' + url); $scope.mediaSource = mediaSource; - let backdropUrl; - if (item.BackdropImageTags && item.BackdropImageTags.length) { - backdropUrl = JellyfinApi.createUrl( - 'Items/' + - item.Id + - '/Images/Backdrop/0?tag=' + - item.BackdropImageTags[0] - ); - } else if ( - item.ParentBackdropItemId && - item.ParentBackdropImageTags && - item.ParentBackdropImageTags.length - ) { - backdropUrl = JellyfinApi.createUrl( - 'Items/' + - item.ParentBackdropItemId + - '/Images/Backdrop/0?tag=' + - item.ParentBackdropImageTags[0] - ); - } - - if (backdropUrl) { - window.mediaElement?.style.setProperty( - '--background-image', - 'url("' + backdropUrl + '")' - ); - } else { - //Replace with a placeholder? - window.mediaElement?.style.removeProperty('--background-image'); - } + DocumentManager.setPlayerBackdrop(item); reportPlaybackStart($scope, getReportingParams($scope)); @@ -271,7 +240,7 @@ export class playbackManager { this.activePlaylist = []; this.activePlaylistIndex = -1; - startBackdropInterval(); + DocumentManager.startBackdropInterval(); promise = promise || Promise.resolve(); diff --git a/src/helpers.ts b/src/helpers.ts index 0cfabd61..e2eeb578 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,4 +1,5 @@ import { JellyfinApi } from './components/jellyfinApi'; +import { DocumentManager } from './components/documentManager'; import { BaseItemDtoQueryResult } from './api/generated/models/base-item-dto-query-result'; import { PlaybackProgressInfo } from './api/generated/models/playback-progress-info'; @@ -208,10 +209,10 @@ export function getSenderReportingData( * @param $scope global context variable */ export function resetPlaybackScope($scope: GlobalScope): void { - setAppStatus('waiting'); + DocumentManager.setAppStatus('waiting'); $scope.startPositionTicks = 0; - setWaitingBackdrop(''); + DocumentManager.setWaitingBackdrop(null); $scope.mediaType = ''; $scope.itemId = ''; @@ -232,8 +233,8 @@ export function resetPlaybackScope($scope: GlobalScope): void { $scope.playSessionId = ''; // Detail content - setLogo(''); - setDetailImage(''); + DocumentManager.setLogo(null); + DocumentManager.setDetailImage(null); } /** @@ -492,131 +493,6 @@ export function getStreamByIndex( })[0]; } -/** - * Get url for backdrop image for a given item - * - * @param item item to look up - * @returns url to backdrop image or null - */ -export function getBackdropUrl(item: BaseItemDto): string | null { - if (item.BackdropImageTags && item.BackdropImageTags.length) { - return JellyfinApi.createUrl( - `Items/${item.Id}/Images/Backdrop/0?tag=${item.BackdropImageTags[0]}` - ); - } else if ( - item.ParentBackdropItemId && - item.ParentBackdropImageTags && - item.ParentBackdropImageTags.length - ) { - return JellyfinApi.createUrl( - `Items/${item.ParentBackdropItemId}/Images/Backdrop/0?tag=${item.ParentBackdropImageTags[0]}` - ); - } - - return null; -} - -/** - * Get url for logo image for a given item - * - * @param item item to look up - * @returns url to logo image or null - */ -export function getLogoUrl(item: BaseItemDto): string | null { - if (item.ImageTags && item.ImageTags.Logo) { - return JellyfinApi.createUrl( - `Items/${item.Id}/Images/Logo/0?tag=${item.ImageTags.Logo}` - ); - } else if (item.ParentLogoItemId && item.ParentLogoImageTag) { - return JellyfinApi.createUrl( - `Items/${item.ParentLogoItemId}/Images/Logo/0?tag=${item.ParentLogoImageTag}` - ); - } - - return null; -} - -/** - * Get url for primary image for a given item - * - * @param item item to look up - * @returns url to primary image or null - */ -export function getPrimaryImageUrl(item: BaseItemDto): string | null { - if (item.AlbumPrimaryImageTag) { - return JellyfinApi.createUrl( - `Items/${item.AlbumId}/Images/Primary?tag=${item.AlbumPrimaryImageTag}` - ); - } else if (item.ImageTags?.Primary) { - return JellyfinApi.createUrl( - `Items/${item.Id}/Images/Primary?tag=${item.ImageTags?.Primary}` - ); - } - - return null; -} - -/** - * Get human readable name for an item - * - * @param item item to get displayname for - * @returns displayname - */ -export function getDisplayName(item: BaseItemDto): string | null { - const name = (item.EpisodeTitle || item.Name) ?? null; - - if (name === null) return null; - - if (item.Type == 'TvChannel') { - if (item.Number) return `${item.Number} ${name}`; - else return name; - } - - if ( - item.Type == 'Episode' && - item.IndexNumber != null && - item.ParentIndexNumber != null - ) { - let episode = `S${item.ParentIndexNumber}, E${item.IndexNumber}`; - - if (item.IndexNumberEnd) { - episode += '-' + item.IndexNumberEnd; - } - - return `${episode} - ${name}`; - } - - return name; -} - -/** - * Get HTML content used to display the rating of an item - * - * @param item to look up - * @returns html string to put in document - */ -export function getRatingHtml(item: BaseItemDto): string { - let html = ''; - - if (item.CommunityRating) { - html = - `
` + - '
' + - item.CommunityRating.toFixed(1) + - '
'; - } - - if (item.CriticRating != null) { - const verdict = item.CriticRating >= 60 ? 'fresh' : 'rotten'; - - html += - `
` + - `
${item.CriticRating}%
`; - } - - return html; -} - // defined for use in the 3 next functions const requiredItemFields = 'MediaSources,Chapters'; @@ -803,6 +679,7 @@ export function getUser(): Promise { * @param items items to resolve * @param smart If enabled it will try to find the next episode given the * current one, if the connected user has enabled that in their settings + * @returns {Promise} Promise for search result containing items to play */ export async function translateRequestedItems( userId: string, @@ -895,220 +772,6 @@ export async function translateRequestedItems( }; } -/** - * Get information about mainly an episode or series - * for the item details page - * - * @param item to look up - * @returns html code to use - */ -export function getMiscInfoHtml(item: BaseItemDto): string { - const miscInfo: string[] = []; - let date: Date; - - if (item.Type == 'Episode') { - if (item.PremiereDate) { - try { - date = parseISO8601Date(item.PremiereDate); - - miscInfo.push(date.toLocaleDateString()); - } catch (e) { - console.log('Error parsing date: ' + item.PremiereDate); - } - } - } - - if (item.StartDate) { - try { - date = parseISO8601Date(item.StartDate); - - miscInfo.push(date.toLocaleDateString()); - } catch (e) { - console.log('Error parsing date: ' + item.PremiereDate); - } - } - - if (item.ProductionYear && item.Type == 'Series') { - if (item.Status == 'Continuing') { - miscInfo.push(item.ProductionYear + '-Present'); - } else if (item.ProductionYear) { - let text: string = item.ProductionYear.toString(); - if (item.EndDate) { - try { - const endYear = parseISO8601Date( - item.EndDate - ).getFullYear(); - - if (endYear != item.ProductionYear) { - text += - '-' + parseISO8601Date(item.EndDate).getFullYear(); - } - } catch (e) { - console.log('Error parsing date: ' + item.EndDate); - } - } - - miscInfo.push(text); - } - } - - if (item.Type != 'Series' && item.Type != 'Episode') { - if (item.ProductionYear) { - miscInfo.push(item.ProductionYear.toString()); - } else if (item.PremiereDate) { - try { - miscInfo.push( - parseISO8601Date(item.PremiereDate).getFullYear().toString() - ); - } catch (e) { - console.log('Error parsing date: ' + item.PremiereDate); - } - } - } - - if (item.RunTimeTicks && item.Type != 'Series') { - if (item.Type == 'Audio') { - miscInfo.push(getDisplayRunningTime(item.RunTimeTicks)); - } else { - miscInfo.push( - Math.round(item.RunTimeTicks / 600000000 || 1).toString() + - 'min' - ); - } - } - - if ( - item.OfficialRating && - item.Type !== 'Season' && - item.Type !== 'Episode' - ) { - miscInfo.push(item.OfficialRating); - } - - if (item.Video3DFormat) { - miscInfo.push('3D'); - } - - return miscInfo.join('    '); -} - -/** - * Set the status of the app, and switch the visible view by - * modifying document.body.className - * - * @param status name of view to show - */ -export function setAppStatus(status: string): void { - $scope.status = status; - document.body.className = status; -} - -/** - * Set the displayname, part of the details page - * - * @param name name to set, if null then remove it - */ -export function setDisplayName(name: string | null = null): void { - if (name === null) name = ''; - const element: HTMLElement = ( - document.querySelector('.displayName') - ); - $scope.displayName = name; - element.innerHTML = name; -} - -/** - * Set the html of the genres container - * - * @param name string or html to insert - */ -export function setGenres(name = ''): void { - const element: HTMLElement = document.querySelector('.genres'); - $scope.genres = name; - element.innerHTML = name; -} - -/** - * Set the html of the overview container - * - * @param name string or html to insert - */ -export function setOverview(name = ''): void { - const element: HTMLElement = ( - document.querySelector('.overview') - ); - $scope.overview = name; - element.innerHTML = name; -} - -/** - * Set the progress of the progress bar in the - * item details page. (Not the same as the playback ui) - * - * @param value percentage to set - */ -export function setPlayedPercentage(value = 0): void { - const element: HTMLInputElement = ( - document.querySelector('.itemProgressBar') - ); - - $scope.playedPercentage = value; - element.value = value.toString(); -} - -/** - * Set the url of the idle screen backdrop - * - * @param src URL to image - */ -export function setWaitingBackdrop(src: string | null): void { - const element: HTMLElement = ( - document.querySelector('#waiting-container-backdrop') - ); - - element.style.backgroundImage = src ? 'url(' + src + ')' : ''; -} - -/** - * Set the visibility of the item progress bar in the - * item details page - * - * @param value show it if true - */ -export function setHasPlayedPercentage(value: boolean): void { - const element: HTMLElement = ( - document.querySelector('.detailImageProgressContainer') - ); - if (value) element.classList.remove('hide'); - else element.classList.add('hide'); -} - -/** - * Set the URL to the item logo, or null to remove it - * - * @param src url or null - */ -export function setLogo(src: string | null): void { - const element: HTMLElement = ( - document.querySelector('.detailLogo') - ); - element.style.backgroundImage = src ? 'url(' + src + ')' : ''; -} - -/** - * Set the URL to the item banner image (I think?), - * or null to remove it - * - * @param src url or null - */ -export function setDetailImage(src: string | null): void { - const element: HTMLElement = ( - document.querySelector('.detailImage') - ); - - element.style.backgroundImage = src ? 'url(' + src + ')' : ''; -} - /** * Take all properties of source and copy them over to target * @@ -1116,6 +779,7 @@ export function setDetailImage(src: string | null): void { * * @param target object that gets populated with entries * @param source object that the entries are copied from + * @returns {any} reference to target object */ export function extend(target: any, source: any): any { for (const i in source) { @@ -1136,49 +800,6 @@ export function parseISO8601Date(date: string): Date { return new Date(date); } -/** - * Get a human readable representation of the current position - * in ticks - * - * @param ticks tick position - * @returns human readable position - */ -export function getDisplayRunningTime(ticks: number): string { - const ticksPerHour = 36000000000; - const ticksPerMinute = 600000000; - const ticksPerSecond = 10000000; - - const parts: string[] = []; - - const hours: number = Math.floor(ticks / ticksPerHour); - - if (hours) { - parts.push(hours.toString()); - } - - ticks -= hours * ticksPerHour; - - const minutes: number = Math.floor(ticks / ticksPerMinute); - - ticks -= minutes * ticksPerMinute; - - if (minutes < 10 && hours) { - parts.push('0' + minutes.toString()); - } else { - parts.push(minutes.toString()); - } - - const seconds: number = Math.floor(ticks / ticksPerSecond); - - if (seconds < 10) { - parts.push('0' + seconds.toString()); - } else { - parts.push(seconds.toString()); - } - - return parts.join(':'); -} - /** * Send a message over the custom message transport * From edc7c724fecaed345b49e0947fdd44fb7d112c59 Mon Sep 17 00:00:00 2001 From: hawken Date: Sat, 2 Jan 2021 15:36:38 +0100 Subject: [PATCH 02/15] fix size of image on details page --- src/css/jellyfin.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/css/jellyfin.css b/src/css/jellyfin.css index bf0f342a..20d52473 100644 --- a/src/css/jellyfin.css +++ b/src/css/jellyfin.css @@ -85,7 +85,7 @@ body { background-repeat: no-repeat; position: absolute; top: 22%; - height: 20%; + height: 63%; left: 8%; width: 20%; } From 10a0c0bad9cf133a83b28cc7c1e6a8b3ed2860ea Mon Sep 17 00:00:00 2001 From: hawken Date: Sat, 2 Jan 2021 15:54:23 +0100 Subject: [PATCH 03/15] smoother background image rotation --- .eslintrc.js | 3 ++- src/components/documentManager.ts | 44 ++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index ee58b528..bbab0d9b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -3,7 +3,8 @@ module.exports = { plugins: ['@typescript-eslint', 'prettier', 'promise', 'import', 'jsdoc'], env: { node: true, - es6: true + es6: true, + browser: true }, extends: [ 'eslint:recommended', diff --git a/src/components/documentManager.ts b/src/components/documentManager.ts index d764ba40..7b6bad5a 100644 --- a/src/components/documentManager.ts +++ b/src/components/documentManager.ts @@ -11,6 +11,29 @@ export abstract class DocumentManager { // TODO make enum private static status = ''; + /** + * Set the background image for a html element, with image preloading. + * + * @param {HTMLElement} element HTML Element + * @param {string | null} src URL to the image or null to remove the active one + */ + private static setBackgroundImage( + element: HTMLElement, + src: string | null + ): void { + if (src) { + const preload = new Image(); + preload.src = src; + preload.addEventListener('load', () => { + requestAnimationFrame(() => { + element.style.backgroundImage = `url(${src})`; + }); + }); + } else { + element.style.backgroundImage = ''; + } + } + /** * Get url for primary image for a given item * @@ -205,13 +228,12 @@ export abstract class DocumentManager { '#waiting-container-backdrop' ); - if (element === null) { + if (element) { + this.setBackgroundImage(element, src); + } else { console.error( 'documentManager: Cannot find #waiting-container-backdrop' ); - } else { - (element).style.backgroundImage = - src != null ? `url(${src})` : ''; } } @@ -342,11 +364,10 @@ export abstract class DocumentManager { const element: HTMLElement | null = document.querySelector( '.detailLogo' ); - if (element === null) { - console.error('documentManager: Cannot find .detailLogo'); + if (element) { + this.setBackgroundImage(element, src); } else { - (element).style.backgroundImage = - src != null ? `url(${src})` : ''; + console.error('documentManager: Cannot find .detailLogo'); } } @@ -360,11 +381,10 @@ export abstract class DocumentManager { const element: HTMLElement | null = document.querySelector( '.detailImage' ); - if (element === null) { - console.error('documentManager: Cannot find .detailImage'); + if (element) { + this.setBackgroundImage(element, src); } else { - (element).style.backgroundImage = - src != null ? `url(${src})` : ''; + console.error('documentManager: Cannot find .detailImage'); } } From ae79a8f6593bef8e0a5c04305b6b83b40048df7e Mon Sep 17 00:00:00 2001 From: hawken Date: Sat, 2 Jan 2021 16:58:35 +0100 Subject: [PATCH 04/15] Easing up DOM operations on CC Audio --- src/components/castDevices.ts | 16 ++++++++---- src/components/documentManager.ts | 42 +++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/components/castDevices.ts b/src/components/castDevices.ts index 94727fc3..7da95f26 100644 --- a/src/components/castDevices.ts +++ b/src/components/castDevices.ts @@ -10,6 +10,9 @@ export enum deviceIds { CCGTV //Chromecast Google TV } +// cached device id, avoid looking it up again and again +let deviceId: number | null = null; + /** * Get device id of the active Cast device. * Tries to identify the active Cast device by testing support for different codecs. @@ -17,18 +20,21 @@ export enum deviceIds { * @returns Active Cast device Id. */ export function getActiveDeviceId(): number { + if (deviceId !== null) return deviceId; + if ( castContext.canDisplayType('video/mp4', 'hev1.1.6.L153.B0') && castContext.canDisplayType('video/webm', 'vp9') ) { - return deviceIds.ULTRA; + deviceId = deviceIds.ULTRA; } else if (castContext.canDisplayType('video/webm', 'vp9')) { - return deviceIds.NESTHUBANDMAX; + deviceId = deviceIds.NESTHUBANDMAX; } else if (castContext.canDisplayType('video/mp4', 'avc1.64002A')) { - return deviceIds.GEN3; + deviceId = deviceIds.GEN3; } else if (castContext.canDisplayType('video/mp4', 'avc1.640029')) { - return deviceIds.GEN1AND2; + deviceId = deviceIds.GEN1AND2; } else { - return deviceIds.AUDIO; + deviceId = deviceIds.AUDIO; } + return deviceId; } diff --git a/src/components/documentManager.ts b/src/components/documentManager.ts index 7b6bad5a..40c4a230 100644 --- a/src/components/documentManager.ts +++ b/src/components/documentManager.ts @@ -1,5 +1,6 @@ import { parseISO8601Date } from '../helpers'; import { JellyfinApi } from './jellyfinApi'; +import { deviceIds, getActiveDeviceId } from './castDevices'; import { BaseItemDto } from '~/api/generated/models/base-item-dto'; export abstract class DocumentManager { @@ -11,6 +12,12 @@ export abstract class DocumentManager { // TODO make enum private static status = ''; + public static initialize(): void { + // Hide the body on cc audio to save resources + if (getActiveDeviceId() === deviceIds.AUDIO) + document.body.style.display = 'none'; + } + /** * Set the background image for a html element, with image preloading. * @@ -78,6 +85,9 @@ export abstract class DocumentManager { * @param {BaseItemDto} item to show information about */ public static showItem(item: BaseItemDto): void { + // no showItem for cc audio + if (getActiveDeviceId() === deviceIds.AUDIO) return; + // stop cycling backdrops this.clearBackdropInterval(); @@ -140,11 +150,19 @@ export abstract class DocumentManager { * @param {string} itemId id of item to look up * @returns {Promise} promise that resolves when the item is shown */ - public static showItemId(itemId: string): Promise { - return JellyfinApi.authAjaxUser('Items/' + itemId, { - dataType: 'json', - type: 'GET' - }).then((item: BaseItemDto) => DocumentManager.showItem(item)); + public static async showItemId(itemId: string): Promise { + // no showItemId for cc audio + if (getActiveDeviceId() === deviceIds.AUDIO) return; + + const item: BaseItemDto = await JellyfinApi.authAjaxUser( + 'Items/' + itemId, + { + dataType: 'json', + type: 'GET' + } + ); + + DocumentManager.showItem(item); } /** @@ -278,14 +296,17 @@ export abstract class DocumentManager { * * @returns {Promise} promise for the first backdrop to be set */ - public static startBackdropInterval(): Promise { + public static async startBackdropInterval(): Promise { + // no backdrop rotation for cc audio + if (getActiveDeviceId() === deviceIds.AUDIO) return; + // avoid running it multiple times this.clearBackdropInterval(); // skip out if it's disabled if (!this.backdropPeriodMs) { this.setWaitingBackdrop(null); - return Promise.resolve(); + return; } this.backdropTimer = ( @@ -295,7 +316,7 @@ export abstract class DocumentManager { ) ); - return this.setRandomUserBackdrop(); + await this.setRandomUserBackdrop(); } /** @@ -328,6 +349,9 @@ export abstract class DocumentManager { * @param {BaseItemDto} item to get backdrop from */ public static setPlayerBackdrop(item: BaseItemDto): void { + // no backdrop rotation for cc audio + if (getActiveDeviceId() === deviceIds.AUDIO) return; + let backdropUrl: string | null = null; if (item.BackdropImageTags && item.BackdropImageTags.length) { @@ -629,3 +653,5 @@ export abstract class DocumentManager { } } } + +DocumentManager.initialize(); From a540b2fef3f5fc957115ffb72817e3111bc23906 Mon Sep 17 00:00:00 2001 From: hawken Date: Sat, 2 Jan 2021 17:32:35 +0100 Subject: [PATCH 05/15] forgot jsdoc --- src/components/documentManager.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/documentManager.ts b/src/components/documentManager.ts index 40c4a230..ed32ad97 100644 --- a/src/components/documentManager.ts +++ b/src/components/documentManager.ts @@ -12,8 +12,10 @@ export abstract class DocumentManager { // TODO make enum private static status = ''; + /** + * Hide the document body on chromecast audio to save resources + */ public static initialize(): void { - // Hide the body on cc audio to save resources if (getActiveDeviceId() === deviceIds.AUDIO) document.body.style.display = 'none'; } From a0105a2807e56f85e0e836e860fd1d871a414c98 Mon Sep 17 00:00:00 2001 From: hawken Date: Sun, 3 Jan 2021 14:01:39 +0100 Subject: [PATCH 06/15] Style update * Removed the h2 text * Moved logo and h1 text to bottom left, logo inline * Add text bottom right to show the backdrop source * Slightly brighter backdrop --- src/components/documentManager.ts | 36 +++++++++++++++++++++++-------- src/css/jellyfin.css | 33 ++++++++++++++++++---------- src/index.html | 4 ++-- 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/src/components/documentManager.ts b/src/components/documentManager.ts index ed32ad97..1b9ca359 100644 --- a/src/components/documentManager.ts +++ b/src/components/documentManager.ts @@ -25,21 +25,29 @@ export abstract class DocumentManager { * * @param {HTMLElement} element HTML Element * @param {string | null} src URL to the image or null to remove the active one + * @returns {Promise} wait for the background to be switched */ private static setBackgroundImage( element: HTMLElement, src: string | null - ): void { + ): Promise { if (src) { - const preload = new Image(); - preload.src = src; - preload.addEventListener('load', () => { - requestAnimationFrame(() => { - element.style.backgroundImage = `url(${src})`; + return new Promise((resolve, reject) => { + const preload = new Image(); + preload.src = src; + preload.addEventListener('load', () => { + requestAnimationFrame(() => { + element.style.backgroundImage = `url(${src})`; + resolve(); + }); + }); + preload.addEventListener('error', () => { + reject(); }); }); } else { element.style.backgroundImage = ''; + return Promise.resolve(); } } @@ -222,7 +230,9 @@ export abstract class DocumentManager { * * @param {BaseItemDto | null} item Item to use for waiting backdrop, null to remove it. */ - public static setWaitingBackdrop(item: BaseItemDto | null): void { + public static async setWaitingBackdrop( + item: BaseItemDto | null + ): Promise { // no backdrop as a fallback let src: string | null = null; @@ -244,17 +254,25 @@ export abstract class DocumentManager { } } - const element: HTMLElement | null = document.querySelector( + let element: HTMLElement | null = document.querySelector( '#waiting-container-backdrop' ); if (element) { - this.setBackgroundImage(element, src); + await this.setBackgroundImage(element, src); } else { console.error( 'documentManager: Cannot find #waiting-container-backdrop' ); } + + element = document.querySelector('.waitingDescription'); + + if (!element) { + console.error('documentManager: Cannot find .detailImage'); + } else { + element.innerHTML = item?.Name ?? ''; + } } /** diff --git a/src/css/jellyfin.css b/src/css/jellyfin.css index 20d52473..22a58756 100644 --- a/src/css/jellyfin.css +++ b/src/css/jellyfin.css @@ -47,7 +47,9 @@ body { background-position: center; background-size: cover; background-repeat: no-repeat; - background-color: rgba(15, 15, 15, 0.82); + + /* Layer on top of the backdrop image: */ + background-color: rgba(15, 15, 15, 0.6); position: absolute; top: 0; left: 0; @@ -244,15 +246,24 @@ body { text-overflow: ellipsis; } +/* Container for "ready to cast" and the logo */ .waitingContent { position: fixed; - top: 50%; - right: 0; - bottom: auto; + bottom: 0; left: 0; - height: 415px; - margin-top: -207px; text-align: center; + font-size: 45px; + margin-bottom: 3%; + margin-left: 5%; +} + +/* Container for backdrop description */ +.waitingDescription { + position: fixed; + bottom: 0; + right: 0; + margin-right: 5%; + margin-bottom: 3%; } #waiting-container h1, @@ -261,7 +272,7 @@ body { } #waiting-container h1 { - font-size: 60px; + font-size: 45px; font-weight: 300; } @@ -273,11 +284,11 @@ body { } /* stylelint-enable no-descending-specificity */ +/* jellyfin logo in the waiting container */ #waiting-container .logo { - height: 200px; - margin: 0 auto; - width: auto; - display: block; + height: 55px; + display: inline-block; + vertical-align: text-bottom; } .waiting > #waiting-container-backdrop, diff --git a/src/index.html b/src/index.html index 6dd29326..f7e3b20a 100644 --- a/src/index.html +++ b/src/index.html @@ -18,9 +18,9 @@
-

Ready to cast

-

Select your media in Jellyfin and play it here

+ Ready to cast
+
From f60db701cf8bc6207b72fad2118e0b9aebad1abe Mon Sep 17 00:00:00 2001 From: hawken Date: Mon, 11 Jan 2021 13:28:45 +0100 Subject: [PATCH 07/15] more helper functions --- src/components/documentManager.ts | 186 +++++++++++++++--------------- src/css/jellyfin.css | 2 +- 2 files changed, 97 insertions(+), 91 deletions(-) diff --git a/src/components/documentManager.ts b/src/components/documentManager.ts index 1b9ca359..cff0c970 100644 --- a/src/components/documentManager.ts +++ b/src/components/documentManager.ts @@ -111,24 +111,15 @@ export abstract class DocumentManager { this.setMiscInfo(item); - const detailRating = document.getElementById('detailRating'); - if (detailRating) { - detailRating.innerHTML = this.getRatingHtml(item); - } - - const playedIndicator = document.getElementById('playedIndicator'); + const detailRating = this.getElementById('detailRating'); + detailRating.innerHTML = this.getRatingHtml(item); - if (playedIndicator) { - if (item?.UserData?.Played) { - playedIndicator.style.display = 'block'; - playedIndicator.innerHTML = - ''; - } else if (item?.UserData?.UnplayedItemCount) { - playedIndicator.style.display = 'block'; - playedIndicator.innerHTML = item.UserData.UnplayedItemCount.toString(); - } else { - playedIndicator.style.display = 'none'; - } + if (item?.UserData?.Played) { + this.setPlayedIndicator(true); + } else if (item?.UserData?.UnplayedItemCount) { + this.setPlayedIndicator(item?.UserData?.UnplayedItemCount); + } else { + this.setPlayedIndicator(false); } let detailImageUrl = this.getPrimaryImageUrl(item); @@ -153,6 +144,29 @@ export abstract class DocumentManager { this.setDetailImage(detailImageUrl); } + /** + * Set value of played indicator + * + * @param {boolean | number} value True = played, false = not visible, number = number of unplayed items + */ + private static setPlayedIndicator(value: boolean | number): void { + const playedIndicator = this.getElementById('playedIndicator'); + + if (value === true) { + // All items played + this.setVisibility(playedIndicator, true); + playedIndicator.innerHTML = + ''; + } else if (value === false) { + // No indicator + this.setVisibility(playedIndicator, false); + } else { + // number + this.setVisibility(playedIndicator, true); + playedIndicator.innerHTML = value.toString(); + } + } + /** * Show item, but from just the id number, not an actual item. * Looks up the item and then calls showItem @@ -254,25 +268,14 @@ export abstract class DocumentManager { } } - let element: HTMLElement | null = document.querySelector( + let element: HTMLElement = this.querySelector( '#waiting-container-backdrop' ); - if (element) { - await this.setBackgroundImage(element, src); - } else { - console.error( - 'documentManager: Cannot find #waiting-container-backdrop' - ); - } - - element = document.querySelector('.waitingDescription'); + await this.setBackgroundImage(element, src); - if (!element) { - console.error('documentManager: Cannot find .detailImage'); - } else { - element.innerHTML = item?.Name ?? ''; - } + element = this.querySelector('.waitingDescription'); + element.innerHTML = item?.Name ?? ''; } /** @@ -405,14 +408,8 @@ export abstract class DocumentManager { * @param {string | null} src Source url or null */ public static setLogo(src: string | null): void { - const element: HTMLElement | null = document.querySelector( - '.detailLogo' - ); - if (element) { - this.setBackgroundImage(element, src); - } else { - console.error('documentManager: Cannot find .detailLogo'); - } + const element: HTMLElement = this.querySelector('.detailLogo'); + this.setBackgroundImage(element, src); } /** @@ -422,14 +419,8 @@ export abstract class DocumentManager { * @param {string | null} src Source url or null */ public static setDetailImage(src: string | null): void { - const element: HTMLElement | null = document.querySelector( - '.detailImage' - ); - if (element) { - this.setBackgroundImage(element, src); - } else { - console.error('documentManager: Cannot find .detailImage'); - } + const element: HTMLElement = this.querySelector('.detailImage'); + this.setBackgroundImage(element, src); } /** @@ -461,14 +452,8 @@ export abstract class DocumentManager { displayName = `${episode} - ${name}`; } - const element: HTMLElement | null = document.querySelector( - '.displayName' - ); - if (element === null) { - console.error('documentManager: Cannot find .displayName'); - } else { - (element).innerHTML = displayName || ''; - } + const element = this.querySelector('.displayName'); + element.innerHTML = displayName || ''; } /** @@ -477,12 +462,8 @@ export abstract class DocumentManager { * @param {string | null} name String/html for genres box, null to empty */ private static setGenres(name: string | null): void { - const element: HTMLElement | null = document.querySelector('.genres'); - if (element === null) { - console.error('documentManager: Cannot find .genres'); - } else { - (element).innerHTML = name || ''; - } + const element = this.querySelector('.genres'); + element.innerHTML = name || ''; } /** @@ -491,12 +472,8 @@ export abstract class DocumentManager { * @param {string | null} name string or html to insert */ private static setOverview(name: string | null): void { - const element: HTMLElement | null = document.querySelector('.overview'); - if (element === null) { - console.error('documentManager: Cannot find .overview'); - } else { - (element).innerHTML = name || ''; - } + const element = this.querySelector('.overview'); + element.innerHTML = name || ''; } /** @@ -506,14 +483,10 @@ export abstract class DocumentManager { * @param {number} value Percentage to set */ private static setPlayedPercentage(value = 0): void { - const element: HTMLInputElement | null = ( - document.querySelector('.itemProgressBar') + const element = ( + this.querySelector('.itemProgressBar') ); - if (element === null) { - console.error('documentManager: Cannot find .itemProgressBar'); - } else { - (element).value = value.toString(); - } + element.value = value.toString(); } /** @@ -523,17 +496,9 @@ export abstract class DocumentManager { * @param {boolean} value If true, show progress on details page */ private static setHasPlayedPercentage(value: boolean): void { - const element: HTMLElement | null = document.querySelector( - '.detailImageProgressContainer' - ); - if (element === null) { - console.error( - 'documentManager: Cannot find .detailImageProgressContainer' - ); - } else { - if (value) (element).classList.remove('hide'); - else (element).classList.add('hide'); - } + const element = this.querySelector('.detailImageProgressContainer'); + if (value) (element).classList.remove('d-none'); + else (element).classList.add('d-none'); } /** @@ -665,13 +630,54 @@ export abstract class DocumentManager { info.push('3D'); } - const element = document.getElementById('miscInfo'); - if (element === null) { - console.error('documentManager: Cannot find element miscInfo'); + const element = this.getElementById('miscInfo'); + element.innerHTML = info.join('    '); + } + + // Generic / Helper functions + /** + * Set the visibility of an element + * + * @param {HTMLElement} element Element to set visibility on + * @param {boolean} visible True if the element should be visible. + */ + private static setVisibility(element: HTMLElement, visible: boolean): void { + if (visible) { + element.classList.remove('d-none'); } else { - element.innerHTML = info.join('    '); + element.classList.add('d-none'); } } + + /** + * Get a HTMLElement from id or throw an error + * + * @param {string} id ID to look up + * @returns {HTMLElement} HTML Element + */ + private static getElementById(id: string): HTMLElement { + const element = document.getElementById(id); + if (!element) { + throw new ReferenceError(`Cannot find element ${id} by id`); + } + + return element; + } + + /** + * Get a HTMLElement by class + * + * @param {string} cls Class to look up + * @returns {HTMLElement} HTML Element + */ + private static querySelector(cls: string): HTMLElement { + const element: HTMLElement | null = document.querySelector(cls); + if (!element) { + throw new ReferenceError(`Cannot find element ${cls} by class`); + } + + return element; + } } DocumentManager.initialize(); diff --git a/src/css/jellyfin.css b/src/css/jellyfin.css index 22a58756..9ab5fc41 100644 --- a/src/css/jellyfin.css +++ b/src/css/jellyfin.css @@ -27,7 +27,7 @@ body { display: none; } -.hide { +.d-none { display: none !important; } From 7d64842e632e9b54b51be5d4bc1a390ffa783e7d Mon Sep 17 00:00:00 2001 From: hawken Date: Wed, 13 Jan 2021 13:46:11 +0100 Subject: [PATCH 08/15] createImageUrl function --- src/components/documentManager.ts | 76 +++++++++++++++++++++---------- src/components/jellyfinApi.ts | 20 ++++++++ 2 files changed, 72 insertions(+), 24 deletions(-) diff --git a/src/components/documentManager.ts b/src/components/documentManager.ts index cff0c970..0e087ef4 100644 --- a/src/components/documentManager.ts +++ b/src/components/documentManager.ts @@ -58,15 +58,21 @@ export abstract class DocumentManager { * @returns {string | null} url to primary image */ private static getPrimaryImageUrl(item: BaseItemDto): string | null { - if (item.AlbumPrimaryImageTag) - return JellyfinApi.createUrl( - `Items/${item.AlbumId}/Images/Primary?tag=${item.AlbumPrimaryImageTag}` + if (item.AlbumPrimaryImageTag && item.AlbumId) { + return JellyfinApi.createImageUrl( + item.AlbumId, + 'Primary', + item.AlbumPrimaryImageTag ); - else if (item.ImageTags?.Primary) - return JellyfinApi.createUrl( - `Items/${item.Id}/Images/Primary?tag=${item.ImageTags.Primary}` + } else if (item.ImageTags?.Primary && item.Id) { + return JellyfinApi.createImageUrl( + item.Id, + 'Primary', + item.ImageTags.Primary ); - else return null; + } else { + return null; + } } /** @@ -76,15 +82,21 @@ export abstract class DocumentManager { * @returns {string | null} url to logo image */ private static getLogoUrl(item: BaseItemDto): string | null { - if (item.ImageTags?.Logo) - return JellyfinApi.createUrl( - `Items/${item.Id}/Images/Logo/0?tag=${item.ImageTags.Logo}` + if (item.ImageTags?.Logo && item.Id) { + return JellyfinApi.createImageUrl( + item.Id, + 'Logo', + item.ImageTags.Logo ); - else if (item.ParentLogoItemId && item.ParentLogoImageTag) - return JellyfinApi.createUrl( - `Items/${item.ParentLogoItemId}/Images/Logo/0?tag=${item.ParentLogoImageTag}` + } else if (item.ParentLogoItemId && item.ParentLogoImageTag) { + return JellyfinApi.createImageUrl( + item.ParentLogoItemId, + 'Logo', + item.ParentLogoImageTag ); - else return null; + } else { + return null; + } } /** @@ -251,10 +263,16 @@ export abstract class DocumentManager { let src: string | null = null; if (item != null) { - if (item.BackdropImageTags && item.BackdropImageTags.length) { + if ( + item.BackdropImageTags && + item.BackdropImageTags.length && + item.Id + ) { // get first backdrop of image if applicable - src = JellyfinApi.createUrl( - `Items/${item.Id}/Images/Backdrop/0?tag=${item.BackdropImageTags[0]}` + src = JellyfinApi.createImageUrl( + item.Id, + 'Backdrop', + item.BackdropImageTags[0] ); } else if ( item.ParentBackdropItemId && @@ -262,8 +280,10 @@ export abstract class DocumentManager { item.ParentBackdropImageTags.length ) { // otherwise get first backdrop from parent - src = JellyfinApi.createUrl( - `Items/${item.ParentBackdropItemId}/Images/Backdrop/0?tag=${item.ParentBackdropImageTags[0]}` + src = JellyfinApi.createImageUrl( + item.ParentBackdropItemId, + 'Backdrop', + item.ParentBackdropImageTags[0] ); } } @@ -377,17 +397,25 @@ export abstract class DocumentManager { let backdropUrl: string | null = null; - if (item.BackdropImageTags && item.BackdropImageTags.length) { - backdropUrl = JellyfinApi.createUrl( - `Items/${item.Id}/Images/Backdrop/0?tag=${item.BackdropImageTags[0]}` + if ( + item.BackdropImageTags && + item.BackdropImageTags.length && + item.Id + ) { + backdropUrl = JellyfinApi.createImageUrl( + item.Id, + 'Backdrop', + item.BackdropImageTags[0] ); } else if ( item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length ) { - backdropUrl = JellyfinApi.createUrl( - `Items/${item.ParentBackdropItemId}/Images/Backdrop/0?tag=${item.ParentBackdropImageTags[0]}` + backdropUrl = JellyfinApi.createImageUrl( + item.ParentBackdropItemId, + 'Backdrop', + item.ParentBackdropImageTags[0] ); } diff --git a/src/components/jellyfinApi.ts b/src/components/jellyfinApi.ts index 3d7d07c2..c463d230 100644 --- a/src/components/jellyfinApi.ts +++ b/src/components/jellyfinApi.ts @@ -70,6 +70,26 @@ export abstract class JellyfinApi { } } + /** + * Create url to image + * + * @param {string} itemId Item id + * @param {string} imgType Image type: Primary, Logo, Backdrop + * @param {string} imgTag Image tag + * @param {number} imgIdx Image index, default 0 + * @returns {string} URL + */ + public static createImageUrl( + itemId: string, + imgType: string, + imgTag: string, + imgIdx = 0 + ): string { + return this.createUrl( + `Items/${itemId}/Images/${imgType}/${imgIdx.toString()}?tag=${imgTag}` + ); + } + // Authenticated ajax public static authAjax(path: string, args: any): Promise { if ( From 75f532ac6a62012527032b69a9ecb5873a344d93 Mon Sep 17 00:00:00 2001 From: hawken Date: Wed, 13 Jan 2021 13:46:29 +0100 Subject: [PATCH 09/15] some more jsdoc work --- src/components/fetchhelper.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/fetchhelper.ts b/src/components/fetchhelper.ts index f1846a24..90363090 100644 --- a/src/components/fetchhelper.ts +++ b/src/components/fetchhelper.ts @@ -1,8 +1,8 @@ /** * Function to send a request, with or without the timeout option * - * @param request custom request object, mostly modeled after RequestInit. - * @returns response promise + * @param {any} request Custom request object, mostly modeled after RequestInit. + * @returns {Promise} response promise */ function getFetchPromise(request: any): Promise { const headers = request.headers || {}; @@ -39,10 +39,10 @@ function getFetchPromise(request: any): Promise { /** * Timeout wrapper for fetch() * - * @param url url to get - * @param options RequestInit with additional options - * @param timeoutMs request timeout in ms - * @returns response promise + * @param {string} url url to get + * @param {RequestInit} options RequestInit with additional options + * @param {number} timeoutMs request timeout in ms + * @returns {Promise} response promise */ function fetchWithTimeout( url: string, @@ -76,8 +76,8 @@ function fetchWithTimeout( /** * Urlencode a dictionary of strings for use in POST form or GET requests * - * @param params Dictionary to encode - * @returns string with encoded values + * @param {Record} params Dictionary to encode + * @returns {string} string with encoded values */ function paramsToString(params: Record): string { const values = []; @@ -96,8 +96,8 @@ function paramsToString(params: Record): string { /** * Make an ajax request * - * @param request RequestInit-like structure but with url/type/timeout parameters as well - * @returns response promise, may be automatically unpacked based on request datatype + * @param {any} request RequestInit-like structure but with url/type/timeout parameters as well + * @returns {Promise} response promise, may be automatically unpacked based on request datatype */ export function ajax(request: any): Promise { if (!request) throw new Error('Request cannot be null'); From fc83c8ca48a520bcb6f79f0c8ddfdcd93887ee1e Mon Sep 17 00:00:00 2001 From: hawken Date: Wed, 13 Jan 2021 23:59:56 +0100 Subject: [PATCH 10/15] move html stuff to the html file --- src/components/documentManager.ts | 66 ++++++++++++++++++++----------- src/css/jellyfin.css | 4 +- src/index.html | 18 +++++++-- 3 files changed, 59 insertions(+), 29 deletions(-) diff --git a/src/components/documentManager.ts b/src/components/documentManager.ts index 0e087ef4..7bd144d1 100644 --- a/src/components/documentManager.ts +++ b/src/components/documentManager.ts @@ -123,8 +123,7 @@ export abstract class DocumentManager { this.setMiscInfo(item); - const detailRating = this.getElementById('detailRating'); - detailRating.innerHTML = this.getRatingHtml(item); + this.setRating(item); if (item?.UserData?.Played) { this.setPlayedIndicator(true); @@ -162,20 +161,24 @@ export abstract class DocumentManager { * @param {boolean | number} value True = played, false = not visible, number = number of unplayed items */ private static setPlayedIndicator(value: boolean | number): void { - const playedIndicator = this.getElementById('playedIndicator'); + const playedIndicatorOk = this.getElementById('played-indicator-ok'); + const playedIndicatorValue = this.getElementById( + 'played-indicator-value' + ); if (value === true) { // All items played - this.setVisibility(playedIndicator, true); - playedIndicator.innerHTML = - ''; + this.setVisibility(playedIndicatorValue, false); + this.setVisibility(playedIndicatorOk, true); } else if (value === false) { // No indicator - this.setVisibility(playedIndicator, false); + this.setVisibility(playedIndicatorValue, false); + this.setVisibility(playedIndicatorOk, false); } else { // number - this.setVisibility(playedIndicator, true); - playedIndicator.innerHTML = value.toString(); + playedIndicatorValue.innerHTML = value.toString(); + this.setVisibility(playedIndicatorValue, true); + this.setVisibility(playedIndicatorOk, false); } } @@ -202,29 +205,44 @@ export abstract class DocumentManager { } /** - * Get HTML content used to display the rating of an item + * Update item rating elements * * @param {BaseItemDto} item to look up - * @returns {string} html to put in document */ - private static getRatingHtml(item: BaseItemDto): string { - let html = ''; + private static setRating(item: BaseItemDto): void { + const starRating = this.getElementById('star-rating'); + const starRatingValue = this.getElementById('star-rating-value'); + if (item.CommunityRating != null) { - html += - `
` + - '
' + - item.CommunityRating.toFixed(1) + - '
'; + starRating.setAttribute('title', item.CommunityRating.toFixed(1)); + starRatingValue.innerHTML = item.CommunityRating.toFixed(1); + this.setVisibility(starRating, true); + this.setVisibility(starRatingValue, true); + } else { + this.setVisibility(starRating, false); + this.setVisibility(starRatingValue, false); } + const criticRating = this.getElementById('critic-rating'); + const criticRatingValue = this.getElementById('critic-rating-value'); + if (item.CriticRating != null) { const verdict = item.CriticRating >= 60 ? 'fresh' : 'rotten'; - html += - `
` + - `
${item.CriticRating}%
`; - } - return html; + criticRating.classList.add(verdict); + criticRating.classList.remove( + verdict == 'fresh' ? 'rotten' : 'fresh' + ); + criticRating.setAttribute('title', verdict); + + criticRatingValue.innerHTML = item.CriticRating.toString(); + + this.setVisibility(criticRating, true); + this.setVisibility(criticRatingValue, true); + } else { + this.setVisibility(criticRating, false); + this.setVisibility(criticRatingValue, false); + } } /** @@ -294,7 +312,7 @@ export abstract class DocumentManager { await this.setBackgroundImage(element, src); - element = this.querySelector('.waitingDescription'); + element = this.getElementById('waiting-description'); element.innerHTML = item?.Name ?? ''; } diff --git a/src/css/jellyfin.css b/src/css/jellyfin.css index 9ab5fc41..20303632 100644 --- a/src/css/jellyfin.css +++ b/src/css/jellyfin.css @@ -92,7 +92,7 @@ body { width: 20%; } -#playedIndicator { +.playedIndicator { display: block; position: absolute; top: 5px; @@ -159,7 +159,7 @@ body { margin: 10px 0; } -#detailRating { +.detailRating { margin: -4px 0 0; } diff --git a/src/index.html b/src/index.html index f7e3b20a..21f2d5c7 100644 --- a/src/index.html +++ b/src/index.html @@ -20,19 +20,31 @@ Ready to cast
-
+
-
+
+

-
+
+
+
+
+
+

From af1c780c5f283818dc5b4666064e6358e1b80f30 Mon Sep 17 00:00:00 2001 From: hawken Date: Thu, 14 Jan 2021 00:03:04 +0100 Subject: [PATCH 11/15] run prettier on html --- .prettierignore | 1 - src/index.html | 104 +++++++++++++++++++++++++++--------------------- 2 files changed, 58 insertions(+), 47 deletions(-) diff --git a/.prettierignore b/.prettierignore index bb7aae0f..05c049de 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,5 +2,4 @@ dist/ node_modules/ src/api/generated/ -*.html LICENSE.md diff --git a/src/index.html b/src/index.html index 21f2d5c7..339c68a2 100644 --- a/src/index.html +++ b/src/index.html @@ -1,54 +1,66 @@ - - - Jellyfin - - - - - -
-
-
- - Ready to cast + + + Jellyfin + + + + + +
+
+
+ + Ready to cast +
+
-
-
- -
-
-
-
+ +
+
+
+
-
-
+
+ +
+
-
-

-
-
-
-
-
+
+

+ +

+
+
+
+
+
+
+

+

-

-

-
- - + + From f9d76d39a224475de100f878952b58921872d3e9 Mon Sep 17 00:00:00 2001 From: hawken Date: Thu, 14 Jan 2021 00:41:10 +0100 Subject: [PATCH 12/15] do text after image preload on the details page --- src/components/documentManager.ts | 205 +++++++++++++++++++----------- src/helpers.ts | 2 +- 2 files changed, 129 insertions(+), 78 deletions(-) diff --git a/src/components/documentManager.ts b/src/components/documentManager.ts index 7bd144d1..bb8b95f7 100644 --- a/src/components/documentManager.ts +++ b/src/components/documentManager.ts @@ -21,33 +21,44 @@ export abstract class DocumentManager { } /** - * Set the background image for a html element, with image preloading. + * Set the background image for a html element, without preload. + * You should do the preloading first with preloadImage. * * @param {HTMLElement} element HTML Element * @param {string | null} src URL to the image or null to remove the active one - * @returns {Promise} wait for the background to be switched */ private static setBackgroundImage( element: HTMLElement, src: string | null - ): Promise { + ): void { + if (src) { + element.style.backgroundImage = `url(${src})`; + } else { + element.style.backgroundImage = ''; + } + } + + /** + * Preload an image + * + * @param {string | null} src URL to the image or null + * @returns {Promise} wait for the preload and return the url to use. Might be nulled after loading error. + */ + private static preloadImage(src: string | null): Promise { if (src) { return new Promise((resolve, reject) => { const preload = new Image(); preload.src = src; preload.addEventListener('load', () => { - requestAnimationFrame(() => { - element.style.backgroundImage = `url(${src})`; - resolve(); - }); + resolve(src); }); preload.addEventListener('error', () => { + // might also resolve and return null here, to have the caller take away the background. reject(); }); }); } else { - element.style.backgroundImage = ''; - return Promise.resolve(); + return Promise.resolve(null); } } @@ -55,48 +66,62 @@ export abstract class DocumentManager { * Get url for primary image for a given item * * @param {BaseItemDto} item to look up - * @returns {string | null} url to primary image + * @returns {Promise} url to image after preload */ - private static getPrimaryImageUrl(item: BaseItemDto): string | null { + private static getPrimaryImageUrl( + item: BaseItemDto + ): Promise { + let src: string | null = null; + if (item.AlbumPrimaryImageTag && item.AlbumId) { - return JellyfinApi.createImageUrl( + src = JellyfinApi.createImageUrl( item.AlbumId, 'Primary', item.AlbumPrimaryImageTag ); } else if (item.ImageTags?.Primary && item.Id) { - return JellyfinApi.createImageUrl( + src = JellyfinApi.createImageUrl( item.Id, 'Primary', item.ImageTags.Primary ); - } else { - return null; } + + if ( + item?.UserData?.PlayedPercentage && + item?.UserData?.PlayedPercentage < 100 && + !item.IsFolder && + src != null + ) { + src += `&PercentPlayed=${item.UserData.PlayedPercentage}`; + } + + return this.preloadImage(src); } /** * Get url for logo image for a given item * * @param {BaseItemDto} item to look up - * @returns {string | null} url to logo image + * @returns {Promise} url to logo image after preload */ - private static getLogoUrl(item: BaseItemDto): string | null { + private static getLogoUrl(item: BaseItemDto): Promise { + let src: string | null = null; if (item.ImageTags?.Logo && item.Id) { - return JellyfinApi.createImageUrl( + src = JellyfinApi.createImageUrl( item.Id, 'Logo', item.ImageTags.Logo ); } else if (item.ParentLogoItemId && item.ParentLogoImageTag) { - return JellyfinApi.createImageUrl( + src = JellyfinApi.createImageUrl( item.ParentLogoItemId, 'Logo', item.ParentLogoImageTag ); - } else { - return null; } + + return this.preloadImage(src); } /** @@ -105,54 +130,58 @@ export abstract class DocumentManager { * and the connected client is browsing the library. * * @param {BaseItemDto} item to show information about + * @returns {Promise} for the page to load */ - public static showItem(item: BaseItemDto): void { + public static showItem(item: BaseItemDto): Promise { // no showItem for cc audio - if (getActiveDeviceId() === deviceIds.AUDIO) return; + if (getActiveDeviceId() === deviceIds.AUDIO) { + return Promise.resolve(); + } // stop cycling backdrops this.clearBackdropInterval(); - this.setAppStatus('details'); - this.setWaitingBackdrop(item); - - this.setLogo(this.getLogoUrl(item)); - this.setOverview(item.Overview ?? null); - this.setGenres(item?.Genres?.join(' / ') ?? null); - this.setDisplayName(item); - - this.setMiscInfo(item); - - this.setRating(item); - - if (item?.UserData?.Played) { - this.setPlayedIndicator(true); - } else if (item?.UserData?.UnplayedItemCount) { - this.setPlayedIndicator(item?.UserData?.UnplayedItemCount); - } else { - this.setPlayedIndicator(false); - } - - let detailImageUrl = this.getPrimaryImageUrl(item); - - if ( - item?.UserData?.PlayedPercentage && - item?.UserData?.PlayedPercentage < 100 && - !item.IsFolder - ) { - this.setHasPlayedPercentage(false); - this.setPlayedPercentage(item.UserData.PlayedPercentage); + return Promise.all([ + this.getWaitingBackdropUrl(item), + this.getPrimaryImageUrl(item), + this.getLogoUrl(item) + ]).then((urls) => { + requestAnimationFrame(() => { + this.setWaitingBackdrop(urls[0], item); + this.setDetailImage(urls[1]); + this.setLogo(urls[2]); + + this.setOverview(item.Overview ?? null); + this.setGenres(item?.Genres?.join(' / ') ?? null); + this.setDisplayName(item); + this.setMiscInfo(item); + + this.setRating(item); + + if (item?.UserData?.Played) { + this.setPlayedIndicator(true); + } else if (item?.UserData?.UnplayedItemCount) { + this.setPlayedIndicator(item?.UserData?.UnplayedItemCount); + } else { + this.setPlayedIndicator(false); + } - if (detailImageUrl != null) - detailImageUrl += - '&PercentPlayed=' + - item.UserData.PlayedPercentage.toString(); - } else { - this.setHasPlayedPercentage(false); - this.setPlayedPercentage(0); - } + if ( + item?.UserData?.PlayedPercentage && + item?.UserData?.PlayedPercentage < 100 && + !item.IsFolder + ) { + this.setHasPlayedPercentage(false); + this.setPlayedPercentage(item.UserData.PlayedPercentage); + } else { + this.setHasPlayedPercentage(false); + this.setPlayedPercentage(0); + } - this.setDetailImage(detailImageUrl); + // Switch visible view! + this.setAppStatus('details'); + }); + }); } /** @@ -265,18 +294,17 @@ export abstract class DocumentManager { return this.status; } + // BACKDROP LOGIC + /** - * BACKDROP LOGIC - * - * Backdrops are set on the waiting container. - * They are switched around every 30 seconds by default - * (governed by startBackdropInterval) + * Get url to the backdrop image, and return a preload promise. * * @param {BaseItemDto | null} item Item to use for waiting backdrop, null to remove it. + * @returns {Promise} promise for the preload to complete */ - public static async setWaitingBackdrop( + public static getWaitingBackdropUrl( item: BaseItemDto | null - ): Promise { + ): Promise { // no backdrop as a fallback let src: string | null = null; @@ -306,11 +334,26 @@ export abstract class DocumentManager { } } + return this.preloadImage(src); + } + + /** + * Backdrops are set on the waiting container. + * They are switched around every 30 seconds by default + * (governed by startBackdropInterval) + * + * @param {string | null} src Url to image + * @param {BaseItemDto | null} item Item to use for waiting backdrop, null to remove it. + */ + public static async setWaitingBackdrop( + src: string | null, + item: BaseItemDto | null + ): Promise { let element: HTMLElement = this.querySelector( '#waiting-container-backdrop' ); - await this.setBackgroundImage(element, src); + this.setBackgroundImage(element, src); element = this.getElementById('waiting-description'); element.innerHTML = item?.Name ?? ''; @@ -321,8 +364,8 @@ export abstract class DocumentManager { * * @returns {Promise} promise waiting for the backdrop to be set */ - private static setRandomUserBackdrop(): Promise { - return JellyfinApi.authAjaxUser('Items', { + private static async setRandomUserBackdrop(): Promise { + const result = await JellyfinApi.authAjaxUser('Items', { dataType: 'json', type: 'GET', query: { @@ -335,10 +378,18 @@ export abstract class DocumentManager { // not everyone will want to see adult backdrops rotating on their TV. MaxOfficialRating: 'PG-13' } - }).then((result) => { - if (result.Items && result.Items[0]) - return DocumentManager.setWaitingBackdrop(result.Items[0]); - else return DocumentManager.setWaitingBackdrop(null); + }); + + let src: string | null = null; + let item: BaseItemDto | null = null; + + if (result.Items && result.Items[0]) { + item = result.Items[0]; + src = await DocumentManager.getWaitingBackdropUrl(item); + } + + requestAnimationFrame(() => { + DocumentManager.setWaitingBackdrop(src, item); }); } @@ -366,7 +417,7 @@ export abstract class DocumentManager { // skip out if it's disabled if (!this.backdropPeriodMs) { - this.setWaitingBackdrop(null); + this.setWaitingBackdrop(null, null); return; } @@ -398,7 +449,7 @@ export abstract class DocumentManager { if (period === null) { // No backdrop is wanted, and the timer has been cleared. // This call will remove any present backdrop. - this.setWaitingBackdrop(null); + this.setWaitingBackdrop(null, null); } } } diff --git a/src/helpers.ts b/src/helpers.ts index e2eeb578..43d468a5 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -212,7 +212,7 @@ export function resetPlaybackScope($scope: GlobalScope): void { DocumentManager.setAppStatus('waiting'); $scope.startPositionTicks = 0; - DocumentManager.setWaitingBackdrop(null); + DocumentManager.setWaitingBackdrop(null, null); $scope.mediaType = ''; $scope.itemId = ''; From 856f391e7ddd7029aba755bfdf0b52b4e8d8f888 Mon Sep 17 00:00:00 2001 From: hawken Date: Thu, 14 Jan 2021 01:55:32 +0100 Subject: [PATCH 13/15] restore glyphicons --- src/app.ts | 1 + src/css/glyphicons.css | 821 ++++++++++++++++++++ src/fonts/glyphicons-halflings-regular.eot | Bin 0 -> 14079 bytes src/fonts/glyphicons-halflings-regular.svg | 228 ++++++ src/fonts/glyphicons-halflings-regular.ttf | Bin 0 -> 29512 bytes src/fonts/glyphicons-halflings-regular.woff | Bin 0 -> 16448 bytes 6 files changed, 1050 insertions(+) create mode 100644 src/css/glyphicons.css create mode 100644 src/fonts/glyphicons-halflings-regular.eot create mode 100644 src/fonts/glyphicons-halflings-regular.svg create mode 100644 src/fonts/glyphicons-halflings-regular.ttf create mode 100644 src/fonts/glyphicons-halflings-regular.woff diff --git a/src/app.ts b/src/app.ts index dd429afa..18b4dd16 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,6 @@ import { RepeatMode } from './api/generated/models/repeat-mode'; import './components/maincontroller'; +import './css/glyphicons.css'; import './css/jellyfin.css'; const senders = cast.framework.CastReceiverContext.getInstance().getSenders(); diff --git a/src/css/glyphicons.css b/src/css/glyphicons.css new file mode 100644 index 00000000..38ce74a7 --- /dev/null +++ b/src/css/glyphicons.css @@ -0,0 +1,821 @@ +@font-face { + font-family: 'Glyphicons Halflings'; + src: url('../fonts/glyphicons-halflings-regular.eot'); + src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') + format('embedded-opentype'), + url('../fonts/glyphicons-halflings-regular.woff') format('woff'), + url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), + url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') + format('svg'); +} + +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; +} + +.glyphicon-asterisk:before { + content: '\2a'; +} + +.glyphicon-plus:before { + content: '\2b'; +} + +.glyphicon-euro:before { + content: '\20ac'; +} + +.glyphicon-minus:before { + content: '\2212'; +} + +.glyphicon-cloud:before { + content: '\2601'; +} + +.glyphicon-envelope:before { + content: '\2709'; +} + +.glyphicon-pencil:before { + content: '\270f'; +} + +.glyphicon-glass:before { + content: '\e001'; +} + +.glyphicon-music:before { + content: '\e002'; +} + +.glyphicon-search:before { + content: '\e003'; +} + +.glyphicon-heart:before { + content: '\e005'; +} + +.glyphicon-star:before { + content: '\e006'; +} + +.glyphicon-star-empty:before { + content: '\e007'; +} + +.glyphicon-user:before { + content: '\e008'; +} + +.glyphicon-film:before { + content: '\e009'; +} + +.glyphicon-th-large:before { + content: '\e010'; +} + +.glyphicon-th:before { + content: '\e011'; +} + +.glyphicon-th-list:before { + content: '\e012'; +} + +.glyphicon-ok:before { + content: '\e013'; +} + +.glyphicon-remove:before { + content: '\e014'; +} + +.glyphicon-zoom-in:before { + content: '\e015'; +} + +.glyphicon-zoom-out:before { + content: '\e016'; +} + +.glyphicon-off:before { + content: '\e017'; +} + +.glyphicon-signal:before { + content: '\e018'; +} + +.glyphicon-cog:before { + content: '\e019'; +} + +.glyphicon-trash:before { + content: '\e020'; +} + +.glyphicon-home:before { + content: '\e021'; +} + +.glyphicon-file:before { + content: '\e022'; +} + +.glyphicon-time:before { + content: '\e023'; +} + +.glyphicon-road:before { + content: '\e024'; +} + +.glyphicon-download-alt:before { + content: '\e025'; +} + +.glyphicon-download:before { + content: '\e026'; +} + +.glyphicon-upload:before { + content: '\e027'; +} + +.glyphicon-inbox:before { + content: '\e028'; +} + +.glyphicon-play-circle:before { + content: '\e029'; +} + +.glyphicon-repeat:before { + content: '\e030'; +} + +.glyphicon-refresh:before { + content: '\e031'; +} + +.glyphicon-list-alt:before { + content: '\e032'; +} + +.glyphicon-flag:before { + content: '\e034'; +} + +.glyphicon-headphones:before { + content: '\e035'; +} + +.glyphicon-volume-off:before { + content: '\e036'; +} + +.glyphicon-volume-down:before { + content: '\e037'; +} + +.glyphicon-volume-up:before { + content: '\e038'; +} + +.glyphicon-qrcode:before { + content: '\e039'; +} + +.glyphicon-barcode:before { + content: '\e040'; +} + +.glyphicon-tag:before { + content: '\e041'; +} + +.glyphicon-tags:before { + content: '\e042'; +} + +.glyphicon-book:before { + content: '\e043'; +} + +.glyphicon-print:before { + content: '\e045'; +} + +.glyphicon-font:before { + content: '\e047'; +} + +.glyphicon-bold:before { + content: '\e048'; +} + +.glyphicon-italic:before { + content: '\e049'; +} + +.glyphicon-text-height:before { + content: '\e050'; +} + +.glyphicon-text-width:before { + content: '\e051'; +} + +.glyphicon-align-left:before { + content: '\e052'; +} + +.glyphicon-align-center:before { + content: '\e053'; +} + +.glyphicon-align-right:before { + content: '\e054'; +} + +.glyphicon-align-justify:before { + content: '\e055'; +} + +.glyphicon-list:before { + content: '\e056'; +} + +.glyphicon-indent-left:before { + content: '\e057'; +} + +.glyphicon-indent-right:before { + content: '\e058'; +} + +.glyphicon-facetime-video:before { + content: '\e059'; +} + +.glyphicon-picture:before { + content: '\e060'; +} + +.glyphicon-map-marker:before { + content: '\e062'; +} + +.glyphicon-adjust:before { + content: '\e063'; +} + +.glyphicon-tint:before { + content: '\e064'; +} + +.glyphicon-edit:before { + content: '\e065'; +} + +.glyphicon-share:before { + content: '\e066'; +} + +.glyphicon-check:before { + content: '\e067'; +} + +.glyphicon-move:before { + content: '\e068'; +} + +.glyphicon-step-backward:before { + content: '\e069'; +} + +.glyphicon-fast-backward:before { + content: '\e070'; +} + +.glyphicon-backward:before { + content: '\e071'; +} + +.glyphicon-play:before { + content: '\e072'; +} + +.glyphicon-pause:before { + content: '\e073'; +} + +.glyphicon-stop:before { + content: '\e074'; +} + +.glyphicon-forward:before { + content: '\e075'; +} + +.glyphicon-fast-forward:before { + content: '\e076'; +} + +.glyphicon-step-forward:before { + content: '\e077'; +} + +.glyphicon-eject:before { + content: '\e078'; +} + +.glyphicon-chevron-left:before { + content: '\e079'; +} + +.glyphicon-chevron-right:before { + content: '\e080'; +} + +.glyphicon-plus-sign:before { + content: '\e081'; +} + +.glyphicon-minus-sign:before { + content: '\e082'; +} + +.glyphicon-remove-sign:before { + content: '\e083'; +} + +.glyphicon-ok-sign:before { + content: '\e084'; +} + +.glyphicon-question-sign:before { + content: '\e085'; +} + +.glyphicon-info-sign:before { + content: '\e086'; +} + +.glyphicon-screenshot:before { + content: '\e087'; +} + +.glyphicon-remove-circle:before { + content: '\e088'; +} + +.glyphicon-ok-circle:before { + content: '\e089'; +} + +.glyphicon-ban-circle:before { + content: '\e090'; +} + +.glyphicon-arrow-left:before { + content: '\e091'; +} + +.glyphicon-arrow-right:before { + content: '\e092'; +} + +.glyphicon-arrow-up:before { + content: '\e093'; +} + +.glyphicon-arrow-down:before { + content: '\e094'; +} + +.glyphicon-share-alt:before { + content: '\e095'; +} + +.glyphicon-resize-full:before { + content: '\e096'; +} + +.glyphicon-resize-small:before { + content: '\e097'; +} + +.glyphicon-exclamation-sign:before { + content: '\e101'; +} + +.glyphicon-gift:before { + content: '\e102'; +} + +.glyphicon-leaf:before { + content: '\e103'; +} + +.glyphicon-eye-open:before { + content: '\e105'; +} + +.glyphicon-eye-close:before { + content: '\e106'; +} + +.glyphicon-warning-sign:before { + content: '\e107'; +} + +.glyphicon-plane:before { + content: '\e108'; +} + +.glyphicon-random:before { + content: '\e110'; +} + +.glyphicon-comment:before { + content: '\e111'; +} + +.glyphicon-magnet:before { + content: '\e112'; +} + +.glyphicon-chevron-up:before { + content: '\e113'; +} + +.glyphicon-chevron-down:before { + content: '\e114'; +} + +.glyphicon-retweet:before { + content: '\e115'; +} + +.glyphicon-shopping-cart:before { + content: '\e116'; +} + +.glyphicon-folder-close:before { + content: '\e117'; +} + +.glyphicon-folder-open:before { + content: '\e118'; +} + +.glyphicon-resize-vertical:before { + content: '\e119'; +} + +.glyphicon-resize-horizontal:before { + content: '\e120'; +} + +.glyphicon-hdd:before { + content: '\e121'; +} + +.glyphicon-bullhorn:before { + content: '\e122'; +} + +.glyphicon-certificate:before { + content: '\e124'; +} + +.glyphicon-thumbs-up:before { + content: '\e125'; +} + +.glyphicon-thumbs-down:before { + content: '\e126'; +} + +.glyphicon-hand-right:before { + content: '\e127'; +} + +.glyphicon-hand-left:before { + content: '\e128'; +} + +.glyphicon-hand-up:before { + content: '\e129'; +} + +.glyphicon-hand-down:before { + content: '\e130'; +} + +.glyphicon-circle-arrow-right:before { + content: '\e131'; +} + +.glyphicon-circle-arrow-left:before { + content: '\e132'; +} + +.glyphicon-circle-arrow-up:before { + content: '\e133'; +} + +.glyphicon-circle-arrow-down:before { + content: '\e134'; +} + +.glyphicon-globe:before { + content: '\e135'; +} + +.glyphicon-tasks:before { + content: '\e137'; +} + +.glyphicon-filter:before { + content: '\e138'; +} + +.glyphicon-fullscreen:before { + content: '\e140'; +} + +.glyphicon-dashboard:before { + content: '\e141'; +} + +.glyphicon-heart-empty:before { + content: '\e143'; +} + +.glyphicon-link:before { + content: '\e144'; +} + +.glyphicon-phone:before { + content: '\e145'; +} + +.glyphicon-usd:before { + content: '\e148'; +} + +.glyphicon-gbp:before { + content: '\e149'; +} + +.glyphicon-sort:before { + content: '\e150'; +} + +.glyphicon-sort-by-alphabet:before { + content: '\e151'; +} + +.glyphicon-sort-by-alphabet-alt:before { + content: '\e152'; +} + +.glyphicon-sort-by-order:before { + content: '\e153'; +} + +.glyphicon-sort-by-order-alt:before { + content: '\e154'; +} + +.glyphicon-sort-by-attributes:before { + content: '\e155'; +} + +.glyphicon-sort-by-attributes-alt:before { + content: '\e156'; +} + +.glyphicon-unchecked:before { + content: '\e157'; +} + +.glyphicon-expand:before { + content: '\e158'; +} + +.glyphicon-collapse-down:before { + content: '\e159'; +} + +.glyphicon-collapse-up:before { + content: '\e160'; +} + +.glyphicon-log-in:before { + content: '\e161'; +} + +.glyphicon-flash:before { + content: '\e162'; +} + +.glyphicon-log-out:before { + content: '\e163'; +} + +.glyphicon-new-window:before { + content: '\e164'; +} + +.glyphicon-record:before { + content: '\e165'; +} + +.glyphicon-save:before { + content: '\e166'; +} + +.glyphicon-open:before { + content: '\e167'; +} + +.glyphicon-saved:before { + content: '\e168'; +} + +.glyphicon-import:before { + content: '\e169'; +} + +.glyphicon-export:before { + content: '\e170'; +} + +.glyphicon-send:before { + content: '\e171'; +} + +.glyphicon-floppy-disk:before { + content: '\e172'; +} + +.glyphicon-floppy-saved:before { + content: '\e173'; +} + +.glyphicon-floppy-remove:before { + content: '\e174'; +} + +.glyphicon-floppy-save:before { + content: '\e175'; +} + +.glyphicon-floppy-open:before { + content: '\e176'; +} + +.glyphicon-credit-card:before { + content: '\e177'; +} + +.glyphicon-transfer:before { + content: '\e178'; +} + +.glyphicon-cutlery:before { + content: '\e179'; +} + +.glyphicon-header:before { + content: '\e180'; +} + +.glyphicon-compressed:before { + content: '\e181'; +} + +.glyphicon-earphone:before { + content: '\e182'; +} + +.glyphicon-phone-alt:before { + content: '\e183'; +} + +.glyphicon-tower:before { + content: '\e184'; +} + +.glyphicon-stats:before { + content: '\e185'; +} + +.glyphicon-sd-video:before { + content: '\e186'; +} + +.glyphicon-hd-video:before { + content: '\e187'; +} + +.glyphicon-subtitles:before { + content: '\e188'; +} + +.glyphicon-sound-stereo:before { + content: '\e189'; +} + +.glyphicon-sound-dolby:before { + content: '\e190'; +} + +.glyphicon-sound-5-1:before { + content: '\e191'; +} + +.glyphicon-sound-6-1:before { + content: '\e192'; +} + +.glyphicon-sound-7-1:before { + content: '\e193'; +} + +.glyphicon-copyright-mark:before { + content: '\e194'; +} + +.glyphicon-registration-mark:before { + content: '\e195'; +} + +.glyphicon-cloud-download:before { + content: '\e197'; +} + +.glyphicon-cloud-upload:before { + content: '\e198'; +} + +.glyphicon-tree-conifer:before { + content: '\e199'; +} + +.glyphicon-tree-deciduous:before { + content: '\e200'; +} + +.glyphicon-briefcase:before { + content: '\1f4bc'; +} + +.glyphicon-calendar:before { + content: '\1f4c5'; +} + +.glyphicon-pushpin:before { + content: '\1f4cc'; +} + +.glyphicon-paperclip:before { + content: '\1f4ce'; +} + +.glyphicon-camera:before { + content: '\1f4f7'; +} + +.glyphicon-lock:before { + content: '\1f512'; +} + +.glyphicon-bell:before { + content: '\1f514'; +} + +.glyphicon-bookmark:before { + content: '\1f516'; +} + +.glyphicon-fire:before { + content: '\1f525'; +} + +.glyphicon-wrench:before { + content: '\1f527'; +} diff --git a/src/fonts/glyphicons-halflings-regular.eot b/src/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 0000000000000000000000000000000000000000..87eaa434234e2a984c261e0450a2f4ad837aa7b4 GIT binary patch literal 14079 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 literal 0 HcmV?d00001 diff --git a/src/fonts/glyphicons-halflings-regular.svg b/src/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 00000000..5fee0685 --- /dev/null +++ b/src/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/fonts/glyphicons-halflings-regular.ttf b/src/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..be784dc1d5bcb92ab155f578f3723524a3dd9688 GIT binary patch literal 29512 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 DlhOCh literal 0 HcmV?d00001 diff --git a/src/fonts/glyphicons-halflings-regular.woff b/src/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..2cc3e4852a5a42e6aadd6284e067b66e14a57bc7 GIT binary patch literal 16448 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 Date: Thu, 14 Jan 2021 02:15:09 +0100 Subject: [PATCH 14/15] fix stylelint for glyphicons & integrate prettier --- .stylelintrc.json | 6 +- src/css/glyphicons.css | 402 ++++++++++++++++++++--------------------- 2 files changed, 203 insertions(+), 205 deletions(-) diff --git a/.stylelintrc.json b/.stylelintrc.json index 6449c3f2..c765e3d3 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,6 +1,4 @@ { - "extends": "stylelint-config-standard", - "rules": { - "indentation": 4 - } + "extends": ["stylelint-config-standard", "stylelint-config-prettier"], + "rules": {} } diff --git a/src/css/glyphicons.css b/src/css/glyphicons.css index 38ce74a7..3b891427 100644 --- a/src/css/glyphicons.css +++ b/src/css/glyphicons.css @@ -13,809 +13,809 @@ position: relative; top: 1px; display: inline-block; - font-family: 'Glyphicons Halflings'; + font-family: 'Glyphicons Halflings'; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */ font-style: normal; font-weight: normal; line-height: 1; -webkit-font-smoothing: antialiased; } -.glyphicon-asterisk:before { +.glyphicon-asterisk::before { content: '\2a'; } -.glyphicon-plus:before { +.glyphicon-plus::before { content: '\2b'; } -.glyphicon-euro:before { +.glyphicon-euro::before { content: '\20ac'; } -.glyphicon-minus:before { +.glyphicon-minus::before { content: '\2212'; } -.glyphicon-cloud:before { +.glyphicon-cloud::before { content: '\2601'; } -.glyphicon-envelope:before { +.glyphicon-envelope::before { content: '\2709'; } -.glyphicon-pencil:before { +.glyphicon-pencil::before { content: '\270f'; } -.glyphicon-glass:before { +.glyphicon-glass::before { content: '\e001'; } -.glyphicon-music:before { +.glyphicon-music::before { content: '\e002'; } -.glyphicon-search:before { +.glyphicon-search::before { content: '\e003'; } -.glyphicon-heart:before { +.glyphicon-heart::before { content: '\e005'; } -.glyphicon-star:before { +.glyphicon-star::before { content: '\e006'; } -.glyphicon-star-empty:before { +.glyphicon-star-empty::before { content: '\e007'; } -.glyphicon-user:before { +.glyphicon-user::before { content: '\e008'; } -.glyphicon-film:before { +.glyphicon-film::before { content: '\e009'; } -.glyphicon-th-large:before { +.glyphicon-th-large::before { content: '\e010'; } -.glyphicon-th:before { +.glyphicon-th::before { content: '\e011'; } -.glyphicon-th-list:before { +.glyphicon-th-list::before { content: '\e012'; } -.glyphicon-ok:before { +.glyphicon-ok::before { content: '\e013'; } -.glyphicon-remove:before { +.glyphicon-remove::before { content: '\e014'; } -.glyphicon-zoom-in:before { +.glyphicon-zoom-in::before { content: '\e015'; } -.glyphicon-zoom-out:before { +.glyphicon-zoom-out::before { content: '\e016'; } -.glyphicon-off:before { +.glyphicon-off::before { content: '\e017'; } -.glyphicon-signal:before { +.glyphicon-signal::before { content: '\e018'; } -.glyphicon-cog:before { +.glyphicon-cog::before { content: '\e019'; } -.glyphicon-trash:before { +.glyphicon-trash::before { content: '\e020'; } -.glyphicon-home:before { +.glyphicon-home::before { content: '\e021'; } -.glyphicon-file:before { +.glyphicon-file::before { content: '\e022'; } -.glyphicon-time:before { +.glyphicon-time::before { content: '\e023'; } -.glyphicon-road:before { +.glyphicon-road::before { content: '\e024'; } -.glyphicon-download-alt:before { +.glyphicon-download-alt::before { content: '\e025'; } -.glyphicon-download:before { +.glyphicon-download::before { content: '\e026'; } -.glyphicon-upload:before { +.glyphicon-upload::before { content: '\e027'; } -.glyphicon-inbox:before { +.glyphicon-inbox::before { content: '\e028'; } -.glyphicon-play-circle:before { +.glyphicon-play-circle::before { content: '\e029'; } -.glyphicon-repeat:before { +.glyphicon-repeat::before { content: '\e030'; } -.glyphicon-refresh:before { +.glyphicon-refresh::before { content: '\e031'; } -.glyphicon-list-alt:before { +.glyphicon-list-alt::before { content: '\e032'; } -.glyphicon-flag:before { +.glyphicon-flag::before { content: '\e034'; } -.glyphicon-headphones:before { +.glyphicon-headphones::before { content: '\e035'; } -.glyphicon-volume-off:before { +.glyphicon-volume-off::before { content: '\e036'; } -.glyphicon-volume-down:before { +.glyphicon-volume-down::before { content: '\e037'; } -.glyphicon-volume-up:before { +.glyphicon-volume-up::before { content: '\e038'; } -.glyphicon-qrcode:before { +.glyphicon-qrcode::before { content: '\e039'; } -.glyphicon-barcode:before { +.glyphicon-barcode::before { content: '\e040'; } -.glyphicon-tag:before { +.glyphicon-tag::before { content: '\e041'; } -.glyphicon-tags:before { +.glyphicon-tags::before { content: '\e042'; } -.glyphicon-book:before { +.glyphicon-book::before { content: '\e043'; } -.glyphicon-print:before { +.glyphicon-print::before { content: '\e045'; } -.glyphicon-font:before { +.glyphicon-font::before { content: '\e047'; } -.glyphicon-bold:before { +.glyphicon-bold::before { content: '\e048'; } -.glyphicon-italic:before { +.glyphicon-italic::before { content: '\e049'; } -.glyphicon-text-height:before { +.glyphicon-text-height::before { content: '\e050'; } -.glyphicon-text-width:before { +.glyphicon-text-width::before { content: '\e051'; } -.glyphicon-align-left:before { +.glyphicon-align-left::before { content: '\e052'; } -.glyphicon-align-center:before { +.glyphicon-align-center::before { content: '\e053'; } -.glyphicon-align-right:before { +.glyphicon-align-right::before { content: '\e054'; } -.glyphicon-align-justify:before { +.glyphicon-align-justify::before { content: '\e055'; } -.glyphicon-list:before { +.glyphicon-list::before { content: '\e056'; } -.glyphicon-indent-left:before { +.glyphicon-indent-left::before { content: '\e057'; } -.glyphicon-indent-right:before { +.glyphicon-indent-right::before { content: '\e058'; } -.glyphicon-facetime-video:before { +.glyphicon-facetime-video::before { content: '\e059'; } -.glyphicon-picture:before { +.glyphicon-picture::before { content: '\e060'; } -.glyphicon-map-marker:before { +.glyphicon-map-marker::before { content: '\e062'; } -.glyphicon-adjust:before { +.glyphicon-adjust::before { content: '\e063'; } -.glyphicon-tint:before { +.glyphicon-tint::before { content: '\e064'; } -.glyphicon-edit:before { +.glyphicon-edit::before { content: '\e065'; } -.glyphicon-share:before { +.glyphicon-share::before { content: '\e066'; } -.glyphicon-check:before { +.glyphicon-check::before { content: '\e067'; } -.glyphicon-move:before { +.glyphicon-move::before { content: '\e068'; } -.glyphicon-step-backward:before { +.glyphicon-step-backward::before { content: '\e069'; } -.glyphicon-fast-backward:before { +.glyphicon-fast-backward::before { content: '\e070'; } -.glyphicon-backward:before { +.glyphicon-backward::before { content: '\e071'; } -.glyphicon-play:before { +.glyphicon-play::before { content: '\e072'; } -.glyphicon-pause:before { +.glyphicon-pause::before { content: '\e073'; } -.glyphicon-stop:before { +.glyphicon-stop::before { content: '\e074'; } -.glyphicon-forward:before { +.glyphicon-forward::before { content: '\e075'; } -.glyphicon-fast-forward:before { +.glyphicon-fast-forward::before { content: '\e076'; } -.glyphicon-step-forward:before { +.glyphicon-step-forward::before { content: '\e077'; } -.glyphicon-eject:before { +.glyphicon-eject::before { content: '\e078'; } -.glyphicon-chevron-left:before { +.glyphicon-chevron-left::before { content: '\e079'; } -.glyphicon-chevron-right:before { +.glyphicon-chevron-right::before { content: '\e080'; } -.glyphicon-plus-sign:before { +.glyphicon-plus-sign::before { content: '\e081'; } -.glyphicon-minus-sign:before { +.glyphicon-minus-sign::before { content: '\e082'; } -.glyphicon-remove-sign:before { +.glyphicon-remove-sign::before { content: '\e083'; } -.glyphicon-ok-sign:before { +.glyphicon-ok-sign::before { content: '\e084'; } -.glyphicon-question-sign:before { +.glyphicon-question-sign::before { content: '\e085'; } -.glyphicon-info-sign:before { +.glyphicon-info-sign::before { content: '\e086'; } -.glyphicon-screenshot:before { +.glyphicon-screenshot::before { content: '\e087'; } -.glyphicon-remove-circle:before { +.glyphicon-remove-circle::before { content: '\e088'; } -.glyphicon-ok-circle:before { +.glyphicon-ok-circle::before { content: '\e089'; } -.glyphicon-ban-circle:before { +.glyphicon-ban-circle::before { content: '\e090'; } -.glyphicon-arrow-left:before { +.glyphicon-arrow-left::before { content: '\e091'; } -.glyphicon-arrow-right:before { +.glyphicon-arrow-right::before { content: '\e092'; } -.glyphicon-arrow-up:before { +.glyphicon-arrow-up::before { content: '\e093'; } -.glyphicon-arrow-down:before { +.glyphicon-arrow-down::before { content: '\e094'; } -.glyphicon-share-alt:before { +.glyphicon-share-alt::before { content: '\e095'; } -.glyphicon-resize-full:before { +.glyphicon-resize-full::before { content: '\e096'; } -.glyphicon-resize-small:before { +.glyphicon-resize-small::before { content: '\e097'; } -.glyphicon-exclamation-sign:before { +.glyphicon-exclamation-sign::before { content: '\e101'; } -.glyphicon-gift:before { +.glyphicon-gift::before { content: '\e102'; } -.glyphicon-leaf:before { +.glyphicon-leaf::before { content: '\e103'; } -.glyphicon-eye-open:before { +.glyphicon-eye-open::before { content: '\e105'; } -.glyphicon-eye-close:before { +.glyphicon-eye-close::before { content: '\e106'; } -.glyphicon-warning-sign:before { +.glyphicon-warning-sign::before { content: '\e107'; } -.glyphicon-plane:before { +.glyphicon-plane::before { content: '\e108'; } -.glyphicon-random:before { +.glyphicon-random::before { content: '\e110'; } -.glyphicon-comment:before { +.glyphicon-comment::before { content: '\e111'; } -.glyphicon-magnet:before { +.glyphicon-magnet::before { content: '\e112'; } -.glyphicon-chevron-up:before { +.glyphicon-chevron-up::before { content: '\e113'; } -.glyphicon-chevron-down:before { +.glyphicon-chevron-down::before { content: '\e114'; } -.glyphicon-retweet:before { +.glyphicon-retweet::before { content: '\e115'; } -.glyphicon-shopping-cart:before { +.glyphicon-shopping-cart::before { content: '\e116'; } -.glyphicon-folder-close:before { +.glyphicon-folder-close::before { content: '\e117'; } -.glyphicon-folder-open:before { +.glyphicon-folder-open::before { content: '\e118'; } -.glyphicon-resize-vertical:before { +.glyphicon-resize-vertical::before { content: '\e119'; } -.glyphicon-resize-horizontal:before { +.glyphicon-resize-horizontal::before { content: '\e120'; } -.glyphicon-hdd:before { +.glyphicon-hdd::before { content: '\e121'; } -.glyphicon-bullhorn:before { +.glyphicon-bullhorn::before { content: '\e122'; } -.glyphicon-certificate:before { +.glyphicon-certificate::before { content: '\e124'; } -.glyphicon-thumbs-up:before { +.glyphicon-thumbs-up::before { content: '\e125'; } -.glyphicon-thumbs-down:before { +.glyphicon-thumbs-down::before { content: '\e126'; } -.glyphicon-hand-right:before { +.glyphicon-hand-right::before { content: '\e127'; } -.glyphicon-hand-left:before { +.glyphicon-hand-left::before { content: '\e128'; } -.glyphicon-hand-up:before { +.glyphicon-hand-up::before { content: '\e129'; } -.glyphicon-hand-down:before { +.glyphicon-hand-down::before { content: '\e130'; } -.glyphicon-circle-arrow-right:before { +.glyphicon-circle-arrow-right::before { content: '\e131'; } -.glyphicon-circle-arrow-left:before { +.glyphicon-circle-arrow-left::before { content: '\e132'; } -.glyphicon-circle-arrow-up:before { +.glyphicon-circle-arrow-up::before { content: '\e133'; } -.glyphicon-circle-arrow-down:before { +.glyphicon-circle-arrow-down::before { content: '\e134'; } -.glyphicon-globe:before { +.glyphicon-globe::before { content: '\e135'; } -.glyphicon-tasks:before { +.glyphicon-tasks::before { content: '\e137'; } -.glyphicon-filter:before { +.glyphicon-filter::before { content: '\e138'; } -.glyphicon-fullscreen:before { +.glyphicon-fullscreen::before { content: '\e140'; } -.glyphicon-dashboard:before { +.glyphicon-dashboard::before { content: '\e141'; } -.glyphicon-heart-empty:before { +.glyphicon-heart-empty::before { content: '\e143'; } -.glyphicon-link:before { +.glyphicon-link::before { content: '\e144'; } -.glyphicon-phone:before { +.glyphicon-phone::before { content: '\e145'; } -.glyphicon-usd:before { +.glyphicon-usd::before { content: '\e148'; } -.glyphicon-gbp:before { +.glyphicon-gbp::before { content: '\e149'; } -.glyphicon-sort:before { +.glyphicon-sort::before { content: '\e150'; } -.glyphicon-sort-by-alphabet:before { +.glyphicon-sort-by-alphabet::before { content: '\e151'; } -.glyphicon-sort-by-alphabet-alt:before { +.glyphicon-sort-by-alphabet-alt::before { content: '\e152'; } -.glyphicon-sort-by-order:before { +.glyphicon-sort-by-order::before { content: '\e153'; } -.glyphicon-sort-by-order-alt:before { +.glyphicon-sort-by-order-alt::before { content: '\e154'; } -.glyphicon-sort-by-attributes:before { +.glyphicon-sort-by-attributes::before { content: '\e155'; } -.glyphicon-sort-by-attributes-alt:before { +.glyphicon-sort-by-attributes-alt::before { content: '\e156'; } -.glyphicon-unchecked:before { +.glyphicon-unchecked::before { content: '\e157'; } -.glyphicon-expand:before { +.glyphicon-expand::before { content: '\e158'; } -.glyphicon-collapse-down:before { +.glyphicon-collapse-down::before { content: '\e159'; } -.glyphicon-collapse-up:before { +.glyphicon-collapse-up::before { content: '\e160'; } -.glyphicon-log-in:before { +.glyphicon-log-in::before { content: '\e161'; } -.glyphicon-flash:before { +.glyphicon-flash::before { content: '\e162'; } -.glyphicon-log-out:before { +.glyphicon-log-out::before { content: '\e163'; } -.glyphicon-new-window:before { +.glyphicon-new-window::before { content: '\e164'; } -.glyphicon-record:before { +.glyphicon-record::before { content: '\e165'; } -.glyphicon-save:before { +.glyphicon-save::before { content: '\e166'; } -.glyphicon-open:before { +.glyphicon-open::before { content: '\e167'; } -.glyphicon-saved:before { +.glyphicon-saved::before { content: '\e168'; } -.glyphicon-import:before { +.glyphicon-import::before { content: '\e169'; } -.glyphicon-export:before { +.glyphicon-export::before { content: '\e170'; } -.glyphicon-send:before { +.glyphicon-send::before { content: '\e171'; } -.glyphicon-floppy-disk:before { +.glyphicon-floppy-disk::before { content: '\e172'; } -.glyphicon-floppy-saved:before { +.glyphicon-floppy-saved::before { content: '\e173'; } -.glyphicon-floppy-remove:before { +.glyphicon-floppy-remove::before { content: '\e174'; } -.glyphicon-floppy-save:before { +.glyphicon-floppy-save::before { content: '\e175'; } -.glyphicon-floppy-open:before { +.glyphicon-floppy-open::before { content: '\e176'; } -.glyphicon-credit-card:before { +.glyphicon-credit-card::before { content: '\e177'; } -.glyphicon-transfer:before { +.glyphicon-transfer::before { content: '\e178'; } -.glyphicon-cutlery:before { +.glyphicon-cutlery::before { content: '\e179'; } -.glyphicon-header:before { +.glyphicon-header::before { content: '\e180'; } -.glyphicon-compressed:before { +.glyphicon-compressed::before { content: '\e181'; } -.glyphicon-earphone:before { +.glyphicon-earphone::before { content: '\e182'; } -.glyphicon-phone-alt:before { +.glyphicon-phone-alt::before { content: '\e183'; } -.glyphicon-tower:before { +.glyphicon-tower::before { content: '\e184'; } -.glyphicon-stats:before { +.glyphicon-stats::before { content: '\e185'; } -.glyphicon-sd-video:before { +.glyphicon-sd-video::before { content: '\e186'; } -.glyphicon-hd-video:before { +.glyphicon-hd-video::before { content: '\e187'; } -.glyphicon-subtitles:before { +.glyphicon-subtitles::before { content: '\e188'; } -.glyphicon-sound-stereo:before { +.glyphicon-sound-stereo::before { content: '\e189'; } -.glyphicon-sound-dolby:before { +.glyphicon-sound-dolby::before { content: '\e190'; } -.glyphicon-sound-5-1:before { +.glyphicon-sound-5-1::before { content: '\e191'; } -.glyphicon-sound-6-1:before { +.glyphicon-sound-6-1::before { content: '\e192'; } -.glyphicon-sound-7-1:before { +.glyphicon-sound-7-1::before { content: '\e193'; } -.glyphicon-copyright-mark:before { +.glyphicon-copyright-mark::before { content: '\e194'; } -.glyphicon-registration-mark:before { +.glyphicon-registration-mark::before { content: '\e195'; } -.glyphicon-cloud-download:before { +.glyphicon-cloud-download::before { content: '\e197'; } -.glyphicon-cloud-upload:before { +.glyphicon-cloud-upload::before { content: '\e198'; } -.glyphicon-tree-conifer:before { +.glyphicon-tree-conifer::before { content: '\e199'; } -.glyphicon-tree-deciduous:before { +.glyphicon-tree-deciduous::before { content: '\e200'; } -.glyphicon-briefcase:before { +.glyphicon-briefcase::before { content: '\1f4bc'; } -.glyphicon-calendar:before { +.glyphicon-calendar::before { content: '\1f4c5'; } -.glyphicon-pushpin:before { +.glyphicon-pushpin::before { content: '\1f4cc'; } -.glyphicon-paperclip:before { +.glyphicon-paperclip::before { content: '\1f4ce'; } -.glyphicon-camera:before { +.glyphicon-camera::before { content: '\1f4f7'; } -.glyphicon-lock:before { +.glyphicon-lock::before { content: '\1f512'; } -.glyphicon-bell:before { +.glyphicon-bell::before { content: '\1f514'; } -.glyphicon-bookmark:before { +.glyphicon-bookmark::before { content: '\1f516'; } -.glyphicon-fire:before { +.glyphicon-fire::before { content: '\1f525'; } -.glyphicon-wrench:before { +.glyphicon-wrench::before { content: '\1f527'; } From c115262d5aa1574d3d7aeb680abef263439cd62e Mon Sep 17 00:00:00 2001 From: hawken Date: Thu, 14 Jan 2021 03:15:21 +0100 Subject: [PATCH 15/15] waitingcontainer to use relative sizes --- src/components/documentManager.ts | 2 -- src/css/jellyfin.css | 38 +++++++++++++++---------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/components/documentManager.ts b/src/components/documentManager.ts index bb8b95f7..b0476096 100644 --- a/src/components/documentManager.ts +++ b/src/components/documentManager.ts @@ -243,7 +243,6 @@ export abstract class DocumentManager { const starRatingValue = this.getElementById('star-rating-value'); if (item.CommunityRating != null) { - starRating.setAttribute('title', item.CommunityRating.toFixed(1)); starRatingValue.innerHTML = item.CommunityRating.toFixed(1); this.setVisibility(starRating, true); this.setVisibility(starRatingValue, true); @@ -262,7 +261,6 @@ export abstract class DocumentManager { criticRating.classList.remove( verdict == 'fresh' ? 'rotten' : 'fresh' ); - criticRating.setAttribute('title', verdict); criticRatingValue.innerHTML = item.CriticRating.toString(); diff --git a/src/css/jellyfin.css b/src/css/jellyfin.css index 20303632..1562d008 100644 --- a/src/css/jellyfin.css +++ b/src/css/jellyfin.css @@ -98,14 +98,13 @@ body { top: 5px; right: 5px; text-align: center; - width: 24px; - height: 19px; - padding-top: 3px; + width: 1.8vw; + height: 1.6vw; + padding-top: 0.1vw; border-radius: 50%; color: #fff; background: rgba(0, 128, 0, 0.8); - font-size: 13px; - line-height: 16.9px; + font-size: 1.1vw; } .detailImageProgressContainer { @@ -151,7 +150,7 @@ body { top: 22%; height: 63%; left: 30.5%; - font-size: 23px; + font-size: 1.2vw; width: 60%; } @@ -168,24 +167,23 @@ body { } .displayName { - font-size: 40px; + font-size: 3vw; } #miscInfo { - font-size: 17px; - margin-left: 25px; + font-size: 1.5vw; + margin-left: 2vw; } .starRating { background-image: url('../img/stars.svg'); background-position: left center; background-repeat: no-repeat; - width: 21px; - height: 18px; - display: inline-block; background-size: cover; - vertical-align: top; - position: relative; + width: 1.6vw; + height: 1.4vw; + display: inline-block; + vertical-align: text-bottom; top: 6px; } @@ -196,13 +194,12 @@ body { .rottentomatoesicon { display: inline-block; - width: 18px; - height: 18px; + width: 1.4vw; + height: 1.4vw; background-size: cover; background-position: left center; background-repeat: no-repeat; - vertical-align: top; - position: relative; + vertical-align: text-bottom; top: 6px; } @@ -252,7 +249,7 @@ body { bottom: 0; left: 0; text-align: center; - font-size: 45px; + font-size: 3vw; margin-bottom: 3%; margin-left: 5%; } @@ -264,6 +261,7 @@ body { right: 0; margin-right: 5%; margin-bottom: 3%; + font-size: 1.5vw; } #waiting-container h1, @@ -286,7 +284,7 @@ body { /* jellyfin logo in the waiting container */ #waiting-container .logo { - height: 55px; + width: 4vw; display: inline-block; vertical-align: text-bottom; }