diff --git a/.eslintrc.js b/.eslintrc.js index 469bd39f12..4238776613 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -52,9 +52,10 @@ module.exports = { 'no-trailing-spaces': ['error'], '@babel/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }], 'no-void': ['error', { 'allowAsStatement': true }], + 'no-nested-ternary': ['error'], 'one-var': ['error', 'never'], 'padded-blocks': ['error', 'never'], - 'prefer-const': ['error', {'destructuring': 'all'}], + 'prefer-const': ['error', { 'destructuring': 'all' }], 'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }], '@babel/semi': ['error'], 'no-var': ['error'], diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e7b9999007..c74a108ee4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -21,11 +21,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index fcf1a8499b..5dbd106d15 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -18,11 +18,11 @@ jobs: comment-id: ${{ github.event.comment.id }} reactions: '+1' - name: Checkout the latest code - uses: actions/checkout@v3.0.1 + uses: actions/checkout@v3.0.2 with: token: ${{ secrets.JF_BOT_TOKEN }} fetch-depth: 0 - name: Automatic Rebase - uses: cirrus-actions/rebase@1.6 + uses: cirrus-actions/rebase@1.7 env: GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d087aa4120..c0e0e01c81 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v3 - name: Setup node environment - uses: actions/setup-node@v3.1.1 + uses: actions/setup-node@v3.2.0 with: node-version: 12 check-latest: true @@ -51,7 +51,7 @@ jobs: uses: actions/checkout@v3 - name: Setup node environment - uses: actions/setup-node@v3.1.1 + uses: actions/setup-node@v3.2.0 with: node-version: 12 check-latest: true @@ -89,7 +89,7 @@ jobs: uses: actions/checkout@v3 - name: Setup node environment - uses: actions/setup-node@v3.1.1 + uses: actions/setup-node@v3.2.0 with: node-version: 12 check-latest: true diff --git a/package-lock.json b/package-lock.json index 1a14a2c72b..b0c1a2f949 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5187,12 +5187,81 @@ "dev": true }, "ejs": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz", - "integrity": "sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.7.tgz", + "integrity": "sha512-BIar7R6abbUxDA3bfXrO4DSgwo8I+fB5/1zgujl3HLLjwd6+9iOnrT+t3grn2qbk9vOgBubXOFwX2m9axoFaGw==", "dev": true, "requires": { - "jake": "^10.6.1" + "jake": "^10.8.5" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "async": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "jake": { + "version": "10.8.5", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz", + "integrity": "sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "electron-to-chromium": { @@ -7815,26 +7884,6 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, - "jake": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz", - "integrity": "sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A==", - "dev": true, - "requires": { - "async": "0.9.x", - "chalk": "^2.4.2", - "filelist": "^1.0.1", - "minimatch": "^3.0.4" - }, - "dependencies": { - "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", - "dev": true - } - } - }, "jellyfin-apiclient": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/jellyfin-apiclient/-/jellyfin-apiclient-1.10.0.tgz", diff --git a/src/components/ServerConnections.js b/src/components/ServerConnections.js index 7e42432b56..a15b384f7d 100644 --- a/src/components/ServerConnections.js +++ b/src/components/ServerConnections.js @@ -1,20 +1,49 @@ import { ConnectionManager, Credentials, ApiClient, Events } from 'jellyfin-apiclient'; + import { appHost } from './apphost'; import Dashboard from '../utils/dashboard'; import { setUserInfo } from '../scripts/settings/userSettings'; +import appSettings from '../scripts/settings/appSettings'; + +const normalizeImageOptions = options => { + if (!options.quality && (options.maxWidth || options.width || options.maxHeight || options.height || options.fillWidth || options.fillHeight)) { + options.quality = 90; + } +}; + +const getMaxBandwidth = () => { + /* eslint-disable compat/compat */ + if (navigator.connection) { + let max = navigator.connection.downlinkMax; + if (max && max > 0 && max < Number.POSITIVE_INFINITY) { + max /= 8; + max *= 1000000; + max *= 0.7; + return parseInt(max, 10); + } + } + /* eslint-enable compat/compat */ + + return null; +}; class ServerConnections extends ConnectionManager { constructor() { super(...arguments); this.localApiClient = null; - Events.on(this, 'localusersignedout', function (eventName, logoutInfo) { + Events.on(this, 'localusersignedout', (_e, logoutInfo) => { setUserInfo(null, null); if (window.NativeShell && typeof window.NativeShell.onLocalUserSignedOut === 'function') { window.NativeShell.onLocalUserSignedOut(logoutInfo); } }); + + Events.on(this, 'apiclientcreated', (_e, apiClient) => { + apiClient.getMaxBandwidth = getMaxBandwidth; + apiClient.normalizeImageOptions = normalizeImageOptions; + }); } initApiClient(server) { @@ -38,6 +67,13 @@ class ServerConnections extends ConnectionManager { console.debug('loaded ApiClient singleton'); } + connect(options) { + return super.connect({ + enableAutoLogin: appSettings.enableAutoLogin(), + ...options + }); + } + setLocalApiClient(apiClient) { if (apiClient) { this.localApiClient = apiClient; diff --git a/src/components/appRouter.js b/src/components/appRouter.js index f0f23096b3..a75d8823df 100644 --- a/src/components/appRouter.js +++ b/src/components/appRouter.js @@ -2,42 +2,38 @@ import { Events } from 'jellyfin-apiclient'; import { Action, createHashHistory } from 'history'; import { appHost } from './apphost'; -import appSettings from '../scripts/settings/appSettings'; import { clearBackdrop, setBackdropTransparency } from './backdrop/backdrop'; import globalize from '../scripts/globalize'; import itemHelper from './itemHelper'; import loading from './loading/loading'; import viewManager from './viewManager/viewManager'; -import Dashboard from '../utils/dashboard'; import ServerConnections from './ServerConnections'; import alert from './alert'; import reactControllerFactory from './reactControllerFactory'; const history = createHashHistory(); +/** + * Page types of "no return" (when "Go back" should behave differently, probably quitting the application). + */ +const START_PAGE_TYPES = ['home', 'login', 'selectserver']; + class AppRouter { allRoutes = new Map(); - backdropContainer; - backgroundContainer; currentRouteInfo; currentViewLoadRequest; firstConnectionResult; forcedLogoutMsg; - isDummyBackToHome; msgTimeout; promiseShow; resolveOnNextShow; previousRoute = {}; - /** - * Pages of "no return" (when "Go back" should behave differently, probably quitting the application). - */ - startPages = ['home', 'login', 'selectserver']; constructor() { document.addEventListener('viewshow', () => this.onViewShow()); // TODO: Can this baseRoute logic be simplified? - this.baseRoute = window.location.href.split('?')[0].replace(this.getRequestFile(), ''); + this.baseRoute = window.location.href.split('?')[0].replace(this.#getRequestFile(), ''); // support hashbang this.baseRoute = this.baseRoute.split('#')[0]; if (this.baseRoute.endsWith('/') && !this.baseRoute.endsWith('://')) { @@ -52,33 +48,11 @@ class AppRouter { }); } - showLocalLogin(serverId) { - Dashboard.navigate('login.html?serverid=' + serverId); - } - - showVideoOsd() { - return Dashboard.navigate('video'); - } - - showSelectServer() { - Dashboard.navigate('selectserver.html'); - } - - showSettings() { - Dashboard.navigate('mypreferencesmenu.html'); - } - - showNowPlaying() { - this.show('queue'); - } - - beginConnectionWizard() { + #beginConnectionWizard() { clearBackdrop(); loading.show(); - ServerConnections.connect({ - enableAutoLogin: appSettings.enableAutoLogin() - }).then((result) => { - this.handleConnectionResult(result); + ServerConnections.connect().then(result => { + this.#handleConnectionResult(result); }); } @@ -128,18 +102,6 @@ class AppRouter { return this.promiseShow; } - async showDirect(path) { - if (this.promiseShow) await this.promiseShow; - - this.promiseShow = new Promise((resolve) => { - this.resolveOnNextShow = resolve; - // Schedule a call to return the promise - setTimeout(() => history.push(this.baseUrl() + path), 0); - }); - - return this.promiseShow; - } - #goToRoute({ location, action }) { // Strip the leading "!" if present const normalizedPath = location.pathname.replace(/^!/, ''); @@ -163,13 +125,18 @@ class AppRouter { start() { loading.show(); - this.initApiClients(); - Events.on(appHost, 'resume', this.onAppResume); + ServerConnections.getApiClients().forEach(apiClient => { + Events.off(apiClient, 'requestfail', this.onRequestFail); + Events.on(apiClient, 'requestfail', this.onRequestFail); + }); - ServerConnections.connect({ - enableAutoLogin: appSettings.enableAutoLogin() - }).then((result) => { + Events.on(ServerConnections, 'apiclientcreated', (_e, apiClient) => { + Events.off(apiClient, 'requestfail', this.onRequestFail); + Events.on(apiClient, 'requestfail', this.onRequestFail); + }); + + ServerConnections.connect().then(result => { this.firstConnectionResult = result; // Handle the initial route @@ -189,40 +156,18 @@ class AppRouter { } canGoBack() { - const curr = this.current(); + const curr = this.currentRouteInfo?.route; if (!curr) { return false; } - if (!document.querySelector('.dialogContainer') && this.startPages.indexOf(curr.type) !== -1) { + if (!document.querySelector('.dialogContainer') && START_PAGE_TYPES.includes(curr.type)) { return false; } return window.history.length > 1; } - current() { - return this.currentRouteInfo ? this.currentRouteInfo.route : null; - } - - invokeShortcut(id) { - if (id.indexOf('library-') === 0) { - id = id.replace('library-', ''); - id = id.split('_'); - - this.showItem(id[0], id[1]); - } else if (id.indexOf('item-') === 0) { - id = id.replace('item-', ''); - id = id.split('_'); - this.showItem(id[0], id[1]); - } else { - id = id.split('_'); - this.show(this.getRouteUrl(id[0], { - serverId: id[1] - })); - } - } - showItem(item, serverId, options) { // TODO: Refactor this so it only gets items, not strings. if (typeof (item) === 'string') { @@ -236,9 +181,7 @@ class AppRouter { } const url = this.getRouteUrl(item, options); - this.show(url, { - item: item - }); + this.show(url, { item }); } } @@ -252,20 +195,14 @@ class AppRouter { setBackdropTransparency(level); } - handleConnectionResult(result) { + #handleConnectionResult(result) { switch (result.State) { case 'SignedIn': loading.hide(); this.goHome(); break; case 'ServerSignIn': - result.ApiClient.getPublicUsers().then((users) => { - if (users.length) { - this.showLocalLogin(result.Servers[0].Id); - } else { - this.showLocalLogin(result.Servers[0].Id, true); - } - }); + this.showLocalLogin(result.ApiClient.serverId()); break; case 'ServerSelection': this.showSelectServer(); @@ -283,7 +220,7 @@ class AppRouter { } } - loadContentUrl(ctx, next, route, request) { + #loadContentUrl(ctx, _next, route, request) { let url; if (route.contentPath && typeof (route.contentPath) === 'function') { url = route.contentPath(ctx.querystring); @@ -305,19 +242,19 @@ class AppRouter { } promise.then((html) => { - this.loadContent(ctx, route, html, request); + this.#loadContent(ctx, route, html, request); }); } - handleRoute(ctx, next, route) { - this.authenticate(ctx, route, () => { - this.initRoute(ctx, next, route); + #handleRoute(ctx, next, route) { + this.#authenticate(ctx, route, () => { + this.#initRoute(ctx, next, route); }); } - initRoute(ctx, next, route) { + #initRoute(ctx, next, route) { const onInitComplete = (controllerFactory) => { - this.sendRouteToViewManager(ctx, next, route, controllerFactory); + this.#sendRouteToViewManager(ctx, next, route, controllerFactory); }; if (route.pageComponent) { @@ -329,20 +266,15 @@ class AppRouter { } } - cancelCurrentLoadRequest() { + #cancelCurrentLoadRequest() { const currentRequest = this.currentViewLoadRequest; if (currentRequest) { currentRequest.cancel = true; } } - sendRouteToViewManager(ctx, next, route, controllerFactory) { - if (this.isDummyBackToHome && route.type === 'home') { - this.isDummyBackToHome = false; - return; - } - - this.cancelCurrentLoadRequest(); + #sendRouteToViewManager(ctx, next, route, controllerFactory) { + this.#cancelCurrentLoadRequest(); const isBackNav = ctx.isBack; const currentRequest = { @@ -364,7 +296,7 @@ class AppRouter { const onNewViewNeeded = () => { if (typeof route.path === 'string') { - this.loadContentUrl(ctx, next, route, currentRequest); + this.#loadContentUrl(ctx, next, route, currentRequest); } else { next(); } @@ -413,7 +345,7 @@ class AppRouter { this.msgTimeout = setTimeout(this.onForcedLogoutMessageTimeout, 100); } - onRequestFail(e, data) { + onRequestFail(_e, data) { const apiClient = this; if (data.status === 403) { @@ -429,62 +361,7 @@ class AppRouter { } } - normalizeImageOptions(options) { - let setQuality; - if (options.maxWidth || options.width || options.maxHeight || options.height || options.fillWidth || options.fillHeight) { - setQuality = true; - } - - if (setQuality && !options.quality) { - options.quality = 90; - } - } - - getMaxBandwidth() { - /* eslint-disable compat/compat */ - if (navigator.connection) { - let max = navigator.connection.downlinkMax; - if (max && max > 0 && max < Number.POSITIVE_INFINITY) { - max /= 8; - max *= 1000000; - max *= 0.7; - return parseInt(max, 10); - } - } - /* eslint-enable compat/compat */ - - return null; - } - - onApiClientCreated(e, newApiClient) { - newApiClient.normalizeImageOptions = this.normalizeImageOptions; - newApiClient.getMaxBandwidth = this.getMaxBandwidth; - - Events.off(newApiClient, 'requestfail', this.onRequestFail); - Events.on(newApiClient, 'requestfail', this.onRequestFail); - } - - initApiClient(apiClient, instance) { - instance.onApiClientCreated({}, apiClient); - } - - initApiClients() { - ServerConnections.getApiClients().forEach((apiClient) => { - this.initApiClient(apiClient, this); - }); - - Events.on(ServerConnections, 'apiclientcreated', this.onApiClientCreated); - } - - onAppResume() { - const apiClient = ServerConnections.currentApiClient(); - - if (apiClient) { - apiClient.ensureWebSocket(); - } - } - - authenticate(ctx, route, callback) { + #authenticate(ctx, route, callback) { const firstResult = this.firstConnectionResult; this.firstConnectionResult = null; @@ -497,9 +374,9 @@ class AppRouter { }).then(data => { if (data !== null && data.StartupWizardCompleted === false) { ServerConnections.setLocalApiClient(firstResult.ApiClient); - Dashboard.navigate('wizardstart.html'); + this.show('wizardstart.html'); } else { - this.handleConnectionResult(firstResult); + this.#handleConnectionResult(firstResult); } }).catch(error => { console.error(error); @@ -507,7 +384,7 @@ class AppRouter { return; } else if (firstResult.State !== 'SignedIn') { - this.handleConnectionResult(firstResult); + this.#handleConnectionResult(firstResult); return; } } @@ -521,7 +398,7 @@ class AppRouter { if (!shouldExitApp && (!apiClient || !apiClient.isLoggedIn()) && !route.anonymous) { console.debug('[appRouter] route does not allow anonymous access: redirecting to login'); - this.beginConnectionWizard(); + this.#beginConnectionWizard(); return; } @@ -541,9 +418,9 @@ class AppRouter { this.goHome(); return; } else if (route.roles) { - this.validateRoles(apiClient, route.roles).then(() => { + this.#validateRoles(apiClient, route.roles).then(() => { callback(); - }, this.beginConnectionWizard); + }, this.#beginConnectionWizard.bind(this)); return; } } @@ -552,13 +429,13 @@ class AppRouter { callback(); } - validateRoles(apiClient, roles) { + #validateRoles(apiClient, roles) { return Promise.all(roles.split(',').map((role) => { - return this.validateRole(apiClient, role); + return this.#validateRole(apiClient, role); })); } - validateRole(apiClient, role) { + #validateRole(apiClient, role) { if (role === 'admin') { return apiClient.getCurrentUser().then((user) => { if (user.Policy.IsAdministrator) { @@ -572,7 +449,7 @@ class AppRouter { return Promise.resolve(); } - loadContent(ctx, route, html, request) { + #loadContent(ctx, route, html, request) { html = globalize.translateHtml(html, route.dictionary); request.view = html; @@ -586,7 +463,7 @@ class AppRouter { ctx.handled = true; } - getRequestFile() { + #getRequestFile() { let path = window.location.pathname || ''; const index = path.lastIndexOf('/'); @@ -613,34 +490,10 @@ class AppRouter { return; } - this.handleRoute(ctx, next, route); + this.#handleRoute(ctx, next, route); }; } - showGuide() { - Dashboard.navigate('livetv.html?tab=1'); - } - - goHome() { - Dashboard.navigate('home.html'); - } - - showSearch() { - Dashboard.navigate('search.html'); - } - - showLiveTV() { - Dashboard.navigate('livetv.html'); - } - - showRecordedTV() { - Dashboard.navigate('livetv.html?tab=3'); - } - - showFavorites() { - Dashboard.navigate('home.html?tab=1'); - } - getRouteUrl(item, options) { if (!item) { throw new Error('item cannot be null'); @@ -835,10 +688,53 @@ class AppRouter { return '#!/details?id=' + id + '&serverId=' + serverId; } + + showLocalLogin(serverId) { + return this.show('login.html?serverid=' + serverId); + } + + showVideoOsd() { + return this.show('video'); + } + + showSelectServer() { + return this.show('selectserver.html'); + } + + showSettings() { + return this.show('mypreferencesmenu.html'); + } + + showNowPlaying() { + return this.show('queue'); + } + + showGuide() { + return this.show('livetv.html?tab=1'); + } + + goHome() { + return this.show('home.html'); + } + + showSearch() { + return this.show('search.html'); + } + + showLiveTV() { + return this.show('livetv.html'); + } + + showRecordedTV() { + return this.show('livetv.html?tab=3'); + } + + showFavorites() { + return this.show('home.html?tab=1'); + } } export const appRouter = new AppRouter(); window.Emby = window.Emby || {}; - window.Emby.Page = appRouter; diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index c9c7b8ac04..be0df0e512 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -657,12 +657,12 @@ import { appRouter } from '../appRouter'; if (str) { const charIndex = Math.floor(str.length / 2); - const character = String(str.substr(charIndex, 1).charCodeAt()); + const character = String(str.slice(charIndex, charIndex + 1).charCodeAt()); let sum = 0; for (let i = 0; i < character.length; i++) { sum += parseInt(character.charAt(i)); } - const index = String(sum).substr(-1); + const index = String(sum).slice(-1); return (index % numRandomColors) + 1; } else { diff --git a/src/components/dashboard/users/AccessContainer.tsx b/src/components/dashboard/users/AccessContainer.tsx new file mode 100644 index 0000000000..5c0d6341b7 --- /dev/null +++ b/src/components/dashboard/users/AccessContainer.tsx @@ -0,0 +1,41 @@ +import React, { FunctionComponent } from 'react'; +import globalize from '../../../scripts/globalize'; +import CheckBoxElement from './CheckBoxElement'; + +type IProps = { + containerClassName?: string; + headerTitle?: string; + checkBoxClassName?: string; + checkBoxTitle?: string; + listContainerClassName?: string; + accessClassName?: string; + listTitle?: string; + description?: string; + children?: React.ReactNode +} + +const AccessContainer: FunctionComponent = ({containerClassName, headerTitle, checkBoxClassName, checkBoxTitle, listContainerClassName, accessClassName, listTitle, description, children }: IProps) => { + return ( +
+

{globalize.translate(headerTitle)}

+ +
+
+

+ {globalize.translate(listTitle)} +

+
+ {children} +
+
+
+ {globalize.translate(description)} +
+
+
+ ); +}; + +export default AccessContainer; diff --git a/src/components/dashboard/users/SectionTitleContainer.tsx b/src/components/dashboard/users/SectionTitleContainer.tsx new file mode 100644 index 0000000000..e5e4b8952a --- /dev/null +++ b/src/components/dashboard/users/SectionTitleContainer.tsx @@ -0,0 +1,35 @@ +import React, { FunctionComponent } from 'react'; +import SectionTitleButtonElement from './SectionTitleButtonElement'; +import SectionTitleLinkElement from './SectionTitleLinkElement'; + +type IProps = { + title: string; + isBtnVisible?: boolean; + titleLink?: string; +} + +const SectionTitleContainer: FunctionComponent = ({title, isBtnVisible = false, titleLink}: IProps) => { + return ( +
+
+

+ {title} +

+ + {isBtnVisible && } + + +
+
+ ); +}; + +export default SectionTitleContainer; diff --git a/src/components/favoriteitems.js b/src/components/favoriteitems.js index 72fb303522..bfd2c9bfc1 100644 --- a/src/components/favoriteitems.js +++ b/src/components/favoriteitems.js @@ -121,10 +121,14 @@ import '../elements/emby-itemscontainer/emby-itemscontainer'; } if (!isSingleSection) { - options.Limit = screenWidth >= 1920 ? 10 : screenWidth >= 1440 ? 8 : 6; + options.Limit = 6; if (enableScrollX()) { options.Limit = 20; + } else if (screenWidth >= 1920) { + options.Limit = 10; + } else if (screenWidth >= 1440) { + options.Limit = 8; } } diff --git a/src/components/filterdialog/filterdialog.js b/src/components/filterdialog/filterdialog.js index 16889f836a..1c12d5367c 100644 --- a/src/components/filterdialog/filterdialog.js +++ b/src/components/filterdialog/filterdialog.js @@ -87,7 +87,7 @@ import template from './filterdialog.template.html'; context.querySelector('.chk3DFilter').checked = query.Is3D === true; context.querySelector('.chkHDFilter').checked = query.IsHD === true; context.querySelector('.chk4KFilter').checked = query.Is4K === true; - context.querySelector('.chkSDFilter').checked = query.IsHD === true; + context.querySelector('.chkSDFilter').checked = query.IsHD === false; context.querySelector('#chkSubtitle').checked = query.HasSubtitles === true; context.querySelector('#chkTrailer').checked = query.HasTrailer === true; context.querySelector('#chkThemeSong').checked = query.HasThemeSong === true; @@ -272,15 +272,25 @@ import template from './filterdialog.template.html'; triggerChange(this); }); const chkHDFilter = context.querySelector('.chkHDFilter'); + const chkSDFilter = context.querySelector('.chkSDFilter'); chkHDFilter.addEventListener('change', () => { query.StartIndex = 0; - query.IsHD = chkHDFilter.checked ? true : null; + if (chkHDFilter.checked) { + chkSDFilter.checked = false; + query.IsHD = true; + } else { + query.IsHD = null; + } triggerChange(this); }); - const chkSDFilter = context.querySelector('.chkSDFilter'); chkSDFilter.addEventListener('change', () => { query.StartIndex = 0; - query.IsHD = chkSDFilter.checked ? false : null; + if (chkSDFilter.checked) { + chkHDFilter.checked = false; + query.IsHD = false; + } else { + query.IsHD = null; + } triggerChange(this); }); for (const elem of context.querySelectorAll('.chkStatus')) { diff --git a/src/components/homesections/homesections.js b/src/components/homesections/homesections.js index 9fa440b1a2..86e154f7b6 100644 --- a/src/components/homesections/homesections.js +++ b/src/components/homesections/homesections.js @@ -1,6 +1,5 @@ import escapeHtml from 'escape-html'; import cardBuilder from '../cardbuilder/cardBuilder'; -import dom from '../../scripts/dom'; import layoutManager from '../layoutManager'; import imageLoader from '../images/imageLoader'; import globalize from '../../scripts/globalize'; @@ -401,15 +400,8 @@ import ServerConnections from '../ServerConnections'; function getItemsToResumeFn(mediaType, serverId) { return function () { const apiClient = ServerConnections.getApiClient(serverId); - const screenWidth = dom.getWindowSize().innerWidth; - let limit; - if (enableScrollX()) { - limit = 12; - } else { - limit = screenWidth >= 1920 ? 8 : (screenWidth >= 1600 ? 8 : (screenWidth >= 1200 ? 9 : 6)); - limit = Math.min(limit, 5); - } + const limit = enableScrollX() ? 12 : 5; const options = { Limit: limit, diff --git a/src/components/itemContextMenu.js b/src/components/itemContextMenu.js index 56fd3705db..34c10ffd33 100644 --- a/src/components/itemContextMenu.js +++ b/src/components/itemContextMenu.js @@ -520,7 +520,14 @@ import toast from './toast/toast'; } function play(item, resume, queue, queueNext) { - const method = queue ? (queueNext ? 'queueNext' : 'queue') : 'play'; + let method = 'play'; + if (queue) { + if (queueNext) { + method = 'queueNext'; + } else { + method = 'queue'; + } + } let startPosition = 0; if (resume && item.UserData && item.UserData.PlaybackPositionTicks) { diff --git a/src/components/libraryoptionseditor/libraryoptionseditor.js b/src/components/libraryoptionseditor/libraryoptionseditor.js index 38760b6e5b..fd91950a7d 100644 --- a/src/components/libraryoptionseditor/libraryoptionseditor.js +++ b/src/components/libraryoptionseditor/libraryoptionseditor.js @@ -550,7 +550,9 @@ import template from './libraryoptionseditor.template.html'; function getOrderedPlugins(plugins, configuredOrder) { plugins = plugins.slice(0); plugins.sort((a, b) => { - return ((a = configuredOrder.indexOf(a.Name), b = configuredOrder.indexOf(b.Name)), a < b ? -1 : a > b ? 1 : 0); + a = configuredOrder.indexOf(a.Name); + b = configuredOrder.indexOf(b.Name); + return a - b; }); return plugins; } diff --git a/src/components/nowPlayingBar/nowPlayingBar.scss b/src/components/nowPlayingBar/nowPlayingBar.scss index a884f65769..b90ff2008d 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.scss +++ b/src/components/nowPlayingBar/nowPlayingBar.scss @@ -13,6 +13,7 @@ will-change: transform; contain: layout style; transition: transform 200ms ease-out; + cursor: pointer; } .nowPlayingBar-hidden { diff --git a/src/components/pages/NewUserPage.tsx b/src/components/pages/NewUserPage.tsx index 9c8110310c..3f94055dae 100644 --- a/src/components/pages/NewUserPage.tsx +++ b/src/components/pages/NewUserPage.tsx @@ -4,12 +4,11 @@ import Dashboard from '../../utils/dashboard'; import globalize from '../../scripts/globalize'; import loading from '../loading/loading'; import toast from '../toast/toast'; - -import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement'; +import SectionTitleContainer from '../dashboard/users/SectionTitleContainer'; import InputElement from '../dashboard/users/InputElement'; -import CheckBoxElement from '../dashboard/users/CheckBoxElement'; import CheckBoxListItem from '../dashboard/users/CheckBoxListItem'; import ButtonElement from '../dashboard/users/ButtonElement'; +import AccessContainer from '../dashboard/users/AccessContainer'; type userInput = { Name?: string; @@ -178,18 +177,10 @@ const NewUserPage: FunctionComponent = () => { return (
-
-
-

- {globalize.translate('ButtonAddUser')} -

- -
-
+
{ label='LabelPassword' />
+ + {mediaFoldersItems.map(Item => ( + + ))} + -
-

{globalize.translate('HeaderLibraryAccess')}

- -
-
-

- {globalize.translate('HeaderLibraries')} -

-
- {mediaFoldersItems.map(Item => ( - - ))} -
-
-
- {globalize.translate('LibraryAccessHelp')} -
-
-
-
-

{globalize.translate('HeaderChannelAccess')}

- -
-
-

- {globalize.translate('Channels')} -

-
- {channelsItems.map(Item => ( - - ))} -
-
-
- {globalize.translate('ChannelAccessHelp')} -
-
-
+ + {channelsItems.map(Item => ( + + ))} +
{ return (
-
-
-

- {userName} -

- -
-
+
{ itemsArr.push({ Id: device.Id, Name: device.Name, - AppName : device.AppName, + AppName: device.AppName, checkedAttribute: checkedAttribute }); } @@ -228,115 +228,75 @@ const UserLibraryAccessPage: FunctionComponent = () => { return (
-
-
-

- {userName} -

- -
-
+ -
-

{globalize.translate('HeaderLibraryAccess')}

- -
-
-

- {globalize.translate('HeaderLibraries')} -

-
- {mediaFoldersItems.map(Item => { - return ( - - ); - })} -
-
-
- {globalize.translate('LibraryAccessHelp')} -
-
-
-
-

{globalize.translate('HeaderChannelAccess')}

- -
-
-

- {globalize.translate('Channels')} -

-
- {channelsItems.map(Item => ( - - ))} -
-
-
- {globalize.translate('ChannelAccessHelp')} -
-
-
-
-
-

{globalize.translate('HeaderDeviceAccess')}

- -
-
-

- {globalize.translate('HeaderDevices')} -

-
- {devicesItems.map(Item => ( - - ))} -
-
-
- {globalize.translate('DeviceAccessHelp')} -
-
-
-
+ + {mediaFoldersItems.map(Item => ( + + ))} + + + + {channelsItems.map(Item => ( + + ))} + + + + {devicesItems.map(Item => ( + + ))} +
{ return (
-
-
-

- {userName} -

- -
-
+
diff --git a/src/components/pages/UserPasswordPage.tsx b/src/components/pages/UserPasswordPage.tsx index 2eba530ae2..305e8a4422 100644 --- a/src/components/pages/UserPasswordPage.tsx +++ b/src/components/pages/UserPasswordPage.tsx @@ -1,8 +1,8 @@ import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'; -import SectionTitleLinkElement from '../dashboard/users/SectionTitleLinkElement'; import SectionTabs from '../dashboard/users/SectionTabs'; import UserPasswordForm from '../dashboard/users/UserPasswordForm'; import { getParameterByName } from '../../utils/url'; +import SectionTitleContainer from '../dashboard/users/SectionTitleContainer'; const UserPasswordPage: FunctionComponent = () => { const userId = getParameterByName('userId'); @@ -23,18 +23,10 @@ const UserPasswordPage: FunctionComponent = () => { return (
-
-
-

- {userName} -

- -
-
+
{ return (
-
-
-

- {globalize.translate('HeaderUsers')} -

- - -
-
- {users.map(user => { - return ; - })} -
+ + +
+ {users.map(user => { + return ; + })}
diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index cd634d71cf..9cc6f62810 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -1827,12 +1827,18 @@ class PlaybackManager { Limit: UNLIMITED_ITEMS }, queryOptions)); } else if (firstItem.IsFolder) { + let sortBy = null; + if (options.shuffle) { + sortBy = 'Random'; + } else if (firstItem.Type !== 'BoxSet') { + sortBy = 'SortName'; + } promise = getItemsForPlayback(serverId, mergePlaybackQueries({ ParentId: firstItem.Id, Filters: 'IsNotFolder', Recursive: true, // These are pre-sorted - SortBy: options.shuffle ? 'Random' : (['BoxSet'].indexOf(firstItem.Type) === -1 ? 'SortName' : null), + SortBy: sortBy, MediaTypes: 'Audio,Video' }, queryOptions)); } else if (firstItem.Type === 'Episode' && items.length === 1 && getPlayer(firstItem, options).supportsProgress !== false) { diff --git a/src/components/scrollManager.js b/src/components/scrollManager.js index da71b157cb..53c09e37e5 100644 --- a/src/components/scrollManager.js +++ b/src/components/scrollManager.js @@ -61,7 +61,12 @@ import layoutManager from './layoutManager'; * @return {number} Clamped value. */ function clamp(value, min, max) { - return value <= min ? min : value >= max ? max : value; + if (value <= min) { + return min; + } else if (value >= max) { + return max; + } + return value; } /** diff --git a/src/components/subtitlesettings/subtitleappearancehelper.js b/src/components/subtitlesettings/subtitleappearancehelper.js index 52a84bc2dc..6cd5440ea6 100644 --- a/src/components/subtitlesettings/subtitleappearancehelper.js +++ b/src/components/subtitlesettings/subtitleappearancehelper.js @@ -28,6 +28,16 @@ function getTextStyles(settings, preview) { break; } + switch (settings.textWeight || '') { + case 'bold': + list.push({ name: 'font-weight', value: 'bold' }); + break; + case 'normal': + default: + list.push({ name: 'font-weight', value: 'normal' }); + break; + } + switch (settings.dropShadow || '') { case 'raised': list.push({ name: 'text-shadow', value: '-1px -1px white, 0px -1px white, -1px 0px white, 1px 1px black, 0px 1px black, 1px 0px black' }); diff --git a/src/components/subtitlesettings/subtitlesettings.js b/src/components/subtitlesettings/subtitlesettings.js index 0342dddf4c..067d8f9e40 100644 --- a/src/components/subtitlesettings/subtitlesettings.js +++ b/src/components/subtitlesettings/subtitlesettings.js @@ -28,6 +28,7 @@ function getSubtitleAppearanceObject(context) { const appearanceSettings = {}; appearanceSettings.textSize = context.querySelector('#selectTextSize').value; + appearanceSettings.textWeight = context.querySelector('#selectTextWeight').value; appearanceSettings.dropShadow = context.querySelector('#selectDropShadow').value; appearanceSettings.font = context.querySelector('#selectFont').value; appearanceSettings.textBackground = context.querySelector('#inputTextBackground').value; @@ -53,6 +54,7 @@ function loadForm(context, user, userSettings, appearanceSettings, apiClient) { context.querySelector('#selectSubtitlePlaybackMode').dispatchEvent(new CustomEvent('change', {})); context.querySelector('#selectTextSize').value = appearanceSettings.textSize || ''; + context.querySelector('#selectTextWeight').value = appearanceSettings.textWeight || 'normal'; context.querySelector('#selectDropShadow').value = appearanceSettings.dropShadow || ''; context.querySelector('#inputTextBackground').value = appearanceSettings.textBackground || 'transparent'; context.querySelector('#inputTextColor').value = appearanceSettings.textColor || '#ffffff'; @@ -166,6 +168,7 @@ function embed(options, self) { options.element.querySelector('#selectSubtitlePlaybackMode').addEventListener('change', onSubtitleModeChange); options.element.querySelector('#selectTextSize').addEventListener('change', onAppearanceFieldChange); + options.element.querySelector('#selectTextWeight').addEventListener('change', onAppearanceFieldChange); options.element.querySelector('#selectDropShadow').addEventListener('change', onAppearanceFieldChange); options.element.querySelector('#selectFont').addEventListener('change', onAppearanceFieldChange); options.element.querySelector('#inputTextColor').addEventListener('change', onAppearanceFieldChange); diff --git a/src/components/subtitlesettings/subtitlesettings.template.html b/src/components/subtitlesettings/subtitlesettings.template.html index 7c41fcdad2..941cd937d9 100644 --- a/src/components/subtitlesettings/subtitlesettings.template.html +++ b/src/components/subtitlesettings/subtitlesettings.template.html @@ -72,6 +72,13 @@
+
+ +
+
+