diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 1b16b94b62..9c231e1391 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -7,38 +7,70 @@ trigger: tags: include: - '*' +pr: + branches: + include: + - '*' jobs: - - job: main_build - displayName: 'Main Build' - - dependsOn: lint - condition: succeeded() + - job: build + displayName: 'Build' pool: vmImage: 'ubuntu-latest' + strategy: + matrix: + Development: + BuildConfiguration: development + Production: + BuildConfiguration: production + Standalone: + BuildConfiguration: standalone + maxParallel: 3 + steps: - task: NodeTool@0 displayName: 'Install Node' inputs: - versionSpec: '10.x' + versionSpec: '12.x' - - script: 'yarn install' + - task: Cache@2 + displayName: 'Check Cache' + inputs: + key: 'yarn | yarn.lock' + path: 'node_modules' + cacheHitVar: CACHE_RESTORED + + - script: 'yarn install --frozen-lockfile' displayName: 'Install Dependencies' + condition: ne(variables.CACHE_RESTORED, 'true') + + - script: 'yarn build:development' + displayName: 'Build Development' + condition: eq(variables['BuildConfiguration'], 'development') + + - script: 'yarn build:production' + displayName: 'Build Bundle' + condition: eq(variables['BuildConfiguration'], 'production') + + - script: 'yarn build:standalone' + displayName: 'Build Standalone' + condition: eq(variables['BuildConfiguration'], 'standalone') - script: 'test -d dist' displayName: 'Check Build' - - script: 'yarn pack --filename jellyfin-web.tgz' - displayName: 'Bundle Release' + - script: 'mv dist jellyfin-web' + displayName: 'Rename Directory' + condition: succeeded() - task: PublishPipelineArtifact@1 displayName: 'Publish Release' condition: succeeded() inputs: - targetPath: '$(Build.SourcesDirectory)/jellyfin-web.tgz' - artifactName: 'jellyfin-web' + targetPath: '$(Build.SourcesDirectory)/jellyfin-web' + artifactName: 'jellyfin-web-$(BuildConfiguration)' - job: lint displayName: 'Lint' @@ -50,14 +82,21 @@ jobs: - task: NodeTool@0 displayName: 'Install Node' inputs: - versionSpec: '10.x' + versionSpec: '12.x' - - script: 'yarn install' + - task: Cache@2 + displayName: 'Check Cache' + inputs: + key: 'yarn | yarn.lock' + path: 'node_modules' + cacheHitVar: CACHE_RESTORED + + - script: 'yarn install --frozen-lockfile' displayName: 'Install Dependencies' + condition: ne(variables.CACHE_RESTORED, 'true') - - script: 'yarn run lint' + - script: 'yarn run lint --quiet' displayName: 'Run ESLint' - - script: | - yarn run stylelint - displayName: 'Run stylelint' + - script: 'yarn run stylelint' + displayName: 'Run Stylelint' diff --git a/.eslintrc.yml b/.eslintrc.yml index f5ce779d44..f501f33546 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,15 +1,31 @@ env: - es6: false - browser: true amd: true + browser: true + es6: true + es2017: true + es2020: true + +parserOptions: + ecmaVersion: 2020 + sourceType: module + ecmaFeatures: + impliedStrict: true + +plugins: + - promise + - import + - eslint-comments + +extends: + - eslint:recommended + - plugin:promise/recommended + - plugin:import/errors + - plugin:import/warnings + - plugin:eslint-comments/recommended globals: - # New browser globals - DataView: readonly + # Browser globals MediaMetadata: readonly - Promise: readonly - # Deprecated browser globals - DocumentTouch: readonly # Tizen globals tizen: readonly webapis: readonly @@ -18,7 +34,6 @@ globals: # Dependency globals $: readonly jQuery: readonly - queryString: readonly requirejs: readonly # Jellyfin globals ApiClient: writable @@ -34,8 +49,7 @@ globals: getWindowLocationSearch: writable Globalize: writable Hls: writable - humaneDate: writable - humaneElapsed: writable + dfnshelper: writable LibraryMenu: writable LinkParser: writable LiveTvHelpers: writable @@ -46,9 +60,6 @@ globals: UserParentalControlPage: writable Windows: readonly -extends: - - eslint:recommended - rules: block-spacing: ["error"] brace-style: ["error"] @@ -69,3 +80,8 @@ rules: no-redeclare: ["warn"] no-unused-vars: ["warn"] no-useless-escape: ["warn"] + promise/catch-or-return: ["warn"] + promise/always-return: ["warn"] + promise/no-return-wrap: ["warn"] + # TODO: Remove after ES6 migration is complete + import/no-unresolved: ["warn"] diff --git a/.gitignore b/.gitignore index 10b2d24f42..2bb5bc64d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,578 +1,10 @@ +# config +config.json -# Created by https://www.gitignore.io/api/node,rider,macos,linux,windows,visualstudio,visualstudiocode -# Edit at https://www.gitignore.io/?templates=node,rider,macos,linux,windows,visualstudio,visualstudiocode - -### Linux ### -*~ - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -### macOS ### -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -### Node ### -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Dependency lockfile -package-lock.json - -# TypeScript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# next.js build output -.next - -# nuxt.js build output -.nuxt - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -### Rider ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/modules.xml -# .idea/*.iml -# .idea/modules - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -### VisualStudioCode ### -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json - -### VisualStudioCode Patch ### -# Ignore all local history of files -.history - -### Windows ### -# Windows thumbnail cache files -Thumbs.db -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -### VisualStudio ### -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ -# ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true -**/wwwroot/lib/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- Backup*.rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# End of https://www.gitignore.io/api/node,rider,macos,linux,windows,visualstudio,visualstudiocode - -# dist for webpack output +# npm dist +node_modules + +# ide +.idea +.vscode diff --git a/README.md b/README.md index 6a80b0b09c..e2aac6b155 100644 --- a/README.md +++ b/README.md @@ -45,20 +45,37 @@ Jellyfin Web is the frontend used for most of the clients available for end user ### Dependencies - Yarn +- Gulp-cli ### Getting Started 1. Clone or download this repository. + ```sh git clone https://github.com/jellyfin/jellyfin-web.git cd jellyfin-web ``` + 2. Install build dependencies in the project directory. + ```sh yarn install ``` 3. Run the web client with webpack for local development. + ```sh yarn serve ``` + +4. Build the client with sourcemaps. + + ```sh + yarn build:development + ``` + + You can build a nginx compatible version as well. + + ```sh + yarn build:standalone + ``` \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000000..0eb5593541 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,201 @@ +'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: ["development", "production"], + default: "development", + verbose: false +}); +const stream = require('webpack-stream'); +const inject = require('gulp-inject'); +const postcss = require('gulp-postcss'); +const sass = require('gulp-sass'); +const gulpif = require('gulp-if'); +const lazypipe = require('lazypipe'); + +sass.compiler = require('node-sass'); + +let config; +if (mode.production()) { + config = require('./webpack.prod.js'); +} else { + 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: { + baseDir: "./dist" + }, + port: 8080 + }); + + let events = ['add', 'change']; + + 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/']); +} + +let pipelineJavascript = lazypipe() + .pipe(function () { + return mode.development(sourcemaps.init({ loadMaps: true })); + }) + .pipe(function () { + return babel({ + presets: [ + ['@babel/preset-env'] + ] + }); + }) + .pipe(function () { + return terser({ + keep_fnames: true, + mangle: false + }); + }) + .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(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()) + .pipe(mode.development(sourcemaps.write('.'))) + .pipe(dest('dist/')) + .pipe(browserSync.stream()); +} + +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(query) { + return src(typeof query !== 'function' ? query : options.images.query, { base: './src/' }) + .pipe(mode.production(imagemin())) + .pipe(dest('dist/')) + .pipe(browserSync.stream()); +} + +function copy(query) { + return src(typeof query !== 'function' ? query : options.copy.query, { base: './src/' }) + .pipe(dest('dist/')) + .pipe(browserSync.stream()); +} + +function injectBundle() { + return src(options.injectBundle.query, { base: './src/' }) + .pipe(inject( + src(['src/scripts/apploader.js'], { read: false }, { base: './src/' }), { relative: true } + )) + .pipe(dest('dist/')) + .pipe(browserSync.stream()); +} + +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 726823208f..bac0d7e251 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,41 @@ "repository": "https://github.com/jellyfin/jellyfin-web", "license": "GPL-2.0-or-later", "devDependencies": { + "@babel/core": "^7.8.6", + "@babel/plugin-transform-modules-amd": "^7.8.3", + "@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", - "html-webpack-plugin": "^3.2.0", + "eslint-plugin-eslint-comments": "^3.1.2", + "eslint-plugin-import": "^2.20.2", + "eslint-plugin-promise": "^4.2.1", + "file-loader": "^6.0.0", + "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-if": "^3.0.0", + "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": "^4.0.2", + "lazypipe": "^1.0.2", + "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 +49,13 @@ "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", + "date-fns": "^2.11.1", "document-register-element": "^1.14.3", "flv.js": "^1.5.0", "hls.js": "^0.13.1", @@ -31,17 +63,42 @@ "jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto", "jquery": "^3.4.1", "jstree": "^3.3.7", - "libass-wasm": "^2.1.1", - "libjass": "^0.11.0", + "libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus#4.0.0-jf", "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", + "shaka-player": "^2.5.10", "sortablejs": "^1.10.2", "swiper": "^5.3.1", "webcomponents.js": "^0.7.24", "whatwg-fetch": "^3.0.0" }, + "babel": { + "presets": [ + "@babel/preset-env" + ], + "overrides": [ + { + "test": [ + "src/components/autoFocuser.js", + "src/components/cardbuilder/cardBuilder.js", + "src/components/dom.js", + "src/components/filedownloader.js", + "src/components/filesystem.js", + "src/components/input/keyboardnavigation.js", + "src/components/sanatizefilename.js", + "src/scripts/settings/webSettings.js", + "src/components/scrollManager.js", + "src/scripts/dfnshelper.js" + ], + "plugins": [ + "@babel/plugin-transform-modules-amd" + ] + } + ] + }, "browserslist": [ "last 2 Firefox versions", "last 2 Chrome versions", @@ -49,6 +106,7 @@ "last 2 Safari versions", "last 2 iOS versions", "last 2 Edge versions", + "Chrome 27", "Chrome 38", "Chrome 47", "Chrome 53", @@ -57,10 +115,12 @@ "Firefox ESR" ], "scripts": { - "serve": "webpack-dev-server --config webpack.dev.js --open", - "build": "webpack --config webpack.prod.js", + "serve": "gulp serve", + "prepare": "gulp --production", + "build:development": "gulp --development", + "build:production": "gulp --production", + "build:standalone": "gulp standalone --development", "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/assets/css/dashboard.css b/src/assets/css/dashboard.css index 8c8a9ca7f1..894d7332f4 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/fonts.css b/src/assets/css/fonts.css index f125bc6a03..cb0da0f80f 100644 --- a/src/assets/css/fonts.css +++ b/src/assets/css/fonts.css @@ -29,7 +29,7 @@ h3 { } .layout-tv { - font-size: 2.5vh; + font-size: 130%; } .layout-mobile { diff --git a/src/assets/css/librarybrowser.css b/src/assets/css/librarybrowser.css index 7a3e0ada68..e4b5bcf8d6 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; } } @@ -439,10 +436,46 @@ background-size: cover; background-repeat: no-repeat; background-position: center; + background-attachment: fixed; height: 50vh; position: relative; } +.layout-mobile .itemBackdrop { + background-attachment: scroll; +} + +.layout-desktop .itemBackdrop::after, +.layout-tv .itemBackdrop::after { + content: ""; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.65); + display: block; +} + +.layout-desktop .noBackdrop .itemBackdrop, +.layout-tv .noBackdrop .itemBackdrop { + display: none; +} + +.detailPageContent { + display: flex; + flex-direction: column; + padding-left: 2%; + padding-right: 2%; +} + +.layout-desktop .noBackdrop .detailPageContent, +.layout-tv .noBackdrop .detailPageContent { + margin-top: 2.5em; +} + +.layout-desktop .noBackdrop .detailImageContainer img, +.layout-tv .noBackdrop .detailImageContainer img { + margin-top: 0; +} + .personBackdrop { background-size: contain; } @@ -503,14 +536,13 @@ display: flex; align-items: center; justify-content: center; + text-align: center; } .detailPagePrimaryContainer { display: flex; align-items: center; align-content: center; - position: sticky; - top: 3.85em; z-index: 2; } @@ -520,13 +552,21 @@ top: 0; } -.layout-tv .detailPagePrimaryContainer { +.layout-tv #itemDetailPage:not(.noBackdrop) .detailPagePrimaryContainer, +.layout-desktop #itemDetailPage:not(.noBackdrop) .detailPagePrimaryContainer { position: relative; top: 0; + padding-left: 32.45vw; } -.detailSticky { - background-color: #101010; +.layout-desktop .detailSticky, +.layout-tv .detailSticky { + margin-top: -7.2em; +} + +.layout-desktop .noBackdrop .detailSticky, +.layout-tv .noBackdrop .detailSticky { + margin-top: 0; } .infoWrapper { @@ -548,23 +588,17 @@ margin: 1.25em 0; } -.detailPageContent { - display: flex; - flex-direction: column; - padding-left: 2%; - padding-right: 2%; -} - .detailImageContainer { - position: sticky; - top: 25%; + position: relative; + margin-top: -25vh; float: left; - width: 22.786458333333332vw; + width: 25vw; + z-index: 3; } -.layout-mobile .detailImageContainer, -.layout-tv .detailImageContainer { - position: relative; +.layout-desktop .noBackdrop .detailImageContainer, +.layout-tv .noBackdrop .detailImageContainer { + margin-top: 0; } .detailPagePrimaryContent { @@ -572,15 +606,19 @@ } .detailLogo { - width: 25em; - height: 9.375em; + width: 67.25vw; + height: 14.5vh; position: absolute; - top: 14.5%; - right: 10.5%; + top: 15vh; + right: 0; -webkit-background-size: contain; background-size: contain; } +.noBackdrop .detailLogo { + display: none; +} + @media all and (max-width: 87.5em) { .detailLogo { right: 5%; @@ -607,8 +645,8 @@ .itemDetailImage { width: 100% !important; - box-shadow: 0 0.0725em 0.29em 0 rgba(0, 0, 0, 0.37); - -webkit-box-shadow: 0 0.0725em 0.29em 0 rgba(0, 0, 0, 0.37); + -webkit-box-shadow: 0 0.1em 0.5em 0 rgba(0, 0, 0, 0.75); + box-shadow: 0 0.1em 0.5em 0 rgba(0, 0, 0, 0.75); } div.itemDetailGalleryLink.defaultCardBackground { @@ -635,6 +673,16 @@ div.itemDetailGalleryLink.defaultCardBackground { position: relative; } + .layout-desktop .detailPageWrapperContainer, + .layout-tv .detailPageWrapperContainer { + margin-top: 7.2em; + } + + .layout-tv #itemDetailPage:not(.noBackdrop) .detailPagePrimaryContainer, + .layout-desktop #itemDetailPage:not(.noBackdrop) .detailPagePrimaryContainer { + padding-left: 3.3%; + } + .btnPlaySimple { display: none !important; } @@ -803,7 +851,7 @@ div.itemDetailGalleryLink.defaultCardBackground { width: auto; align-items: center; justify-content: center; - margin-top: -4.3em; + margin-top: -4.2em; position: relative; } @@ -849,6 +897,11 @@ div.itemDetailGalleryLink.defaultCardBackground { border-collapse: collapse; } +.layout-desktop .noBackdrop .detailPageWrapperContainer, +.layout-tv .noBackdrop .detailPageWrapperContainer { + margin-top: 3.8em; +} + .mediaInfoStream { margin: 0 3em 0 0; display: inline-block; @@ -1066,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 6a352c5776..316f42c94a 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,7 +96,30 @@ _define("material-icons", function() { return material_icons; }); -var jellyfin_noto = require("jellyfin-noto"); +// noto font +var noto = require("jellyfin-noto"); _define("jellyfin-noto", function () { - return jellyfin_noto; + return noto; +}); + +// page.js +var page = require("page"); +_define("page", function() { + return page; +}); + +var polyfill = require("@babel/polyfill/dist/polyfill"); +_define("polyfill", function () { + return polyfill; +}); + +// Date-FNS +var date_fns = require("date-fns"); +_define("date-fns", function () { + return date_fns; +}); + +var date_fns_locale = require("date-fns/locale"); +_define("date-fns/locale", function () { + return date_fns_locale; }); diff --git a/src/components/activitylog.js b/src/components/activitylog.js index 05971f01b8..934a610ad0 100644 --- a/src/components/activitylog.js +++ b/src/components/activitylog.js @@ -1,4 +1,4 @@ -define(["events", "globalize", "dom", "datetime", "userSettings", "serverNotifications", "connectionManager", "emby-button", "listViewStyle"], function (events, globalize, dom, datetime, userSettings, serverNotifications, connectionManager) { +define(["events", "globalize", "dom", "date-fns", "dfnshelper", "userSettings", "serverNotifications", "connectionManager", "emby-button", "listViewStyle"], function (events, globalize, dom, datefns, dfnshelper, userSettings, serverNotifications, connectionManager) { "use strict"; function getEntryHtml(entry, apiClient) { @@ -26,8 +26,7 @@ define(["events", "globalize", "dom", "datetime", "userSettings", "serverNotific html += entry.Name; html += ""; html += '
'; - var date = datetime.parseISO8601Date(entry.Date, true); - html += datetime.toLocaleString(date).toLowerCase(); + html += datefns.formatRelative(Date.parse(entry.Date), Date.parse(new Date()), { locale: dfnshelper.getLocale() }); html += "
"; html += '
'; html += entry.ShortOverview || ""; diff --git a/src/components/appRouter.js b/src/components/appRouter.js index 74b1a5cd5f..62bfb3cf40 100644 --- a/src/components/appRouter.js +++ b/src/components/appRouter.js @@ -370,7 +370,7 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM } function enableNativeHistory() { - return page.enableNativeHistory(); + return false; } function authenticate(ctx, route, callback) { @@ -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; }; } @@ -562,7 +569,10 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM if (!document.querySelector('.dialogContainer') && startPages.indexOf(curr.type) !== -1) { return false; } - return page.canGoBack(); + if (enableHistory()) { + return history.length > 1; + } + return (page.len || 0) > 0; } function showDirect(path) { @@ -666,7 +676,8 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM function pushState(state, title, url) { state.navigate = false; - page.pushState(state, title, url); + history.pushState(state, title, url); + } function setBaseRoute() { @@ -716,7 +727,7 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM appRouter.getRoutes = getRoutes; appRouter.pushState = pushState; appRouter.enableNativeHistory = enableNativeHistory; - appRouter.handleAnchorClick = page.handleAnchorClick; + appRouter.handleAnchorClick = page.clickHandler; appRouter.TransparencyLevel = { None: 0, Backdrop: 1, diff --git a/src/components/appfooter/appfooter.css b/src/components/appfooter/appfooter.css index 93cb3a75a5..6dc00b00c2 100644 --- a/src/components/appfooter/appfooter.css +++ b/src/components/appfooter/appfooter.css @@ -2,7 +2,7 @@ position: fixed; left: 0; right: 0; - z-index: 1; + z-index: 10; bottom: 0; transition: transform 180ms linear; contain: layout style; diff --git a/src/components/apphost.js b/src/components/apphost.js index f3e89ed29e..868416b369 100644 --- a/src/components/apphost.js +++ b/src/components/apphost.js @@ -1,4 +1,4 @@ -define(["appSettings", "browser", "events", "htmlMediaHelper"], function (appSettings, browser, events, htmlMediaHelper) { +define(["appSettings", "browser", "events", "htmlMediaHelper", "webSettings"], function (appSettings, browser, events, htmlMediaHelper, webSettings) { "use strict"; function getBaseProfileOptions(item) { @@ -276,15 +276,17 @@ define(["appSettings", "browser", "events", "htmlMediaHelper"], function (appSet features.push("otherapppromotions"); features.push("displaymode"); features.push("targetblank"); - // allows users to connect to more than one server - //features.push("multiserver"); features.push("screensaver"); - if (!browser.orsay && !browser.tizen && !browser.msie && (browser.firefox || browser.ps4 || browser.edge || supportsCue())) { + webSettings.enableMultiServer().then(enabled => { + if (enabled) features.push("multiserver") + }) + + 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/autoFocuser.js b/src/components/autoFocuser.js index 6d99009e67..43c341bfdf 100644 --- a/src/components/autoFocuser.js +++ b/src/components/autoFocuser.js @@ -1,22 +1,29 @@ -define(["focusManager", "layoutManager"], function (focusManager, layoutManager) { - "use strict"; +/* eslint-disable indent */ + +/** + * Module for performing auto-focus. + * @module components/autoFocuser + */ + +import focusManager from "focusManager"; +import layoutManager from "layoutManager"; /** * Previously selected element. */ - var activeElement; + let activeElement; /** - * Returns true if AutoFocuser is enabled. + * Returns _true_ if AutoFocuser is enabled. */ - function isEnabled() { + export function isEnabled() { return layoutManager.tv; } /** - * Start AutoFocuser + * Start AutoFocuser. */ - function enable() { + export function enable() { if (!isEnabled()) { return; } @@ -28,24 +35,19 @@ define(["focusManager", "layoutManager"], function (focusManager, layoutManager) console.debug("AutoFocuser enabled"); } - /** - * Create an array from some source. - */ - var arrayFrom = Array.prototype.from || function (src) { - return Array.prototype.slice.call(src); - } - /** * Set focus on a suitable element, taking into account the previously selected. + * @param {HTMLElement} [container] - Element to limit scope. + * @returns {HTMLElement} Focused element. */ - function autoFocus(container) { + export function autoFocus(container) { if (!isEnabled()) { - return; + return null; } container = container || document.body; - var candidates = []; + let candidates = []; if (activeElement) { // These elements are recreated @@ -62,10 +64,10 @@ define(["focusManager", "layoutManager"], function (focusManager, layoutManager) candidates.push(activeElement); } - candidates = candidates.concat(arrayFrom(container.querySelectorAll(".btnResume"))); - candidates = candidates.concat(arrayFrom(container.querySelectorAll(".btnPlay"))); + candidates = candidates.concat(Array.from(container.querySelectorAll(".btnResume"))); + candidates = candidates.concat(Array.from(container.querySelectorAll(".btnPlay"))); - var focusedElement; + let focusedElement; candidates.every(function (element) { if (focusManager.isCurrentlyFocusable(element)) { @@ -79,7 +81,7 @@ define(["focusManager", "layoutManager"], function (focusManager, layoutManager) if (!focusedElement) { // FIXME: Multiple itemsContainers - var itemsContainer = container.querySelector(".itemsContainer"); + const itemsContainer = container.querySelector(".itemsContainer"); if (itemsContainer) { focusedElement = focusManager.autoFocus(itemsContainer); @@ -93,9 +95,10 @@ define(["focusManager", "layoutManager"], function (focusManager, layoutManager) return focusedElement; } - return { - isEnabled: isEnabled, - enable: enable, - autoFocus: autoFocus - }; -}); +/* eslint-enable indent */ + +export default { + isEnabled: isEnabled, + enable: enable, + autoFocus: autoFocus +}; diff --git a/src/components/backdrop/backdrop.js b/src/components/backdrop/backdrop.js index 7320978f71..ec5b411853 100644 --- a/src/components/backdrop/backdrop.js +++ b/src/components/backdrop/backdrop.js @@ -182,6 +182,7 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', "userSettings" return apiClient.getScaledImageUrl(item.BackdropItemId || item.Id, Object.assign(imageOptions, { type: "Backdrop", tag: imgTag, + maxWidth: dom.getScreenWidth(), index: index })); }); @@ -192,6 +193,7 @@ define(['browser', 'connectionManager', 'playbackManager', 'dom', "userSettings" return apiClient.getScaledImageUrl(item.ParentBackdropItemId, Object.assign(imageOptions, { type: "Backdrop", tag: imgTag, + maxWidth: dom.getScreenWidth(), index: index })); }); diff --git a/src/components/cardbuilder/card.css b/src/components/cardbuilder/card.css index ea9f42927f..3cd038cd09 100644 --- a/src/components/cardbuilder/card.css +++ b/src/components/cardbuilder/card.css @@ -429,6 +429,12 @@ button::-moz-focus-inner { font-size: 1.66956521739130434em !important; } +.cardOverlayButtonIcon.material-icons { + /* material-icons override display, so we need to + make a better matching selector to set it to flex */ + display: flex; +} + .cardOverlayButton-centered { bottom: initial; right: initial; diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index bec641f407..a4cf6edadc 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -1,11 +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 devicePixelRatio = window.devicePixelRatio || 1; - 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; @@ -14,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': @@ -218,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; @@ -231,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 = screenWidth / imagesPerRow; - - return Math.round(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) { @@ -284,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; } @@ -296,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; @@ -306,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; @@ -331,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') { @@ -413,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 += '
 
'; - } else { - cardFooterHtml += '
 
'; - } - } - return html; } + /** + * Computes the aspect ratio for a card given its shape. + * @param {string} shape - Shape for which to get the aspect ratio. + * @returns {null|number} Ratio of the shape. + */ function getDesiredAspect(shape) { - if (shape) { shape = shape.toLowerCase(); if (shape.indexOf('portrait') !== -1) { @@ -446,23 +488,29 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana return null; } + /** Get the URL of the card's image. + * @param {Object} item - Item for which to generate a card. + * @param {Object} apiClient - API client object. + * @param {Object} options - Options of the card. + * @param {string} shape - Shape of the desired image. + * @returns {Object} Object representing the URL of the card's image. + */ function getCardImageUrl(item, apiClient, options, shape) { + item = item.ProgramInfo || item; - var imageItem = item.ProgramInfo || item; - item = imageItem; - - var width = options.width; - var height = null; - var primaryImageAspectRatio = item.PrimaryImageAspectRatio; - var forceName = false; - var imgUrl = null; - var coverImage = false; - var uiAspect = null; + const width = options.width; + let height = null; + const primaryImageAspectRatio = item.PrimaryImageAspectRatio; + let forceName = false; + let imgUrl = null; + let coverImage = false; + let uiAspect = null; if (options.preferThumb && item.ImageTags && item.ImageTags.Thumb) { imgUrl = apiClient.getScaledImageUrl(item.Id, { type: "Thumb", + maxWidth: width, tag: item.ImageTags.Thumb }); @@ -470,6 +518,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.Id, { type: "Banner", + maxWidth: width, tag: item.ImageTags.Banner }); @@ -477,6 +526,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.Id, { type: "Disc", + maxWidth: width, tag: item.ImageTags.Disc }); @@ -484,6 +534,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.Id, { type: "Logo", + maxWidth: width, tag: item.ImageTags.Logo }); @@ -491,6 +542,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.ParentLogoItemId, { type: "Logo", + maxWidth: width, tag: item.ParentLogoImageTag }); @@ -498,6 +550,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.SeriesId, { type: "Thumb", + maxWidth: width, tag: item.SeriesThumbImageTag }); @@ -505,6 +558,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.ParentThumbItemId, { type: "Thumb", + maxWidth: width, tag: item.ParentThumbImageTag }); @@ -512,6 +566,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.Id, { type: "Backdrop", + maxWidth: width, tag: item.BackdropImageTags[0] }); @@ -521,6 +576,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, { type: "Backdrop", + maxWidth: width, tag: item.ParentBackdropImageTags[0] }); @@ -530,6 +586,8 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.Id, { type: "Primary", + maxHeight: height, + maxWidth: width, tag: item.ImageTags.Primary }); @@ -550,6 +608,8 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.PrimaryImageItemId || item.Id || item.ItemId, { type: "Primary", + maxHeight: height, + maxWidth: width, tag: item.PrimaryImageTag }); @@ -567,20 +627,24 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.ParentPrimaryImageItemId, { type: "Primary", + maxWidth: width, tag: item.ParentPrimaryImageTag }); } else if (item.SeriesPrimaryImageTag) { imgUrl = apiClient.getScaledImageUrl(item.SeriesId, { type: "Primary", + maxWidth: width, tag: item.SeriesPrimaryImageTag }); } else if (item.AlbumId && item.AlbumPrimaryImageTag) { - width = primaryImageAspectRatio ? Math.round(height * primaryImageAspectRatio) : null; + height = width && primaryImageAspectRatio ? Math.round(width / primaryImageAspectRatio) : null; imgUrl = apiClient.getScaledImageUrl(item.AlbumId, { type: "Primary", + maxHeight: height, + maxWidth: width, tag: item.AlbumPrimaryImageTag }); @@ -594,6 +658,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.Id, { type: "Thumb", + maxWidth: width, tag: item.ImageTags.Thumb }); @@ -601,6 +666,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.Id, { type: "Backdrop", + maxWidth: width, tag: item.BackdropImageTags[0] }); @@ -608,6 +674,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.Id, { type: "Thumb", + maxWidth: width, tag: item.ImageTags.Thumb }); @@ -615,6 +682,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.SeriesId, { type: "Thumb", + maxWidth: width, tag: item.SeriesThumbImageTag }); @@ -622,6 +690,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.ParentThumbItemId, { type: "Thumb", + maxWidth: width, tag: item.ParentThumbImageTag }); @@ -629,6 +698,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, { type: "Backdrop", + maxWidth: width, tag: item.ParentBackdropImageTags[0] }); @@ -641,21 +711,32 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana }; } + /** + * Generates a random integer in a given range. + * @param {number} min - Minimum of the range. + * @param {number} max - Maximum of the range. + * @returns {number} Randomly generated number. + */ function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } - var numRandomColors = 5; + /** + * Generates an index used to select the default color of a card based on a string. + * @param {string} str - String to use for generating the index. + * @returns {number} Index of the color. + */ function getDefaultColorIndex(str) { + const numRandomColors = 5; if (str) { - var charIndex = Math.floor(str.length / 2); - var character = String(str.substr(charIndex, 1).charCodeAt()); - var sum = 0; - for (var i = 0; i < character.length; i++) { + const charIndex = Math.floor(str.length / 2); + const character = String(str.substr(charIndex, 1).charCodeAt()); + let sum = 0; + for (let i = 0; i < character.length; i++) { sum += parseInt(character.charAt(i)); } - var index = String(sum).substr(-1); + let index = String(sum).substr(-1); return (index % numRandomColors) + 1; } else { @@ -663,18 +744,26 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } + /** + * Generates the HTML markup for a card's text. + * @param {Array} lines - Array containing the text lines. + * @param {string} cssClass - Base CSS class to use for the lines. + * @param {boolean} forceLines - Flag to force the rendering of all lines. + * @param {boolean} isOuterFooter - Flag to mark the text lines as outer footer. + * @param {string} cardLayout - DEPRECATED + * @param {boolean} addRightMargin - Flag to add a right margin to the text. + * @param {number} maxLines - Maximum number of lines to render. + * @returns {string} HTML markup for the card's text. + */ function getCardTextLines(lines, cssClass, forceLines, isOuterFooter, cardLayout, addRightMargin, maxLines) { + let html = ''; - var html = ''; + let valid = 0; - var valid = 0; - var i; - var length; + for (let i = 0; i < lines.length; i++) { - for (i = 0, length = lines.length; i < length; i++) { - - var currentCssClass = cssClass; - var text = lines[i]; + let currentCssClass = cssClass; + let text = lines[i]; if (valid > 0 && isOuterFooter) { currentCssClass += ' cardText-secondary'; @@ -700,9 +789,9 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana if (forceLines) { - length = maxLines || Math.min(lines.length, maxLines || lines.length); + let linesLength = maxLines || Math.min(lines.length, maxLines || lines.length); - while (valid < length) { + while (valid < linesLength) { html += "
 
"; valid++; } @@ -711,17 +800,29 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana return html; } + /** + * Determines if the item is live TV. + * @param {Object} item - Item to use for the check. + * @returns {boolean} Flag showing if the item is live TV. + */ function isUsingLiveTvNaming(item) { return item.Type === 'Program' || item.Type === 'Timer' || item.Type === 'Recording'; } + /** + * Returns the air time text for the item based on the given times. + * @param {object} item - Item used to generate the air time text. + * @param {string} showAirDateTime - ISO8601 date for the start of the show. + * @param {string} showAirEndTime - ISO8601 date for the end of the show. + * @returns {string} The air time text for the item based on the given dates. + */ function getAirTimeText(item, showAirDateTime, showAirEndTime) { + let airTimeText = ''; - var airTimeText = ''; if (item.StartDate) { try { - var date = datetime.parseISO8601Date(item.StartDate); + let date = datetime.parseISO8601Date(item.StartDate); if (showAirDateTime) { airTimeText += datetime.toLocaleDateString(date, { weekday: 'short', month: 'short', day: 'numeric' }) + ' '; @@ -741,15 +842,29 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana return airTimeText; } + /** + * Generates the HTML markup for the card's footer text. + * @param {Object} item - Item used to generate the footer text. + * @param {Object} apiClient - API client instance. + * @param {Object} options - Options used to generate the footer text. + * @param {string} showTitle - Flag to show the title in the footer. + * @param {boolean} forceName - Flag to force showing the name of the item. + * @param {boolean} overlayText - Flag to show overlay text. + * @param {Object} imgUrl - Object representing the card's image URL. + * @param {string} footerClass - CSS classes of the footer element. + * @param {string} progressHtml - HTML markup of the progress bar element. + * @param {string} logoUrl - URL of the logo for the item. + * @param {boolean} isOuterFooter - Flag to mark the text as outer footer. + * @returns {string} HTML markup of the card's footer text element. + */ function getCardFooterText(item, apiClient, options, showTitle, forceName, overlayText, imgUrl, footerClass, progressHtml, logoUrl, isOuterFooter) { - - var html = ''; + let html = ''; if (logoUrl) { html += ''; } - var showOtherText = isOuterFooter ? !overlayText : overlayText; + const showOtherText = isOuterFooter ? !overlayText : overlayText; if (isOuterFooter && options.cardLayout && layoutManager.mobile) { @@ -758,12 +873,12 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } - var cssClass = options.centerText ? "cardText cardTextCentered" : "cardText"; - var serverId = item.ServerId || options.serverId; + const cssClass = options.centerText ? "cardText cardTextCentered" : "cardText"; + const serverId = item.ServerId || options.serverId; - var lines = []; - var parentTitleUnderneath = item.Type === 'MusicAlbum' || item.Type === 'Audio' || item.Type === 'MusicVideo'; - var titleAdded; + let lines = []; + const parentTitleUnderneath = item.Type === 'MusicAlbum' || item.Type === 'Audio' || item.Type === 'MusicVideo'; + let titleAdded; if (showOtherText) { if ((options.showParentTitle || options.showParentTitleOrTitle) && !parentTitleUnderneath) { @@ -792,7 +907,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } else { - var parentTitle = item.SeriesName || item.Series || item.Album || item.AlbumArtist || ""; + const parentTitle = item.SeriesName || item.Series || item.Album || item.AlbumArtist || ""; if (parentTitle || showTitle) { lines.push(parentTitle); @@ -802,14 +917,14 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } - var showMediaTitle = (showTitle && !titleAdded) || (options.showParentTitleOrTitle && !lines.length); + let showMediaTitle = (showTitle && !titleAdded) || (options.showParentTitleOrTitle && !lines.length); if (!showMediaTitle && !titleAdded && (showTitle || forceName)) { showMediaTitle = true; } if (showMediaTitle) { - var name = options.showTitle === 'auto' && !item.IsFolder && item.MediaType === 'Photo' ? '' : itemHelper.getDisplayName(item, { + const name = options.showTitle === 'auto' && !item.IsFolder && item.MediaType === 'Photo' ? '' : itemHelper.getDisplayName(item, { includeParentInfo: options.includeParentInfoInTitle }); @@ -836,22 +951,18 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } if (options.showItemCounts) { - - var itemCountHtml = getItemCountsHtml(options, item); - - lines.push(itemCountHtml); + lines.push(getItemCountsHtml(options, item)); } if (options.textLines) { - var additionalLines = options.textLines(item); - for (var i = 0, length = additionalLines.length; i < length; i++) { + const additionalLines = options.textLines(item); + for (let i = 0; i < additionalLines.length; i++) { lines.push(additionalLines[i]); } } if (options.showSongCount) { - - var songLine = ''; + let songLine = ''; if (item.SongCount) { songLine = item.SongCount === 1 ? @@ -889,7 +1000,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } else { if (item.EndDate && item.ProductionYear) { - var endYear = datetime.parseISO8601Date(item.EndDate).getFullYear(); + const endYear = datetime.parseISO8601Date(item.EndDate).getFullYear(); lines.push(item.ProductionYear + ((endYear === item.ProductionYear) ? '' : (' - ' + endYear))); } else { lines.push(item.ProductionYear || ''); @@ -971,11 +1082,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana if (options.showPersonRoleOrType) { if (item.Role) { - lines.push('as ' + item.Role); - } else if (item.Type) { - lines.push(globalize.translate('' + item.Type)); - } else { - lines.push(''); + lines.push(globalize.translate('PersonRole', item.Role)); } } } @@ -984,7 +1091,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana lines = []; } - var addRightTextMargin = isOuterFooter && options.cardLayout && !options.centerText && options.cardFooterAside !== 'none' && layoutManager.mobile; + const addRightTextMargin = isOuterFooter && options.cardLayout && !options.centerText && options.cardFooterAside !== 'none' && layoutManager.mobile; html += getCardTextLines(lines, cssClass, !options.overlayText, isOuterFooter, options.cardLayout, addRightTextMargin, options.lines); @@ -1005,8 +1112,14 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana return html; } + /** + * Generates the HTML markup for the action button. + * @param {Object} item - Item used to generate the action button. + * @param {string} text - Text of the action button. + * @param {string} serverId - ID of the server. + * @returns {string} HTML markup of the action button. + */ function getTextActionButton(item, text, serverId) { - if (!text) { text = itemHelper.getDisplayName(item); } @@ -1015,18 +1128,22 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana return text; } - var html = ''; return html; } + /** + * Generates HTML markup for the item count indicator. + * @param {Object} options - Options used to generate the item count. + * @param {Object} item - Item used to generate the item count. + * @returns {string} HTML markup for the item count indicator. + */ function getItemCountsHtml(options, item) { - - var counts = []; - - var childText; + let counts = []; + let childText; if (item.Type === 'Playlist') { @@ -1034,7 +1151,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana if (item.RunTimeTicks) { - var minutes = item.RunTimeTicks / 600000000; + let minutes = item.RunTimeTicks / 600000000; minutes = minutes || 1; @@ -1113,49 +1230,37 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana return counts.join(', '); } - function getProgramIndicators(item) { + let refreshIndicatorLoaded; - item = item.ProgramInfo || item; - - var html = ''; - - if (item.IsLive) { - html += '
' + globalize.translate('Live') + '
'; - } - - if (item.IsPremiere) { - html += '
' + globalize.translate('Premiere') + '
'; - } else if (item.IsSeries && !item.IsRepeat) { - html += '
' + globalize.translate('AttributeNew') + '
'; - } - //else if (item.IsRepeat) { - // html += '
' + globalize.translate('Repeat') + '
'; - //} - - if (html) { - html = '
' + html; - html += '
'; - } - - return html; - } - - var refreshIndicatorLoaded; + /** + * Imports the refresh indicator element. + */ function requireRefreshIndicator() { - if (!refreshIndicatorLoaded) { refreshIndicatorLoaded = true; require(['emby-itemrefreshindicator']); } } - function getDefaultBackgroundClass(str) { + /** + * Returns the default background class for a card based on a string. + * @param {string} str - Text used to generate the background class. + * @returns {string} CSS classes for default card backgrounds. + */ + export function getDefaultBackgroundClass(str) { return 'defaultCardBackground defaultCardBackground' + getDefaultColorIndex(str); } + /** + * Builds the HTML markup for an individual card. + * @param {number} index - Index of the card + * @param {object} item - Item used to generate the card. + * @param {object} apiClient - API client instance. + * @param {object} options - Options used to generate the card. + * @returns {string} HTML markup for the generated card. + */ function buildCard(index, item, apiClient, options) { - - var action = options.action || 'link'; + let action = options.action || 'link'; if (action === 'play' && item.IsFolder) { // If this hard-coding is ever removed make sure to test nested photo albums @@ -1164,13 +1269,13 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana action = 'play'; } - var shape = options.shape; + let shape = options.shape; if (shape === 'mixed') { shape = null; - var primaryImageAspectRatio = item.PrimaryImageAspectRatio; + const primaryImageAspectRatio = item.PrimaryImageAspectRatio; if (primaryImageAspectRatio) { @@ -1188,7 +1293,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana // TODO move card creation code to Card component - var className = 'card'; + let className = 'card'; if (shape) { className += ' ' + shape + 'Card'; @@ -1214,16 +1319,16 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } - var imgInfo = getCardImageUrl(item, apiClient, options, shape); - var imgUrl = imgInfo.imgUrl; + const imgInfo = getCardImageUrl(item, apiClient, options, shape); + const imgUrl = imgInfo.imgUrl; - var forceName = imgInfo.forceName; + const forceName = imgInfo.forceName; - var showTitle = options.showTitle === 'auto' ? true : (options.showTitle || item.Type === 'PhotoAlbum' || item.Type === 'Folder'); - var overlayText = options.overlayText; + const showTitle = options.showTitle === 'auto' ? true : (options.showTitle || item.Type === 'PhotoAlbum' || item.Type === 'Folder'); + const overlayText = options.overlayText; - var cardImageContainerClass = 'cardImageContainer'; - var coveredImage = options.coverImage || imgInfo.coverImage; + let cardImageContainerClass = 'cardImageContainer'; + const coveredImage = options.coverImage || imgInfo.coverImage; if (coveredImage) { cardImageContainerClass += ' coveredImage'; @@ -1237,17 +1342,17 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana cardImageContainerClass += ' ' + getDefaultBackgroundClass(item.Name); } - var cardBoxClass = options.cardLayout ? 'cardBox visualCardBox' : 'cardBox'; + let cardBoxClass = options.cardLayout ? 'cardBox visualCardBox' : 'cardBox'; - var footerCssClass; - var progressHtml = indicators.getProgressBarHtml(item); + let footerCssClass; + let progressHtml = indicators.getProgressBarHtml(item); - var innerCardFooter = ''; + let innerCardFooter = ''; - var footerOverlayed = false; + let footerOverlayed = false; - var logoUrl; - var logoHeight = 40; + let logoUrl; + const logoHeight = 40; if (options.showChannelLogo && item.ChannelPrimaryImageTag) { logoUrl = apiClient.getScaledImageUrl(item.ChannelId, { @@ -1278,12 +1383,12 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana progressHtml = ''; } - var mediaSourceCount = item.MediaSourceCount || 1; + const mediaSourceCount = item.MediaSourceCount || 1; if (mediaSourceCount > 1) { innerCardFooter += '
' + mediaSourceCount + '
'; } - var outerCardFooter = ''; + let outerCardFooter = ''; if (!overlayText && !footerOverlayed) { footerCssClass = options.cardLayout ? 'cardFooter' : 'cardFooter cardFooter-transparent'; @@ -1302,15 +1407,15 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana cardBoxClass += ' cardBox-bottompadded'; } - var overlayButtons = ''; + let overlayButtons = ''; if (layoutManager.mobile) { - var overlayPlayButton = options.overlayPlayButton; + let overlayPlayButton = options.overlayPlayButton; if (overlayPlayButton == null && !options.overlayMoreButton && !options.overlayInfoButton && !options.cardLayout) { overlayPlayButton = item.MediaType === 'Video'; } - var btnCssClass = 'cardOverlayButton cardOverlayButton-br itemAction'; + const btnCssClass = 'cardOverlayButton cardOverlayButton-br itemAction'; if (options.centerPlayButton) { overlayButtons += ''; @@ -1330,12 +1435,12 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } // cardBox can be it's own separate element if an outer footer is ever needed - var cardImageContainerOpen; - var cardImageContainerClose = ''; - var cardBoxClose = ''; - var cardScalableClose = ''; + let cardImageContainerOpen; + let cardImageContainerClose = ''; + let cardBoxClose = ''; + let cardScalableClose = ''; - var cardContentClass = 'cardContent'; + let cardContentClass = 'cardContent'; if (!options.cardLayout) { cardContentClass += ' cardContent-shadow'; } @@ -1353,13 +1458,13 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana cardImageContainerClose = ''; } - var cardScalableClass = 'cardScalable'; + let cardScalableClass = 'cardScalable'; cardImageContainerOpen = '
' + cardImageContainerOpen; cardBoxClose = '
'; cardScalableClose = '
'; - var indicatorsHtml = ''; + let indicatorsHtml = ''; if (options.missingIndicator !== false) { indicatorsHtml += indicators.getMissingIndicator(item); @@ -1380,7 +1485,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } if (item.Type === 'CollectionFolder' || item.CollectionType) { - var refreshClass = item.RefreshProgress ? '' : ' class="hide"'; + const refreshClass = item.RefreshProgress ? '' : ' class="hide"'; indicatorsHtml += '
'; requireRefreshIndicator(); } @@ -1389,24 +1494,20 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana cardImageContainerOpen += '
' + indicatorsHtml + '
'; } - //if (item.Type === 'Program' || item.Type === 'Timer') { - // cardImageContainerOpen += getProgramIndicators(item); - //} - if (!imgUrl) { cardImageContainerOpen += getDefaultText(item, options); } - var tagName = (layoutManager.tv) && !overlayButtons ? 'button' : 'div'; + const tagName = (layoutManager.tv) && !overlayButtons ? 'button' : 'div'; - var nameWithPrefix = (item.SortName || item.Name || ''); - var prefix = nameWithPrefix.substring(0, Math.min(3, nameWithPrefix.length)); + const nameWithPrefix = (item.SortName || item.Name || ''); + let prefix = nameWithPrefix.substring(0, Math.min(3, nameWithPrefix.length)); if (prefix) { prefix = prefix.toUpperCase(); } - var timerAttributes = ''; + let timerAttributes = ''; if (item.TimerId) { timerAttributes += ' data-timerid="' + item.TimerId + '"'; } @@ -1414,7 +1515,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana timerAttributes += ' data-seriestimerid="' + item.SeriesTimerId + '"'; } - var actionAttribute; + let actionAttribute; if (tagName === 'button') { className += " itemAction"; @@ -1427,16 +1528,16 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana className += ' card-withuserdata'; } - var positionTicksData = item.UserData && item.UserData.PlaybackPositionTicks ? (' data-positionticks="' + item.UserData.PlaybackPositionTicks + '"') : ''; - var collectionIdData = options.collectionId ? (' data-collectionid="' + options.collectionId + '"') : ''; - var playlistIdData = options.playlistId ? (' data-playlistid="' + options.playlistId + '"') : ''; - var mediaTypeData = item.MediaType ? (' data-mediatype="' + item.MediaType + '"') : ''; - var collectionTypeData = item.CollectionType ? (' data-collectiontype="' + item.CollectionType + '"') : ''; - var channelIdData = item.ChannelId ? (' data-channelid="' + item.ChannelId + '"') : ''; - var contextData = options.context ? (' data-context="' + options.context + '"') : ''; - var parentIdData = options.parentId ? (' data-parentid="' + options.parentId + '"') : ''; + const positionTicksData = item.UserData && item.UserData.PlaybackPositionTicks ? (' data-positionticks="' + item.UserData.PlaybackPositionTicks + '"') : ''; + const collectionIdData = options.collectionId ? (' data-collectionid="' + options.collectionId + '"') : ''; + const playlistIdData = options.playlistId ? (' data-playlistid="' + options.playlistId + '"') : ''; + const mediaTypeData = item.MediaType ? (' data-mediatype="' + item.MediaType + '"') : ''; + const collectionTypeData = item.CollectionType ? (' data-collectiontype="' + item.CollectionType + '"') : ''; + const channelIdData = item.ChannelId ? (' data-channelid="' + item.ChannelId + '"') : ''; + const contextData = options.context ? (' data-context="' + options.context + '"') : ''; + const parentIdData = options.parentId ? (' data-parentid="' + options.parentId + '"') : ''; - var additionalCardContent = ''; + let additionalCardContent = ''; if (layoutManager.desktop) { additionalCardContent += getHoverMenuHtml(item, action); @@ -1445,21 +1546,26 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana 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 + positionTicksData + collectionIdData + playlistIdData + contextData + parentIdData + ' data-prefix="' + prefix + '" class="' + className + '">' + cardImageContainerOpen + innerCardFooter + cardImageContainerClose + overlayButtons + additionalCardContent + cardScalableClose + outerCardFooter + cardBoxClose + ''; } + /** + * Generates HTML markup for the card overlay. + * @param {object} item - Item used to generate the card overlay. + * @param {string} action - Action assigned to the overlay. + * @returns {string} HTML markup of the card overlay. + */ function getHoverMenuHtml(item, action) { - - var html = ''; + let html = ''; html += '
'; - var btnCssClass = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light'; + const btnCssClass = 'cardOverlayButton cardOverlayButton-hover itemAction paper-icon-button-light'; if (playbackManager.canPlay(item)) { html += ''; } - html += '
'; + html += '
'; - var userData = item.UserData || {}; + const userData = item.UserData || {}; if (itemHelper.canMarkPlayed(item)) { require(['emby-playstatebutton']); @@ -1468,7 +1574,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana if (itemHelper.canRate(item)) { - var likes = userData.Likes == null ? '' : userData.Likes; + const likes = userData.Likes == null ? '' : userData.Likes; require(['emby-ratingbutton']); html += ''; @@ -1482,9 +1588,15 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana return html; } - function getDefaultText(item, options) { + /** + * Generates the text or icon used for default card backgrounds. + * @param {object} item - Item used to generate the card overlay. + * @param {object} options - Options used to generate the card overlay. + * @returns {string} HTML markup of the card overlay. + */ + export function getDefaultText(item, options) { if (item.CollectionType) { - return '' + imageHelper.getLibraryIcon(item.CollectionType) + '' + return ''; } switch (item.Type) { @@ -1507,12 +1619,16 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana return '' + options.defaultCardImageIcon + ''; } - var defaultName = isUsingLiveTvNaming(item) ? item.Name : itemHelper.getDisplayName(item); + const defaultName = isUsingLiveTvNaming(item) ? item.Name : itemHelper.getDisplayName(item); return '
' + defaultName + '
'; } - function buildCards(items, options) { - + /** + * Builds a set of cards and inserts them into the page. + * @param {Array} items - Array of items used to build the cards. + * @param {options} options - Options of the cards to build. + */ + export function buildCards(items, options) { // Abort if the container has been disposed if (!document.body.contains(options.itemsContainer)) { return; @@ -1527,7 +1643,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } - var html = buildCardsHtmlInternal(items, options); + const html = buildCardsHtmlInternal(items, options); if (html) { @@ -1553,8 +1669,13 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } + /** + * Ensures the indicators for a card exist and creates them if they don't exist. + * @param {HTMLDivElement} card - DOM element of the card. + * @param {HTMLDivElement} indicatorsElem - DOM element of the indicators. + * @returns {HTMLDivElement} - DOM element of the indicators. + */ function ensureIndicators(card, indicatorsElem) { - if (indicatorsElem) { return indicatorsElem; } @@ -1563,7 +1684,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana if (!indicatorsElem) { - var cardImageContainer = card.querySelector('.cardImageContainer'); + const cardImageContainer = card.querySelector('.cardImageContainer'); indicatorsElem = document.createElement('div'); indicatorsElem.classList.add('cardIndicators'); cardImageContainer.appendChild(indicatorsElem); @@ -1572,14 +1693,18 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana return indicatorsElem; } + /** + * Adds user data to the card such as progress indicators and played status. + * @param {HTMLDivElement} card - DOM element of the card. + * @param {Object} userData - User data to apply to the card. + */ function updateUserData(card, userData) { - - var type = card.getAttribute('data-type'); - var enableCountIndicator = type === 'Series' || type === 'BoxSet' || type === 'Season'; - var indicatorsElem = null; - var playedIndicator = null; - var countIndicator = null; - var itemProgressBar = null; + const type = card.getAttribute('data-type'); + const enableCountIndicator = type === 'Series' || type === 'BoxSet' || type === 'Season'; + let indicatorsElem = null; + let playedIndicator = null; + let countIndicator = null; + let itemProgressBar = null; if (userData.Played) { @@ -1622,7 +1747,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } - var progressHtml = indicators.getProgressBarHtml({ + const progressHtml = indicators.getProgressBarHtml({ Type: type, UserData: userData, MediaType: 'Video' @@ -1636,11 +1761,11 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana itemProgressBar = document.createElement('div'); itemProgressBar.classList.add('itemProgressBar'); - var innerCardFooter = card.querySelector('.innerCardFooter'); + let innerCardFooter = card.querySelector('.innerCardFooter'); if (!innerCardFooter) { innerCardFooter = document.createElement('div'); innerCardFooter.classList.add('innerCardFooter'); - var cardImageContainer = card.querySelector('.cardImageContainer'); + const cardImageContainer = card.querySelector('.cardImageContainer'); cardImageContainer.appendChild(innerCardFooter); } innerCardFooter.appendChild(itemProgressBar); @@ -1656,37 +1781,50 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } - function onUserDataChanged(userData, scope) { + /** + * Handles when user data has changed. + * @param {Object} userData - User data to apply to the card. + * @param {HTMLElement} scope - DOM element to use as a scope when selecting cards. + */ + export function onUserDataChanged(userData, scope) { + const cards = (scope || document.body).querySelectorAll('.card-withuserdata[data-id="' + userData.ItemId + '"]'); - var cards = (scope || document.body).querySelectorAll('.card-withuserdata[data-id="' + userData.ItemId + '"]'); - - for (var i = 0, length = cards.length; i < length; i++) { + for (let i = 0, length = cards.length; i < length; i++) { updateUserData(cards[i], userData); } } - function onTimerCreated(programId, newTimerId, itemsContainer) { + /** + * Handles when a timer has been created. + * @param {string} programId - ID of the program. + * @param {string} newTimerId - ID of the new timer. + * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. + */ + export function onTimerCreated(programId, newTimerId, itemsContainer) { + const cells = itemsContainer.querySelectorAll('.card[data-id="' + programId + '"]'); - var cells = itemsContainer.querySelectorAll('.card[data-id="' + programId + '"]'); - - for (var i = 0, length = cells.length; i < length; i++) { - var cell = cells[i]; - var icon = cell.querySelector('.timerIndicator'); + for (let i = 0, length = cells.length; i < length; i++) { + let cell = cells[i]; + const icon = cell.querySelector('.timerIndicator'); if (!icon) { - var indicatorsElem = ensureIndicators(cell); + const indicatorsElem = ensureIndicators(cell); indicatorsElem.insertAdjacentHTML('beforeend', ''); } cell.setAttribute('data-timerid', newTimerId); } } - function onTimerCancelled(id, itemsContainer) { + /** + * Handles when a timer has been cancelled. + * @param {string} timerId - ID of the cancelled timer. + * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. + */ + export function onTimerCancelled(timerId, itemsContainer) { + const cells = itemsContainer.querySelectorAll('.card[data-timerid="' + timerId + '"]'); - var cells = itemsContainer.querySelectorAll('.card[data-timerid="' + id + '"]'); - - for (var i = 0, length = cells.length; i < length; i++) { - var cell = cells[i]; - var icon = cell.querySelector('.timerIndicator'); + for (let i = 0; i < cells.length; i++) { + let cell = cells[i]; + let icon = cell.querySelector('.timerIndicator'); if (icon) { icon.parentNode.removeChild(icon); } @@ -1694,13 +1832,17 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } - function onSeriesTimerCancelled(id, itemsContainer) { + /** + * Handles when a series timer has been cancelled. + * @param {string} cancelledTimerId - ID of the cancelled timer. + * @param {HTMLElement} itemsContainer - DOM element of the itemsContainer. + */ + export function onSeriesTimerCancelled(cancelledTimerId, itemsContainer) { + const cells = itemsContainer.querySelectorAll('.card[data-seriestimerid="' + cancelledTimerId + '"]'); - var cells = itemsContainer.querySelectorAll('.card[data-seriestimerid="' + id + '"]'); - - for (var i = 0, length = cells.length; i < length; i++) { - var cell = cells[i]; - var icon = cell.querySelector('.timerIndicator'); + for (let i = 0; i < cells.length; i++) { + let cell = cells[i]; + let icon = cell.querySelector('.timerIndicator'); if (icon) { icon.parentNode.removeChild(icon); } @@ -1708,14 +1850,15 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } - return { - getCardsHtml: getCardsHtml, - getDefaultBackgroundClass: getDefaultBackgroundClass, - getDefaultText: getDefaultText, - buildCards: buildCards, - onUserDataChanged: onUserDataChanged, - onTimerCreated: onTimerCreated, - onTimerCancelled: onTimerCancelled, - onSeriesTimerCancelled: onSeriesTimerCancelled - }; - }); +/* eslint-enable indent */ + +export default { + getCardsHtml: getCardsHtml, + getDefaultBackgroundClass: getDefaultBackgroundClass, + getDefaultText: getDefaultText, + buildCards: buildCards, + onUserDataChanged: onUserDataChanged, + onTimerCreated: onTimerCreated, + onTimerCancelled: onTimerCancelled, + onSeriesTimerCancelled: onSeriesTimerCancelled +}; diff --git a/src/components/cardbuilder/chaptercardbuilder.js b/src/components/cardbuilder/chaptercardbuilder.js index 02d583abc0..16326b6c59 100644 --- a/src/components/cardbuilder/chaptercardbuilder.js +++ b/src/components/cardbuilder/chaptercardbuilder.js @@ -68,7 +68,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'layoutManager', 'browse return apiClient.getScaledImageUrl(item.Id, { - maxWidth: maxWidth, + maxWidth: maxWidth * 2, tag: chapter.ImageTag, type: "Chapter", index: index diff --git a/src/components/castSenderApi.js b/src/components/castSenderApi.js new file mode 100644 index 0000000000..a1e7bd8755 --- /dev/null +++ b/src/components/castSenderApi.js @@ -0,0 +1,34 @@ +define([], function() { + 'use strict'; + + if (window.appMode === "cordova" || window.appMode === "android") { + return { + load: function () { + window.chrome = window.chrome || {}; + return Promise.resolve(); + } + }; + } else { + var ccLoaded = false; + return { + load: function () { + if (ccLoaded) { + return Promise.resolve(); + } + + return new Promise(function (resolve, reject) { + var fileref = document.createElement("script"); + fileref.setAttribute("type", "text/javascript"); + + fileref.onload = function () { + ccLoaded = true; + resolve(); + }; + + fileref.setAttribute("src", "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js"); + document.querySelector("head").appendChild(fileref); + }); + } + }; + } +}); diff --git a/src/components/channelmapper/channelmapper.js b/src/components/channelmapper/channelmapper.js index 1b536f440b..2ea7a3a13a 100644 --- a/src/components/channelmapper/channelmapper.js +++ b/src/components/channelmapper/channelmapper.js @@ -1,18 +1,7 @@ -define(["dialogHelper", "loading", "connectionManager", "globalize", "actionsheet", "emby-input", "paper-icon-button-light", "emby-button", "listViewStyle", "material-icons", "formDialogStyle"], function (dialogHelper, loading, connectionManager, globalize, actionsheet) { +define(["dom", "dialogHelper", "loading", "connectionManager", "globalize", "actionsheet", "emby-input", "paper-icon-button-light", "emby-button", "listViewStyle", "material-icons", "formDialogStyle"], function (dom, dialogHelper, loading, connectionManager, globalize, actionsheet) { "use strict"; return function (options) { - function parentWithClass(elem, className) { - while (!elem.classList || !elem.classList.contains(className)) { - elem = elem.parentNode; - if (!elem) { - return null; - } - } - - return elem; - } - function mapChannel(button, channelId, providerChannelId) { loading.show(); var providerId = options.providerId; @@ -26,7 +15,7 @@ define(["dialogHelper", "loading", "connectionManager", "globalize", "actionshee }, dataType: "json" }).then(function (mapping) { - var listItem = parentWithClass(button, "listItem"); + var listItem = dom.parentWithClass(button, "listItem"); button.setAttribute("data-providerid", mapping.ProviderChannelId); listItem.querySelector(".secondary").innerHTML = getMappingSecondaryName(mapping, currentMappingOptions.ProviderName); loading.hide(); @@ -34,7 +23,7 @@ define(["dialogHelper", "loading", "connectionManager", "globalize", "actionshee } function onChannelsElementClick(e) { - var btnMap = parentWithClass(e.target, "btnMap"); + var btnMap = dom.parentWithClass(e.target, "btnMap"); if (btnMap) { var channelId = btnMap.getAttribute("data-id"); diff --git a/src/components/collectioneditor/collectioneditor.js b/src/components/collectioneditor/collectioneditor.js index 79220ac335..49784df498 100644 --- a/src/components/collectioneditor/collectioneditor.js +++ b/src/components/collectioneditor/collectioneditor.js @@ -1,25 +1,12 @@ -define(['dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectionManager', 'appRouter', 'globalize', 'emby-checkbox', 'emby-input', 'paper-icon-button-light', 'emby-select', 'material-icons', 'css!./../formdialog', 'emby-button', 'flexStyles'], function (dialogHelper, loading, appHost, layoutManager, connectionManager, appRouter, globalize) { +define(['dom', 'dialogHelper', 'loading', 'apphost', 'layoutManager', 'connectionManager', 'appRouter', 'globalize', 'emby-checkbox', 'emby-input', 'paper-icon-button-light', 'emby-select', 'material-icons', 'css!./../formdialog', 'emby-button', 'flexStyles'], function (dom, dialogHelper, loading, appHost, layoutManager, connectionManager, appRouter, globalize) { 'use strict'; var currentServerId; - function parentWithClass(elem, className) { - - while (!elem.classList || !elem.classList.contains(className)) { - elem = elem.parentNode; - - if (!elem) { - return null; - } - } - - return elem; - } - function onSubmit(e) { loading.show(); - var panel = parentWithClass(this, 'dialog'); + var panel = dom.parentWithClass(this, 'dialog'); var collectionId = panel.querySelector('#selectCollectionToAddTo').value; diff --git a/src/components/confirm/confirm.js b/src/components/confirm/confirm.js index f104350c87..fa9a156679 100644 --- a/src/components/confirm/confirm.js +++ b/src/components/confirm/confirm.js @@ -1,40 +1,65 @@ -define(['dialog', 'globalize'], function (dialog, globalize) { +define(["browser", "dialog", "globalize"], function(browser, dialog, globalize) { 'use strict'; - return function (text, title) { + function replaceAll(str, find, replace) { + return str.split(find).join(replace); + } - var options; - if (typeof text === 'string') { - options = { - title: title, - text: text - }; - } else { - options = text; - } - - var items = []; - - items.push({ - name: options.cancelText || globalize.translate('ButtonCancel'), - id: 'cancel', - type: 'cancel' - }); - - items.push({ - name: options.confirmText || globalize.translate('ButtonOk'), - id: 'ok', - type: options.primary === 'delete' ? 'delete' : 'submit' - }); - - options.buttons = items; - - return dialog(options).then(function (result) { - if (result === 'ok') { - return Promise.resolve(); + if (browser.tv && window.confirm) { + // Use the native confirm dialog + return function (options) { + if (typeof options === 'string') { + options = { + title: '', + text: options + }; } - return Promise.reject(); - }); - }; + var text = replaceAll(options.text || '', '
', '\n'); + var result = confirm(text); + + if (result) { + return Promise.resolve(); + } else { + return Promise.reject(); + } + }; + } else { + // Use our own dialog + return function (text, title) { + var options; + if (typeof text === 'string') { + options = { + title: title, + text: text + }; + } else { + options = text; + } + + var items = []; + + items.push({ + name: options.cancelText || globalize.translate('ButtonCancel'), + id: 'cancel', + type: 'cancel' + }); + + items.push({ + name: options.confirmText || globalize.translate('ButtonOk'), + id: 'ok', + type: options.primary === 'delete' ? 'delete' : 'submit' + }); + + options.buttons = items; + + return dialog(options).then(function (result) { + if (result === 'ok') { + return Promise.resolve(); + } + + return Promise.reject(); + }); + }; + } }); diff --git a/src/components/confirm/nativeconfirm.js b/src/components/confirm/nativeconfirm.js deleted file mode 100644 index 7d72bc5eaf..0000000000 --- a/src/components/confirm/nativeconfirm.js +++ /dev/null @@ -1,27 +0,0 @@ -define([], function () { - 'use strict'; - - function replaceAll(str, find, replace) { - - return str.split(find).join(replace); - } - - return function (options) { - - if (typeof options === 'string') { - options = { - title: '', - text: options - }; - } - - var text = replaceAll(options.text || '', '
', '\n'); - var result = confirm(text); - - if (result) { - return Promise.resolve(); - } else { - return Promise.reject(); - } - }; -}); diff --git a/src/components/dialogHelper/dialogHelper.js b/src/components/dialogHelper/dialogHelper.js index 6ee96df318..e3410776a8 100644 --- a/src/components/dialogHelper/dialogHelper.js +++ b/src/components/dialogHelper/dialogHelper.js @@ -169,6 +169,15 @@ define(['appRouter', 'focusManager', 'browser', 'layoutManager', 'inputManager', }, { passive: true }); + + dom.addEventListener((dlg.dialogContainer || backdrop), 'contextmenu', function (e) { + if (e.target === dlg.dialogContainer) { + // Close the application dialog menu + close(dlg); + // Prevent the default browser context menu from appearing + e.preventDefault(); + } + }); } function isHistoryEnabled(dlg) { @@ -242,9 +251,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/directorybrowser/directorybrowser.js b/src/components/directorybrowser/directorybrowser.js index b71f7bbb05..1d5c23eb1f 100644 --- a/src/components/directorybrowser/directorybrowser.js +++ b/src/components/directorybrowser/directorybrowser.js @@ -89,7 +89,7 @@ define(['loading', 'dialogHelper', 'dom', 'listViewStyle', 'emby-input', 'paper- var instruction = options.instruction ? options.instruction + "

" : ""; html += '
'; html += instruction; - html += Globalize.translate("MessageDirectoryPickerInstruction").replace("{0}", "\\\\server").replace("{1}", "\\\\192.168.1.101"); + html += Globalize.translate("MessageDirectoryPickerInstruction", "\\\\server", "\\\\192.168.1.101"); if ("bsd" === systemInfo.OperatingSystem.toLowerCase()) { html += "
"; html += "
"; @@ -163,16 +163,15 @@ define(['loading', 'dialogHelper', 'dom', 'listViewStyle', 'emby-input', 'paper- } }).catch(function(response) { if (response) { - // TODO All alerts (across the project), should use Globalize.translate() if (response.status === 404) { - alertText("The path could not be found. Please ensure the path is valid and try again."); + alertText(Globalize.translate("PathNotFound")); return Promise.reject(); } if (response.status === 500) { if (validateWriteable) { - alertText("Jellyfin Server requires write access to this folder. Please ensure write access and try again."); + alertText(Globalize.translate("WriteAccessRequired")); } else { - alertText("The path could not be found. Please ensure the path is valid and try again.") + alertText(Globalize.translate("PathNotFound")) } return Promise.reject() } diff --git a/src/components/displaysettings/displaysettings.template.html b/src/components/displaysettings/displaysettings.template.html index 16bbf0dd8a..4ef8c8b1ca 100644 --- a/src/components/displaysettings/displaysettings.template.html +++ b/src/components/displaysettings/displaysettings.template.html @@ -63,7 +63,7 @@
'; diff --git a/src/components/keyboardnavigation.js b/src/components/keyboardnavigation.js deleted file mode 100644 index 12246a0d80..0000000000 --- a/src/components/keyboardnavigation.js +++ /dev/null @@ -1,154 +0,0 @@ -define(["inputManager", "layoutManager"], function (inputManager, layoutManager) { - "use strict"; - - /** - * Key name mapping. - */ - // Add more to support old browsers - var KeyNames = { - 13: "Enter", - 19: "Pause", - 27: "Escape", - 32: "Space", - 37: "ArrowLeft", - 38: "ArrowUp", - 39: "ArrowRight", - 40: "ArrowDown", - // MediaRewind (Tizen/WebOS) - 412: "MediaRewind", - // MediaStop (Tizen/WebOS) - 413: "MediaStop", - // MediaPlay (Tizen/WebOS) - 415: "MediaPlay", - // MediaFastForward (Tizen/WebOS) - 417: "MediaFastForward", - // Back (WebOS) - 461: "Back", - // Back (Tizen) - 10009: "Back", - // MediaTrackPrevious (Tizen) - 10232: "MediaTrackPrevious", - // MediaTrackNext (Tizen) - 10233: "MediaTrackNext", - // MediaPlayPause (Tizen) - 10252: "MediaPlayPause" - }; - - /** - * Keys used for keyboard navigation. - */ - var NavigationKeys = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"]; - - var hasFieldKey = false; - try { - hasFieldKey = "key" in new KeyboardEvent("keydown"); - } catch (e) { - console.error("error checking 'key' field"); - } - - if (!hasFieldKey) { - // Add [a..z] - for (var i = 65; i <= 90; i++) { - KeyNames[i] = String.fromCharCode(i).toLowerCase(); - } - } - - /** - * Returns key name from event. - * - * @param {KeyboardEvent} keyboard event - * @return {string} key name - */ - function getKeyName(event) { - return KeyNames[event.keyCode] || event.key; - } - - /** - * Returns _true_ if key is used for navigation. - * - * @param {string} key name - * @return {boolean} _true_ if key is used for navigation - */ - function isNavigationKey(key) { - return NavigationKeys.indexOf(key) != -1; - } - - function enable() { - document.addEventListener("keydown", function (e) { - var key = getKeyName(e); - - // Ignore navigation keys for non-TV - if (!layoutManager.tv && isNavigationKey(key)) { - return; - } - - var capture = true; - - switch (key) { - case "ArrowLeft": - inputManager.handle("left"); - break; - case "ArrowUp": - inputManager.handle("up"); - break; - case "ArrowRight": - inputManager.handle("right"); - break; - case "ArrowDown": - inputManager.handle("down"); - break; - - case "Back": - inputManager.handle("back"); - break; - - case "Escape": - if (layoutManager.tv) { - inputManager.handle("back"); - } else { - capture = false; - } - break; - - case "MediaPlay": - inputManager.handle("play"); - break; - case "Pause": - inputManager.handle("pause"); - break; - case "MediaPlayPause": - inputManager.handle("playpause"); - break; - case "MediaRewind": - inputManager.handle("rewind"); - break; - case "MediaFastForward": - inputManager.handle("fastforward"); - break; - case "MediaStop": - inputManager.handle("stop"); - break; - case "MediaTrackPrevious": - inputManager.handle("previoustrack"); - break; - case "MediaTrackNext": - inputManager.handle("nexttrack"); - break; - - default: - capture = false; - } - - if (capture) { - console.debug("disabling default event handling"); - e.preventDefault(); - } - }); - } - - return { - enable: enable, - getKeyName: getKeyName, - isNavigationKey: isNavigationKey - }; -}); diff --git a/src/components/libraryoptionseditor/libraryoptionseditor.js b/src/components/libraryoptionseditor/libraryoptionseditor.js index 193533bfc2..a398d7043a 100644 --- a/src/components/libraryoptionseditor/libraryoptionseditor.js +++ b/src/components/libraryoptionseditor/libraryoptionseditor.js @@ -107,7 +107,7 @@ define(["globalize", "dom", "emby-checkbox", "emby-select", "emby-input"], funct if (!plugins.length) return html; html += '
'; - html += '

' + globalize.translate("LabelTypeMetadataDownloaders", availableTypeOptions.Type) + "

"; + html += '

' + globalize.translate("LabelTypeMetadataDownloaders", globalize.translate(availableTypeOptions.Type)) + "

"; html += '
'; for (var i = 0; i < plugins.length; i++) { var plugin = plugins[i]; @@ -273,14 +273,19 @@ define(["globalize", "dom", "emby-checkbox", "emby-select", "emby-input"], funct function adjustSortableListElement(elem) { var btnSortable = elem.querySelector(".btnSortable"); + var inner = btnSortable.querySelector("i"); if (elem.previousSibling) { + btnSortable.title = globalize.translate("ButtonUp"); btnSortable.classList.add("btnSortableMoveUp"); btnSortable.classList.remove("btnSortableMoveDown"); - btnSortable.querySelector("i").innerHTML = "keyboard_arrow_up"; + inner.classList.remove("keyboard_arrow_down"); + inner.classList.add("keyboard_arrow_up"); } else { + btnSortable.title = globalize.translate("ButtonDown"); btnSortable.classList.remove("btnSortableMoveUp"); btnSortable.classList.add("btnSortableMoveDown"); - btnSortable.querySelector("i").innerHTML = "keyboard_arrow_down"; + inner.classList.remove("keyboard_arrow_up"); + inner.classList.add("keyboard_arrow_down"); } } @@ -391,6 +396,12 @@ define(["globalize", "dom", "emby-checkbox", "emby-select", "emby-input"], funct parent.querySelector(".chkEnableEmbeddedTitlesContainer").classList.remove("hide"); } + if (contentType === "tvshows") { + parent.querySelector(".chkEnableEmbeddedEpisodeInfosContainer").classList.remove("hide"); + } else { + parent.querySelector(".chkEnableEmbeddedEpisodeInfosContainer").classList.add("hide"); + } + return populateMetadataSettings(parent, contentType); } @@ -488,6 +499,7 @@ define(["globalize", "dom", "emby-checkbox", "emby-select", "emby-input"], funct SeasonZeroDisplayName: parent.querySelector("#txtSeasonZeroName").value, AutomaticRefreshIntervalDays: parseInt(parent.querySelector("#selectAutoRefreshInterval").value), EnableEmbeddedTitles: parent.querySelector("#chkEnableEmbeddedTitles").checked, + EnableEmbeddedEpisodeInfos: parent.querySelector("#chkEnableEmbeddedEpisodeInfos").checked, SkipSubtitlesIfEmbeddedSubtitlesPresent: parent.querySelector("#chkSkipIfGraphicalSubsPresent").checked, SkipSubtitlesIfAudioTrackMatches: parent.querySelector("#chkSkipIfAudioTrackPresent").checked, SaveSubtitlesWithMedia: parent.querySelector("#chkSaveSubtitlesLocally").checked, @@ -540,6 +552,7 @@ define(["globalize", "dom", "emby-checkbox", "emby-select", "emby-input"], funct parent.querySelector("#chkImportMissingEpisodes").checked = options.ImportMissingEpisodes; parent.querySelector(".chkAutomaticallyGroupSeries").checked = options.EnableAutomaticSeriesGrouping; parent.querySelector("#chkEnableEmbeddedTitles").checked = options.EnableEmbeddedTitles; + parent.querySelector("#chkEnableEmbeddedEpisodeInfos").checked = options.EnableEmbeddedEpisodeInfos; parent.querySelector("#chkSkipIfGraphicalSubsPresent").checked = options.SkipSubtitlesIfEmbeddedSubtitlesPresent; parent.querySelector("#chkSaveSubtitlesLocally").checked = options.SaveSubtitlesWithMedia; parent.querySelector("#chkSkipIfAudioTrackPresent").checked = options.SkipSubtitlesIfAudioTrackMatches; diff --git a/src/components/libraryoptionseditor/libraryoptionseditor.template.html b/src/components/libraryoptionseditor/libraryoptionseditor.template.html index 377488f44f..caa177108d 100644 --- a/src/components/libraryoptionseditor/libraryoptionseditor.template.html +++ b/src/components/libraryoptionseditor/libraryoptionseditor.template.html @@ -28,6 +28,13 @@
${PreferEmbeddedTitlesOverFileNamesHelp}
+
+ +
${PreferEmbeddedEpisodeInfosOverFileNamesHelp}
+