diff --git a/.eslintrc.yml b/.eslintrc.yml index 4bc22fc1d..377716d53 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -3,6 +3,12 @@ env: browser: true amd: true +parserOptions: + ecmaVersion: 6 + sourceType: module + ecmaFeatures: + impliedStrict: true + globals: # New browser globals DataView: readonly diff --git a/gulpfile.js b/gulpfile.js index ca6cf36dd..0eb559354 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -18,16 +18,42 @@ const stream = require('webpack-stream'); const inject = require('gulp-inject'); const postcss = require('gulp-postcss'); const sass = require('gulp-sass'); - -sass.compiler = require('node-sass') +const gulpif = require('gulp-if'); +const lazypipe = require('lazypipe'); +sass.compiler = require('node-sass'); +let config; if (mode.production()) { - var config = require('./webpack.prod.js'); + config = require('./webpack.prod.js'); } else { - var config = require('./webpack.dev.js'); + config = require('./webpack.dev.js'); } +const options = { + javascript: { + query: ['src/**/*.js', '!src/bundle.js', '!src/standalone.js', '!src/scripts/apploader.js'] + }, + apploader: { + query: ['src/standalone.js', 'src/scripts/apploader.js'] + }, + css: { + query: ['src/**/*.css', 'src/**/*.scss'] + }, + html: { + query: ['src/**/*.html', '!src/index.html'] + }, + images: { + query: ['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg'] + }, + copy: { + query: ['src/**/*.json', 'src/**/*.ico'] + }, + injectBundle: { + query: 'src/index.html' + } +}; + function serve() { browserSync.init({ server: { @@ -36,51 +62,99 @@ function serve() { 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'], standalone); -} + let events = ['add', 'change']; -function standalone() { - return src(['src/standalone.js', 'src/scripts/apploader.js'], { base: './src/' }) - .pipe(concat('scripts/apploader.js')) - .pipe(dest('dist/')); + watch(options.javascript.query).on('all', function (event, path) { + if (events.includes(event)) { + javascript(path); + } + }); + + watch(options.apploader.query, apploader(true)); + + watch('src/bundle.js', webpack); + + watch(options.css.query).on('all', function (event, path) { + if (events.includes(event)) { + css(path); + } + }); + + watch(options.html.query).on('all', function (event, path) { + if (events.includes(event)) { + html(path); + } + }); + + watch(options.images.query).on('all', function (event, path) { + if (events.includes(event)) { + images(path); + } + }); + + watch(options.copy.query).on('all', function (event, path) { + if (events.includes(event)) { + copy(path); + } + }); + + watch(options.injectBundle.query, injectBundle); } 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({ +let pipelineJavascript = lazypipe() + .pipe(function () { + return mode.development(sourcemaps.init({ loadMaps: true })); + }) + .pipe(function () { + return babel({ presets: [ ['@babel/preset-env'] ] - })) - .pipe(terser({ + }); + }) + .pipe(function () { + return terser({ keep_fnames: true, mangle: false - })) - .pipe(mode.development(sourcemaps.write('.'))) + }); + }) + .pipe(function () { + return mode.development(sourcemaps.write('.')); + }); + +function javascript(query) { + return src(typeof query !== 'function' ? query : options.javascript.query, { base: './src/' }) + .pipe(pipelineJavascript()) .pipe(dest('dist/')) .pipe(browserSync.stream()); } +function apploader(standalone) { + function task() { + return src(options.apploader.query, { base: './src/' }) + .pipe(gulpif(standalone, concat('scripts/apploader.js'))) + .pipe(pipelineJavascript()) + .pipe(dest('dist/')) + .pipe(browserSync.stream()); + }; + + task.displayName = 'apploader'; + + return task; +} + function webpack() { return stream(config) .pipe(dest('dist/')) .pipe(browserSync.stream()); } -function css() { - return src(['src/**/*.css', 'src/**/*.scss'], { base: './src/' }) +function css(query) { + return src(typeof query !== 'function' ? query : options.css.query, { base: './src/' }) .pipe(mode.development(sourcemaps.init({ loadMaps: true }))) .pipe(sass().on('error', sass.logError)) .pipe(postcss()) @@ -89,28 +163,28 @@ function css() { .pipe(browserSync.stream()); } -function html() { - return src(['src/**/*.html', '!src/index.html'], { base: './src/' }) +function html(query) { + return src(typeof query !== 'function' ? query : options.html.query, { 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/' }) +function images(query) { + return src(typeof query !== 'function' ? query : options.images.query, { base: './src/' }) .pipe(mode.production(imagemin())) .pipe(dest('dist/')) .pipe(browserSync.stream()); } -function copy() { - return src(['src/**/*.json', 'src/**/*.ico'], { base: './src/' }) +function copy(query) { + return src(typeof query !== 'function' ? query : options.copy.query, { base: './src/' }) .pipe(dest('dist/')) .pipe(browserSync.stream()); } function injectBundle() { - return src('src/index.html', { base: './src/' }) + return src(options.injectBundle.query, { base: './src/' }) .pipe(inject( src(['src/scripts/apploader.js'], { read: false }, { base: './src/' }), { relative: true } )) @@ -118,6 +192,10 @@ function injectBundle() { .pipe(browserSync.stream()); } -exports.default = series(clean, parallel(javascript, webpack, css, html, images, copy), injectBundle) -exports.standalone = series(exports.default, standalone) -exports.serve = series(exports.standalone, serve) +function build(standalone) { + return series(clean, parallel(javascript, apploader(standalone), webpack, css, html, images, copy), injectBundle); +} + +exports.default = build(false); +exports.standalone = build(true); +exports.serve = series(exports.standalone, serve); diff --git a/package.json b/package.json index 90cfa954a..2df8c1a63 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "devDependencies": { "@babel/core": "^7.8.6", "@babel/polyfill": "^7.8.7", + "@babel/plugin-transform-modules-amd": "^7.8.3", "@babel/preset-env": "^7.8.6", "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", @@ -23,6 +24,7 @@ "gulp-cli": "^2.2.0", "gulp-concat": "^2.6.1", "gulp-htmlmin": "^5.0.1", + "gulp-if": "^3.0.0", "gulp-imagemin": "^7.1.0", "gulp-inject": "^5.0.5", "gulp-mode": "^1.0.2", @@ -31,6 +33,7 @@ "gulp-sourcemaps": "^2.6.5", "gulp-terser": "^1.2.0", "html-webpack-plugin": "^3.2.0", + "lazypipe": "^1.0.2", "node-sass": "^4.13.1", "postcss-loader": "^3.0.0", "postcss-preset-env": "^6.7.0", @@ -57,10 +60,10 @@ "jquery": "^3.4.1", "jstree": "^3.3.7", "libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus", - "libjass": "^0.11.0", "material-design-icons-iconfont": "^5.0.1", "native-promise-only": "^0.8.0-a", "page": "^1.11.5", + "query-string": "^6.11.1", "resize-observer-polyfill": "^1.5.1", "shaka-player": "^2.5.9", "sortablejs": "^1.10.2", @@ -69,9 +72,17 @@ "whatwg-fetch": "^3.0.0" }, "babel": { - "presets": [ - "@babel/preset-env" - ] + "presets": ["@babel/preset-env"], + "overrides": [{ + "test": [ + "src/components/cardbuilder/cardBuilder.js", + "src/components/filedownloader.js", + "src/components/filesystem.js", + "src/components/input/keyboardnavigation.js", + "src/components/sanatizefilename.js" + ], + "plugins": ["@babel/plugin-transform-modules-amd"] + }] }, "browserslist": [ "last 2 Firefox versions", diff --git a/src/assets/css/dashboard.css b/src/assets/css/dashboard.css index 8c8a9ca7f..894d7332f 100644 --- a/src/assets/css/dashboard.css +++ b/src/assets/css/dashboard.css @@ -63,6 +63,10 @@ progress[aria-valuenow]::before { } .adminDrawerLogo { + display: none; +} + +.layout-mobile .adminDrawerLogo { padding: 1.5em 1em 1.2em; border-bottom: 1px solid #e0e0e0; margin-bottom: 1em; @@ -161,7 +165,7 @@ div[data-role=controlgroup] a.ui-btn-active { @media all and (min-width: 40em) { .content-primary { - padding-top: 7em; + padding-top: 4.6em; } .withTabs .content-primary { diff --git a/src/assets/css/librarybrowser.css b/src/assets/css/librarybrowser.css index 67afef237..e4b5bcf8d 100644 --- a/src/assets/css/librarybrowser.css +++ b/src/assets/css/librarybrowser.css @@ -21,7 +21,7 @@ } .libraryPage { - padding-top: 7em !important; + padding-top: 7em; } .itemDetailPage { @@ -115,7 +115,7 @@ display: -webkit-inline-box; display: -webkit-inline-flex; display: inline-flex; - margin: 0.3em 0 0 0.5em; + margin: 0 0 0 0.5em; height: 1.7em; -webkit-box-align: center; -webkit-align-items: center; @@ -128,6 +128,10 @@ margin-top: 0; } +.layout-mobile .pageTitleWithDefaultLogo { + background-image: url(../img/icon-transparent.png); +} + .headerLeft, .skinHeader { display: -webkit-box; @@ -242,7 +246,6 @@ } @media all and (min-width: 40em) { - .dashboardDocument .adminDrawerLogo, .dashboardDocument .mainDrawerButton { display: none !important; } @@ -268,12 +271,6 @@ } } -@media all and (max-width: 60em) { - .libraryDocument .mainDrawerButton { - display: none; - } -} - @media all and (max-width: 84em) { .withSectionTabs .headerTop { padding-bottom: 0.55em; @@ -316,7 +313,7 @@ } .dashboardDocument .mainDrawer-scrollContainer { - margin-top: 6em !important; + margin-top: 4.6em !important; } } @@ -1122,3 +1119,50 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards { .itemsViewSettingsContainer > .button-flat { margin: 0; } + +.layout-mobile #myPreferencesMenuPage { + padding-top: 3.75em; +} + +.itemDetailsGroup { + margin-bottom: 1.5em; +} + +.trackSelections { + max-width: 44em; +} + +.detailsGroupItem, +.trackSelections .selectContainer { + display: flex; + max-width: 44em; + margin: 0 0 0.5em !important; +} + +.trackSelections .selectContainer { + margin: 0 0 0.3em !important; +} + +.detailsGroupItem .label, +.trackSelections .selectContainer .selectLabel { + cursor: default; + flex-grow: 0; + flex-shrink: 0; + flex-basis: 6.25em; + margin: 0 0.6em 0 0; +} + +.trackSelections .selectContainer .selectLabel { + margin: 0 0.2em 0 0; +} + +.trackSelections .selectContainer .detailTrackSelect { + font-size: inherit; + padding: 0; + overflow: hidden; +} + +.trackSelections .selectContainer .selectArrowContainer .selectArrow { + margin-top: 0; + font-size: 1.4em; +} diff --git a/src/bundle.js b/src/bundle.js index 5e1a2ab3e..ba5f74b16 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -16,6 +16,12 @@ _define("fetch", function() { return fetch }); +// query-string +var query = require("query-string"); +_define("queryString", function() { + return query; +}); + // flvjs var flvjs = require("flv.js/dist/flv").default; _define("flvjs", function() { @@ -75,14 +81,7 @@ _define("sortable", function() { // webcomponents var webcomponents = require("webcomponents.js/webcomponents-lite"); _define("webcomponents", function() { - return webcomponents -}); - -// libjass -var libjass = require("libjass"); -require("libjass/libjass.css"); -_define("libjass", function() { - return libjass; + return webcomponents; }); // libass-wasm @@ -97,11 +96,10 @@ _define("material-icons", function() { return material_icons; }); -// Noto Sans - -var jellyfin_noto = require("jellyfin-noto"); +// noto font +var noto = require("jellyfin-noto"); _define("jellyfin-noto", function () { - return jellyfin_noto; + return noto; }); // page.js diff --git a/src/components/appRouter.js b/src/components/appRouter.js index efb58a089..62bfb3cf4 100644 --- a/src/components/appRouter.js +++ b/src/components/appRouter.js @@ -511,9 +511,16 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM return baseRoute; } + var popstateOccurred = false; + window.addEventListener('popstate', function () { + popstateOccurred = true; + }); + function getHandler(route) { return function (ctx, next) { + ctx.isBack = popstateOccurred; handleRoute(ctx, next, route); + popstateOccurred = false; }; } diff --git a/src/components/apphost.js b/src/components/apphost.js index 90b57a784..590b0e842 100644 --- a/src/components/apphost.js +++ b/src/components/apphost.js @@ -280,11 +280,11 @@ define(["appSettings", "browser", "events", "htmlMediaHelper", "globalize"], fun //features.push("multiserver"); features.push("screensaver"); - if (!browser.orsay && !browser.tizen && !browser.msie && (browser.firefox || browser.ps4 || browser.edge || supportsCue())) { + if (!browser.orsay && !browser.msie && (browser.firefox || browser.ps4 || browser.edge || supportsCue())) { features.push("subtitleappearancesettings"); } - if (!browser.orsay && !browser.tizen) { + if (!browser.orsay) { features.push("subtitleburnsettings"); } diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 7f562f1fd..9d86bc9d7 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -1,10 +1,36 @@ -define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusManager', 'indicators', 'globalize', 'layoutManager', 'apphost', 'dom', 'browser', 'playbackManager', 'itemShortcuts', 'scripts/imagehelper', 'css!./card', 'paper-icon-button-light', 'programStyles'], - function (datetime, imageLoader, connectionManager, itemHelper, focusManager, indicators, globalize, layoutManager, appHost, dom, browser, playbackManager, itemShortcuts, imageHelper) { - 'use strict'; +/* eslint-disable indent */ - var enableFocusTransform = !browser.slow && !browser.edge; +/** + * Module for building cards from item data. + * @module components/cardBuilder/cardBuilder + */ - function getCardsHtml(items, options) { +import datetime from 'datetime'; +import imageLoader from 'imageLoader'; +import connectionManager from 'connectionManager'; +import itemHelper from 'itemHelper'; +import focusManager from 'focusManager'; +import indicators from 'indicators'; +import globalize from 'globalize'; +import layoutManager from 'layoutManager'; +import dom from 'dom'; +import browser from 'browser'; +import playbackManager from 'playbackManager'; +import itemShortcuts from 'itemShortcuts'; +import imageHelper from 'scripts/imagehelper'; +import 'css!./card'; +import 'paper-icon-button-light'; +import 'programStyles'; + + const enableFocusTransform = !browser.slow && !browser.edge; + + /** + * Generate the HTML markup for cards for a set of items. + * @param items - The items used to generate cards. + * @param options - The options of the cards. + * @returns {string} The HTML markup for the cards. + */ + export function getCardsHtml(items, options) { if (arguments.length === 1) { options = arguments[0]; items = options.items; @@ -13,6 +39,13 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana return buildCardsHtmlInternal(items, options); } + /** + * Computes the number of posters per row. + * @param {string} shape - Shape of the cards. + * @param {number} screenWidth - Width of the screen. + * @param {boolean} isOrientationLandscape - Flag for the orientation of the screen. + * @returns {number} Number of cards per row for an itemsContainer. + */ function getPostersPerRow(shape, screenWidth, isOrientationLandscape) { switch (shape) { case 'portrait': @@ -217,10 +250,15 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } + /** + * Checks if the window is resizable. + * @param {number} windowWidth - Width of the device's screen. + * @returns {boolean} - Result of the check. + */ function isResizable(windowWidth) { - var screen = window.screen; + const screen = window.screen; if (screen) { - var screenWidth = screen.availWidth; + const screenWidth = screen.availWidth; if ((screenWidth - windowWidth) > 20) { return true; @@ -230,22 +268,31 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana return false; } + /** + * Gets the width of a card's image according to the shape and amount of cards per row. + * @param {string} shape - Shape of the card. + * @param {number} screenWidth - Width of the screen. + * @param {boolean} isOrientationLandscape - Flag for the orientation of the screen. + * @returns {number} Width of the image for a card. + */ function getImageWidth(shape, screenWidth, isOrientationLandscape) { - var imagesPerRow = getPostersPerRow(shape, screenWidth, isOrientationLandscape); - var shapeWidth = Math.round(screenWidth / imagesPerRow) * 2; - - return shapeWidth; + const imagesPerRow = getPostersPerRow(shape, screenWidth, isOrientationLandscape); + return Math.round(screenWidth / imagesPerRow) * 2; } + /** + * Normalizes the options for a card. + * @param {Object} items - A set of items. + * @param {Object} options - Options for handling the items. + */ function setCardData(items, options) { - options.shape = options.shape || "auto"; - var primaryImageAspectRatio = imageLoader.getPrimaryImageAspectRatio(items); + const primaryImageAspectRatio = imageLoader.getPrimaryImageAspectRatio(items); - if (options.shape === 'auto' || options.shape === 'autohome' || options.shape === 'autooverflow' || options.shape === 'autoVertical') { + if (['auto', 'autohome', 'autooverflow', 'autoVertical'].includes(options.shape)) { - var requestedShape = options.shape; + const requestedShape = options.shape; options.shape = null; if (primaryImageAspectRatio) { @@ -283,11 +330,11 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } if (!options.width) { - var screenWidth = dom.getWindowSize().innerWidth; - var screenHeight = dom.getWindowSize().innerHeight; + let screenWidth = dom.getWindowSize().innerWidth; + const screenHeight = dom.getWindowSize().innerHeight; if (isResizable(screenWidth)) { - var roundScreenTo = 100; + const roundScreenTo = 100; screenWidth = Math.floor(screenWidth / roundScreenTo) * roundScreenTo; } @@ -295,9 +342,14 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } + /** + * Generates the internal HTML markup for cards. + * @param {Object} items - Items for which to generate the markup. + * @param {Object} options - Options for generating the markup. + * @returns {string} The internal HTML markup of the cards. + */ function buildCardsHtmlInternal(items, options) { - - var isVertical; + let isVertical = false; if (options.shape === 'autoVertical') { isVertical = true; @@ -305,24 +357,21 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana setCardData(items, options); - var html = ''; - var itemsInRow = 0; + let html = ''; + let itemsInRow = 0; - var currentIndexValue; - var hasOpenRow; - var hasOpenSection; + let currentIndexValue; + let hasOpenRow; + let hasOpenSection; - var sectionTitleTagName = options.sectionTitleTagName || 'div'; - var apiClient; - var lastServerId; + let sectionTitleTagName = options.sectionTitleTagName || 'div'; + let apiClient; + let lastServerId; - var i; - var length; + for (let i = 0; i < items.length; i++) { - for (i = 0, length = items.length; i < length; i++) { - - var item = items[i]; - var serverId = item.ServerId || options.serverId; + let item = items[i]; + let serverId = item.ServerId || options.serverId; if (serverId !== lastServerId) { lastServerId = serverId; @@ -330,14 +379,14 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } if (options.indexBy) { - var newIndexValue = ''; + let newIndexValue = ''; if (options.indexBy === 'PremiereDate') { if (item.PremiereDate) { try { newIndexValue = datetime.toLocaleDateString(datetime.parseISO8601Date(item.PremiereDate), { weekday: 'long', month: 'long', day: 'numeric' }); - } catch (err) { - console.error('error parsing timestamp for premiere date'); + } catch (error) { + console.error('error parsing timestamp for premiere date', error); } } } else if (options.indexBy === 'ProductionYear') { @@ -412,21 +461,15 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } - var cardFooterHtml = ''; - for (i = 0, length = (options.lines || 0); i < length; i++) { - - if (i === 0) { - cardFooterHtml += '