diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index f94934b4471..00000000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - schedule: - - cron: '30 7 * * 6' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - queries: +security-extended - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 8b9ca0b2f06..00000000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: Lint - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - run-eslint: - name: Run eslint - runs-on: ubuntu-latest - - steps: - - name: Check out Git repository - uses: actions/checkout@v2 - - - name: Set up Node.js - uses: actions/setup-node@v1 - with: - node-version: 12 - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - - - name: Install Node.js dependencies - run: yarn install --frozen-lockfile - env: - SKIP_PREPARE: true - - - name: Run eslint - run: yarn lint - - run-stylelint-css: - name: Run stylelint (css) - runs-on: ubuntu-latest - - steps: - - name: Check out Git repository - uses: actions/checkout@v2 - - - name: Set up Node.js - uses: actions/setup-node@v1 - with: - node-version: 12 - - - name: Set up stylelint matcher - uses: xt0rted/stylelint-problem-matcher@v1 - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - - - name: Install Node.js dependencies - run: yarn install --frozen-lockfile - env: - SKIP_PREPARE: true - - - name: Run stylelint - run: yarn stylelint:css - - run-stylelint-scss: - name: Run stylelint (scss) - runs-on: ubuntu-latest - - steps: - - name: Check out Git repository - uses: actions/checkout@v2 - - - name: Set up Node.js - uses: actions/setup-node@v1 - with: - node-version: 12 - - - name: Set up stylelint matcher - uses: xt0rted/stylelint-problem-matcher@v1 - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: '**/node_modules' - key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} - - - name: Install Node.js dependencies - run: yarn install --frozen-lockfile - env: - SKIP_PREPARE: true - - - name: Run stylelint - run: yarn stylelint:scss diff --git a/.github/workflows/merge-conflicts.yml b/.github/workflows/merge-conflicts.yml deleted file mode 100644 index 9f4d95d884d..00000000000 --- a/.github/workflows/merge-conflicts.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: "Merge Conflicts" - -on: - push: - branches: - - master -jobs: - triage: - runs-on: ubuntu-latest - if: github.repository == 'jellyfin/jellyfin-web' - steps: - - uses: mschilde/auto-label-merge-conflicts@master - with: - CONFLICT_LABEL_NAME: "merge conflict" - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.stylelintrc.scss.json b/.stylelintrc.scss.json index 7c5b0dd401a..90a8bcac8c7 100644 --- a/.stylelintrc.scss.json +++ b/.stylelintrc.scss.json @@ -3,6 +3,7 @@ "plugins": [ "stylelint-scss" ], "rules": { "at-rule-no-unknown": null, - "scss/at-rule-no-unknown": true + "scss/at-rule-no-unknown": true, + "plugin/no-browser-hacks": null } } diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 72a13043e9f..5b2a783de12 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -34,6 +34,7 @@ - [Ryan Hartzell](https://github.com/ryan-hartzell) - [Thibault Nocchi](https://github.com/ThibaultNocchi) - [MrTimscampi](https://github.com/MrTimscampi) + - [artiume](https://github.com/Artiume) - [ConfusedPolarBear](https://github.com/ConfusedPolarBear) - [Sarab Singh](https://github.com/sarab97) - [DesertCookie](https://github.com/desertcookie) diff --git a/babel.config.js b/babel.config.js index 08e71d91f59..e68d3fd7403 100644 --- a/babel.config.js +++ b/babel.config.js @@ -3,6 +3,7 @@ module.exports = { // Keep the root as a root '.' ], + sourceType: 'unambiguous', presets: [ [ '@babel/preset-env', diff --git a/debian/changelog b/debian/changelog index ab5e13196d1..85ffa0ba202 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,11 +1,3 @@ jellyfin-web (10.7.0-1) unstable; urgency=medium - * Forthcoming stable release - - -- Jellyfin Packaging Team Mon, 27 Jul 2020 19:13:31 -0400 - -jellyfin-web (10.6.0-1) unstable; urgency=medium - - * New upstream version 10.6.0; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.6.0 - - -- Jellyfin Packaging Team Mon, 16 Mar 2020 11:15:00 -0400 + * New upstream version 10.7.0; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.7.0 diff --git a/fedora/jellyfin-web.spec b/fedora/jellyfin-web.spec index c35a1caab28..99e05c3fcfc 100644 --- a/fedora/jellyfin-web.spec +++ b/fedora/jellyfin-web.spec @@ -42,6 +42,8 @@ mv dist %{buildroot}%{_datadir}/jellyfin-web %{_datadir}/licenses/jellyfin/LICENSE %changelog +* Mon Mar 08 2021 Jellyfin Packaging Team +- New stable release 10.7.0; release changelog at https://github.com/jellyfin/jellyfin-web/releases/tag/v10.7.0 * Mon Jul 27 2020 Jellyfin Packaging Team - Forthcoming stable release * Mon Mar 23 2020 Jellyfin Packaging Team diff --git a/package.json b/package.json index 0ef62015320..9fa8619c6e2 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@babel/eslint-plugin": "^7.12.1", "@babel/plugin-proposal-class-properties": "^7.10.1", "@babel/plugin-proposal-private-methods": "^7.12.1", - "@babel/plugin-transform-modules-umd": "^7.12.1", + "@babel/plugin-transform-modules-umd": "^7.13.0", "@babel/preset-env": "^7.12.7", "@uupaa/dynamic-import-polyfill": "^1.0.2", "autoprefixer": "^9.8.6", @@ -56,15 +56,18 @@ "epubjs": "^0.3.85", "fast-text-encoding": "^1.0.3", "flv.js": "^1.5.0", + "fontsource-noto-sans": "^3.1.5", + "fontsource-noto-sans-hk": "^3.1.5", + "fontsource-noto-sans-jp": "^3.1.5", + "fontsource-noto-sans-kr": "^3.1.5", + "fontsource-noto-sans-sc": "^3.1.5", "headroom.js": "^0.12.0", - "hls.js": "^0.14.16", - "intersection-observer": "^0.11.0", - "jellyfin-apiclient": "^1.4.2", - "jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto", + "hls.js": "^0.14.17", + "intersection-observer": "^0.12.0", + "jellyfin-apiclient": "^1.6.0", "jquery": "^3.5.1", "jstree": "^3.3.10", "libarchive.js": "^1.3.0", - "libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv", "material-design-icons-iconfont": "^6.1.0", "native-promise-only": "^0.8.0-a", "page": "^1.11.6", @@ -97,7 +100,7 @@ "scripts": { "start": "yarn serve", "serve": "webpack serve --config webpack.dev.js", - "prepare": "./scripts/prepare.sh", + "prepare": "node ./scripts/prepare.js", "build:development": "webpack --config webpack.dev.js", "build:production": "webpack --config webpack.prod.js", "lint": "eslint \"src/\"", diff --git a/scripts/prepare.js b/scripts/prepare.js new file mode 100755 index 00000000000..898b105e2f7 --- /dev/null +++ b/scripts/prepare.js @@ -0,0 +1,12 @@ +const { execSync } = require('child_process'); + +/** + * The npm `prepare` script needs to run a build to support installing + * a package from git repositories (this is dumb but a limitation of how + * npm behaves). We don't want to run these in CI though because + * building is slow so this script will skip the build when the + * `SKIP_PREPARE` environment variable has been set. + */ +if (!process.env.SKIP_PREPARE) { + execSync('webpack --config webpack.prod.js', { stdio: 'inherit' }); +} diff --git a/scripts/prepare.sh b/scripts/prepare.sh deleted file mode 100755 index bde12b36a59..00000000000 --- a/scripts/prepare.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -if [ -z "${SKIP_PREPARE}" ]; then - webpack --config webpack.prod.js -fi diff --git a/src/assets/css/dashboard.css b/src/assets/css/dashboard.css index 48e6fe807e1..efa0c092060 100644 --- a/src/assets/css/dashboard.css +++ b/src/assets/css/dashboard.css @@ -127,8 +127,8 @@ div[data-role=controlgroup] a.ui-btn-active { } .sessionAppInfo img { - max-width: 40px; - max-height: 40px; + max-width: 2.5em; + max-height: 2.5em; margin-right: 8px; } @@ -204,6 +204,10 @@ div[data-role=controlgroup] a.ui-btn-active { flex-grow: 1; } +.dashboardActionsContainer { + margin: 1em -0.3em 0; +} + .sessionNowPlayingContent { -webkit-background-size: cover; background-size: cover; @@ -231,6 +235,13 @@ div[data-role=controlgroup] a.ui-btn-active { margin-bottom: 0.5em; } +.dashboardSection .sectionTitleTextButton > .material-icons.material-icons { + font-size: 1.17em; + margin-top: 0.5em; + margin-bottom: 0.5em; + padding-top: 0; +} + .activeRecordingItems > .card { width: 50%; } @@ -246,20 +257,12 @@ div[data-role=controlgroup] a.ui-btn-active { @media all and (min-width: 70em) { .dashboardSections { - -webkit-flex-wrap: wrap; flex-wrap: wrap; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -webkit-flex-direction: row; flex-direction: row; } .dashboardColumn-2-60 { - width: 46%; - } - - .dashboardColumn-2-40 { - width: 27%; + flex-grow: 2; } .dashboardSection { @@ -291,6 +294,7 @@ div[data-role=controlgroup] a.ui-btn-active { } .activeSession { + min-width: 20rem; width: 100% !important; } @@ -304,27 +308,24 @@ div[data-role=controlgroup] a.ui-btn-active { background-position: center; } -@media all and (min-width: 40em) { - .activeSession { - width: 100% !important; - } -} - @media all and (min-width: 50em) { .activeSession { - width: 50% !important; + max-width: 25rem; + flex-grow: 0; + flex-shrink: 0; + flex-basis: 50%; } } .sessionCardFooter { padding-top: 0.5em !important; padding-bottom: 1em !important; - border-top: 1px solid #eee; text-align: center; position: relative; } .sessionAppInfo { + flex-grow: 1; padding: 0.5em; overflow: hidden; } @@ -344,11 +345,9 @@ div[data-role=controlgroup] a.ui-btn-active { right: 0; bottom: 0; font-weight: 400; -} - -.sessionNowPlayingContent-withbackground + .sessionNowPlayingInnerContent { - color: #fff !important; background: rgba(0, 0, 0, 0.7); + display: flex; + flex-direction: column; } .sessionAppName { @@ -358,9 +357,6 @@ div[data-role=controlgroup] a.ui-btn-active { .sessionNowPlayingDetails { display: flex; - position: absolute; - bottom: 0; - width: 100%; } .sessionNowPlayingInfo { @@ -376,10 +372,6 @@ div[data-role=controlgroup] a.ui-btn-active { padding: 0.8em 0.5em; } -.sessionNowPlayingStreamInfo { - white-space: nowrap; -} - .playbackProgress, .transcodingProgress { margin: 0; @@ -387,6 +379,12 @@ div[data-role=controlgroup] a.ui-btn-active { background: transparent !important; } +.activeDevices.itemsContainer { + /* offset for cardBox margin */ + margin: -0.6em; +} + +.activeSession .backgroundProgress, .activeSession .playbackProgress, .activeSession .transcodingProgress { position: absolute; @@ -403,9 +401,14 @@ div[data-role=controlgroup] a.ui-btn-active { } .transcodingProgress > div { + z-index: 10; background-color: #dd4919; } +.backgroundProgress > div { + background-color: #303030; +} + @media all and (max-width: 34.375em) { .sessionAppName { max-width: 160px; diff --git a/src/assets/css/fonts.scss b/src/assets/css/fonts.scss index f7aeff76e3f..545819d2f65 100644 --- a/src/assets/css/fonts.scss +++ b/src/assets/css/fonts.scss @@ -1,5 +1,7 @@ +@import "../../styles/noto-sans/index.scss"; + @mixin font($weight: null, $size: null) { - font-family: "Noto Sans", sans-serif; + font-family: "Noto Sans", "Noto Sans HK", "Noto Sans JP", "Noto Sans KR", "Noto Sans SC", sans-serif; font-weight: $weight; font-size: $size; } diff --git a/src/assets/css/librarybrowser.css b/src/assets/css/librarybrowser.css index 58356366be0..902e1c68aff 100644 --- a/src/assets/css/librarybrowser.css +++ b/src/assets/css/librarybrowser.css @@ -250,6 +250,26 @@ padding-bottom: 10vh; } +.primaryImageWrapper { + display: none; +} + +.primaryImageWrapper > img { + display: block; + margin: 0 auto; + max-width: 80vw; + max-height: 50vh; +} + +.primaryImageWrapper > img.aspect-square { + max-height: 45vh; +} + +.layout-mobile .primaryImageWrapper { + display: block; + flex: 1 0 auto; +} + @media all and (min-width: 40em) { .dashboardDocument .adminDrawerLogo, .dashboardDocument .mainDrawerButton { @@ -453,8 +473,7 @@ } .layout-mobile .itemBackdrop { - background-attachment: scroll; - height: 26.5vh; + display: none; } .layout-desktop .itemBackdrop::after { @@ -614,7 +633,8 @@ } .layout-mobile .mainDetailButtons { - margin-top: 1em; + flex: 2 0 70%; + margin-top: 0.5em; margin-bottom: 0.5em; } @@ -638,9 +658,9 @@ } .layout-mobile .detailPagePrimaryContainer { - display: block; + flex-wrap: wrap; position: relative; - padding: 0.5em 3.3% 0.5em; + padding: 4.5rem 3.3% 0.5rem; } .layout-tv #itemDetailPage:not(.noBackdrop) .detailPagePrimaryContainer, @@ -669,6 +689,10 @@ flex: 1 0 0; } +.layout-mobile .infoWrapper { + flex: 2 0 70%; +} + .infoText { white-space: nowrap; text-overflow: ellipsis; @@ -729,7 +753,8 @@ background-size: contain; } -.noBackdrop .detailLogo { +.noBackdrop .detailLogo, +.layout-mobile .detailLogo { display: none; } @@ -754,6 +779,17 @@ div.itemDetailGalleryLink.defaultCardBackground { height: 23vw; } +.sectionTitleTextButton > .material-icons { + font-size: 1.5em; + margin-bottom: 0.35em; + margin-top: 0; +} + +.layout-mobile .sectionTitleTextButton > .material-icons { + margin-bottom: 0; + padding-top: 0.5em; +} + .itemDetailGalleryLink.defaultCardBackground > .material-icons { font-size: 15vw; margin-top: 50%; diff --git a/src/assets/css/videoosd.css b/src/assets/css/videoosd.css index 1c1fe2a5a53..c0ef57dd5c9 100644 --- a/src/assets/css/videoosd.css +++ b/src/assets/css/videoosd.css @@ -22,6 +22,7 @@ color: #fff; user-select: none; -webkit-touch-callout: none; + pointer-events: none; } .skinHeader-withBackground.osdHeader { @@ -32,6 +33,7 @@ backdrop-filter: none; color: #eee; height: 7.5em; + pointer-events: none; } .osdHeader-hidden { @@ -39,6 +41,7 @@ } .osdHeader .headerTop { + pointer-events: all; max-height: 3.5em; } @@ -104,6 +107,7 @@ } .osdControls { + pointer-events: all; flex-grow: 1; padding: 0 0.8em; } @@ -261,6 +265,7 @@ justify-content: center; align-items: center; position: absolute; + pointer-events: none; top: 0; bottom: 0; right: 0; diff --git a/src/components/ServerConnections.js b/src/components/ServerConnections.js index 0242e549cdb..b3ba69a84ac 100644 --- a/src/components/ServerConnections.js +++ b/src/components/ServerConnections.js @@ -3,6 +3,87 @@ import { appHost } from './apphost'; import Dashboard from '../scripts/clientUtils'; import { setUserInfo } from '../scripts/settings/userSettings'; +// BEGIN Patches for MPV Shim +// It's got a new home! +import { playbackManager } from '../components/playback/playbackmanager'; +(function() { + let oldLogout = ApiClient.prototype.logout; + ApiClient.prototype.logout = function() { + // Logout Callback + var xhr = new XMLHttpRequest(); + xhr.open('POST', "/destroy_session", true); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + xhr.send("{}"); + + return oldLogout.bind(this)(); + } + + let oldAuthenticateUserByName = ApiClient.prototype.authenticateUserByName; + ApiClient.prototype.authenticateUserByName = function(name, password) { + // Password Provider + return new Promise((resolve, reject) => { + var xhr = new XMLHttpRequest(); + xhr.open('POST', "/mpv_shim_password", true); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + xhr.onloadend = (result) => { + var res = JSON.parse(result.target.response); + if (!res.success) { + alert("MPV Shim Login Failed"); + reject(); + } + oldAuthenticateUserByName.bind(this)(name, password).then(resolve).catch(reject); + }; + xhr.onerror = () => { + reject(); + } + xhr.send(JSON.stringify({ + server: this.serverAddress(), + username: name, + password: password + })); + }) + } + + let oldOpenWebSocket = ApiClient.prototype.openWebSocket; + ApiClient.prototype.openWebSocket = function() { + oldOpenWebSocket.bind(this)(); + let oldOnOpen = this._webSocket.onopen; + function onOpen() { + // Auto-Connect + var xhr = new XMLHttpRequest(); + xhr.open('POST', "/mpv_shim_id", true); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + xhr.onloadend = function (result) { + var res = JSON.parse(result.target.response); + playbackManager.getTargets().then(function (targets) { + for (var i = 0; i < targets.length; i++) { + if (targets[i].appName == res.appName && + targets[i].deviceName == res.deviceName) + playbackManager.trySetActivePlayer(targets[i].playerName, targets[i]); + } + }); + }; + xhr.send("{}"); + + oldOnOpen(); + } + this._webSocket.onopen = onOpen; + }; + + ApiClient.prototype.joinSyncPlayGroup = function(options = {}) { + return new Promise((resolve) => { + group_id = options.GroupId; + // Syncplay Join Group + var xhr = new XMLHttpRequest(); + xhr.open('POST', "/mpv_shim_syncplay_join", true); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + xhr.send(JSON.stringify(options)); + resolve(); + }) + }; +})() +// END Patches for MPV Shim + class ServerConnections extends ConnectionManager { constructor() { super(...arguments); diff --git a/src/components/appRouter.js b/src/components/appRouter.js index c02c7edd329..c14434180d8 100644 --- a/src/components/appRouter.js +++ b/src/components/appRouter.js @@ -471,18 +471,9 @@ class AppRouter { return null; } - getMaxBandwidthIOS() { - return 800000; - } - onApiClientCreated(e, newApiClient) { newApiClient.normalizeImageOptions = this.normalizeImageOptions; - - if (browser.iOS) { - newApiClient.getMaxBandwidth = this.getMaxBandwidthIOS; - } else { - newApiClient.getMaxBandwidth = this.getMaxBandwidth; - } + newApiClient.getMaxBandwidth = this.getMaxBandwidth; Events.off(newApiClient, 'requestfail', this.onRequestFail); Events.on(newApiClient, 'requestfail', this.onRequestFail); @@ -821,14 +812,20 @@ class AppRouter { url = '#!/tv.html?topParentId=' + item.Id; if (options && options.section === 'latest') { - url += '&tab=2'; + url += '&tab=1'; } return url; } if (item.CollectionType == 'music') { - return '#!/music.html?topParentId=' + item.Id; + url = '#!/music.html?topParentId=' + item.Id; + + if (options?.section === 'latest') { + url += '&tab=1'; + } + + return url; } } diff --git a/src/components/cardbuilder/card.css b/src/components/cardbuilder/card.css index 4c046ce984f..69a0e4e9096 100644 --- a/src/components/cardbuilder/card.css +++ b/src/components/cardbuilder/card.css @@ -160,7 +160,6 @@ button::-moz-focus-inner { background-size: cover; background-repeat: no-repeat; background-position: center center; - display: -webkit-flex; display: flex; align-items: center; justify-content: center; @@ -169,8 +168,13 @@ button::-moz-focus-inner { color: inherit; } +.cardContent.cardImageContainer { + display: flex; +} + .cardScalable .cardImageContainer { height: 100%; + width: 100%; contain: strict; } @@ -222,8 +226,8 @@ button::-moz-focus-inner { } .visualCardBox .cardContent { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; + border-top-left-radius: 0.2em; + border-top-right-radius: 0.2em; } .cardContent-shadow, @@ -360,16 +364,11 @@ button::-moz-focus-inner { font-weight: bold; } -.cardImageContainer .cardImageIcon { +.cardImageIcon { font-size: 5em; color: inherit; } -.cardImageIcon-small { - font-size: 3em !important; - margin-bottom: 0.1em; -} - .cardIndicators { right: 0.225em; top: 0.225em; diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 7f43db47ffe..b34c3969a48 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -1421,6 +1421,8 @@ import ServerConnections from '../ServerConnections'; const pathData = item.Path ? (' data-path="' + item.Path + '"') : ''; const contextData = options.context ? (' data-context="' + options.context + '"') : ''; const parentIdData = options.parentId ? (' data-parentid="' + options.parentId + '"') : ''; + const startDate = item.StartDate ? (' data-startdate="' + item.StartDate.toString() + '"') : ''; + const endDate = item.EndDate ? (' data-enddate="' + item.EndDate.toString() + '"') : ''; let additionalCardContent = ''; @@ -1428,7 +1430,7 @@ import ServerConnections from '../ServerConnections'; additionalCardContent += getHoverMenuHtml(item, action); } - return '<' + tagName + ' data-index="' + index + '"' + timerAttributes + actionAttribute + ' data-isfolder="' + (item.IsFolder || false) + '" data-serverid="' + (item.ServerId || options.serverId) + '" data-id="' + (item.Id || item.ItemId) + '" data-type="' + item.Type + '"' + mediaTypeData + collectionTypeData + channelIdData + pathData + positionTicksData + collectionIdData + playlistIdData + contextData + parentIdData + ' data-prefix="' + prefix + '" class="' + className + '">' + cardImageContainerOpen + innerCardFooter + cardImageContainerClose + overlayButtons + additionalCardContent + cardScalableClose + outerCardFooter + cardBoxClose + ''; + return '<' + tagName + ' data-index="' + index + '"' + timerAttributes + actionAttribute + ' data-isfolder="' + (item.IsFolder || false) + '" data-serverid="' + (item.ServerId || options.serverId) + '" data-id="' + (item.Id || item.ItemId) + '" data-type="' + item.Type + '"' + mediaTypeData + collectionTypeData + channelIdData + pathData + positionTicksData + collectionIdData + playlistIdData + contextData + parentIdData + startDate + endDate + ' data-prefix="' + prefix + '" class="' + className + '">' + cardImageContainerOpen + innerCardFooter + cardImageContainerClose + overlayButtons + additionalCardContent + cardScalableClose + outerCardFooter + cardBoxClose + ''; } /** diff --git a/src/components/displaySettings/displaySettings.js b/src/components/displaySettings/displaySettings.js index 289fa40d505..261c25a89ca 100644 --- a/src/components/displaySettings/displaySettings.js +++ b/src/components/displaySettings/displaySettings.js @@ -17,21 +17,17 @@ import template from './displaySettings.template.html'; /* eslint-disable indent */ - function fillThemes(context, userSettings) { - const select = context.querySelector('#selectTheme'); - + function fillThemes(select, selectedTheme) { skinManager.getThemes().then(themes => { select.innerHTML = themes.map(t => { return ``; }).join(''); // get default theme - const defaultTheme = themes.find(theme => { - return theme.default; - }); + const defaultTheme = themes.find(theme => theme.default); // set the current theme - select.value = userSettings.theme() || defaultTheme.id; + select.value = selectedTheme || defaultTheme.id; }); } @@ -89,6 +85,8 @@ import template from './displaySettings.template.html'; context.querySelector('.learnHowToContributeContainer').classList.add('hide'); } + context.querySelector('.selectDashboardThemeContainer').classList.toggle('hide', !user.Policy.IsAdministrator); + if (appHost.supports('screensaver')) { context.querySelector('.selectScreensaverContainer').classList.remove('hide'); } else { @@ -111,7 +109,9 @@ import template from './displaySettings.template.html'; context.querySelector('.fldThemeVideo').classList.add('hide'); } - fillThemes(context, userSettings); + fillThemes(context.querySelector('#selectTheme'), userSettings.theme()); + fillThemes(context.querySelector('#selectDashboardTheme'), userSettings.dashboardTheme()); + loadScreensavers(context, userSettings); context.querySelector('.chkDisplayMissingEpisodes').checked = user.Configuration.DisplayMissingEpisodes || false; @@ -147,6 +147,7 @@ import template from './displaySettings.template.html'; userSettingsInstance.enableThemeSongs(context.querySelector('#chkThemeSong').checked); userSettingsInstance.enableThemeVideos(context.querySelector('#chkThemeVideo').checked); userSettingsInstance.theme(context.querySelector('#selectTheme').value); + userSettingsInstance.dashboardTheme(context.querySelector('#selectDashboardTheme').value); userSettingsInstance.screensaver(context.querySelector('.selectScreensaver').value); userSettingsInstance.libraryPageSize(context.querySelector('#txtLibraryPageSize').value); diff --git a/src/components/displaySettings/displaySettings.template.html b/src/components/displaySettings/displaySettings.template.html index 1b9bf003760..f79155b7fde 100644 --- a/src/components/displaySettings/displaySettings.template.html +++ b/src/components/displaySettings/displaySettings.template.html @@ -6,27 +6,35 @@

@@ -63,27 +78,35 @@

@@ -126,6 +156,10 @@

+
+ +
+
diff --git a/src/components/homeScreenSettings/homeScreenSettings.js b/src/components/homeScreenSettings/homeScreenSettings.js index 5898844434c..b07203442f9 100644 --- a/src/components/homeScreenSettings/homeScreenSettings.js +++ b/src/components/homeScreenSettings/homeScreenSettings.js @@ -57,8 +57,8 @@ import template from './homeScreenSettings.template.html'; value: 'suggestions' }); list.push({ - name: globalize.translate('Genres'), - value: 'genres' + name: globalize.translate('Trailers'), + value: 'trailers' }); list.push({ name: globalize.translate('Favorites'), @@ -68,6 +68,10 @@ import template from './homeScreenSettings.template.html'; name: globalize.translate('Collections'), value: 'collections' }); + list.push({ + name: globalize.translate('Genres'), + value: 'genres' + }); } else if (type === 'tvshows') { list.push({ name: globalize.translate('Shows'), @@ -79,7 +83,7 @@ import template from './homeScreenSettings.template.html'; value: 'suggestions' }); list.push({ - name: globalize.translate('Upcoming'), + name: globalize.translate('TabUpcoming'), value: 'upcoming' }); list.push({ @@ -87,7 +91,7 @@ import template from './homeScreenSettings.template.html'; value: 'genres' }); list.push({ - name: globalize.translate('Networks'), + name: globalize.translate('TabNetworks'), value: 'networks' }); list.push({ @@ -116,20 +120,40 @@ import template from './homeScreenSettings.template.html'; name: globalize.translate('Playlists'), value: 'playlists' }); + list.push({ + name: globalize.translate('Songs'), + value: 'songs' + }); list.push({ name: globalize.translate('Genres'), value: 'genres' }); } else if (type === 'livetv') { list.push({ - name: globalize.translate('Suggestions'), - value: 'suggestions', + name: globalize.translate('Programs'), + value: 'programs', isDefault: true }); list.push({ name: globalize.translate('Guide'), value: 'guide' }); + list.push({ + name: globalize.translate('Channels'), + value: 'channels' + }); + list.push({ + name: globalize.translate('Recordings'), + value: 'recordings' + }); + list.push({ + name: globalize.translate('Schedule'), + value: 'schedule' + }); + list.push({ + name: globalize.translate('Series'), + value: 'series' + }); } return list; diff --git a/src/components/homesections/homesections.js b/src/components/homesections/homesections.js index f6fb8ec7bb9..421f74e1257 100644 --- a/src/components/homesections/homesections.js +++ b/src/components/homesections/homesections.js @@ -491,7 +491,7 @@ import ServerConnections from '../ServerConnections'; function loadResumeAudio(elem, apiClient, userId) { let html = ''; - html += '

' + globalize.translate('HeaderContinueWatching') + '

'; + html += '

' + globalize.translate('HeaderContinueListening') + '

'; if (enableScrollX()) { html += '
'; html += '
'; diff --git a/src/components/htmlMediaHelper.js b/src/components/htmlMediaHelper.js index 4f803aa4b61..da53d6a063e 100644 --- a/src/components/htmlMediaHelper.js +++ b/src/components/htmlMediaHelper.js @@ -158,15 +158,11 @@ import { Events } from 'jellyfin-apiclient'; // (but rewinding cannot happen as the first event with media of non-empty duration) console.debug(`seeking to ${seconds} on ${e.type} event`); setCurrentTimeIfNeeded(element, seconds); - events.map(function(name) { - element.removeEventListener(name, onMediaChange); - }); + events.forEach(name => element.removeEventListener(name, onMediaChange)); if (onMediaReady) onMediaReady(); } }; - events.map(function (name) { - return element.addEventListener(name, onMediaChange); - }); + events.forEach(name => element.addEventListener(name, onMediaChange)); } } } diff --git a/src/components/images/imageLoader.js b/src/components/images/imageLoader.js index 1811b117e17..53759b77cca 100644 --- a/src/components/images/imageLoader.js +++ b/src/components/images/imageLoader.js @@ -87,9 +87,6 @@ import './style.css'; requestAnimationFrame(() => { if (elem.tagName !== 'IMG') { elem.style.backgroundImage = "url('" + url + "')"; - if (elem.classList.contains('blurhashed')) { - elem.style.backgroundColor = '#fff'; - } } else { elem.setAttribute('src', url); } @@ -101,6 +98,12 @@ import './style.css'; } else { elem.classList.add('lazy-image-fadein'); } + + const canvas = elem.previousSibling; + if (elem.classList.contains('blurhashed') && canvas && canvas.tagName === 'CANVAS') { + canvas.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein'); + canvas.classList.add('lazy-hidden'); + } }); }); } @@ -111,7 +114,6 @@ import './style.css'; if (elem.tagName !== 'IMG') { url = elem.style.backgroundImage.slice(4, -1).replace(/"/g, ''); elem.style.backgroundImage = 'none'; - elem.style.backgroundColor = null; } else { url = elem.getAttribute('src'); elem.setAttribute('src', ''); @@ -120,6 +122,16 @@ import './style.css'; elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein'); elem.classList.add('lazy-hidden'); + + const canvas = elem.previousSibling; + if (canvas && canvas.tagName === 'CANVAS') { + canvas.classList.remove('lazy-hidden'); + if (userSettings.enableFastFadein()) { + canvas.classList.add('lazy-image-fadein-fast'); + } else { + canvas.classList.add('lazy-image-fadein'); + } + } } export function lazyChildren(elem) { diff --git a/src/components/multiSelect/multiSelect.js b/src/components/multiSelect/multiSelect.js index 8cfc838d09e..b99ce8da7ce 100644 --- a/src/components/multiSelect/multiSelect.js +++ b/src/components/multiSelect/multiSelect.js @@ -196,11 +196,14 @@ import confirm from '../confirm/confirm'; } if (user.Policy.EnableContentDownloading && appHost.supports('filedownload')) { + // Disabled because there is no callback for this item + /* menuItems.push({ name: globalize.translate('Download'), id: 'download', icon: 'file_download' }); + */ } if (user.Policy.IsAdministrator) { diff --git a/src/components/nowPlayingBar/nowPlayingBar.js b/src/components/nowPlayingBar/nowPlayingBar.js index 2613b5a8558..cbeb9813dd1 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.js +++ b/src/components/nowPlayingBar/nowPlayingBar.js @@ -556,6 +556,7 @@ import { appRouter } from '../appRouter'; const options = { play: false, queue: false, + stopPlayback: true, clearQueue: true, positionTo: contextButton }; diff --git a/src/components/playback/playbackmanager.js b/src/components/playback/playbackmanager.js index 6d9aebdacbb..f21ca42bc6f 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -1892,6 +1892,16 @@ class PlaybackManager { } } + // BEGIN Patches for MPV Shim + // Explicitly break non-MPV playback + alert({ + text: "MPV Shim's player backend is not connected. This could be caused by " + + "many things. Try logging out/in again and making sure websockets work.", + title: globalize.translate('HeaderPlaybackError') + }); + return Promise.reject(); + // END Patches for MPV Shim + if (options.fullscreen) { loading.show(); } diff --git a/src/components/playerstats/playerstats.js b/src/components/playerstats/playerstats.js index 106ca0a7cda..805f0115ce9 100644 --- a/src/components/playerstats/playerstats.js +++ b/src/components/playerstats/playerstats.js @@ -142,14 +142,13 @@ import ServerConnections from '../ServerConnections'; }); } - if (audioChannels) { - sessionStats.push({ - label: globalize.translate('LabelAudioChannels'), - value: audioChannels - }); - } - if (displayPlayMethod === 'Transcode') { + if (audioChannels) { + sessionStats.push({ + label: globalize.translate('LabelAudioChannels'), + value: audioChannels + }); + } if (totalBitrate) { sessionStats.push({ label: globalize.translate('LabelBitrate'), diff --git a/src/components/playlisteditor/playlisteditor.js b/src/components/playlisteditor/playlisteditor.js index e6c0349719c..675e8e09c99 100644 --- a/src/components/playlisteditor/playlisteditor.js +++ b/src/components/playlisteditor/playlisteditor.js @@ -49,7 +49,8 @@ import ServerConnections from '../ServerConnections'; apiClient.ajax({ type: 'POST', url: url, - dataType: 'json' + dataType: 'json', + contentType: 'application/json' }).then(result => { loading.hide(); diff --git a/src/components/pluginManager.js b/src/components/pluginManager.js index 6c48b7ef7fa..985b76725f2 100644 --- a/src/components/pluginManager.js +++ b/src/components/pluginManager.js @@ -74,14 +74,19 @@ import { playbackManager } from './playback/playbackmanager'; if (typeof pluginSpec === 'string') { if (pluginSpec in window) { console.log(`Loading plugin (via window): ${pluginSpec}`); - let pluginInstance = await window[pluginSpec]; - if (typeof pluginInstance === 'function') { - pluginInstance = await new pluginInstance(); + const pluginDefinition = await window[pluginSpec]; + if (typeof pluginDefinition !== 'function') { + throw new TypeError('Plugin definitions in window have to be an (async) function returning the plugin class'); + } + + const pluginClass = await pluginDefinition(); + if (typeof pluginClass !== 'function') { + throw new TypeError(`Plugin definition doesn't return a class for '${pluginSpec}'`); } // init plugin and pass basic dependencies - plugin = new pluginInstance({ + plugin = new pluginClass({ events: Events, loading, appSettings, @@ -98,9 +103,7 @@ import { playbackManager } from './playback/playbackmanager'; const pluginResult = await pluginSpec; plugin = new pluginResult.default; } else { - const err = new TypeError('Plugins have to be a Promise that resolves to a plugin builder function'); - console.error(err); - throw err; + throw new TypeError('Plugins have to be a Promise that resolves to a plugin builder function'); } return this.#preparePlugin(pluginSpec, plugin); diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index dec95a470c4..49fb8370c5f 100644 --- a/src/components/remotecontrol/remotecontrol.js +++ b/src/components/remotecontrol/remotecontrol.js @@ -209,11 +209,10 @@ function updateNowPlayingInfo(context, state, serverId) { if (autoFocusContextButton) { contextButton.focus(); } - const stopPlayback = !!layoutManager.mobile; const options = { play: false, queue: false, - stopPlayback: stopPlayback, + stopPlayback: true, clearQueue: true, openAlbum: false, positionTo: contextButton diff --git a/src/components/search/searchfields.template.html b/src/components/search/searchfields.template.html index 7da5e240c53..2ba21492b7c 100644 --- a/src/components/search/searchfields.template.html +++ b/src/components/search/searchfields.template.html @@ -1,7 +1,7 @@
- +
diff --git a/src/components/shortcuts.js b/src/components/shortcuts.js index 90515fc16f9..b3d82c4988a 100644 --- a/src/components/shortcuts.js +++ b/src/components/shortcuts.js @@ -147,6 +147,8 @@ import toast from './toast/toast'; MediaType: card.getAttribute('data-mediatype'), Path: card.getAttribute('data-path'), IsFolder: card.getAttribute('data-isfolder') === 'true', + StartDate: card.getAttribute('data-startdate'), + EndDate: card.getAttribute('data-enddate'), UserData: { PlaybackPositionTicks: parseInt(card.getAttribute('data-positionticks') || '0') } diff --git a/src/components/subtitlesync/subtitlesync.js b/src/components/subtitlesync/subtitlesync.js index d3477932c38..f04ce58e7ad 100644 --- a/src/components/subtitlesync/subtitlesync.js +++ b/src/components/subtitlesync/subtitlesync.js @@ -45,11 +45,11 @@ function init(instance) { let inputOffset = /[-+]?\d+\.?\d*/g.exec(this.textContent); if (inputOffset) { inputOffset = inputOffset[0]; + inputOffset = parseFloat(inputOffset); + inputOffset = Math.min(30, Math.max(-30, inputOffset)); // replace current text by considered offset this.textContent = inputOffset + 's'; - - inputOffset = parseFloat(inputOffset); // set new offset playbackManager.setSubtitleOffset(inputOffset, player); // synchronize with slider value @@ -121,7 +121,7 @@ function getPercentageFromOffset(value) { // convert fraction to percent percentValue *= 50; percentValue += 50; - return Math.min(100, Math.max(0, percentValue.toFixed())); + return Math.min(100, Math.max(0, percentValue.toFixed(1))); } class SubtitleSync { diff --git a/src/components/subtitlesync/subtitlesync.template.html b/src/components/subtitlesync/subtitlesync.template.html index fe202ebf606..8de75416351 100644 --- a/src/components/subtitlesync/subtitlesync.template.html +++ b/src/components/subtitlesync/subtitlesync.template.html @@ -3,7 +3,7 @@
0s
- +
diff --git a/src/components/syncPlay/core/PlaybackCore.js b/src/components/syncPlay/core/PlaybackCore.js index 203af7ebcd5..12e0c67abba 100644 --- a/src/components/syncPlay/core/PlaybackCore.js +++ b/src/components/syncPlay/core/PlaybackCore.js @@ -70,7 +70,6 @@ class PlaybackCore { onPlaybackStop(stopInfo) { this.lastCommand = null; Events.trigger(this.manager, 'playbackstop', [stopInfo]); - this.manager.releaseCurrentPlayer(); } /** diff --git a/src/config.json b/src/config.json index 9dd6fa01d6c..473af757b5c 100644 --- a/src/config.json +++ b/src/config.json @@ -1,6 +1,6 @@ { "includeCorsCredentials": false, - "multiserver": false, + "multiserver": true, "themes": [ { "name": "Apple TV", diff --git a/src/controllers/dashboard/dashboard.html b/src/controllers/dashboard/dashboard.html index 9b177e3f345..423cfa4c698 100644 --- a/src/controllers/dashboard/dashboard.html +++ b/src/controllers/dashboard/dashboard.html @@ -15,11 +15,11 @@

${TabServer}

-
+
- '; html += ''; - - btnCssClass = session.TranscodingInfo && session.TranscodingInfo.TranscodeReasons && session.TranscodingInfo.TranscodeReasons.length ? '' : ' hide'; html += ''; btnCssClass = session.ServerId && session.SupportedCommands.indexOf('DisplayMessage') !== -1 && session.DeviceId !== ServerConnections.deviceId() ? '' : ' hide'; html += ''; html += '
'; - html += '
'; - html += DashboardPage.getSessionNowPlayingStreamInfo(session); - html += '
'; - html += '
'; const userImage = DashboardPage.getUserImage(session); html += userImage ? '
" : '
'; @@ -409,10 +404,8 @@ import confirm from '../../components/confirm/confirm'; } else if (displayPlayMethod === 'DirectStream') { html += globalize.translate('DirectStreaming'); } else if (displayPlayMethod === 'Transcode') { - html += globalize.translate('Transcoding'); - if (session.TranscodingInfo && session.TranscodingInfo.Framerate) { - html += ' (' + session.TranscodingInfo.Framerate + ' fps)'; + html += `${globalize.translate('Framerate')}: ${session.TranscodingInfo.Framerate}fps`; } showTranscodingInfo = true; @@ -556,8 +549,10 @@ import confirm from '../../components/confirm/confirm'; if (nowPlayingItem) { row.classList.add('playingSession'); + row.querySelector('.btnSessionInfo').classList.remove('hide'); } else { row.classList.remove('playingSession'); + row.querySelector('.btnSessionInfo').classList.add('hide'); } if (session.ServerId && session.SupportedCommands.indexOf('DisplayMessage') !== -1) { @@ -566,12 +561,6 @@ import confirm from '../../components/confirm/confirm'; row.querySelector('.btnSessionSendMessage').classList.add('hide'); } - if (session.TranscodingInfo && session.TranscodingInfo.TranscodeReasons && session.TranscodingInfo) { - row.querySelector('.btnSessionInfo').classList.remove('hide'); - } else { - row.querySelector('.btnSessionInfo').classList.add('hide'); - } - const btnSessionPlayPause = row.querySelector('.btnSessionPlayPause'); if (session.ServerId && nowPlayingItem && session.SupportsRemoteControl) { @@ -586,7 +575,6 @@ import confirm from '../../components/confirm/confirm'; btnSessionPlayPauseIcon.classList.remove('play_arrow', 'pause'); btnSessionPlayPauseIcon.classList.add(session.PlayState && session.PlayState.IsPaused ? 'play_arrow' : 'pause'); - row.querySelector('.sessionNowPlayingStreamInfo').innerHTML = DashboardPage.getSessionNowPlayingStreamInfo(session); row.querySelector('.sessionNowPlayingTime').innerHTML = DashboardPage.getSessionNowPlayingTime(session); row.querySelector('.sessionUserName').innerHTML = DashboardPage.getUsersHtml(session); row.querySelector('.sessionAppSecondaryText').innerHTML = DashboardPage.getAppSecondaryText(session); @@ -599,30 +587,17 @@ import confirm from '../../components/confirm/confirm'; } const playbackProgressElem = row.querySelector('.playbackProgress'); - - if (nowPlayingItem && nowPlayingItem.RunTimeTicks) { - const percent = 100 * (session.PlayState.PositionTicks || 0) / nowPlayingItem.RunTimeTicks; - playbackProgressElem.outerHTML = indicators.getProgressHtml(percent, { - containerClass: 'playbackProgress' - }); - } else { - playbackProgressElem.outerHTML = indicators.getProgressHtml(0, { - containerClass: 'playbackProgress hide' - }); - } - const transcodingProgress = row.querySelector('.transcodingProgress'); - if (session.TranscodingInfo && session.TranscodingInfo.CompletionPercentage) { - const percent = session.TranscodingInfo.CompletionPercentage.toFixed(1); - transcodingProgress.outerHTML = indicators.getProgressHtml(percent, { - containerClass: 'transcodingProgress' - }); - } else { - transcodingProgress.outerHTML = indicators.getProgressHtml(0, { - containerClass: 'transcodingProgress hide' - }); - } + let percent = 100 * session?.PlayState?.PositionTicks / nowPlayingItem?.RunTimeTicks; + playbackProgressElem.outerHTML = indicators.getProgressHtml(percent || 0, { + containerClass: 'playbackProgress' + }); + + percent = session?.TranscodingInfo?.CompletionPercentage?.toFixed(1); + transcodingProgress.outerHTML = indicators.getProgressHtml(percent || 0, { + containerClass: 'transcodingProgress' + }); const imgUrl = DashboardPage.getNowPlayingImageUrl(nowPlayingItem) || ''; const imgElem = row.querySelector('.sessionNowPlayingContent'); diff --git a/src/controllers/dashboard/devices/devices.js b/src/controllers/dashboard/devices/devices.js index 63d2a7645e9..7dd597f8022 100644 --- a/src/controllers/dashboard/devices/devices.js +++ b/src/controllers/dashboard/devices/devices.js @@ -25,7 +25,7 @@ import confirm from '../../../components/confirm/confirm'; confirm({ text: msg, title: globalize.translate('HeaderDeleteDevices'), - confirmText: globalize.translate('ButtonDelete'), + confirmText: globalize.translate('Delete'), primary: 'delete' }).then(async () => { loading.show(); @@ -96,7 +96,7 @@ import confirm from '../../../components/confirm/confirm'; deviceHtml += '
'; deviceHtml += '
'; deviceHtml += '
'; - deviceHtml += ''; + deviceHtml += ``; const iconUrl = imageHelper.getDeviceIcon(device); if (iconUrl) { diff --git a/src/controllers/dashboard/dlna/profiles.js b/src/controllers/dashboard/dlna/profiles.js index 3eedc347289..a0186c79ab8 100644 --- a/src/controllers/dashboard/dlna/profiles.js +++ b/src/controllers/dashboard/dlna/profiles.js @@ -41,7 +41,7 @@ import confirm from '../../../components/confirm/confirm'; html += '
'; html += ''; html += ''; diff --git a/src/controllers/dashboard/encodingsettings.html b/src/controllers/dashboard/encodingsettings.html index 4c7b8c3a57a..aa5d0307464 100644 --- a/src/controllers/dashboard/encodingsettings.html +++ b/src/controllers/dashboard/encodingsettings.html @@ -84,6 +84,13 @@

${LabelEnableHardwareDecodingFor}

+
+ +
+
+
+ +
${AllowVppTonemappingHelp}
+