diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 48f042d729..7c7801b866 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -33,7 +33,7 @@ jobs: - task: NodeTool@0 displayName: 'Install Node' inputs: - versionSpec: '10.x' + versionSpec: '12.x' - task: Cache@2 displayName: 'Check Cache' @@ -82,7 +82,7 @@ jobs: - task: NodeTool@0 displayName: 'Install Node' inputs: - versionSpec: '10.x' + versionSpec: '12.x' - task: Cache@2 displayName: 'Check Cache' diff --git a/.eslintrc.yml b/.eslintrc.yml index 4bc22fc1d4..377716d53c 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -3,6 +3,12 @@ env: browser: true amd: true +parserOptions: + ecmaVersion: 6 + sourceType: module + ecmaFeatures: + impliedStrict: true + globals: # New browser globals DataView: readonly diff --git a/.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/gulpfile.js b/gulpfile.js index ca6cf36dd2..0eb5593541 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -18,16 +18,42 @@ const stream = require('webpack-stream'); const inject = require('gulp-inject'); const postcss = require('gulp-postcss'); const sass = require('gulp-sass'); - -sass.compiler = require('node-sass') +const gulpif = require('gulp-if'); +const lazypipe = require('lazypipe'); +sass.compiler = require('node-sass'); +let config; if (mode.production()) { - var config = require('./webpack.prod.js'); + config = require('./webpack.prod.js'); } else { - var config = require('./webpack.dev.js'); + config = require('./webpack.dev.js'); } +const options = { + javascript: { + query: ['src/**/*.js', '!src/bundle.js', '!src/standalone.js', '!src/scripts/apploader.js'] + }, + apploader: { + query: ['src/standalone.js', 'src/scripts/apploader.js'] + }, + css: { + query: ['src/**/*.css', 'src/**/*.scss'] + }, + html: { + query: ['src/**/*.html', '!src/index.html'] + }, + images: { + query: ['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg'] + }, + copy: { + query: ['src/**/*.json', 'src/**/*.ico'] + }, + injectBundle: { + query: 'src/index.html' + } +}; + function serve() { browserSync.init({ server: { @@ -36,51 +62,99 @@ function serve() { port: 8080 }); - watch(['src/**/*.js', '!src/bundle.js'], javascript); - watch('src/bundle.js', webpack); - watch('src/**/*.css', css); - watch(['src/**/*.html', '!src/index.html'], html); - watch(['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg'], images); - watch(['src/**/*.json', 'src/**/*.ico'], copy); - watch('src/index.html', injectBundle); - watch(['src/standalone.js', 'src/scripts/apploader.js'], standalone); -} + let events = ['add', 'change']; -function standalone() { - return src(['src/standalone.js', 'src/scripts/apploader.js'], { base: './src/' }) - .pipe(concat('scripts/apploader.js')) - .pipe(dest('dist/')); + watch(options.javascript.query).on('all', function (event, path) { + if (events.includes(event)) { + javascript(path); + } + }); + + watch(options.apploader.query, apploader(true)); + + watch('src/bundle.js', webpack); + + watch(options.css.query).on('all', function (event, path) { + if (events.includes(event)) { + css(path); + } + }); + + watch(options.html.query).on('all', function (event, path) { + if (events.includes(event)) { + html(path); + } + }); + + watch(options.images.query).on('all', function (event, path) { + if (events.includes(event)) { + images(path); + } + }); + + watch(options.copy.query).on('all', function (event, path) { + if (events.includes(event)) { + copy(path); + } + }); + + watch(options.injectBundle.query, injectBundle); } function clean() { return del(['dist/']); } -function javascript() { - return src(['src/**/*.js', '!src/bundle.js'], { base: './src/' }) - .pipe(mode.development(sourcemaps.init({ loadMaps: true }))) - .pipe(babel({ +let pipelineJavascript = lazypipe() + .pipe(function () { + return mode.development(sourcemaps.init({ loadMaps: true })); + }) + .pipe(function () { + return babel({ presets: [ ['@babel/preset-env'] ] - })) - .pipe(terser({ + }); + }) + .pipe(function () { + return terser({ keep_fnames: true, mangle: false - })) - .pipe(mode.development(sourcemaps.write('.'))) + }); + }) + .pipe(function () { + return mode.development(sourcemaps.write('.')); + }); + +function javascript(query) { + return src(typeof query !== 'function' ? query : options.javascript.query, { base: './src/' }) + .pipe(pipelineJavascript()) .pipe(dest('dist/')) .pipe(browserSync.stream()); } +function apploader(standalone) { + function task() { + return src(options.apploader.query, { base: './src/' }) + .pipe(gulpif(standalone, concat('scripts/apploader.js'))) + .pipe(pipelineJavascript()) + .pipe(dest('dist/')) + .pipe(browserSync.stream()); + }; + + task.displayName = 'apploader'; + + return task; +} + function webpack() { return stream(config) .pipe(dest('dist/')) .pipe(browserSync.stream()); } -function css() { - return src(['src/**/*.css', 'src/**/*.scss'], { base: './src/' }) +function css(query) { + return src(typeof query !== 'function' ? query : options.css.query, { base: './src/' }) .pipe(mode.development(sourcemaps.init({ loadMaps: true }))) .pipe(sass().on('error', sass.logError)) .pipe(postcss()) @@ -89,28 +163,28 @@ function css() { .pipe(browserSync.stream()); } -function html() { - return src(['src/**/*.html', '!src/index.html'], { base: './src/' }) +function html(query) { + return src(typeof query !== 'function' ? query : options.html.query, { base: './src/' }) .pipe(mode.production(htmlmin({ collapseWhitespace: true }))) .pipe(dest('dist/')) .pipe(browserSync.stream()); } -function images() { - return src(['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg'], { base: './src/' }) +function images(query) { + return src(typeof query !== 'function' ? query : options.images.query, { base: './src/' }) .pipe(mode.production(imagemin())) .pipe(dest('dist/')) .pipe(browserSync.stream()); } -function copy() { - return src(['src/**/*.json', 'src/**/*.ico'], { base: './src/' }) +function copy(query) { + return src(typeof query !== 'function' ? query : options.copy.query, { base: './src/' }) .pipe(dest('dist/')) .pipe(browserSync.stream()); } function injectBundle() { - return src('src/index.html', { base: './src/' }) + return src(options.injectBundle.query, { base: './src/' }) .pipe(inject( src(['src/scripts/apploader.js'], { read: false }, { base: './src/' }), { relative: true } )) @@ -118,6 +192,10 @@ function injectBundle() { .pipe(browserSync.stream()); } -exports.default = series(clean, parallel(javascript, webpack, css, html, images, copy), injectBundle) -exports.standalone = series(exports.default, standalone) -exports.serve = series(exports.standalone, serve) +function build(standalone) { + return series(clean, parallel(javascript, apploader(standalone), webpack, css, html, images, copy), injectBundle); +} + +exports.default = build(false); +exports.standalone = build(true); +exports.serve = series(exports.standalone, serve); diff --git a/package.json b/package.json index 6d07f9a6fa..c0ff158ec9 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "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", @@ -17,12 +18,13 @@ "cssnano": "^4.1.10", "del": "^5.1.0", "eslint": "^6.8.0", - "file-loader": "^5.0.2", + "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", @@ -30,7 +32,8 @@ "gulp-sass": "^4.0.2", "gulp-sourcemaps": "^2.6.5", "gulp-terser": "^1.2.0", - "html-webpack-plugin": "^3.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", @@ -56,14 +59,13 @@ "jellyfin-noto": "https://github.com/jellyfin/jellyfin-noto", "jquery": "^3.4.1", "jstree": "^3.3.7", - "libass-wasm": "https://github.com/jellyfin/JavascriptSubtitlesOctopus", - "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", @@ -72,6 +74,24 @@ "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" + ], + "plugins": [ + "@babel/plugin-transform-modules-amd" + ] + } ] }, "browserslist": [ 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/librarybrowser.css b/src/assets/css/librarybrowser.css index 67afef2376..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; } } @@ -1122,3 +1119,50 @@ div:not(.sectionTitleContainer-cards) > .sectionTitle-cards { .itemsViewSettingsContainer > .button-flat { margin: 0; } + +.layout-mobile #myPreferencesMenuPage { + padding-top: 3.75em; +} + +.itemDetailsGroup { + margin-bottom: 1.5em; +} + +.trackSelections { + max-width: 44em; +} + +.detailsGroupItem, +.trackSelections .selectContainer { + display: flex; + max-width: 44em; + margin: 0 0 0.5em !important; +} + +.trackSelections .selectContainer { + margin: 0 0 0.3em !important; +} + +.detailsGroupItem .label, +.trackSelections .selectContainer .selectLabel { + cursor: default; + flex-grow: 0; + flex-shrink: 0; + flex-basis: 6.25em; + margin: 0 0.6em 0 0; +} + +.trackSelections .selectContainer .selectLabel { + margin: 0 0.2em 0 0; +} + +.trackSelections .selectContainer .detailTrackSelect { + font-size: inherit; + padding: 0; + overflow: hidden; +} + +.trackSelections .selectContainer .selectArrowContainer .selectArrow { + margin-top: 0; + font-size: 1.4em; +} diff --git a/src/bundle.js b/src/bundle.js index c5c5e1bcaf..ba5f74b163 100644 --- a/src/bundle.js +++ b/src/bundle.js @@ -84,13 +84,6 @@ _define("webcomponents", function() { return webcomponents; }); -// libjass -var libjass = require("libjass"); -require("libjass/libjass.css"); -_define("libjass", function() { - return libjass; -}); - // libass-wasm var libass_wasm = require("libass-wasm"); _define("JavascriptSubtitlesOctopus", function() { diff --git a/src/components/appRouter.js b/src/components/appRouter.js index efb58a089f..62bfb3cf40 100644 --- a/src/components/appRouter.js +++ b/src/components/appRouter.js @@ -511,9 +511,16 @@ define(['loading', 'globalize', 'events', 'viewManager', 'layoutManager', 'skinM return baseRoute; } + var popstateOccurred = false; + window.addEventListener('popstate', function () { + popstateOccurred = true; + }); + function getHandler(route) { return function (ctx, next) { + ctx.isBack = popstateOccurred; handleRoute(ctx, next, route); + popstateOccurred = false; }; } diff --git a/src/components/apphost.js b/src/components/apphost.js index 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..a469eb8854 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,8 @@ define(["focusManager", "layoutManager"], function (focusManager, layoutManager) return focusedElement; } - return { + export default { isEnabled: isEnabled, enable: enable, autoFocus: autoFocus }; -}); diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js index 7f562f1fd0..1249f802af 100644 --- a/src/components/cardbuilder/cardBuilder.js +++ b/src/components/cardbuilder/cardBuilder.js @@ -1,10 +1,36 @@ -define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusManager', 'indicators', 'globalize', 'layoutManager', 'apphost', 'dom', 'browser', 'playbackManager', 'itemShortcuts', 'scripts/imagehelper', 'css!./card', 'paper-icon-button-light', 'programStyles'], - function (datetime, imageLoader, connectionManager, itemHelper, focusManager, indicators, globalize, layoutManager, appHost, dom, browser, playbackManager, itemShortcuts, imageHelper) { - 'use strict'; +/* eslint-disable indent */ - var enableFocusTransform = !browser.slow && !browser.edge; +/** + * Module for building cards from item data. + * @module components/cardBuilder/cardBuilder + */ - function getCardsHtml(items, options) { +import datetime from 'datetime'; +import imageLoader from 'imageLoader'; +import connectionManager from 'connectionManager'; +import itemHelper from 'itemHelper'; +import focusManager from 'focusManager'; +import indicators from 'indicators'; +import globalize from 'globalize'; +import layoutManager from 'layoutManager'; +import dom from 'dom'; +import browser from 'browser'; +import playbackManager from 'playbackManager'; +import itemShortcuts from 'itemShortcuts'; +import imageHelper from 'scripts/imagehelper'; +import 'css!./card'; +import 'paper-icon-button-light'; +import 'programStyles'; + + const enableFocusTransform = !browser.slow && !browser.edge; + + /** + * Generate the HTML markup for cards for a set of items. + * @param items - The items used to generate cards. + * @param options - The options of the cards. + * @returns {string} The HTML markup for the cards. + */ + export function getCardsHtml(items, options) { if (arguments.length === 1) { options = arguments[0]; items = options.items; @@ -13,6 +39,13 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana return buildCardsHtmlInternal(items, options); } + /** + * Computes the number of posters per row. + * @param {string} shape - Shape of the cards. + * @param {number} screenWidth - Width of the screen. + * @param {boolean} isOrientationLandscape - Flag for the orientation of the screen. + * @returns {number} Number of cards per row for an itemsContainer. + */ function getPostersPerRow(shape, screenWidth, isOrientationLandscape) { switch (shape) { case 'portrait': @@ -217,10 +250,15 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } + /** + * Checks if the window is resizable. + * @param {number} windowWidth - Width of the device's screen. + * @returns {boolean} - Result of the check. + */ function isResizable(windowWidth) { - var screen = window.screen; + const screen = window.screen; if (screen) { - var screenWidth = screen.availWidth; + const screenWidth = screen.availWidth; if ((screenWidth - windowWidth) > 20) { return true; @@ -230,22 +268,31 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana return false; } + /** + * Gets the width of a card's image according to the shape and amount of cards per row. + * @param {string} shape - Shape of the card. + * @param {number} screenWidth - Width of the screen. + * @param {boolean} isOrientationLandscape - Flag for the orientation of the screen. + * @returns {number} Width of the image for a card. + */ function getImageWidth(shape, screenWidth, isOrientationLandscape) { - var imagesPerRow = getPostersPerRow(shape, screenWidth, isOrientationLandscape); - var shapeWidth = Math.round(screenWidth / imagesPerRow) * 2; - - return shapeWidth; + const imagesPerRow = getPostersPerRow(shape, screenWidth, isOrientationLandscape); + return Math.round(screenWidth / imagesPerRow) * 2; } + /** + * Normalizes the options for a card. + * @param {Object} items - A set of items. + * @param {Object} options - Options for handling the items. + */ function setCardData(items, options) { - options.shape = options.shape || "auto"; - var primaryImageAspectRatio = imageLoader.getPrimaryImageAspectRatio(items); + const primaryImageAspectRatio = imageLoader.getPrimaryImageAspectRatio(items); - if (options.shape === 'auto' || options.shape === 'autohome' || options.shape === 'autooverflow' || options.shape === 'autoVertical') { + if (['auto', 'autohome', 'autooverflow', 'autoVertical'].includes(options.shape)) { - var requestedShape = options.shape; + const requestedShape = options.shape; options.shape = null; if (primaryImageAspectRatio) { @@ -283,11 +330,11 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } if (!options.width) { - var screenWidth = dom.getWindowSize().innerWidth; - var screenHeight = dom.getWindowSize().innerHeight; + let screenWidth = dom.getWindowSize().innerWidth; + const screenHeight = dom.getWindowSize().innerHeight; if (isResizable(screenWidth)) { - var roundScreenTo = 100; + const roundScreenTo = 100; screenWidth = Math.floor(screenWidth / roundScreenTo) * roundScreenTo; } @@ -295,9 +342,14 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } + /** + * Generates the internal HTML markup for cards. + * @param {Object} items - Items for which to generate the markup. + * @param {Object} options - Options for generating the markup. + * @returns {string} The internal HTML markup of the cards. + */ function buildCardsHtmlInternal(items, options) { - - var isVertical; + let isVertical = false; if (options.shape === 'autoVertical') { isVertical = true; @@ -305,24 +357,21 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana setCardData(items, options); - var html = ''; - var itemsInRow = 0; + let html = ''; + let itemsInRow = 0; - var currentIndexValue; - var hasOpenRow; - var hasOpenSection; + let currentIndexValue; + let hasOpenRow; + let hasOpenSection; - var sectionTitleTagName = options.sectionTitleTagName || 'div'; - var apiClient; - var lastServerId; + let sectionTitleTagName = options.sectionTitleTagName || 'div'; + let apiClient; + let lastServerId; - var i; - var length; + for (let i = 0; i < items.length; i++) { - for (i = 0, length = items.length; i < length; i++) { - - var item = items[i]; - var serverId = item.ServerId || options.serverId; + let item = items[i]; + let serverId = item.ServerId || options.serverId; if (serverId !== lastServerId) { lastServerId = serverId; @@ -330,14 +379,14 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } if (options.indexBy) { - var newIndexValue = ''; + let newIndexValue = ''; if (options.indexBy === 'PremiereDate') { if (item.PremiereDate) { try { newIndexValue = datetime.toLocaleDateString(datetime.parseISO8601Date(item.PremiereDate), { weekday: 'long', month: 'long', day: 'numeric' }); - } catch (err) { - console.error('error parsing timestamp for premiere date'); + } catch (error) { + console.error('error parsing timestamp for premiere date', error); } } } else if (options.indexBy === 'ProductionYear') { @@ -412,21 +461,15 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } - var cardFooterHtml = ''; - for (i = 0, length = (options.lines || 0); i < length; i++) { - - if (i === 0) { - cardFooterHtml += '
 
'; - } 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) { @@ -445,18 +488,23 @@ 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) { @@ -663,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 { @@ -685,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'; @@ -722,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++; } @@ -733,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' }) + ' '; @@ -763,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) { @@ -780,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) { @@ -814,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); @@ -824,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 }); @@ -858,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 ? @@ -911,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 || ''); @@ -1006,7 +1095,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); @@ -1027,8 +1116,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); } @@ -1037,18 +1132,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') { @@ -1056,7 +1155,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana if (item.RunTimeTicks) { - var minutes = item.RunTimeTicks / 600000000; + let minutes = item.RunTimeTicks / 600000000; minutes = minutes || 1; @@ -1135,49 +1234,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 @@ -1186,13 +1273,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) { @@ -1210,7 +1297,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'; @@ -1236,16 +1323,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'; @@ -1259,17 +1346,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, { @@ -1300,12 +1387,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'; @@ -1324,15 +1411,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 += ''; @@ -1352,12 +1439,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'; } @@ -1375,13 +1462,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); @@ -1402,7 +1489,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(); } @@ -1411,24 +1498,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 + '"'; } @@ -1436,7 +1519,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana timerAttributes += ' data-seriestimerid="' + item.SeriesTimerId + '"'; } - var actionAttribute; + let actionAttribute; if (tagName === 'button') { className += " itemAction"; @@ -1449,16 +1532,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); @@ -1467,13 +1550,18 @@ 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 += ''; @@ -1481,7 +1569,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana html += '
'; - var userData = item.UserData || {}; + const userData = item.UserData || {}; if (itemHelper.canMarkPlayed(item)) { require(['emby-playstatebutton']); @@ -1490,7 +1578,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 += ''; @@ -1504,9 +1592,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 '' + return ''; } switch (item.Type) { @@ -1529,12 +1623,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; @@ -1549,7 +1647,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } - var html = buildCardsHtmlInternal(items, options); + const html = buildCardsHtmlInternal(items, options); if (html) { @@ -1575,8 +1673,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; } @@ -1585,7 +1688,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); @@ -1594,14 +1697,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) { @@ -1644,7 +1751,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } - var progressHtml = indicators.getProgressBarHtml({ + const progressHtml = indicators.getProgressBarHtml({ Type: type, UserData: userData, MediaType: 'Video' @@ -1658,11 +1765,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); @@ -1678,37 +1785,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); } @@ -1716,13 +1836,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); } @@ -1730,14 +1854,13 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana } } - return { - getCardsHtml: getCardsHtml, - getDefaultBackgroundClass: getDefaultBackgroundClass, - getDefaultText: getDefaultText, - buildCards: buildCards, - onUserDataChanged: onUserDataChanged, - onTimerCreated: onTimerCreated, - onTimerCancelled: onTimerCancelled, - onSeriesTimerCancelled: onSeriesTimerCancelled - }; - }); +export default { + getCardsHtml: getCardsHtml, + getDefaultBackgroundClass: getDefaultBackgroundClass, + getDefaultText: getDefaultText, + buildCards: buildCards, + onUserDataChanged: onUserDataChanged, + onTimerCreated: onTimerCreated, + onTimerCancelled: onTimerCancelled, + onSeriesTimerCancelled: onSeriesTimerCancelled +}; 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/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/libraryoptionseditor/libraryoptionseditor.js b/src/components/libraryoptionseditor/libraryoptionseditor.js index 5f3b20fda2..a398d7043a 100644 --- a/src/components/libraryoptionseditor/libraryoptionseditor.js +++ b/src/components/libraryoptionseditor/libraryoptionseditor.js @@ -396,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); } @@ -493,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, @@ -545,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}
+