diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 1b16b94b62..bfe9aa5c69 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -7,6 +7,10 @@ trigger: tags: include: - '*' +pr: + branches: + include: + - '*' jobs: - job: main_build @@ -27,6 +31,9 @@ jobs: - script: 'yarn install' displayName: 'Install Dependencies' + - script: 'yarn build' + displayName: 'Build' + - script: 'test -d dist' displayName: 'Check Build' diff --git a/README.md b/README.md index 6a80b0b09c..4ee78ed786 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Jellyfin Web is the frontend used for most of the clients available for end user ### Dependencies - Yarn +- Gulp-cli ### Getting Started @@ -62,3 +63,12 @@ Jellyfin Web is the frontend used for most of the clients available for end user ```sh yarn serve ``` + +4. Build the client with sourcemaps. + '''sh + yarn + ''' + Or without sourcemaps + '''sh + yarn --production + ''' \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000000..04a8028f53 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,123 @@ +'use strict'; + +const { src, dest, series, parallel, watch } = require('gulp'); +const browserSync = require('browser-sync').create(); +const del = require('del'); +const babel = require('gulp-babel'); +const concat = require('gulp-concat'); +const terser = require('gulp-terser'); +const htmlmin = require('gulp-htmlmin'); +const imagemin = require('gulp-imagemin'); +const sourcemaps = require('gulp-sourcemaps'); +const mode = require('gulp-mode')({ + modes: ["production", "development"], + default: "development", + verbose: false +}); +const webpack_stream = require('webpack-stream'); +const inject = require('gulp-inject'); +const postcss = require('gulp-postcss'); +const sass = require('gulp-sass'); + +sass.compiler = require('node-sass') + + +if (mode.production()) { + var webpack_config = require('./webpack.prod.js'); +} else { + var webpack_config = require('./webpack.dev.js'); +} + +function serve() { + browserSync.init({ + server: { + baseDir: "./dist" + }, + port: 8080 + }); + + watch(['src/**/*.js', '!src/bundle.js'], javascript); + watch('src/bundle.js', webpack); + watch('src/**/*.css', css); + watch(['src/**/*.html', '!src/index.html'], html); + watch(['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg'], images); + watch(['src/**/*.json', 'src/**/*.ico'], copy); + watch('src/index.html', injectBundle); + watch(['src/standalone.js', 'src/scripts/apploader.js'], setStandalone); +} + +function setStandalone() { + return src(['src/standalone.js', 'src/scripts/apploader.js'], {base: './src/'}) + .pipe(concat('scripts/apploader.js')) + .pipe(dest('dist/')); +} + +// Clean assets +function clean() { + return del(['dist/']); +} + +function javascript() { + return src(['src/**/*.js', '!src/bundle.js'], {base: './src/'}) + .pipe(mode.development(sourcemaps.init({loadMaps: true}))) + .pipe(babel({ + presets: [ + ['@babel/preset-env'] + ] + })) + .pipe(terser({ + keep_fnames: true, + mangle: false + })) + .pipe(mode.development(sourcemaps.write('.'))) + .pipe(dest('dist/')) + .pipe(browserSync.stream()); +} + +function webpack() { + return webpack_stream(webpack_config) + .pipe(dest('dist/')) + .pipe(browserSync.stream()); +} + +function css() { + return src(['src/**/*.css', 'src/**/*.scss'], {base: './src/'}) + .pipe(mode.development(sourcemaps.init({loadMaps: true}))) + .pipe(sass().on('error', sass.logError)) + .pipe(postcss()) + .pipe(mode.development(sourcemaps.write('.'))) + .pipe(dest('dist/')) + .pipe(browserSync.stream()); +} + +function html() { + return src(['src/**/*.html', '!src/index.html'], {base: './src/'}) + .pipe(mode.production(htmlmin({ collapseWhitespace: true }))) + .pipe(dest('dist/')) + .pipe(browserSync.stream()); +} + +function images() { + return src(['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg'], {base: './src/'}) + .pipe(imagemin()) + .pipe(dest('dist/')) + .pipe(browserSync.stream()); +} + +function copy() { + return src(['src/**/*.json', 'src/**/*.ico'], {base: './src/'}) + .pipe(dest('dist/')) + .pipe(browserSync.stream()); +} + +function injectBundle() { + return src('src/index.html', {base: './src/'}) + .pipe(inject( + src(['src/scripts/apploader.js'], {read: false}, {base: './src/'}), {relative: true} + )) + .pipe(dest('dist/')) + .pipe(browserSync.stream()); +} + +exports.default = series(clean, parallel(javascript, webpack, css, html, images, copy), injectBundle) +exports.serve = series(exports.default, setStandalone, serve) diff --git a/package.json b/package.json index 41a9c41961..aa401f3a64 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,35 @@ "repository": "https://github.com/jellyfin/jellyfin-web", "license": "GPL-2.0-or-later", "devDependencies": { + "@babel/core": "^7.8.6", + "@babel/polyfill": "^7.8.7", + "@babel/preset-env": "^7.8.6", + "autoprefixer": "^9.7.4", + "babel-loader": "^8.0.6", + "browser-sync": "^2.26.7", "clean-webpack-plugin": "^3.0.0", "copy-webpack-plugin": "^5.1.1", "css-loader": "^3.4.2", + "cssnano": "^4.1.10", + "del": "^5.1.0", "eslint": "^6.8.0", "file-loader": "^5.0.2", + "gulp": "^4.0.2", + "gulp-babel": "^8.0.0", + "gulp-cli": "^2.2.0", + "gulp-concat": "^2.6.1", + "gulp-htmlmin": "^5.0.1", + "gulp-imagemin": "^7.1.0", + "gulp-inject": "^5.0.5", + "gulp-mode": "^1.0.2", + "gulp-postcss": "^8.0.0", + "gulp-sass": "^4.0.2", + "gulp-sourcemaps": "^2.6.5", + "gulp-terser": "^1.2.0", "html-webpack-plugin": "^3.2.0", + "node-sass": "^4.13.1", + "postcss-loader": "^3.0.0", + "postcss-preset-env": "^6.7.0", "style-loader": "^1.1.3", "stylelint": "^13.1.0", "stylelint-config-rational-order": "^0.1.2", @@ -20,10 +43,12 @@ "webpack-cli": "^3.3.10", "webpack-concat-plugin": "^3.0.0", "webpack-dev-server": "^3.10.3", - "webpack-merge": "^4.2.2" + "webpack-merge": "^4.2.2", + "webpack-stream": "^5.2.1" }, "dependencies": { "alameda": "^1.4.0", + "core-js": "^3.6.4", "document-register-element": "^1.14.3", "flv.js": "^1.5.0", "hls.js": "^0.13.1", @@ -44,6 +69,11 @@ "webcomponents.js": "^0.7.24", "whatwg-fetch": "^3.0.0" }, + "babel": { + "presets": [ + "@babel/preset-env" + ] + }, "browserslist": [ "last 2 Firefox versions", "last 2 Chrome versions", @@ -51,6 +81,7 @@ "last 2 Safari versions", "last 2 iOS versions", "last 2 Edge versions", + "Chrome 27", "Chrome 38", "Chrome 47", "Chrome 53", @@ -59,10 +90,10 @@ "Firefox ESR" ], "scripts": { - "serve": "webpack-dev-server --config webpack.dev.js --open", - "build": "webpack --config webpack.prod.js", + "serve": "gulp serve", + "build": "gulp --production", + "build dev": "gulp", "lint": "eslint \"src\"", - "stylelint": "stylelint \"src/**/*.css\"", - "prepare": "webpack --config webpack.prod.js" + "stylelint": "stylelint \"src/**/*.css\"" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000..23159fd295 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,11 @@ +const postcssPresetEnv = require('postcss-preset-env'); +const cssnano = require('cssnano'); + +const config = () => ({ + plugins: [ + postcssPresetEnv(), + cssnano() + ] +}); + +module.exports = config diff --git a/src/bundle.js b/src/bundle.js index 6a462b4221..c5c5e1bcaf 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -103,6 +103,7 @@ _define("material-icons", function() { return material_icons; }); +// noto font var noto = require("jellyfin-noto"); _define("jellyfin-noto", function () { return noto; @@ -113,3 +114,8 @@ var page = require("page"); _define("page", function() { return page; }); + +var polyfill = require("@babel/polyfill/dist/polyfill"); +_define("polyfill", function () { + return polyfill; +}); diff --git a/src/components/dialogHelper/dialogHelper.js b/src/components/dialogHelper/dialogHelper.js index 6ee96df318..d8ddc13c0a 100644 --- a/src/components/dialogHelper/dialogHelper.js +++ b/src/components/dialogHelper/dialogHelper.js @@ -242,9 +242,15 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', var onAnimationFinish = function () { focusManager.pushScope(dlg); + if (dlg.getAttribute('data-autofocus') === 'true') { focusManager.autoFocus(dlg); } + + if (document.activeElement && !dlg.contains(document.activeElement)) { + // Blur foreign element to prevent triggering of an action from the previous scope + document.activeElement.blur(); + } }; if (enableAnimation()) { diff --git a/src/components/itemcontextmenu.js b/src/components/itemcontextmenu.js index 62048345ea..bdbcfc782b 100644 --- a/src/components/itemcontextmenu.js +++ b/src/components/itemcontextmenu.js @@ -346,11 +346,7 @@ define(["apphost", "globalize", "connectionManager", "itemHelper", "appRouter", break; case "copy-stream": var downloadHref = apiClient.getItemDownloadUrl(itemId); - navigator.clipboard.writeText(downloadHref).then(function () { - require(["toast"], function (toast) { - toast(globalize.translate("CopyStreamURLSuccess")); - }); - }, function () { + var textAreaCopy = function () { var textArea = document.createElement("textarea"); textArea.value = downloadHref; document.body.appendChild(textArea); @@ -364,7 +360,16 @@ define(["apphost", "globalize", "connectionManager", "itemHelper", "appRouter", prompt(globalize.translate("CopyStreamURL"), downloadHref); } document.body.removeChild(textArea); - }); + }; + if (navigator.clipboard === undefined) { + textAreaCopy(); + } else { + navigator.clipboard.writeText(downloadHref).then(function () { + require(["toast"], function (toast) { + toast(globalize.translate("CopyStreamURLSuccess")); + }); + }, textAreaCopy); + } getResolveFunction(resolve, id)(); break; case "editsubtitles": diff --git a/src/controllers/dashboard/networking.js b/src/controllers/dashboard/networking.js index b9f990d629..c0e4d76d0d 100644 --- a/src/controllers/dashboard/networking.js +++ b/src/controllers/dashboard/networking.js @@ -90,10 +90,11 @@ define(["loading", "libraryMenu", "globalize", "emby-checkbox", "emby-select"], } function validateHttps(form) { + var remoteAccess = form.querySelector("#chkRemoteAccess").checked; var certPath = form.querySelector("#txtCertificatePath").value || null; var httpsMode = form.querySelector("#selectHttpsMode").value; - if ("enabled" !== httpsMode && "required" !== httpsMode || certPath) { + if (!remoteAccess || ("enabled" !== httpsMode && "required" !== httpsMode || certPath)) { return Promise.resolve(); } diff --git a/src/controllers/itemdetailpage.js b/src/controllers/itemdetailpage.js index e18311048c..8069b15529 100644 --- a/src/controllers/itemdetailpage.js +++ b/src/controllers/itemdetailpage.js @@ -459,7 +459,6 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti var usePrimaryImage = item.MediaType === "Video" && item.Type !== "Movie" && item.Type !== "Trailer" || item.MediaType && item.MediaType !== "Video" || item.Type === "MusicAlbum" || - item.Type === "MusicArtist" || item.Type === "Person"; if (!layoutManager.mobile && !userSettings.enableBackdrops()) { diff --git a/src/index.html b/src/index.html index 8e9c8db3b7..624be19360 100644 --- a/src/index.html +++ b/src/index.html @@ -105,5 +105,8 @@
+ + +