From f2fd96aced6cfd9f14d55d997b4228dd56ea6ba2 Mon Sep 17 00:00:00 2001 From: Ivan Schurawel Date: Sun, 12 Feb 2023 17:16:20 -0500 Subject: [PATCH 01/21] fix: clear stuck subtitle track cues --- src/plugins/htmlVideoPlayer/plugin.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index f2a2b8d30b..8902b9cf25 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -620,6 +620,28 @@ function tryRemoveElement(elem) { return relativeOffset; } + /** + * @private + * These browsers will not clear the existing active cue when setting an offset + * for native TextTracks. + * Any previous text tracks that are on the screen when the offset changes will + * remain next to the new tracks until they reach the new offset's instance of the track. + */ + requiresHidingActiveCuesOnOffsetChange() { + return !!browser.firefox; + } + + /** + * @private + */ + hideTextTrackActiveCues(currentTrack) { + if (currentTrack.activeCues) { + Array.from(currentTrack.activeCues).forEach((cue) => { + cue.text = ''; + }); + } + } + /** * @private */ @@ -629,6 +651,9 @@ function tryRemoveElement(elem) { if (offsetValue === 0) { return; } + if (this.requiresHidingActiveCuesOnOffsetChange()) { + this.hideTextTrackActiveCues(currentTrack); + } Array.from(currentTrack.cues) .forEach(function (cue) { cue.startTime -= offsetValue; From f20ee0b2ea0c486b97dcc1c197ed76b813013e29 Mon Sep 17 00:00:00 2001 From: Ivan Schurawel Date: Tue, 14 Feb 2023 18:43:19 -0500 Subject: [PATCH 02/21] fix: disable track mode to force clear active cues --- src/plugins/htmlVideoPlayer/plugin.js | 48 ++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index 8902b9cf25..695829ca2b 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -245,6 +245,12 @@ function tryRemoveElement(elem) { * @type {any | null | undefined} */ #currentSecondaryTrackEvents; + /** + * Used to temporarily store the text track when + * force-clearing the `activeCue` for certain browsers + * @type {TextTrack | null | undefined} + */ + #currentTextTrack; /** * @type {string[] | undefined} */ @@ -624,8 +630,8 @@ function tryRemoveElement(elem) { * @private * These browsers will not clear the existing active cue when setting an offset * for native TextTracks. - * Any previous text tracks that are on the screen when the offset changes will - * remain next to the new tracks until they reach the new offset's instance of the track. + * Any previous text tracks that are on the screen when the offset changes will remain next + * to the new tracks until they reach the end time of the new offset's instance of the track. */ requiresHidingActiveCuesOnOffsetChange() { return !!browser.firefox; @@ -634,11 +640,30 @@ function tryRemoveElement(elem) { /** * @private */ - hideTextTrackActiveCues(currentTrack) { + hideTextTrackWithActiveCues(currentTrack) { if (currentTrack.activeCues) { - Array.from(currentTrack.activeCues).forEach((cue) => { - cue.text = ''; - }); + currentTrack.mode = 'hidden'; + } + } + + /** + * Forces the active cue to clear by disabling then re-enabling the track. + * The track mode is reverted inside of a 0ms timeout to free up the track + * and allow it to disable and clear the active cue. + * The track needs to be temporarily stored in order for us to access it + * inside the timeout. The stored value is reset after it is used. + * @private + */ + forceClearTextTrackActiveCues(currentTrack) { + if (currentTrack.activeCues) { + this.#currentTextTrack = currentTrack; + currentTrack.mode = 'disabled'; + setTimeout(() => { + if (this.#currentTextTrack) { + this.#currentTextTrack.mode = 'showing'; + this.#currentTextTrack = null; + } + }, 0); } } @@ -651,14 +676,21 @@ function tryRemoveElement(elem) { if (offsetValue === 0) { return; } - if (this.requiresHidingActiveCuesOnOffsetChange()) { - this.hideTextTrackActiveCues(currentTrack); + + const shouldClearActiveCues = this.requiresClearingActiveCuesOnOffsetChange(); + if (shouldClearActiveCues) { + this.hideTextTrackWithActiveCues(currentTrack); } + Array.from(currentTrack.cues) .forEach(function (cue) { cue.startTime -= offsetValue; cue.endTime -= offsetValue; }); + + if (shouldClearActiveCues) { + this.forceClearTextTrackActiveCues(currentTrack); + } } } From 2caa2851b9b9a08cafa607cd5d5582a27c3cc5e6 Mon Sep 17 00:00:00 2001 From: Ivan Schurawel <30599893+is343@users.noreply.github.com> Date: Fri, 17 Feb 2023 09:43:04 -0500 Subject: [PATCH 03/21] fix: use correct name Co-authored-by: Dmitry Lyzo <56478732+dmitrylyzo@users.noreply.github.com> --- src/plugins/htmlVideoPlayer/plugin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index 695829ca2b..aaf22f759b 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -677,7 +677,7 @@ function tryRemoveElement(elem) { return; } - const shouldClearActiveCues = this.requiresClearingActiveCuesOnOffsetChange(); + const shouldClearActiveCues = this.requiresHidingActiveCuesOnOffsetChange(); if (shouldClearActiveCues) { this.hideTextTrackWithActiveCues(currentTrack); } From 032d03d20135d0cdb357ad6f2dda1603af5ede1e Mon Sep 17 00:00:00 2001 From: Ivan Schurawel Date: Sun, 19 Feb 2023 10:56:10 -0500 Subject: [PATCH 04/21] fix: don't set temp variable --- src/plugins/htmlVideoPlayer/plugin.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index aaf22f759b..7936067145 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -245,12 +245,6 @@ function tryRemoveElement(elem) { * @type {any | null | undefined} */ #currentSecondaryTrackEvents; - /** - * Used to temporarily store the text track when - * force-clearing the `activeCue` for certain browsers - * @type {TextTrack | null | undefined} - */ - #currentTextTrack; /** * @type {string[] | undefined} */ @@ -650,19 +644,13 @@ function tryRemoveElement(elem) { * Forces the active cue to clear by disabling then re-enabling the track. * The track mode is reverted inside of a 0ms timeout to free up the track * and allow it to disable and clear the active cue. - * The track needs to be temporarily stored in order for us to access it - * inside the timeout. The stored value is reset after it is used. * @private */ forceClearTextTrackActiveCues(currentTrack) { if (currentTrack.activeCues) { - this.#currentTextTrack = currentTrack; currentTrack.mode = 'disabled'; setTimeout(() => { - if (this.#currentTextTrack) { - this.#currentTextTrack.mode = 'showing'; - this.#currentTextTrack = null; - } + currentTrack.mode = 'showing'; }, 0); } } From acdbf59b50e7a6d2b0114d490048aee337cfaa1b Mon Sep 17 00:00:00 2001 From: Ivan Schurawel Date: Sun, 19 Feb 2023 17:29:34 -0500 Subject: [PATCH 05/21] chore: add debounce to setSubtitleOffset --- src/plugins/htmlVideoPlayer/plugin.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index 7936067145..cef47fe119 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -32,6 +32,7 @@ import { getIncludeCorsCredentials } from '../../scripts/settings/webSettings'; import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../components/backdrop/backdrop'; import Events from '../../utils/events.ts'; import { includesAny } from '../../utils/container.ts'; +import debounce from 'lodash-es/debounce'; /** * Returns resolved URL. @@ -571,7 +572,12 @@ function tryRemoveElement(elem) { } } - setSubtitleOffset(offset) { + setSubtitleOffset = debounce(this._setSubtitleOffset, 500); + + /** + * @private + */ + _setSubtitleOffset(offset) { const offsetValue = parseFloat(offset); // if .ass currently rendering From 4c71de481547f61fbb46e5065b895b6ce5a6895b Mon Sep 17 00:00:00 2001 From: Ivan Schurawel Date: Mon, 20 Feb 2023 15:26:17 -0500 Subject: [PATCH 06/21] chore: use smaller debounce time --- src/plugins/htmlVideoPlayer/plugin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index cef47fe119..4e120e5f78 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -572,7 +572,7 @@ function tryRemoveElement(elem) { } } - setSubtitleOffset = debounce(this._setSubtitleOffset, 500); + setSubtitleOffset = debounce(this._setSubtitleOffset, 100); /** * @private From c0fc93dedcbdf87c49cef6f4d72817bb5cd57172 Mon Sep 17 00:00:00 2001 From: Ivan Schurawel Date: Tue, 21 Feb 2023 09:12:39 -0500 Subject: [PATCH 07/21] fix: cancel debounce on player unmount --- src/plugins/htmlVideoPlayer/plugin.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index 4e120e5f78..8b5afd90d5 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -822,6 +822,8 @@ function tryRemoveElement(elem) { } destroy() { + this.setSubtitleOffset.cancel(); + destroyHlsPlayer(this); destroyFlvPlayer(this); From 52363c59d9556cbaf6178a448ad7d823cb107336 Mon Sep 17 00:00:00 2001 From: knackebrot Date: Mon, 6 Mar 2023 10:02:38 +0100 Subject: [PATCH 08/21] Update hls.js to 1.3.4 --- package-lock.json | 38 ++++++++------------------- package.json | 2 +- src/plugins/htmlAudioPlayer/plugin.js | 3 +++ src/plugins/htmlVideoPlayer/plugin.js | 3 +++ webpack.common.js | 1 + 5 files changed, 19 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 97116e92d6..dcc6fb05f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "flv.js": "1.6.2", "headroom.js": "0.12.0", "history": "5.3.0", - "hls.js": "0.14.17", + "hls.js": "1.3.4", "intersection-observer": "0.12.2", "jellyfin-apiclient": "1.10.0", "jquery": "3.6.3", @@ -7301,7 +7301,8 @@ "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true }, "node_modules/events": { "version": "3.3.0", @@ -8467,13 +8468,9 @@ } }, "node_modules/hls.js": { - "version": "0.14.17", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-0.14.17.tgz", - "integrity": "sha512-25A7+m6qqp6UVkuzUQ//VVh2EEOPYlOBg32ypr34bcPO7liBMOkKFvbjbCBfiPAOTA/7BSx1Dujft3Th57WyFg==", - "dependencies": { - "eventemitter3": "^4.0.3", - "url-toolkit": "^2.1.6" - } + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.3.4.tgz", + "integrity": "sha512-iFEwVqtEDk6sKotcTwtJ5OMo/nuDTk9PrpB8FI2J2WYf8EriTVfR4FaK0aNyYtwbYeRSWCXJKlz23xeREdlNYg==" }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", @@ -18229,11 +18226,6 @@ "deprecated": "Please see https://github.com/lydell/urix#deprecated", "dev": true }, - "node_modules/url-toolkit": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.3.tgz", - "integrity": "sha512-Da75SQoxsZ+2wXS56CZBrj2nukQ4nlGUZUP/dqUBG5E1su5GKThgT94Q00x81eVII7AyS1Pn+CtTTZ4Z0pLUtQ==" - }, "node_modules/use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -24492,7 +24484,8 @@ "eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true }, "events": { "version": "3.3.0", @@ -25405,13 +25398,9 @@ } }, "hls.js": { - "version": "0.14.17", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-0.14.17.tgz", - "integrity": "sha512-25A7+m6qqp6UVkuzUQ//VVh2EEOPYlOBg32ypr34bcPO7liBMOkKFvbjbCBfiPAOTA/7BSx1Dujft3Th57WyFg==", - "requires": { - "eventemitter3": "^4.0.3", - "url-toolkit": "^2.1.6" - } + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.3.4.tgz", + "integrity": "sha512-iFEwVqtEDk6sKotcTwtJ5OMo/nuDTk9PrpB8FI2J2WYf8EriTVfR4FaK0aNyYtwbYeRSWCXJKlz23xeREdlNYg==" }, "hoist-non-react-statics": { "version": "3.3.2", @@ -32698,11 +32687,6 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, - "url-toolkit": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.3.tgz", - "integrity": "sha512-Da75SQoxsZ+2wXS56CZBrj2nukQ4nlGUZUP/dqUBG5E1su5GKThgT94Q00x81eVII7AyS1Pn+CtTTZ4Z0pLUtQ==" - }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", diff --git a/package.json b/package.json index 3d25ed4276..bc19c04a9e 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "flv.js": "1.6.2", "headroom.js": "0.12.0", "history": "5.3.0", - "hls.js": "0.14.17", + "hls.js": "1.3.4", "intersection-observer": "0.12.2", "jellyfin-apiclient": "1.10.0", "jquery": "3.6.3", diff --git a/src/plugins/htmlAudioPlayer/plugin.js b/src/plugins/htmlAudioPlayer/plugin.js index b5d3c8bd02..a1cad660fe 100644 --- a/src/plugins/htmlAudioPlayer/plugin.js +++ b/src/plugins/htmlAudioPlayer/plugin.js @@ -48,6 +48,9 @@ function supportsFade() { function requireHlsPlayer(callback) { import('hls.js').then(({ default: hls }) => { + hls.DefaultConfig.lowLatencyMode = false; + hls.DefaultConfig.backBufferLength = Infinity; + hls.DefaultConfig.liveBackBufferLength = 90; window.Hls = hls; callback(); }); diff --git a/src/plugins/htmlVideoPlayer/plugin.js b/src/plugins/htmlVideoPlayer/plugin.js index f2a2b8d30b..388cb4caad 100644 --- a/src/plugins/htmlVideoPlayer/plugin.js +++ b/src/plugins/htmlVideoPlayer/plugin.js @@ -106,6 +106,9 @@ function tryRemoveElement(elem) { function requireHlsPlayer(callback) { import('hls.js').then(({default: hls}) => { + hls.DefaultConfig.lowLatencyMode = false; + hls.DefaultConfig.backBufferLength = Infinity; + hls.DefaultConfig.liveBackBufferLength = 90; window.Hls = hls; callback(); }); diff --git a/webpack.common.js b/webpack.common.js index eebd423d98..1b5d41fc42 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -158,6 +158,7 @@ const config = { path.resolve(__dirname, 'node_modules/dom7'), path.resolve(__dirname, 'node_modules/epubjs'), path.resolve(__dirname, 'node_modules/flv.js'), + path.resolve(__dirname, 'node_modules/hls.js'), path.resolve(__dirname, 'node_modules/libarchive.js'), path.resolve(__dirname, 'node_modules/marked'), path.resolve(__dirname, 'node_modules/react-router'), From 47c13444ccef523bbfa192cbcdb685babe76f76c Mon Sep 17 00:00:00 2001 From: Pier-Luc Ducharme Date: Fri, 30 Dec 2022 10:24:09 -0500 Subject: [PATCH 09/21] Add eslint max-params rule with a max of 7 --- .eslintrc.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.js b/.eslintrc.js index 53dd01ff25..e2811f35a9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,6 +46,7 @@ module.exports = { 'keyword-spacing': ['error'], 'no-throw-literal': ['error'], 'max-statements-per-line': ['error'], + 'max-params': ['error', 7], 'no-duplicate-imports': ['error'], 'no-empty-function': ['error'], 'no-floating-decimal': ['error'], From dc5ab265f694beaf09d888767bc1d8cbb47adfa2 Mon Sep 17 00:00:00 2001 From: Pier-Luc Ducharme Date: Fri, 30 Dec 2022 10:40:20 -0500 Subject: [PATCH 10/21] Refactor cardBuilder to follow max-params rule --- src/components/cardbuilder/cardBuilder.js | 40 ++++++++++------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 658837df83..657a4102d9 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -773,27 +773,24 @@ import { appRouter } from '../appRouter'; * @param {Object} item - Item used to generate the footer text. * @param {Object} apiClient - API client instance. * @param {Object} options - Options used to generate the footer text. - * @param {string} showTitle - Flag to show the title in the footer. - * @param {boolean} forceName - Flag to force showing the name of the item. - * @param {boolean} overlayText - Flag to show overlay text. - * @param {Object} imgUrl - Object representing the card's image URL. * @param {string} footerClass - CSS classes of the footer element. * @param {string} progressHtml - HTML markup of the progress bar element. - * @param {string} logoUrl - URL of the logo for the item. - * @param {boolean} isOuterFooter - Flag to mark the text as outer footer. + * @param {Object} flags - Various flags for the footer + * @param {Object} urls - Various urls for the footer * @returns {string} HTML markup of the card's footer text element. */ - function getCardFooterText(item, apiClient, options, showTitle, forceName, overlayText, imgUrl, footerClass, progressHtml, logoUrl, isOuterFooter) { + function getCardFooterText(item, apiClient, options, footerClass, progressHtml, flags, urls) { item = item.ProgramInfo || item; let html = ''; - if (logoUrl) { - html += ''; + if (urls.logoUrl) { + html += ''; } - const showOtherText = isOuterFooter ? !overlayText : overlayText; + const showTitle = options.showTitle === 'auto' ? true : (options.showTitle || item.Type === 'PhotoAlbum' || item.Type === 'Folder'); + const showOtherText = flags.isOuterFooter ? !flags.overlayText : flags.overlayText; - if (isOuterFooter && options.cardLayout && layoutManager.mobile && options.cardFooterAside !== 'none') { + if (flags.isOuterFooter && options.cardLayout && layoutManager.mobile && options.cardFooterAside !== 'none') { html += ``; } @@ -805,7 +802,7 @@ import { appRouter } from '../appRouter'; let titleAdded; if (showOtherText && (options.showParentTitle || options.showParentTitleOrTitle) && !parentTitleUnderneath) { - if (isOuterFooter && item.Type === 'Episode' && item.SeriesName) { + if (flags.isOuterFooter && item.Type === 'Episode' && item.SeriesName) { if (item.SeriesId) { lines.push(getTextActionButton({ Id: item.SeriesId, @@ -835,7 +832,7 @@ import { appRouter } from '../appRouter'; } let showMediaTitle = (showTitle && !titleAdded) || (options.showParentTitleOrTitle && !lines.length); - if (!showMediaTitle && !titleAdded && (showTitle || forceName)) { + if (!showMediaTitle && !titleAdded && (showTitle || flags.forceName)) { showMediaTitle = true; } @@ -856,7 +853,7 @@ import { appRouter } from '../appRouter'; if (showOtherText) { if (options.showParentTitle && parentTitleUnderneath) { - if (isOuterFooter && item.AlbumArtists && item.AlbumArtists.length) { + if (flags.isOuterFooter && item.AlbumArtists && item.AlbumArtists.length) { item.AlbumArtists[0].Type = 'MusicArtist'; item.AlbumArtists[0].IsFolder = true; lines.push(getTextActionButton(item.AlbumArtists[0], null, serverId)); @@ -991,23 +988,23 @@ import { appRouter } from '../appRouter'; } } - if ((showTitle || !imgUrl) && forceName && overlayText && lines.length === 1) { + if ((showTitle || !urls.imgUrl) && flags.forceName && flags.overlayText && lines.length === 1) { lines = []; } - if (overlayText && showTitle) { + if (flags.overlayText && showTitle) { lines = [escapeHtml(item.Name)]; } - const addRightTextMargin = isOuterFooter && options.cardLayout && !options.centerText && options.cardFooterAside !== 'none' && layoutManager.mobile; + const addRightTextMargin = flags.isOuterFooter && options.cardLayout && !options.centerText && options.cardFooterAside !== 'none' && layoutManager.mobile; - html += getCardTextLines(lines, cssClass, !options.overlayText, isOuterFooter, options.cardLayout, addRightTextMargin, options.lines); + html += getCardTextLines(lines, cssClass, !options.overlayText, flags.isOuterFooter, options.cardLayout, addRightTextMargin, options.lines); if (progressHtml) { html += progressHtml; } - if (html && (!isOuterFooter || logoUrl || options.cardLayout)) { + if (html && (!flags.isOuterFooter || urls.logoUrl || options.cardLayout)) { html = '
' + html; //cardFooter @@ -1217,7 +1214,6 @@ import { appRouter } from '../appRouter'; const forceName = imgInfo.forceName; - const showTitle = options.showTitle === 'auto' ? true : (options.showTitle || item.Type === 'PhotoAlbum' || item.Type === 'Folder'); const overlayText = options.overlayText; let cardImageContainerClass = 'cardImageContainer'; @@ -1265,7 +1261,7 @@ import { appRouter } from '../appRouter'; logoUrl = null; footerCssClass = progressHtml ? 'innerCardFooter fullInnerCardFooter' : 'innerCardFooter'; - innerCardFooter += getCardFooterText(item, apiClient, options, showTitle, forceName, overlayText, imgUrl, footerCssClass, progressHtml, logoUrl, false); + innerCardFooter += getCardFooterText(item, apiClient, options, footerCssClass, progressHtml, {forceName: forceName, overlayText: overlayText, isOuterFooter: false}, {imgUrl: imgUrl, logoUrl: logoUrl}); footerOverlayed = true; } else if (progressHtml) { innerCardFooter += '
'; @@ -1292,7 +1288,7 @@ import { appRouter } from '../appRouter'; logoUrl = null; } - outerCardFooter = getCardFooterText(item, apiClient, options, showTitle, forceName, overlayText, imgUrl, footerCssClass, progressHtml, logoUrl, true); + outerCardFooter = getCardFooterText(item, apiClient, options, footerCssClass, progressHtml, {forceName: forceName, overlayText: overlayText, isOuterFooter: true}, {imgUrl: imgUrl, logoUrl: logoUrl}); } if (outerCardFooter && !options.cardLayout) { From 5e5988c1077451b44c44d403394fc2b3726bd43e Mon Sep 17 00:00:00 2001 From: Pier-Luc Ducharme Date: Fri, 30 Dec 2022 12:55:07 -0500 Subject: [PATCH 11/21] Refactor guide to follow max-params rule --- src/components/guide/guide.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/guide/guide.js b/src/components/guide/guide.js index e353d562ab..5e4ee8af00 100644 --- a/src/components/guide/guide.js +++ b/src/components/guide/guide.js @@ -345,7 +345,9 @@ function Guide(options) { } apiClient.getLiveTvPrograms(programQuery).then(function (programsResult) { - renderGuide(context, date, channelsResult.Items, programsResult.Items, renderOptions, apiClient, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs, focusProgramOnRender); + const guideOptions = {focusProgramOnRender: focusProgramOnRender, scrollToTimeMs: scrollToTimeMs, focusToTimeMs: focusToTimeMs, startTimeOfDayMs: startTimeOfDayMs}; + + renderGuide(context, date, channelsResult.Items, programsResult.Items, renderOptions, guideOptions, apiClient); hideLoading(); }); @@ -667,7 +669,7 @@ function Guide(options) { return (channelIndex * 10000000) + (start.getTime() / 60000); } - function renderGuide(context, date, channels, programs, renderOptions, apiClient, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs, focusProgramOnRender) { + function renderGuide(context, date, channels, programs, renderOptions, guideOptions, apiClient) { programs.sort(function (a, b) { return getProgramSortOrder(a, channels) - getProgramSortOrder(b, channels); }); @@ -689,11 +691,11 @@ function Guide(options) { items = {}; renderPrograms(context, date, channels, programs, renderOptions); - if (focusProgramOnRender) { - focusProgram(context, itemId, channelRowId, focusToTimeMs, startTimeOfDayMs); + if (guideOptions.focusProgramOnRender) { + focusProgram(context, itemId, channelRowId, guideOptions.focusToTimeMs, guideOptions.startTimeOfDayMs); } - scrollProgramGridToTimeMs(context, scrollToTimeMs, startTimeOfDayMs); + scrollProgramGridToTimeMs(context, guideOptions.scrollToTimeMs, guideOptions.startTimeOfDayMs); } function scrollProgramGridToTimeMs(context, scrollToTimeMs, startTimeOfDayMs) { From e9369e40bdcd41a4f6f1c8c9c86dd424f162d63b Mon Sep 17 00:00:00 2001 From: Pier-Luc Ducharme Date: Fri, 30 Dec 2022 13:34:50 -0500 Subject: [PATCH 12/21] Refactor imageeditor to follow max-params rule --- src/components/imageeditor/imageeditor.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/imageeditor/imageeditor.js b/src/components/imageeditor/imageeditor.js index 039770a217..85916e5cf8 100644 --- a/src/components/imageeditor/imageeditor.js +++ b/src/components/imageeditor/imageeditor.js @@ -96,7 +96,7 @@ import template from './imageeditor.template.html'; return apiClient.getScaledImageUrl(item.Id || item.ItemId, options); } - function getCardHtml(image, index, numImages, apiClient, imageProviders, imageSize, tagName, enableFooterButtons) { + function getCardHtml(image, apiClient, options) { // TODO move card creation code to Card component let html = ''; @@ -106,7 +106,7 @@ import template from './imageeditor.template.html'; cssClass += ' backdropCard backdropCard-scalable'; - if (tagName === 'button') { + if (options.tagName === 'button') { cssClass += ' btnImageCard'; if (layoutManager.tv) { @@ -122,7 +122,7 @@ import template from './imageeditor.template.html'; html += '
'; @@ -151,23 +151,23 @@ import template from './imageeditor.template.html'; } html += '
'; - if (enableFooterButtons) { + if (options.enableFooterButtons) { html += '
'; if (image.ImageType === 'Backdrop') { - if (index > 0) { + if (options.index > 0) { html += ''; } else { html += ''; } - if (index < numImages - 1) { + if (options.index < options.numImages - 1) { html += ''; } else { html += ''; } } else { - if (imageProviders.length) { + if (options.imageProviders.length) { html += ''; } } @@ -178,7 +178,7 @@ import template from './imageeditor.template.html'; html += '
'; html += '
'; - html += ''; + html += ''; return html; } @@ -226,7 +226,8 @@ import template from './imageeditor.template.html'; for (let i = 0, length = images.length; i < length; i++) { const image = images[i]; - html += getCardHtml(image, i, length, apiClient, imageProviders, imageSize, tagName, enableFooterButtons); + const options = {index: i, numImages: length, imageProviders: imageProviders, imageSize: imageSize, tagName: tagName, enableFooterButtons: enableFooterButtons}; + html += getCardHtml(image, apiClient, options); } elem.innerHTML = html; From 6544b7c698c204cc06d0810a5dcdd2e4f86b78a9 Mon Sep 17 00:00:00 2001 From: Pier-Luc Ducharme Date: Fri, 30 Dec 2022 21:04:16 -0500 Subject: [PATCH 13/21] Refactor playbackmanager to follow max-params rule --- src/components/playback/playbackmanager.js | 166 +++++++++++++-------- 1 file changed, 102 insertions(+), 64 deletions(-) diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index ffcd0e4d91..7591c22917 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -299,20 +299,20 @@ function getAudioMaxValues(deviceProfile) { } let startingPlaySession = new Date().getTime(); -function getAudioStreamUrl(item, transcodingProfile, directPlayContainers, maxBitrate, apiClient, maxAudioSampleRate, maxAudioBitDepth, maxAudioBitrate, startPosition) { +function getAudioStreamUrl(item, transcodingProfile, directPlayContainers, apiClient, startPosition, maxValues) { const url = 'Audio/' + item.Id + '/universal'; startingPlaySession++; return apiClient.getUrl(url, { UserId: apiClient.getCurrentUserId(), DeviceId: apiClient.deviceId(), - MaxStreamingBitrate: maxAudioBitrate || maxBitrate, + MaxStreamingBitrate: maxValues.maxAudioBitrate || maxValues.maxBitrate, Container: directPlayContainers, TranscodingContainer: transcodingProfile.Container || null, TranscodingProtocol: transcodingProfile.Protocol || null, AudioCodec: transcodingProfile.AudioCodec, - MaxAudioSampleRate: maxAudioSampleRate, - MaxAudioBitDepth: maxAudioBitDepth, + MaxAudioSampleRate: maxValues.maxAudioSampleRate, + MaxAudioBitDepth: maxValues.maxAudioBitDepth, api_key: apiClient.accessToken(), PlaySessionId: startingPlaySession, StartTimeTicks: startPosition || 0, @@ -344,7 +344,7 @@ function getAudioStreamUrlFromDeviceProfile(item, deviceProfile, maxBitrate, api const maxValues = getAudioMaxValues(deviceProfile); - return getAudioStreamUrl(item, transcodingProfile, directPlayContainers, maxBitrate, apiClient, maxValues.maxAudioSampleRate, maxValues.maxAudioBitDepth, maxValues.maxAudioBitrate, startPosition); + return getAudioStreamUrl(item, transcodingProfile, directPlayContainers, apiClient, startPosition, { maxBitrate: maxBitrate, ...maxValues }); } function getStreamUrls(items, deviceProfile, maxBitrate, apiClient, startPosition) { @@ -377,7 +377,7 @@ function getStreamUrls(items, deviceProfile, maxBitrate, apiClient, startPositio let streamUrl; if (item.MediaType === 'Audio' && !itemHelper.isLocalItem(item)) { - streamUrl = getAudioStreamUrl(item, audioTranscodingProfile, audioDirectPlayContainers, maxBitrate, apiClient, maxValues.maxAudioSampleRate, maxValues.maxAudioBitDepth, maxValues.maxAudioBitrate, startPosition); + streamUrl = getAudioStreamUrl(item, audioTranscodingProfile, audioDirectPlayContainers, apiClient, startPosition, { maxBitrate: maxBitrate, ...maxValues }); } streamUrls.push(streamUrl || ''); @@ -408,27 +408,12 @@ function setStreamUrls(items, deviceProfile, maxBitrate, apiClient, startPositio }); } -function getPlaybackInfo(player, - apiClient, - item, - deviceProfile, - maxBitrate, - startPosition, - isPlayback, - mediaSourceId, - audioStreamIndex, - subtitleStreamIndex, - liveStreamId, - enableDirectPlay, - enableDirectStream, - allowVideoStreamCopy, - allowAudioStreamCopy, - secondarySubtitleStreamIndex) { +function getPlaybackInfo(player, apiClient, item, deviceProfile, mediaSourceId, liveStreamId, options) { if (!itemHelper.isLocalItem(item) && item.MediaType === 'Audio' && !player.useServerPlaybackInfoForAudio) { return Promise.resolve({ MediaSources: [ { - StreamUrl: getAudioStreamUrlFromDeviceProfile(item, deviceProfile, maxBitrate, apiClient, startPosition), + StreamUrl: getAudioStreamUrlFromDeviceProfile(item, deviceProfile, options.maxBitrate, apiClient, options.startPosition), Id: item.Id, MediaStreams: [], RunTimeTicks: item.RunTimeTicks @@ -446,10 +431,10 @@ function getPlaybackInfo(player, const query = { UserId: apiClient.getCurrentUserId(), - StartTimeTicks: startPosition || 0 + StartTimeTicks: options.startPosition || 0 }; - if (isPlayback) { + if (options.isPlayback) { query.IsPlayback = true; query.AutoOpenLiveStream = true; } else { @@ -457,27 +442,26 @@ function getPlaybackInfo(player, query.AutoOpenLiveStream = false; } - if (audioStreamIndex != null) { - query.AudioStreamIndex = audioStreamIndex; + if (options.audioStreamIndex != null) { + query.AudioStreamIndex = options.audioStreamIndex; } - if (subtitleStreamIndex != null) { - query.SubtitleStreamIndex = subtitleStreamIndex; + if (options.subtitleStreamIndex != null) { + query.SubtitleStreamIndex = options.subtitleStreamIndex; } - if (secondarySubtitleStreamIndex != null) { - query.SecondarySubtitleStreamIndex = secondarySubtitleStreamIndex; + if (options.secondarySubtitleStreamIndex != null) { + query.SecondarySubtitleStreamIndex = options.secondarySubtitleStreamIndex; } - if (enableDirectPlay != null) { - query.EnableDirectPlay = enableDirectPlay; + if (options.enableDirectPlay != null) { + query.EnableDirectPlay = options.enableDirectPlay; } - - if (enableDirectStream != null) { - query.EnableDirectStream = enableDirectStream; + if (options.enableDirectStream != null) { + query.EnableDirectStream = options.enableDirectStream; } - if (allowVideoStreamCopy != null) { - query.AllowVideoStreamCopy = allowVideoStreamCopy; + if (options.allowVideoStreamCopy != null) { + query.AllowVideoStreamCopy = options.allowVideoStreamCopy; } - if (allowAudioStreamCopy != null) { - query.AllowAudioStreamCopy = allowAudioStreamCopy; + if (options.allowAudioStreamCopy != null) { + query.AllowAudioStreamCopy = options.allowAudioStreamCopy; } if (mediaSourceId) { query.MediaSourceId = mediaSourceId; @@ -485,8 +469,8 @@ function getPlaybackInfo(player, if (liveStreamId) { query.LiveStreamId = liveStreamId; } - if (maxBitrate) { - query.MaxStreamingBitrate = maxBitrate; + if (options.maxBitrate) { + query.MaxStreamingBitrate = options.maxBitrate; } if (player.enableMediaProbe && !player.enableMediaProbe(item)) { query.EnableMediaProbe = false; @@ -537,7 +521,7 @@ function getOptimalMediaSource(apiClient, item, versions) { }); } -function getLiveStream(player, apiClient, item, playSessionId, deviceProfile, maxBitrate, startPosition, mediaSource, audioStreamIndex, subtitleStreamIndex) { +function getLiveStream(player, apiClient, item, playSessionId, deviceProfile, mediaSource, options) { const postData = { DeviceProfile: deviceProfile, OpenToken: mediaSource.OpenToken @@ -545,19 +529,19 @@ function getLiveStream(player, apiClient, item, playSessionId, deviceProfile, ma const query = { UserId: apiClient.getCurrentUserId(), - StartTimeTicks: startPosition || 0, + StartTimeTicks: options.startPosition || 0, ItemId: item.Id, PlaySessionId: playSessionId }; - if (maxBitrate) { - query.MaxStreamingBitrate = maxBitrate; + if (options.maxBitrate) { + query.MaxStreamingBitrate = options.maxBitrate; } - if (audioStreamIndex != null) { - query.AudioStreamIndex = audioStreamIndex; + if (options.audioStreamIndex != null) { + query.AudioStreamIndex = options.audioStreamIndex; } - if (subtitleStreamIndex != null) { - query.SubtitleStreamIndex = subtitleStreamIndex; + if (options.subtitleStreamIndex != null) { + query.SubtitleStreamIndex = options.subtitleStreamIndex; } // lastly, enforce player overrides for special situations @@ -1737,7 +1721,19 @@ class PlaybackManager { const currentPlayOptions = currentItem.playOptions || getDefaultPlayOptions(); - getPlaybackInfo(player, apiClient, currentItem, deviceProfile, maxBitrate, ticks, true, currentMediaSource.Id, audioStreamIndex, subtitleStreamIndex, liveStreamId, params.EnableDirectPlay, params.EnableDirectStream, params.AllowVideoStreamCopy, params.AllowAudioStreamCopy).then(function (result) { + const options = { + maxBitrate: maxBitrate, + startPosition: ticks, + isPlayback: true, + audioStreamIndex: audioStreamIndex, + subtitleStreamIndex: subtitleStreamIndex, + enableDirectPlay: params.EnableDirectPlay, + enableDirectStream: params.EnableDirectStream, + allowVideoStreamCopy: params.AllowVideoStreamCopy, + allowAudioStreamCopy: params.AllowAudioStreamCopy + }; + + getPlaybackInfo(player, apiClient, currentItem, deviceProfile, currentMediaSource.Id, liveStreamId, options).then(function (result) { if (validatePlaybackInfoResult(self, result)) { currentMediaSource = result.MediaSources[0]; @@ -2303,17 +2299,17 @@ class PlaybackManager { }, reject); } - function sendPlaybackListToPlayer(player, items, deviceProfile, maxBitrate, apiClient, startPositionTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, startIndex) { - return setStreamUrls(items, deviceProfile, maxBitrate, apiClient, startPositionTicks).then(function () { + function sendPlaybackListToPlayer(player, items, deviceProfile, apiClient, mediaSourceId, options) { + return setStreamUrls(items, deviceProfile, options.maxBitrate, apiClient, options.startPosition).then(function () { loading.hide(); return player.play({ items: items, - startPositionTicks: startPositionTicks || 0, + startPositionTicks: options.startPosition || 0, mediaSourceId: mediaSourceId, - audioStreamIndex: audioStreamIndex, - subtitleStreamIndex: subtitleStreamIndex, - startIndex: startIndex + audioStreamIndex: options.audioStreamIndex, + subtitleStreamIndex: options.subtitleStreamIndex, + startIndex: options.startIndex }); }); } @@ -2474,15 +2470,27 @@ class PlaybackManager { const mediaSourceId = playOptions.mediaSourceId; const audioStreamIndex = playOptions.audioStreamIndex; const subtitleStreamIndex = playOptions.subtitleStreamIndex; + const options = { + maxBitrate: maxBitrate, + startPosition: startPosition, + isPlayback: null, + audioStreamIndex: audioStreamIndex, + subtitleStreamIndex: subtitleStreamIndex, + startIndex: playOptions.startIndex, + enableDirectPlay: null, + enableDirectStream: null, + allowVideoStreamCopy: null, + allowAudioStreamCopy: null + }; if (player && !enableLocalPlaylistManagement(player)) { - return sendPlaybackListToPlayer(player, playOptions.items, deviceProfile, maxBitrate, apiClient, startPosition, mediaSourceId, audioStreamIndex, subtitleStreamIndex, playOptions.startIndex); + return sendPlaybackListToPlayer(player, playOptions.items, deviceProfile, apiClient, mediaSourceId, options); } // this reference was only needed by sendPlaybackListToPlayer playOptions.items = null; - return getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, mediaSourceId, audioStreamIndex, subtitleStreamIndex).then(async (mediaSource) => { + return getPlaybackMediaSource(player, apiClient, deviceProfile, item, mediaSourceId, options).then(async (mediaSource) => { const user = await apiClient.getCurrentUser(); autoSetNextTracks(prevSource, mediaSource, user.Configuration.RememberAudioSelections, user.Configuration.RememberSubtitleSelections); @@ -2540,7 +2548,20 @@ class PlaybackManager { const maxBitrate = getSavedMaxStreamingBitrate(ServerConnections.getApiClient(item.ServerId), mediaType); return player.getDeviceProfile(item).then(function (deviceProfile) { - return getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, options.mediaSourceId, options.audioStreamIndex, options.subtitleStreamIndex).then(function (mediaSource) { + const mediaOptions = { + maxBitrate: maxBitrate, + startPosition: startPosition, + isPlayback: null, + audioStreamIndex: options.audioStreamIndex, + subtitleStreamIndex: options.subtitleStreamIndex, + startIndex: null, + enableDirectPlay: null, + enableDirectStream: null, + allowVideoStreamCopy: null, + allowAudioStreamCopy: null + }; + + return getPlaybackMediaSource(player, apiClient, deviceProfile, item, options.mediaSourceId, mediaOptions).then(function (mediaSource) { return createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition, player); }); }); @@ -2560,7 +2581,19 @@ class PlaybackManager { const maxBitrate = getSavedMaxStreamingBitrate(ServerConnections.getApiClient(item.ServerId), mediaType); return player.getDeviceProfile(item).then(function (deviceProfile) { - return getPlaybackInfo(player, apiClient, item, deviceProfile, maxBitrate, startPosition, false, null, null, null, null).then(function (playbackInfoResult) { + const mediaOptions = { + maxBitrate: maxBitrate, + startPosition: startPosition, + isPlayback: true, + audioStreamIndex: null, + subtitleStreamIndex: null, + enableDirectPlay: null, + enableDirectStream: null, + allowVideoStreamCopy: null, + allowAudioStreamCopy: null + }; + + return getPlaybackInfo(player, apiClient, item, deviceProfile, null, null, mediaOptions).then(function (playbackInfoResult) { return playbackInfoResult.MediaSources; }); }); @@ -2701,13 +2734,18 @@ class PlaybackManager { return tracks; } - function getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, mediaSourceId, audioStreamIndex, subtitleStreamIndex) { - return getPlaybackInfo(player, apiClient, item, deviceProfile, maxBitrate, startPosition, true, mediaSourceId, audioStreamIndex, subtitleStreamIndex, null).then(function (playbackInfoResult) { + function getPlaybackMediaSource(player, apiClient, deviceProfile, item, mediaSourceId, options) { + options.isPlayback = true; + + return getPlaybackInfo(player, apiClient, item, deviceProfile, mediaSourceId, null, options).then(function (playbackInfoResult) { if (validatePlaybackInfoResult(self, playbackInfoResult)) { return getOptimalMediaSource(apiClient, item, playbackInfoResult.MediaSources).then(function (mediaSource) { if (mediaSource) { if (mediaSource.RequiresOpening && !mediaSource.LiveStreamId) { - return getLiveStream(player, apiClient, item, playbackInfoResult.PlaySessionId, deviceProfile, maxBitrate, startPosition, mediaSource, null, null).then(function (openLiveStreamResult) { + options.audioStreamIndex = null; + options.subtitleStreamIndex = null; + + return getLiveStream(player, apiClient, item, playbackInfoResult.PlaySessionId, deviceProfile, mediaSource, options).then(function (openLiveStreamResult) { return supportsDirectPlay(apiClient, item, openLiveStreamResult.MediaSource).then(function (result) { openLiveStreamResult.MediaSource.enableDirectPlay = result; return openLiveStreamResult.MediaSource; From 29f16d5a04089d8ff87b40405bce296537c19084 Mon Sep 17 00:00:00 2001 From: Pier-Luc Ducharme Date: Mon, 16 Jan 2023 21:27:49 -0500 Subject: [PATCH 14/21] Refactor userdatabuttons to follow max-params rule --- .../userdatabuttons/userdatabuttons.js | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/components/userdatabuttons/userdatabuttons.js b/src/components/userdatabuttons/userdatabuttons.js index c170a7cc0f..c5cb746f9c 100644 --- a/src/components/userdatabuttons/userdatabuttons.js +++ b/src/components/userdatabuttons/userdatabuttons.js @@ -12,7 +12,10 @@ const userDataMethods = { markFavorite: markFavorite }; -function getUserDataButtonHtml(method, itemId, serverId, buttonCssClass, iconCssClass, icon, tooltip, style) { +function getUserDataButtonHtml(method, itemId, serverId, icon, tooltip, style, classes) { + let buttonCssClass = classes.buttonCssClass; + let iconCssClass = classes.iconCssClass; + if (style === 'fab-mini') { style = 'fab'; buttonCssClass = buttonCssClass ? (buttonCssClass + ' mini') : 'mini'; @@ -97,6 +100,11 @@ function getIconsHtml(options) { const iconCssClass = options.iconCssClass; + const classes = { + buttonCssClass: btnCssClass, + iconCssClass: iconCssClass + }; + const serverId = item.ServerId; if (includePlayed !== false) { @@ -104,18 +112,21 @@ function getIconsHtml(options) { if (itemHelper.canMarkPlayed(item)) { if (userData.Played) { - html += getUserDataButtonHtml('markPlayed', itemId, serverId, btnCssClass + ' btnUserDataOn', iconCssClass, 'check', tooltipPlayed, style); + const buttonCssClass = classes.buttonCssClass + ' btnUserDataOn'; + html += getUserDataButtonHtml('markPlayed', itemId, serverId, 'check', tooltipPlayed, style, {buttonCssClass: buttonCssClass, ...classes}); } else { - html += getUserDataButtonHtml('markPlayed', itemId, serverId, btnCssClass, iconCssClass, 'check', tooltipPlayed, style); + html += getUserDataButtonHtml('markPlayed', itemId, serverId, 'check', tooltipPlayed, style, classes); } } } const tooltipFavorite = globalize.translate('Favorite'); if (userData.IsFavorite) { - html += getUserDataButtonHtml('markFavorite', itemId, serverId, btnCssClass + ' btnUserData btnUserDataOn', iconCssClass, 'favorite', tooltipFavorite, style); + const buttonCssClass = classes.buttonCssClass + ' btnUserData btnUserDataOn'; + html += getUserDataButtonHtml('markFavorite', itemId, serverId, 'favorite', tooltipFavorite, style, {buttonCssClass: buttonCssClass, ...classes}); } else { - html += getUserDataButtonHtml('markFavorite', itemId, serverId, btnCssClass + ' btnUserData', iconCssClass, 'favorite', tooltipFavorite, style); + classes.buttonCssClass += ' btnUserData'; + html += getUserDataButtonHtml('markFavorite', itemId, serverId, 'favorite', tooltipFavorite, style, classes); } return html; From a3c330b4e1c67b75e8a3e3d05e5f1d8a102b184c Mon Sep 17 00:00:00 2001 From: Pier-Luc Ducharme Date: Mon, 30 Jan 2023 22:52:49 -0500 Subject: [PATCH 15/21] Add contributor to project --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c867d81580..f3dbfec6da 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -59,6 +59,7 @@ - [Vankerkom](https://github.com/vankerkom) - [edvwib](https://github.com/edvwib) - [Rob Farraher](https://github.com/farraherbg) + - [Pier-Luc Ducharme](https://github.com/pl-ducharme) # Emby Contributors From cf5d65d86ec4c44c72e8c73deee998d931543102 Mon Sep 17 00:00:00 2001 From: Pier-Luc Ducharme Date: Thu, 9 Feb 2023 21:35:59 -0500 Subject: [PATCH 16/21] Simplify code with object shorthand syntax --- src/components/cardbuilder/cardBuilder.js | 4 +-- src/components/guide/guide.js | 2 +- src/components/imageeditor/imageeditor.js | 4 +-- src/components/playback/playbackmanager.js | 30 +++++++++---------- .../userdatabuttons/userdatabuttons.js | 11 ++----- 5 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 657a4102d9..a0c4aaf09b 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -1261,7 +1261,7 @@ import { appRouter } from '../appRouter'; logoUrl = null; footerCssClass = progressHtml ? 'innerCardFooter fullInnerCardFooter' : 'innerCardFooter'; - innerCardFooter += getCardFooterText(item, apiClient, options, footerCssClass, progressHtml, {forceName: forceName, overlayText: overlayText, isOuterFooter: false}, {imgUrl: imgUrl, logoUrl: logoUrl}); + innerCardFooter += getCardFooterText(item, apiClient, options, footerCssClass, progressHtml, { forceName, overlayText, isOuterFooter: false }, { imgUrl, logoUrl }); footerOverlayed = true; } else if (progressHtml) { innerCardFooter += '
'; @@ -1288,7 +1288,7 @@ import { appRouter } from '../appRouter'; logoUrl = null; } - outerCardFooter = getCardFooterText(item, apiClient, options, footerCssClass, progressHtml, {forceName: forceName, overlayText: overlayText, isOuterFooter: true}, {imgUrl: imgUrl, logoUrl: logoUrl}); + outerCardFooter = getCardFooterText(item, apiClient, options, footerCssClass, progressHtml, { forceName, overlayText, isOuterFooter: true }, { imgUrl, logoUrl }); } if (outerCardFooter && !options.cardLayout) { diff --git a/src/components/guide/guide.js b/src/components/guide/guide.js index 5e4ee8af00..63a682ee65 100644 --- a/src/components/guide/guide.js +++ b/src/components/guide/guide.js @@ -345,7 +345,7 @@ function Guide(options) { } apiClient.getLiveTvPrograms(programQuery).then(function (programsResult) { - const guideOptions = {focusProgramOnRender: focusProgramOnRender, scrollToTimeMs: scrollToTimeMs, focusToTimeMs: focusToTimeMs, startTimeOfDayMs: startTimeOfDayMs}; + const guideOptions = { focusProgramOnRender, scrollToTimeMs, focusToTimeMs, startTimeOfDayMs }; renderGuide(context, date, channelsResult.Items, programsResult.Items, renderOptions, guideOptions, apiClient); diff --git a/src/components/imageeditor/imageeditor.js b/src/components/imageeditor/imageeditor.js index 85916e5cf8..9130bd311c 100644 --- a/src/components/imageeditor/imageeditor.js +++ b/src/components/imageeditor/imageeditor.js @@ -132,7 +132,7 @@ import template from './imageeditor.template.html'; html += '
'; - const imageUrl = getImageUrl(currentItem, apiClient, image.ImageType, image.ImageIndex, {maxWidth: options.imageSize}); + const imageUrl = getImageUrl(currentItem, apiClient, image.ImageType, image.ImageIndex, { maxWidth: options.imageSize }); html += '
'; @@ -226,7 +226,7 @@ import template from './imageeditor.template.html'; for (let i = 0, length = images.length; i < length; i++) { const image = images[i]; - const options = {index: i, numImages: length, imageProviders: imageProviders, imageSize: imageSize, tagName: tagName, enableFooterButtons: enableFooterButtons}; + const options = { index: i, numImages: length, imageProviders, imageSize, tagName, enableFooterButtons }; html += getCardHtml(image, apiClient, options); } diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 7591c22917..947f46eec8 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -344,7 +344,7 @@ function getAudioStreamUrlFromDeviceProfile(item, deviceProfile, maxBitrate, api const maxValues = getAudioMaxValues(deviceProfile); - return getAudioStreamUrl(item, transcodingProfile, directPlayContainers, apiClient, startPosition, { maxBitrate: maxBitrate, ...maxValues }); + return getAudioStreamUrl(item, transcodingProfile, directPlayContainers, apiClient, startPosition, { maxBitrate, ...maxValues }); } function getStreamUrls(items, deviceProfile, maxBitrate, apiClient, startPosition) { @@ -377,7 +377,7 @@ function getStreamUrls(items, deviceProfile, maxBitrate, apiClient, startPositio let streamUrl; if (item.MediaType === 'Audio' && !itemHelper.isLocalItem(item)) { - streamUrl = getAudioStreamUrl(item, audioTranscodingProfile, audioDirectPlayContainers, apiClient, startPosition, { maxBitrate: maxBitrate, ...maxValues }); + streamUrl = getAudioStreamUrl(item, audioTranscodingProfile, audioDirectPlayContainers, apiClient, startPosition, { maxBitrate, ...maxValues }); } streamUrls.push(streamUrl || ''); @@ -1722,11 +1722,11 @@ class PlaybackManager { const currentPlayOptions = currentItem.playOptions || getDefaultPlayOptions(); const options = { - maxBitrate: maxBitrate, + maxBitrate, startPosition: ticks, isPlayback: true, - audioStreamIndex: audioStreamIndex, - subtitleStreamIndex: subtitleStreamIndex, + audioStreamIndex, + subtitleStreamIndex, enableDirectPlay: params.EnableDirectPlay, enableDirectStream: params.EnableDirectStream, allowVideoStreamCopy: params.AllowVideoStreamCopy, @@ -2304,9 +2304,9 @@ class PlaybackManager { loading.hide(); return player.play({ - items: items, + items, startPositionTicks: options.startPosition || 0, - mediaSourceId: mediaSourceId, + mediaSourceId, audioStreamIndex: options.audioStreamIndex, subtitleStreamIndex: options.subtitleStreamIndex, startIndex: options.startIndex @@ -2471,11 +2471,11 @@ class PlaybackManager { const audioStreamIndex = playOptions.audioStreamIndex; const subtitleStreamIndex = playOptions.subtitleStreamIndex; const options = { - maxBitrate: maxBitrate, - startPosition: startPosition, + maxBitrate, + startPosition, isPlayback: null, - audioStreamIndex: audioStreamIndex, - subtitleStreamIndex: subtitleStreamIndex, + audioStreamIndex, + subtitleStreamIndex, startIndex: playOptions.startIndex, enableDirectPlay: null, enableDirectStream: null, @@ -2549,8 +2549,8 @@ class PlaybackManager { return player.getDeviceProfile(item).then(function (deviceProfile) { const mediaOptions = { - maxBitrate: maxBitrate, - startPosition: startPosition, + maxBitrate, + startPosition, isPlayback: null, audioStreamIndex: options.audioStreamIndex, subtitleStreamIndex: options.subtitleStreamIndex, @@ -2582,8 +2582,8 @@ class PlaybackManager { return player.getDeviceProfile(item).then(function (deviceProfile) { const mediaOptions = { - maxBitrate: maxBitrate, - startPosition: startPosition, + maxBitrate, + startPosition, isPlayback: true, audioStreamIndex: null, subtitleStreamIndex: null, diff --git a/src/components/userdatabuttons/userdatabuttons.js b/src/components/userdatabuttons/userdatabuttons.js index c5cb746f9c..f75bbce81e 100644 --- a/src/components/userdatabuttons/userdatabuttons.js +++ b/src/components/userdatabuttons/userdatabuttons.js @@ -99,12 +99,7 @@ function getIconsHtml(options) { } const iconCssClass = options.iconCssClass; - - const classes = { - buttonCssClass: btnCssClass, - iconCssClass: iconCssClass - }; - + const classes = { buttonCssClass: btnCssClass, iconCssClass: iconCssClass }; const serverId = item.ServerId; if (includePlayed !== false) { @@ -113,7 +108,7 @@ function getIconsHtml(options) { if (itemHelper.canMarkPlayed(item)) { if (userData.Played) { const buttonCssClass = classes.buttonCssClass + ' btnUserDataOn'; - html += getUserDataButtonHtml('markPlayed', itemId, serverId, 'check', tooltipPlayed, style, {buttonCssClass: buttonCssClass, ...classes}); + html += getUserDataButtonHtml('markPlayed', itemId, serverId, 'check', tooltipPlayed, style, { buttonCssClass, ...classes }); } else { html += getUserDataButtonHtml('markPlayed', itemId, serverId, 'check', tooltipPlayed, style, classes); } @@ -123,7 +118,7 @@ function getIconsHtml(options) { const tooltipFavorite = globalize.translate('Favorite'); if (userData.IsFavorite) { const buttonCssClass = classes.buttonCssClass + ' btnUserData btnUserDataOn'; - html += getUserDataButtonHtml('markFavorite', itemId, serverId, 'favorite', tooltipFavorite, style, {buttonCssClass: buttonCssClass, ...classes}); + html += getUserDataButtonHtml('markFavorite', itemId, serverId, 'favorite', tooltipFavorite, style, { buttonCssClass, ...classes }); } else { classes.buttonCssClass += ' btnUserData'; html += getUserDataButtonHtml('markFavorite', itemId, serverId, 'favorite', tooltipFavorite, style, classes); From ecb84ff351eddca7f069431edca3c795cfea5a00 Mon Sep 17 00:00:00 2001 From: WontTell Date: Wed, 8 Mar 2023 02:55:32 +0000 Subject: [PATCH 17/21] Translated using Weblate (Spanish (Latin America)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/es_419/ --- src/strings/es_419.json | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/strings/es_419.json b/src/strings/es_419.json index db7ec1884c..a09b64a0b9 100644 --- a/src/strings/es_419.json +++ b/src/strings/es_419.json @@ -571,7 +571,7 @@ "LabelSelectVersionToInstall": "Seleccionar versión a instalar:", "LabelSelectUsers": "Seleccionar usuarios:", "LabelSelectFolderGroupsHelp": "Las carpetas sin marcar serán mostradas individualmente en su propia vista.", - "LabelSelectFolderGroups": "Agrupar automáticamente el contenido de las siguientes carpetas a vistas como Películas, Música y TV:", + "LabelSelectFolderGroups": "Agrupar automáticamente el contenido de las siguientes carpetas a vistas como «Películas», «Música» y «TV»:", "LabelSeasonNumber": "Temporada número:", "LabelScreensaver": "Protector de pantalla:", "LabelScheduledTaskLastRan": "Última ejecución {0}, tomando {1}.", @@ -1006,7 +1006,7 @@ "LabelMaxResumePercentageHelp": "Los medios se consideran totalmente reproducidos si se detienen después de este tiempo.", "LabelMaxResumePercentage": "Porcentaje máximo para la reanudación:", "LabelMaxParentalRating": "Máxima clasificación parental permitida:", - "LabelMaxChromecastBitrate": "Calidad de transmisión de Chromecast:", + "LabelMaxChromecastBitrate": "Calidad de transmisión de Google Cast:", "LabelMaxBackdropsPerItem": "Número máximo de imágenes de fondo por elemento:", "LabelMatchType": "Tipo de coincidencia:", "LabelManufacturerUrl": "URL del fabricante:", @@ -1354,7 +1354,7 @@ "MessagePluginInstallError": "Ocurrió un error instalando el complemento.", "PlaybackRate": "Tasa de reproducción", "Data": "Datos", - "ButtonUseQuickConnect": "Usar conexión rápida", + "ButtonUseQuickConnect": "Usar «Conexión rápida»", "ButtonActivate": "Activar", "Authorize": "Autorizar", "LabelCurrentStatus": "Estado actual:", @@ -1366,7 +1366,7 @@ "TonemappingAlgorithmHelp": "El mapeo de tonos puede ser modificado. Si no estas familiarizado con estas opciones, solo manten el predeterminado. El valor recomendado es Hable.", "LabelTonemappingThresholdHelp": "El algoritmo de mapeo de tonos puede ser finamente ajustado para cada escena. Y el umbral es usado para detectar si la escena ha cambiado. Si los valores entre el promedio de brillo del fotograma actual y el promedio actual excede el valor de umbral, se vuelve a calcular el brillo promedio y máximo. Los valores recomendados y por defecto son entre 0.2 y 0.8.", "ThumbCard": "Miniatura", - "LabelMaxMuxingQueueSizeHelp": "Numero máximo de paquetes que pueden estar en búfer mientras se espera a que se inicialicen todas las trasmisiones. Intenta incrementarlos si encuentras el error \"Too many packets buffered for output stream\" en los registros de ffmpeg. El valor recomendado es 2048.", + "LabelMaxMuxingQueueSizeHelp": "Numero máximo de paquetes que pueden estar en búfer mientras se espera a que se inicialicen todas las trasmisiones. Intenta incrementarlos si encuentras el error «Too many packets buffered for output stream» en los registros de ffmpeg. El valor recomendado es 2048.", "LabelMaxMuxingQueueSize": "Tamaño máximo cola de mezclado:", "LabelTonemappingParamHelp": "Modifica el algoritmo de mapeo de tonos. Los valores recomendados y por defecto son NaN. Se puede dejar en blanco.", "LabelTonemappingParam": "Parámetro Mapeo de tonos:", @@ -1380,7 +1380,7 @@ "LabelTonemappingAlgorithm": "Seleccione el algoritmo de mapeo de tonos:", "AllowTonemappingHelp": "El mapeo de tonos puede transformar el rango dinámico de un video de HDR a SDR manteniendo la calidad de imagen y los colores, que son datos muy importantes para mostrar la imagen original. Actualmente solo funciona con videos HDR10 o HLG. Esto requiere los tiempos de ejecución OpenCL o CUDA correspondientes.", "EnableTonemapping": "Habilitar mapeo de tonos", - "LabelOpenclDeviceHelp": "Este es el dispositivo OpenCL que se utiliza para el mapeo de tonos. El lado izquierdo del punto es el número de plataforma y el lado derecho es el número de dispositivo en la plataforma. El valor predeterminado es 0.0. Se requiere la aplicación ffmpeg que contiene el método de aceleración de hardware OpenCL.", + "LabelOpenclDeviceHelp": "Este es el dispositivo OpenCL que se utiliza para el mapeo de tonos. El lado izquierdo del punto es el número de plataforma y el lado derecho es el número de dispositivo en la plataforma. El valor predeterminado es 0.0. Se requiere la aplicación FFmpeg que contiene el método de aceleración de hardware OpenCL.", "LabelOpenclDevice": "Dispositivo OpenCL:", "LabelColorPrimaries": "Colores primarios:", "LabelColorTransfer": "Transferencia de Color:", @@ -1400,22 +1400,22 @@ "SelectServer": "Seleccionar Servidor", "Restart": "Reiniciar", "ResetPassword": "Resetear contraseña", - "QuickConnectNotActive": "Conexión Rápida no esta habilitado en el servidor", - "QuickConnectNotAvailable": "Pregunte al administrador del servidor para habilitar Conexión Rápida", - "QuickConnectInvalidCode": "Código Conexión Rápida inválido", - "QuickConnectDescription": "Para ingresar con Conexión Rápida, seleccione el botón Conexión Rápida en el dispositivo del cual estas ingresando e ingrese el siguiente código.", - "QuickConnectDeactivated": "Conexión Rápida se desactivó antes de que se pudiera aprobar la solicitud de inicio de sesión", - "QuickConnectAuthorizeFail": "Código Conexión Rápida desconocido", - "QuickConnectAuthorizeSuccess": "Requiere autorización", + "QuickConnectNotActive": "«Conexión rápida» no está habilitado en este servidor", + "QuickConnectNotAvailable": "Pide al administrador del servidor que habilite la «Conexión rápida»", + "QuickConnectInvalidCode": "Código de «Conexión rápida» inválido", + "QuickConnectDescription": "Para ingresar con «Conexión rápida», seleccione el botón «Conexión rápida» en el dispositivo del cual estas ingresando e ingrese el código mostrado a continuación.", + "QuickConnectDeactivated": "La «Conexión rápida» se desactivó antes de que se pudiera aprobar la solicitud de inicio de sesión", + "QuickConnectAuthorizeFail": "Código de «Conexión rápida» desconocido", + "QuickConnectAuthorizeSuccess": "Solicitud autorizada", "QuickConnectAuthorizeCode": "Ingrese el código {0} para acceder", - "QuickConnectActivationSuccessful": "Activación Exitosa", - "QuickConnect": "Conexión Rápida", + "QuickConnectActivationSuccessful": "Activado exitosamente", + "QuickConnect": "Conexión rápida", "Profile": "Perfil", "PosterCard": "Imagen del Cartel", "Poster": "Cartel", "Photo": "Foto", "MusicVideos": "Videos musicales", - "LabelQuickConnectCode": "Código conexión rápida:", + "LabelQuickConnectCode": "Código de «Conexión rápida»:", "LabelKnownProxies": "Proxies conocidos:", "LabelIconMaxResHelp": "Resolución maxima de los iconos por medio de la función 'upnp:icon'.", "EnableAutoCast": "Establecer como Predeterminado", @@ -1519,7 +1519,7 @@ "LabelSlowResponseEnabled": "Log de alarma si la respuesta del servidor fue lenta", "UseEpisodeImagesInNextUpHelp": "Las secciones Siguiente y Continuar viendo utilizaran imagenes del episodio como miniaturas en lugar de miniaturas del show.", "UseEpisodeImagesInNextUp": "Usar imágenes de los episodios en \"Siguiente\" y \"Continuar Viendo\"", - "LabelLocalCustomCss": "El CSS personalizado aplica solo a este cliente. Puede quieras deshabilitar el CSS del servidor.", + "LabelLocalCustomCss": "Código CSS personalizado para estilo que aplica solo a este cliente. Puede que quiera deshabilitar el código CSS personalizado del servidor.", "LabelDisableCustomCss": "Desactivar el código CSS personalizado para la tematización/marca proporcionada desde el servidor.", "DisableCustomCss": "Deshabilitar CSS proveniente del servidor", "LabelAutomaticallyAddToCollectionHelp": "Cuando al menos 2 películas tengan el mismo nombre de colección, se añadirán automáticamente a dicha colección.", @@ -1574,5 +1574,6 @@ "LabelDummyChapterDurationHelp": "El intervalo de la extracción de las imágenes de los capítulos, en segundos.", "LabelDummyChapterCount": "Límite:", "LabelChapterImageResolution": "Resolución:", - "LabelChapterImageResolutionHelp": "La resolución de las imágenes de capítulos extraídas." + "LabelChapterImageResolutionHelp": "La resolución de las imágenes de capítulos extraídas.", + "LabelMaxDaysForNextUp": "Días máximos en «A continuación»:" } From 2d95a95eaea64ad76565351776bba3b5800c1ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20=C3=96rn=20Ketilsson?= Date: Wed, 8 Mar 2023 16:17:30 +0000 Subject: [PATCH 18/21] Translated using Weblate (Icelandic) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/is/ --- src/strings/is-is.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/strings/is-is.json b/src/strings/is-is.json index 585b335c18..1d8d08631c 100644 --- a/src/strings/is-is.json +++ b/src/strings/is-is.json @@ -455,5 +455,16 @@ "ButtonActivate": "Virkja", "ButtonClose": "Loka", "ButtonSpace": "Bil", - "Authorize": "Heimila" + "Authorize": "Heimila", + "ChangingMetadataImageSettingsNewContent": "Breytingar á niðurhali lýsigögnum eða myndum mun aðeins gilda um ný efni sem bætt hafa verið í safnið. Til að breyta núverandi titla þarftu að endurnýja lýsigögnin handvirkt.", + "ColorTransfer": "Litaflutningur", + "Data": "Gögn", + "ClearQueue": "Hreinsa biðröð", + "DailyAt": "Daglega um {0}", + "DashboardServerName": "Þjónn: {0}", + "DashboardVersionNumber": "Útgáfa: {0}", + "ColorSpace": "Litarými", + "Copied": "Aftritað", + "Copy": "Afrita", + "CopyFailed": "Gat ekki afritað" } From aab068565d50b3ed617ea415133cea1f401e98bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3bert=20=C3=96rn=20Ketilsson?= Date: Wed, 8 Mar 2023 19:16:06 +0000 Subject: [PATCH 19/21] Translated using Weblate (Icelandic) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/is/ --- src/strings/is-is.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/strings/is-is.json b/src/strings/is-is.json index 1d8d08631c..4d131ff8ad 100644 --- a/src/strings/is-is.json +++ b/src/strings/is-is.json @@ -466,5 +466,17 @@ "ColorSpace": "Litarými", "Copied": "Aftritað", "Copy": "Afrita", - "CopyFailed": "Gat ekki afritað" + "CopyFailed": "Gat ekki afritað", + "ButtonExitApp": "Fara úr forriti", + "EnableFasterAnimations": "Hraðari hreyfimyndir", + "EnablePlugin": "Virkja", + "Bwdif": "BWDIF", + "DisablePlugin": "Slökkva", + "EnableAutoCast": "Setja sem Sjálfgefið", + "EnableDetailsBanner": "Upplýsingar borði", + "DeleteAll": "Eyða Öllu", + "ButtonBackspace": "Backspace", + "ButtonUseQuickConnect": "Nota hraðtengingu", + "Digital": "Stafrænt", + "DownloadAll": "Sækja allt" } From 16c47fc74f473e91f4c56b3bcc9fcf3438f2cca8 Mon Sep 17 00:00:00 2001 From: fdolbec123 Date: Thu, 9 Mar 2023 16:51:22 +0000 Subject: [PATCH 20/21] Translated using Weblate (French (Canada)) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/fr_CA/ --- src/strings/fr-ca.json | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/strings/fr-ca.json b/src/strings/fr-ca.json index 26e2fe3a71..15f1fe017b 100644 --- a/src/strings/fr-ca.json +++ b/src/strings/fr-ca.json @@ -334,7 +334,7 @@ "DownloadsValue": "{0} téléchargements", "DisplayModeHelp": "Sélectionner l'agencement que vous désirez pour l'interface.", "DisplayMissingEpisodesWithinSeasons": "Afficher les épisodes manquants dans les saisons", - "DisplayInOtherHomeScreenSections": "Afficher dans les sections de l’écran d’accueil comme « Ajouts récents » et reprendre le visionnement", + "DisplayInOtherHomeScreenSections": "Afficher dans les sections de l’écran d’accueil comme « Ajouts récents » et «Reprendre le visionnement»", "Directors": "Réalisateurs", "Director": "Réalisateur", "DetectingDevices": "Détection des appareils", @@ -349,7 +349,7 @@ "HeaderCodecProfile": "Profil de codec", "HeaderChapterImages": "Images des chapitres", "HeaderChannelAccess": "Accès aux chaînes", - "LatestFromLibrary": "{0}, ajouts récents", + "LatestFromLibrary": "{0} ajouts récents", "HideWatchedContentFromLatestMedia": "Masquer le contenu déjà vu dans les 'Derniers Médias'", "HeaderLatestRecordings": "Derniers enregistrements", "HeaderLatestMusic": "Dernière musique", @@ -1020,5 +1020,25 @@ "LabelMaxParentalRating": "Classification parentale maximale :", "SpecialFeatures": "Bonus", "Sort": "Trier", - "SortByValue": "Trier par" + "SortByValue": "Trier par", + "LabelMovieCategories": "Catégories de films:", + "LabelNewPassword": "Nouveau mot de passe:", + "LabelOriginalName": "Nom original:", + "LabelPasswordRecoveryPinCode": "Code NIP:", + "LabelOriginalTitle": "Titre original:", + "LabelMaxStreamingBitrate": "Qualité maximale de streaming:", + "LabelNotificationEnabled": "Activer cette notification", + "LabelNewName": "Nouveau nom:", + "LabelNewPasswordConfirm": "Confirmer le nouveau mot de passe:", + "LabelPersonRole": "Rôle:", + "LabelPasswordConfirm": "Confirmer le mot de passe:", + "LabelPersonRoleHelp": "Exemple: Conducteur de camion à crème glacée", + "LabelPleaseRestart": "Les changements prendront effets après avoir rechargé manuellement le client web.", + "LabelMinAudiobookResumeHelp": "Les titres sont considérés comme non joué s'ils sont arrêtés avant cette durée.", + "LabelPassword": "Mot de passe:", + "LabelPath": "Chemin:", + "LabelMetadataPath": "Chemin des métadonnées:", + "LabelDummyChapterDuration": "Intervalle:", + "LabelDummyChapterCount": "Limite:", + "LabelChapterImageResolution": "Résolution:" } From 339311008e1c06c81ff21507a2cd8fa4d3c79f94 Mon Sep 17 00:00:00 2001 From: stanol Date: Thu, 9 Mar 2023 18:59:01 +0000 Subject: [PATCH 21/21] Translated using Weblate (Ukrainian) Translation: Jellyfin/Jellyfin Web Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-web/uk/ --- src/strings/uk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strings/uk.json b/src/strings/uk.json index 3a6d695e64..7a7527dc02 100644 --- a/src/strings/uk.json +++ b/src/strings/uk.json @@ -1439,7 +1439,7 @@ "SubtitleDownloadersHelp": "Увімкніть та оцініть бажані завантажувачі субтитрів у порядку пріоритету.", "SubtitleAppearanceSettingsDisclaimer": "Наведені нижче налаштування не застосовуються до графічних субтитрів, згаданих вище, або субтитрів ASS/SSA, які вбудовують власні стилі.", "SubtitleAppearanceSettingsAlsoPassedToCastDevices": "Ці налаштування також застосовуються до будь-якого відтворення Google Cast, запущеного цим пристроєм.", - "Subtitle": "Підзаголовок", + "Subtitle": "Субтитри", "Studios": "Студії", "StopRecording": "Зупинити запис", "StopPlayback": "Зупинити відтворення",