diff --git a/.eslintrc.js b/.eslintrc.js index 400c528a48..d45cc5dd05 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,6 +29,7 @@ module.exports = { 'plugin:compat/recommended' ], rules: { + 'array-callback-return': ['error'], 'block-spacing': ['error'], 'brace-style': ['error', '1tbs', { 'allowSingleLine': true }], 'comma-dangle': ['error', 'never'], diff --git a/.stylelintrc.scss.json b/.stylelintrc.scss.json index 7c5b0dd401..90a8bcac8c 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/README.md b/README.md index 1108ec9f21..3a2fb3b4fb 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,7 @@ Jellyfin Web is the frontend used for most of the clients available for end user ### Dependencies - [Node.js](https://nodejs.org/en/download) -- [Yarn 1.22.4](https://classic.yarnpkg.com/en/docs/install) -- Gulp-cli +- [Yarn 1.22.5](https://classic.yarnpkg.com/en/docs/install) ### Getting Started diff --git a/package.json b/package.json index 83dad17983..0d69171b32 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,28 @@ { "name": "jellyfin-web", - "version": "0.0.0", + "version": "10.8.0", "description": "Web interface for Jellyfin", "repository": "https://github.com/jellyfin/jellyfin-web", "license": "GPL-2.0-or-later", "devDependencies": { - "@babel/core": "^7.12.9", + "@babel/core": "^7.12.10", "@babel/eslint-parser": "^7.12.1", "@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/preset-env": "^7.12.7", + "@babel/preset-env": "^7.12.11", "@uupaa/dynamic-import-polyfill": "^1.0.2", "autoprefixer": "^9.8.6", "babel-loader": "^8.2.2", "babel-plugin-dynamic-import-polyfill": "^1.0.0", "clean-webpack-plugin": "^3.0.0", "confusing-browser-globals": "^1.0.10", - "copy-webpack-plugin": "^6.3.2", + "copy-webpack-plugin": "^7.0.0", "css-loader": "^5.0.1", "cssnano": "^4.1.10", - "eslint": "^7.15.0", - "eslint-plugin-compat": "^3.5.1", + "eslint": "^7.16.0", + "eslint-plugin-compat": "^3.9.0", "eslint-plugin-eslint-comments": "^3.2.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-promise": "^4.2.1", @@ -41,7 +41,7 @@ "stylelint-no-browser-hacks": "^1.2.1", "stylelint-order": "^4.1.0", "stylelint-scss": "^3.18.0", - "webpack": "^5.10.0", + "webpack": "^5.11.0", "webpack-cli": "^4.0.0", "webpack-dev-server": "^3.11.0", "webpack-merge": "^4.2.2", @@ -56,13 +56,17 @@ "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", + "hls.js": "^0.14.17", + "intersection-observer": "^0.12.0", "jellyfin-apiclient": "^1.5.0", - "jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto", "jquery": "^3.5.1", - "jstree": "^3.3.10", + "jstree": "^3.3.11", "libarchive.js": "^1.3.0", "libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf-smarttv", "material-design-icons-iconfont": "^6.1.0", @@ -72,7 +76,7 @@ "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.2", "sortablejs": "^1.12.0", - "swiper": "^6.3.5", + "swiper": "^6.4.5", "webcomponents.js": "^0.7.24", "whatwg-fetch": "^3.5.0", "workbox-core": "^5.1.4", @@ -97,7 +101,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 0000000000..898b105e2f --- /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 bde12b36a5..0000000000 --- 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 48e6fe807e..547f7dab24 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,15 +308,12 @@ 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%; } } @@ -325,6 +326,7 @@ div[data-role=controlgroup] a.ui-btn-active { } .sessionAppInfo { + flex-grow: 1; padding: 0.5em; overflow: hidden; } @@ -344,6 +346,8 @@ div[data-role=controlgroup] a.ui-btn-active { right: 0; bottom: 0; font-weight: 400; + display: flex; + flex-direction: column; } .sessionNowPlayingContent-withbackground + .sessionNowPlayingInnerContent { @@ -358,9 +362,6 @@ div[data-role=controlgroup] a.ui-btn-active { .sessionNowPlayingDetails { display: flex; - position: absolute; - bottom: 0; - width: 100%; } .sessionNowPlayingInfo { @@ -387,6 +388,11 @@ div[data-role=controlgroup] a.ui-btn-active { background: transparent !important; } +.activeDevices.itemsContainer { + /* offset for cardBox margin */ + margin: -0.6em; +} + .activeSession .playbackProgress, .activeSession .transcodingProgress { position: absolute; diff --git a/src/assets/css/fonts.scss b/src/assets/css/fonts.scss index f7aeff76e3..545819d2f6 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 58356366be..902e1c68af 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/components/apphost.js b/src/components/apphost.js index aefdee7340..36feb896f3 100644 --- a/src/components/apphost.js +++ b/src/components/apphost.js @@ -1,4 +1,4 @@ - +import { version as appVersion } from '../../package.json'; import appSettings from '../scripts/settings/appSettings'; import browser from '../scripts/browser'; import { Events } from 'jellyfin-apiclient'; @@ -8,7 +8,6 @@ import globalize from '../scripts/globalize'; import profileBuilder from '../scripts/browserDeviceProfile'; const appName = 'Jellyfin Web'; -const appVersion = '10.7.0'; function getBaseProfileOptions(item) { const disableHlsVideoAudioCodecs = []; @@ -29,7 +28,7 @@ function getBaseProfileOptions(item) { }; } -function getDeviceProfile(item, options = {}) { +function getDeviceProfile(item) { return new Promise(function (resolve) { let profile; diff --git a/src/components/cardbuilder/card.css b/src/components/cardbuilder/card.css index 4c046ce984..f00db4080c 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,6 +168,10 @@ button::-moz-focus-inner { color: inherit; } +.cardContent.cardImageContainer { + display: flex; +} + .cardScalable .cardImageContainer { height: 100%; contain: strict; diff --git a/src/components/displaySettings/displaySettings.js b/src/components/displaySettings/displaySettings.js index 289fa40d50..261c25a89c 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 1b9bf00376..ab298f7802 100644 --- a/src/components/displaySettings/displaySettings.template.html +++ b/src/components/displaySettings/displaySettings.template.html @@ -126,6 +126,10 @@ +
+ +
+
diff --git a/src/components/homesections/homesections.js b/src/components/homesections/homesections.js index f6fb8ec7bb..421f74e125 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/multiSelect/multiSelect.js b/src/components/multiSelect/multiSelect.js index 8cfc838d09..d552a9f1b2 100644 --- a/src/components/multiSelect/multiSelect.js +++ b/src/components/multiSelect/multiSelect.js @@ -157,9 +157,7 @@ import confirm from '../confirm/confirm'; } confirm(msg, title).then(() => { - const promises = itemIds.map(itemId => { - apiClient.deleteItem(itemId); - }); + const promises = itemIds.map(itemId => apiClient.deleteItem(itemId)); Promise.all(promises).then(resolve, () => { alertText(globalize.translate('ErrorDeletingItem')).then(reject, reject); diff --git a/src/components/nowPlayingBar/nowPlayingBar.js b/src/components/nowPlayingBar/nowPlayingBar.js index 2613b5a855..cbeb9813dd 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 6d9aebdacb..2b6c13bb1c 100644 --- a/src/components/playback/playbackmanager.js +++ b/src/components/playback/playbackmanager.js @@ -283,15 +283,15 @@ function getAudioMaxValues(deviceProfile) { let maxAudioBitDepth = null; let maxAudioBitrate = null; - deviceProfile.CodecProfiles.map(function (codecProfile) { + deviceProfile.CodecProfiles.forEach(codecProfile => { if (codecProfile.Type === 'Audio') { - (codecProfile.Conditions || []).map(function (condition) { + (codecProfile.Conditions || []).forEach(condition => { if (condition.Condition === 'LessThanEqual' && condition.Property === 'AudioBitDepth') { - return maxAudioBitDepth = condition.Value; + maxAudioBitDepth = condition.Value; } else if (condition.Condition === 'LessThanEqual' && condition.Property === 'AudioSampleRate') { - return maxAudioSampleRate = condition.Value; + maxAudioSampleRate = condition.Value; } else if (condition.Condition === 'LessThanEqual' && condition.Property === 'AudioBitrate') { - return maxAudioBitrate = condition.Value; + maxAudioBitrate = condition.Value; } }); } @@ -334,7 +334,7 @@ function getAudioStreamUrlFromDeviceProfile(item, deviceProfile, maxBitrate, api let directPlayContainers = ''; - deviceProfile.DirectPlayProfiles.map(function (p) { + deviceProfile.DirectPlayProfiles.forEach(p => { if (p.Type === 'Audio') { if (directPlayContainers) { directPlayContainers += ',' + p.Container; @@ -360,7 +360,7 @@ function getStreamUrls(items, deviceProfile, maxBitrate, apiClient, startPositio let audioDirectPlayContainers = ''; - deviceProfile.DirectPlayProfiles.map(function (p) { + deviceProfile.DirectPlayProfiles.forEach(p => { if (p.Type === 'Audio') { if (audioDirectPlayContainers) { audioDirectPlayContainers += ',' + p.Container; diff --git a/src/components/remotecontrol/remotecontrol.js b/src/components/remotecontrol/remotecontrol.js index dec95a470c..49fb8370c5 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/controllers/dashboard/dashboard.html b/src/controllers/dashboard/dashboard.html index 9b177e3f34..423cfa4c69 100644 --- a/src/controllers/dashboard/dashboard.html +++ b/src/controllers/dashboard/dashboard.html @@ -15,11 +15,11 @@

-
+
-