diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 1b16b94b62..48f042d729 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'
- - 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'
@@ -52,12 +84,19 @@ jobs:
inputs:
versionSpec: '10.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'
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..4bc22fc1d4 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -1,5 +1,5 @@
env:
- es6: false
+ es6: true
browser: true
amd: true
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..ca6cf36dd2
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,123 @@
+'use strict';
+
+const { src, dest, series, parallel, watch } = require('gulp');
+const browserSync = require('browser-sync').create();
+const del = require('del');
+const babel = require('gulp-babel');
+const concat = require('gulp-concat');
+const terser = require('gulp-terser');
+const htmlmin = require('gulp-htmlmin');
+const imagemin = require('gulp-imagemin');
+const sourcemaps = require('gulp-sourcemaps');
+const mode = require('gulp-mode')({
+ modes: ["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');
+
+sass.compiler = require('node-sass')
+
+
+if (mode.production()) {
+ var config = require('./webpack.prod.js');
+} else {
+ var config = require('./webpack.dev.js');
+}
+
+function serve() {
+ browserSync.init({
+ server: {
+ baseDir: "./dist"
+ },
+ port: 8080
+ });
+
+ watch(['src/**/*.js', '!src/bundle.js'], javascript);
+ watch('src/bundle.js', webpack);
+ watch('src/**/*.css', css);
+ watch(['src/**/*.html', '!src/index.html'], html);
+ watch(['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg'], images);
+ watch(['src/**/*.json', 'src/**/*.ico'], copy);
+ watch('src/index.html', injectBundle);
+ watch(['src/standalone.js', 'src/scripts/apploader.js'], standalone);
+}
+
+function standalone() {
+ return src(['src/standalone.js', 'src/scripts/apploader.js'], { base: './src/' })
+ .pipe(concat('scripts/apploader.js'))
+ .pipe(dest('dist/'));
+}
+
+function clean() {
+ return del(['dist/']);
+}
+
+function javascript() {
+ return src(['src/**/*.js', '!src/bundle.js'], { base: './src/' })
+ .pipe(mode.development(sourcemaps.init({ loadMaps: true })))
+ .pipe(babel({
+ presets: [
+ ['@babel/preset-env']
+ ]
+ }))
+ .pipe(terser({
+ keep_fnames: true,
+ mangle: false
+ }))
+ .pipe(mode.development(sourcemaps.write('.')))
+ .pipe(dest('dist/'))
+ .pipe(browserSync.stream());
+}
+
+function webpack() {
+ return stream(config)
+ .pipe(dest('dist/'))
+ .pipe(browserSync.stream());
+}
+
+function css() {
+ return src(['src/**/*.css', 'src/**/*.scss'], { base: './src/' })
+ .pipe(mode.development(sourcemaps.init({ loadMaps: true })))
+ .pipe(sass().on('error', sass.logError))
+ .pipe(postcss())
+ .pipe(mode.development(sourcemaps.write('.')))
+ .pipe(dest('dist/'))
+ .pipe(browserSync.stream());
+}
+
+function html() {
+ return src(['src/**/*.html', '!src/index.html'], { base: './src/' })
+ .pipe(mode.production(htmlmin({ collapseWhitespace: true })))
+ .pipe(dest('dist/'))
+ .pipe(browserSync.stream());
+}
+
+function images() {
+ return src(['src/**/*.png', 'src/**/*.jpg', 'src/**/*.gif', 'src/**/*.svg'], { base: './src/' })
+ .pipe(mode.production(imagemin()))
+ .pipe(dest('dist/'))
+ .pipe(browserSync.stream());
+}
+
+function copy() {
+ return src(['src/**/*.json', 'src/**/*.ico'], { base: './src/' })
+ .pipe(dest('dist/'))
+ .pipe(browserSync.stream());
+}
+
+function injectBundle() {
+ return src('src/index.html', { base: './src/' })
+ .pipe(inject(
+ src(['src/scripts/apploader.js'], { read: false }, { base: './src/' }), { relative: true }
+ ))
+ .pipe(dest('dist/'))
+ .pipe(browserSync.stream());
+}
+
+exports.default = series(clean, parallel(javascript, webpack, css, html, images, copy), injectBundle)
+exports.standalone = series(exports.default, standalone)
+exports.serve = series(exports.standalone, serve)
diff --git a/package.json b/package.json
index dbc6c8fa8b..6d07f9a6fa 100644
--- a/package.json
+++ b/package.json
@@ -5,12 +5,35 @@
"repository": "https://github.com/jellyfin/jellyfin-web",
"license": "GPL-2.0-or-later",
"devDependencies": {
+ "@babel/core": "^7.8.6",
+ "@babel/polyfill": "^7.8.7",
+ "@babel/preset-env": "^7.8.6",
+ "autoprefixer": "^9.7.4",
+ "babel-loader": "^8.0.6",
+ "browser-sync": "^2.26.7",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.1.1",
"css-loader": "^3.4.2",
+ "cssnano": "^4.1.10",
+ "del": "^5.1.0",
"eslint": "^6.8.0",
"file-loader": "^5.0.2",
+ "gulp": "^4.0.2",
+ "gulp-babel": "^8.0.0",
+ "gulp-cli": "^2.2.0",
+ "gulp-concat": "^2.6.1",
+ "gulp-htmlmin": "^5.0.1",
+ "gulp-imagemin": "^7.1.0",
+ "gulp-inject": "^5.0.5",
+ "gulp-mode": "^1.0.2",
+ "gulp-postcss": "^8.0.0",
+ "gulp-sass": "^4.0.2",
+ "gulp-sourcemaps": "^2.6.5",
+ "gulp-terser": "^1.2.0",
"html-webpack-plugin": "^3.2.0",
+ "node-sass": "^4.13.1",
+ "postcss-loader": "^3.0.0",
+ "postcss-preset-env": "^6.7.0",
"style-loader": "^1.1.3",
"stylelint": "^13.1.0",
"stylelint-config-rational-order": "^0.1.2",
@@ -20,10 +43,12 @@
"webpack-cli": "^3.3.10",
"webpack-concat-plugin": "^3.0.0",
"webpack-dev-server": "^3.10.3",
- "webpack-merge": "^4.2.2"
+ "webpack-merge": "^4.2.2",
+ "webpack-stream": "^5.2.1"
},
"dependencies": {
"alameda": "^1.4.0",
+ "core-js": "^3.6.4",
"document-register-element": "^1.14.3",
"flv.js": "^1.5.0",
"hls.js": "^0.13.1",
@@ -35,6 +60,8 @@
"libjass": "^0.11.0",
"material-design-icons-iconfont": "^5.0.1",
"native-promise-only": "^0.8.0-a",
+ "page": "^1.11.5",
+ "query-string": "^6.11.1",
"resize-observer-polyfill": "^1.5.1",
"shaka-player": "^2.5.9",
"sortablejs": "^1.10.2",
@@ -42,6 +69,11 @@
"webcomponents.js": "^0.7.24",
"whatwg-fetch": "^3.0.0"
},
+ "babel": {
+ "presets": [
+ "@babel/preset-env"
+ ]
+ },
"browserslist": [
"last 2 Firefox versions",
"last 2 Chrome versions",
@@ -49,6 +81,7 @@
"last 2 Safari versions",
"last 2 iOS versions",
"last 2 Edge versions",
+ "Chrome 27",
"Chrome 38",
"Chrome 47",
"Chrome 53",
@@ -57,10 +90,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/librarybrowser.css b/src/assets/css/librarybrowser.css
index d3a3438b0b..0bf65d83a7 100644
--- a/src/assets/css/librarybrowser.css
+++ b/src/assets/css/librarybrowser.css
@@ -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;
@@ -444,6 +441,10 @@
position: relative;
}
+.layout-mobile .itemBackdrop {
+ background-attachment: scroll;
+}
+
.layout-desktop .itemBackdrop::after,
.layout-tv .itemBackdrop::after {
content: "";
@@ -535,6 +536,7 @@
display: flex;
align-items: center;
justify-content: center;
+ text-align: center;
}
.detailPagePrimaryContainer {
@@ -849,7 +851,7 @@ div.itemDetailGalleryLink.defaultCardBackground {
width: auto;
align-items: center;
justify-content: center;
- margin-top: -4.3em;
+ margin-top: -4.2em;
position: relative;
}
diff --git a/src/bundle.js b/src/bundle.js
index 6a352c5776..c5c5e1bcaf 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,7 +81,7 @@ _define("sortable", function() {
// webcomponents
var webcomponents = require("webcomponents.js/webcomponents-lite");
_define("webcomponents", function() {
- return webcomponents
+ return webcomponents;
});
// libjass
@@ -97,7 +103,19 @@ _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;
});
diff --git a/src/components/appRouter.js b/src/components/appRouter.js
index 74b1a5cd5f..efb58a089f 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) {
@@ -562,7 +562,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 +669,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 +720,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/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/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js
index bec641f407..7f562f1fd0 100644
--- a/src/components/cardbuilder/cardBuilder.js
+++ b/src/components/cardbuilder/cardBuilder.js
@@ -2,7 +2,6 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
function (datetime, imageLoader, connectionManager, itemHelper, focusManager, indicators, globalize, layoutManager, appHost, dom, browser, playbackManager, itemShortcuts, imageHelper) {
'use strict';
- var devicePixelRatio = window.devicePixelRatio || 1;
var enableFocusTransform = !browser.slow && !browser.edge;
function getCardsHtml(items, options) {
@@ -233,9 +232,9 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
function getImageWidth(shape, screenWidth, isOrientationLandscape) {
var imagesPerRow = getPostersPerRow(shape, screenWidth, isOrientationLandscape);
- var shapeWidth = screenWidth / imagesPerRow;
+ var shapeWidth = Math.round(screenWidth / imagesPerRow) * 2;
- return Math.round(shapeWidth);
+ return shapeWidth;
}
function setCardData(items, options) {
@@ -463,6 +462,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: "Thumb",
+ maxWidth: width,
tag: item.ImageTags.Thumb
});
@@ -470,6 +470,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: "Banner",
+ maxWidth: width,
tag: item.ImageTags.Banner
});
@@ -477,6 +478,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: "Disc",
+ maxWidth: width,
tag: item.ImageTags.Disc
});
@@ -484,6 +486,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: "Logo",
+ maxWidth: width,
tag: item.ImageTags.Logo
});
@@ -491,6 +494,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.ParentLogoItemId, {
type: "Logo",
+ maxWidth: width,
tag: item.ParentLogoImageTag
});
@@ -498,6 +502,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.SeriesId, {
type: "Thumb",
+ maxWidth: width,
tag: item.SeriesThumbImageTag
});
@@ -505,6 +510,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.ParentThumbItemId, {
type: "Thumb",
+ maxWidth: width,
tag: item.ParentThumbImageTag
});
@@ -512,6 +518,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: "Backdrop",
+ maxWidth: width,
tag: item.BackdropImageTags[0]
});
@@ -521,6 +528,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, {
type: "Backdrop",
+ maxWidth: width,
tag: item.ParentBackdropImageTags[0]
});
@@ -530,6 +538,8 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: "Primary",
+ maxHeight: height,
+ maxWidth: width,
tag: item.ImageTags.Primary
});
@@ -550,6 +560,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 +579,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 +610,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: "Thumb",
+ maxWidth: width,
tag: item.ImageTags.Thumb
});
@@ -601,6 +618,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: "Backdrop",
+ maxWidth: width,
tag: item.BackdropImageTags[0]
});
@@ -608,6 +626,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: "Thumb",
+ maxWidth: width,
tag: item.ImageTags.Thumb
});
@@ -615,6 +634,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.SeriesId, {
type: "Thumb",
+ maxWidth: width,
tag: item.SeriesThumbImageTag
});
@@ -622,6 +642,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.ParentThumbItemId, {
type: "Thumb",
+ maxWidth: width,
tag: item.ParentThumbImageTag
});
@@ -629,6 +650,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, {
type: "Backdrop",
+ maxWidth: width,
tag: item.ParentBackdropImageTags[0]
});
@@ -1457,7 +1479,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
html += '';
}
- html += '
';
+ html += '
';
var userData = item.UserData || {};
@@ -1484,7 +1506,7 @@ define(['datetime', 'imageLoader', 'connectionManager', 'itemHelper', 'focusMana
function getDefaultText(item, options) {
if (item.CollectionType) {
- return '
' + imageHelper.getLibraryIcon(item.CollectionType) + ''
+ return '
'
}
switch (item.Type) {
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/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/dom.js b/src/components/dom.js
index 072ff5c77c..cc37e2fc5b 100644
--- a/src/components/dom.js
+++ b/src/components/dom.js
@@ -112,6 +112,22 @@ define([], function () {
return windowSize;
}
+ var standardWidths = [480, 720, 1280, 1440, 1920, 2560, 3840, 5120, 7680];
+ function getScreenWidth() {
+ var width = window.innerWidth;
+ var height = window.innerHeight;
+
+ if (height > width) {
+ width = height * (16.0 / 9.0);
+ }
+
+ var closest = standardWidths.sort(function (a, b) {
+ return Math.abs(width - a) - Math.abs(width - b);
+ })[0];
+
+ return closest;
+ }
+
var _animationEvent;
function whichAnimationEvent() {
@@ -175,6 +191,7 @@ define([], function () {
addEventListener: addEventListenerWithOptions,
removeEventListener: removeEventListenerWithOptions,
getWindowSize: getWindowSize,
+ getScreenWidth: getScreenWidth,
whichTransitionEvent: whichTransitionEvent,
whichAnimationEvent: whichAnimationEvent,
whichAnimationCancelEvent: whichAnimationCancelEvent
diff --git a/src/components/emby-tabs/emby-tabs.css b/src/components/emby-tabs/emby-tabs.css
index 733a05fb0b..355f904cb8 100644
--- a/src/components/emby-tabs/emby-tabs.css
+++ b/src/components/emby-tabs/emby-tabs.css
@@ -13,11 +13,11 @@
vertical-align: middle;
flex-shrink: 0;
margin: 0;
- padding: 1.5em;
+ padding: 1.5em 1.5em;
position: relative;
height: auto;
min-width: initial;
- line-height: initial;
+ line-height: 1.25;
border-radius: 0;
overflow: hidden;
font-weight: 600;
diff --git a/src/components/homesections/homesections.js b/src/components/homesections/homesections.js
index 430c06bc57..b024528549 100644
--- a/src/components/homesections/homesections.js
+++ b/src/components/homesections/homesections.js
@@ -183,7 +183,7 @@ define(['connectionManager', 'cardBuilder', 'appSettings', 'dom', 'apphost', 'la
for (var i = 0, length = items.length; i < length; i++) {
var item = items[i];
var icon = imageHelper.getLibraryIcon(item.CollectionType);
- html += '
' + icon + '' + item.Name + '';
+ html += '
' + item.Name + '';
}
html += '
';
diff --git a/src/components/itemcontextmenu.js b/src/components/itemcontextmenu.js
index 62048345ea..bdbcfc782b 100644
--- a/src/components/itemcontextmenu.js
+++ b/src/components/itemcontextmenu.js
@@ -346,11 +346,7 @@ define(["apphost", "globalize", "connectionManager", "itemHelper", "appRouter",
break;
case "copy-stream":
var downloadHref = apiClient.getItemDownloadUrl(itemId);
- navigator.clipboard.writeText(downloadHref).then(function () {
- require(["toast"], function (toast) {
- toast(globalize.translate("CopyStreamURLSuccess"));
- });
- }, function () {
+ var textAreaCopy = function () {
var textArea = document.createElement("textarea");
textArea.value = downloadHref;
document.body.appendChild(textArea);
@@ -364,7 +360,16 @@ define(["apphost", "globalize", "connectionManager", "itemHelper", "appRouter",
prompt(globalize.translate("CopyStreamURL"), downloadHref);
}
document.body.removeChild(textArea);
- });
+ };
+ if (navigator.clipboard === undefined) {
+ textAreaCopy();
+ } else {
+ navigator.clipboard.writeText(downloadHref).then(function () {
+ require(["toast"], function (toast) {
+ toast(globalize.translate("CopyStreamURLSuccess"));
+ });
+ }, textAreaCopy);
+ }
getResolveFunction(resolve, id)();
break;
case "editsubtitles":
diff --git a/src/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}
+
";
if (task.State === "Running") {
- html += '
';
+ html += '
';
} else if (task.State === "Idle") {
html += '
';
}
@@ -90,16 +90,22 @@ define(["jQuery", "loading", "events", "globalize", "serverNotifications", "huma
return html;
}
+ function setTaskButtonIcon(button, icon) {
+ var inner = button.querySelector("i");
+ inner.classList.remove("stop", "play_arrow");
+ inner.classList.add(icon);
+ }
+
function updateTaskButton(elem, state) {
if (state === "Running") {
elem.classList.remove("btnStartTask");
elem.classList.add("btnStopTask");
- elem.querySelector("i").innerHTML = "stop";
+ setTaskButtonIcon(elem, "stop");
elem.title = globalize.translate("ButtonStop");
} else if (state === "Idle") {
elem.classList.add("btnStartTask");
elem.classList.remove("btnStopTask");
- elem.querySelector("i").innerHTML = "";
+ setTaskButtonIcon(elem, "play_arrow");
elem.title = globalize.translate("ButtonStart");
}
$(elem).parents(".listItem")[0].setAttribute("data-status", state);
diff --git a/src/controllers/itemdetailpage.js b/src/controllers/itemdetailpage.js
index e18311048c..82569835ca 100644
--- a/src/controllers/itemdetailpage.js
+++ b/src/controllers/itemdetailpage.js
@@ -459,7 +459,6 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti
var usePrimaryImage = item.MediaType === "Video" && item.Type !== "Movie" && item.Type !== "Trailer" ||
item.MediaType && item.MediaType !== "Video" ||
item.Type === "MusicAlbum" ||
- item.Type === "MusicArtist" ||
item.Type === "Person";
if (!layoutManager.mobile && !userSettings.enableBackdrops()) {
@@ -469,6 +468,7 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti
if ("Program" === item.Type && item.ImageTags && item.ImageTags.Thumb) {
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: "Thumb",
+ maxWidth: dom.getScreenWidth(),
index: 0,
tag: item.ImageTags.Thumb
});
@@ -478,6 +478,7 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti
} else if (usePrimaryImage && item.ImageTags && item.ImageTags.Primary) {
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: "Primary",
+ maxWidth: dom.getScreenWidth(),
index: 0,
tag: item.ImageTags.Primary
});
@@ -487,6 +488,7 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti
} else if (item.BackdropImageTags && item.BackdropImageTags.length) {
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: "Backdrop",
+ maxWidth: dom.getScreenWidth(),
index: 0,
tag: item.BackdropImageTags[0]
});
@@ -496,6 +498,7 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti
} else if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) {
imgUrl = apiClient.getScaledImageUrl(item.ParentBackdropItemId, {
type: "Backdrop",
+ maxWidth: dom.getScreenWidth(),
index: 0,
tag: item.ParentBackdropImageTags[0]
});
@@ -505,6 +508,7 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti
} else if (item.ImageTags && item.ImageTags.Thumb) {
imgUrl = apiClient.getScaledImageUrl(item.Id, {
type: "Thumb",
+ maxWidth: dom.getScreenWidth(),
index: 0,
tag: item.ImageTags.Thumb
});
@@ -762,44 +766,54 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti
var shape = "portrait";
var detectRatio = false;
+ /* In the following section, getScreenWidth() is multiplied by 0.5 as the posters
+ are 25vw and we need double the resolution to counter Skia's scaling. */
+ // TODO: Find a reliable way to get the poster width
if (imageTags.Primary) {
url = apiClient.getScaledImageUrl(item.Id, {
type: "Primary",
+ maxWidth: Math.round(dom.getScreenWidth() * 0.5),
tag: item.ImageTags.Primary
});
detectRatio = true;
} else if (item.BackdropImageTags && item.BackdropImageTags.length) {
url = apiClient.getScaledImageUrl(item.Id, {
type: "Backdrop",
+ maxWidth: Math.round(dom.getScreenWidth() * 0.5),
tag: item.BackdropImageTags[0]
});
shape = "thumb";
} else if (imageTags.Thumb) {
url = apiClient.getScaledImageUrl(item.Id, {
type: "Thumb",
+ maxWidth: Math.round(dom.getScreenWidth() * 0.5),
tag: item.ImageTags.Thumb
});
shape = "thumb";
} else if (imageTags.Disc) {
url = apiClient.getScaledImageUrl(item.Id, {
type: "Disc",
+ maxWidth: Math.round(dom.getScreenWidth() * 0.5),
tag: item.ImageTags.Disc
});
shape = "square";
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
url = apiClient.getScaledImageUrl(item.AlbumId, {
type: "Primary",
+ maxWidth: Math.round(dom.getScreenWidth() * 0.5),
tag: item.AlbumPrimaryImageTag
});
shape = "square";
} else if (item.SeriesId && item.SeriesPrimaryImageTag) {
url = apiClient.getScaledImageUrl(item.SeriesId, {
type: "Primary",
+ maxWidth: Math.round(dom.getScreenWidth() * 0.5),
tag: item.SeriesPrimaryImageTag
});
} else if (item.ParentPrimaryImageItemId && item.ParentPrimaryImageTag) {
url = apiClient.getScaledImageUrl(item.ParentPrimaryImageItemId, {
type: "Primary",
+ maxWidth: Math.round(dom.getScreenWidth() * 0.5),
tag: item.ParentPrimaryImageTag
});
}
@@ -1806,7 +1820,6 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti
require(["chaptercardbuilder"], function (chaptercardbuilder) {
chaptercardbuilder.buildChapterCards(item, chapters, {
itemsContainer: scenesContent,
- width: 400,
backdropShape: "overflowBackdrop",
squareShape: "overflowSquare"
});
@@ -1859,7 +1872,6 @@ define(["loading", "appRouter", "layoutManager", "connectionManager", "userSetti
itemsContainer: castContent,
coverImage: true,
serverId: item.ServerId,
- width: 160,
shape: "overflowPortrait"
});
});
diff --git a/src/controllers/list.js b/src/controllers/list.js
index 6e25242a01..bcc38f27cf 100644
--- a/src/controllers/list.js
+++ b/src/controllers/list.js
@@ -157,6 +157,12 @@ define(["globalize", "listView", "layoutManager", "userSettings", "focusManager"
return query;
}
+ function setSortButtonIcon(btnSortIcon, icon) {
+ btnSortIcon.classList.remove("arrow_downward");
+ btnSortIcon.classList.remove("arrow_upward");
+ btnSortIcon.classList.add(icon);
+ }
+
function updateSortText(instance) {
var btnSortText = instance.btnSortText;
@@ -175,7 +181,7 @@ define(["globalize", "listView", "layoutManager", "userSettings", "focusManager"
var btnSortIcon = instance.btnSortIcon;
if (btnSortIcon) {
- btnSortIcon.innerHTML = "Descending" === values.sortOrder ? "" : "";
+ setSortButtonIcon(btnSortIcon, "Descending" === values.sortOrder ? "arrow_downward" : "arrow_upward");
}
}
}
diff --git a/src/controllers/livetv/livetvrecordings.js b/src/controllers/livetv/livetvrecordings.js
index ed3ae24087..f8e49f2590 100644
--- a/src/controllers/livetv/livetvrecordings.js
+++ b/src/controllers/livetv/livetvrecordings.js
@@ -2,6 +2,10 @@ define(["layoutManager", "loading", "cardBuilder", "apphost", "imageLoader", "sc
"use strict";
function renderRecordings(elem, recordings, cardOptions, scrollX) {
+ if (!elem) {
+ return;
+ }
+
if (recordings.length) {
elem.classList.remove("hide");
} else {
diff --git a/src/controllers/medialibrarypage.js b/src/controllers/medialibrarypage.js
index 03c84f2ca3..d838c2145e 100644
--- a/src/controllers/medialibrarypage.js
+++ b/src/controllers/medialibrarypage.js
@@ -256,6 +256,7 @@ define(["jQuery", "apphost", "scripts/taskbutton", "loading", "libraryMenu", "gl
if (virtualFolder.PrimaryImageItemId) {
imgUrl = ApiClient.getScaledImageUrl(virtualFolder.PrimaryImageItemId, {
+ maxWidth: Math.round(dom.getScreenWidth() * 0.40),
type: "Primary"
});
}
diff --git a/src/controllers/playback/videoosd.js b/src/controllers/playback/videoosd.js
index c02c9b4171..c61fd14a7a 100644
--- a/src/controllers/playback/videoosd.js
+++ b/src/controllers/playback/videoosd.js
@@ -334,18 +334,24 @@ define(["playbackManager", "dom", "inputManager", "datetime", "itemHelper", "med
if (item) {
var imgUrl = seriesImageUrl(item, {
+ maxWidth: osdPoster.clientWidth * 2,
type: "Primary"
}) || seriesImageUrl(item, {
+ maxWidth: osdPoster.clientWidth * 2,
type: "Thumb"
}) || imageUrl(item, {
+ maxWidth: osdPoster.clientWidth * 2,
type: "Primary"
});
if (!imgUrl && secondaryItem && (imgUrl = seriesImageUrl(secondaryItem, {
+ maxWidth: osdPoster.clientWidth * 2,
type: "Primary"
}) || seriesImageUrl(secondaryItem, {
+ maxWidth: osdPoster.clientWidth * 2,
type: "Thumb"
}) || imageUrl(secondaryItem, {
+ maxWidth: osdPoster.clientWidth * 2,
type: "Primary"
})), imgUrl) {
return void (osdPoster.innerHTML = '

');
@@ -376,7 +382,7 @@ define(["playbackManager", "dom", "inputManager", "datetime", "itemHelper", "med
function startOsdHideTimer() {
stopOsdHideTimer();
- osdHideTimeout = setTimeout(hideOsd, 5e3);
+ osdHideTimeout = setTimeout(hideOsd, 3e3);
}
function stopOsdHideTimer() {
@@ -665,7 +671,8 @@ define(["playbackManager", "dom", "inputManager", "datetime", "itemHelper", "med
}
function onTimeUpdate(e) {
- if (isEnabled) {
+ // Test for 'currentItem' is required for Firefox since its player spams 'timeupdate' events even being at breakpoint
+ if (isEnabled && currentItem) {
var now = new Date().getTime();
if (!(now - lastUpdateTime < 700)) {
diff --git a/src/controllers/user/display.js b/src/controllers/user/display.js
index e3a56e1552..f348f28750 100644
--- a/src/controllers/user/display.js
+++ b/src/controllers/user/display.js
@@ -1,4 +1,4 @@
-define(["displaySettings", "userSettings", "autoFocuser"], function (DisplaySettings, currentUserSettings, autoFocuser) {
+define(["displaySettings", "userSettingsBuilder", "userSettings", "autoFocuser"], function (DisplaySettings, userSettingsBuilder, currentUserSettings, autoFocuser) {
"use strict";
return function (view, params) {
@@ -11,7 +11,7 @@ define(["displaySettings", "userSettings", "autoFocuser"], function (DisplaySett
var settingsInstance;
var hasChanges;
var userId = params.userId || ApiClient.getCurrentUserId();
- var userSettings = userId === ApiClient.getCurrentUserId() ? currentUserSettings : new userSettings();
+ var userSettings = userId === ApiClient.getCurrentUserId() ? currentUserSettings : new userSettingsBuilder();
view.addEventListener("viewshow", function () {
window.addEventListener("beforeunload", onBeforeUnload);
diff --git a/src/controllers/user/home.js b/src/controllers/user/home.js
index 20a42a7dfb..7f12efc7fb 100644
--- a/src/controllers/user/home.js
+++ b/src/controllers/user/home.js
@@ -1,4 +1,4 @@
-define(["homescreenSettings", "dom", "globalize", "loading", "userSettings", "autoFocuser", "listViewStyle"], function (HomescreenSettings, dom, globalize, loading, currentUserSettings, autoFocuser) {
+define(["homescreenSettings", "userSettingsBuilder", "dom", "globalize", "loading", "userSettings", "autoFocuser", "listViewStyle"], function (HomescreenSettings, userSettingsBuilder, dom, globalize, loading, currentUserSettings, autoFocuser) {
"use strict";
return function (view, params) {
@@ -11,7 +11,7 @@ define(["homescreenSettings", "dom", "globalize", "loading", "userSettings", "au
var homescreenSettingsInstance;
var hasChanges;
var userId = params.userId || ApiClient.getCurrentUserId();
- var userSettings = userId === ApiClient.getCurrentUserId() ? currentUserSettings : new userSettings();
+ var userSettings = userId === ApiClient.getCurrentUserId() ? currentUserSettings : new userSettingsBuilder();
view.addEventListener("viewshow", function () {
window.addEventListener("beforeunload", onBeforeUnload);
diff --git a/src/controllers/user/playback.js b/src/controllers/user/playback.js
index d0a127efe4..3def9d1931 100644
--- a/src/controllers/user/playback.js
+++ b/src/controllers/user/playback.js
@@ -1,4 +1,4 @@
-define(["playbackSettings", "dom", "globalize", "loading", "userSettings", "autoFocuser", "listViewStyle"], function (PlaybackSettings, dom, globalize, loading, currentUserSettings, autoFocuser) {
+define(["playbackSettings", "userSettingsBuilder", "dom", "globalize", "loading", "userSettings", "autoFocuser", "listViewStyle"], function (PlaybackSettings, userSettingsBuilder, dom, globalize, loading, currentUserSettings, autoFocuser) {
"use strict";
return function (view, params) {
@@ -11,7 +11,7 @@ define(["playbackSettings", "dom", "globalize", "loading", "userSettings", "auto
var settingsInstance;
var hasChanges;
var userId = params.userId || ApiClient.getCurrentUserId();
- var userSettings = userId === ApiClient.getCurrentUserId() ? currentUserSettings : new userSettings();
+ var userSettings = userId === ApiClient.getCurrentUserId() ? currentUserSettings : new userSettingsBuilder();
view.addEventListener("viewshow", function () {
window.addEventListener("beforeunload", onBeforeUnload);
diff --git a/src/controllers/user/subtitles.js b/src/controllers/user/subtitles.js
index 8e2dcc8780..1f1102194e 100644
--- a/src/controllers/user/subtitles.js
+++ b/src/controllers/user/subtitles.js
@@ -1,4 +1,4 @@
-define(["subtitleSettings", "userSettings", "autoFocuser"], function (SubtitleSettings, currentUserSettings, autoFocuser) {
+define(["subtitleSettings", "userSettingsBuilder", "userSettings", "autoFocuser"], function (SubtitleSettings, userSettingsBuilder, currentUserSettings, autoFocuser) {
"use strict";
return function (view, params) {
@@ -11,7 +11,7 @@ define(["subtitleSettings", "userSettings", "autoFocuser"], function (SubtitleSe
var subtitleSettingsInstance;
var hasChanges;
var userId = params.userId || ApiClient.getCurrentUserId();
- var userSettings = userId === ApiClient.getCurrentUserId() ? currentUserSettings : new userSettings();
+ var userSettings = userId === ApiClient.getCurrentUserId() ? currentUserSettings : new userSettingsBuilder();
view.addEventListener("viewshow", function () {
window.addEventListener("beforeunload", onBeforeUnload);
diff --git a/src/dlnasettings.html b/src/dlnasettings.html
index 872d546a53..8068acf5ab 100644
--- a/src/dlnasettings.html
+++ b/src/dlnasettings.html
@@ -8,7 +8,7 @@
diff --git a/src/elements/emby-checkbox/emby-checkbox.js b/src/elements/emby-checkbox/emby-checkbox.js
index 2e49a2d185..b5e587d5a6 100644
--- a/src/elements/emby-checkbox/emby-checkbox.js
+++ b/src/elements/emby-checkbox/emby-checkbox.js
@@ -5,7 +5,8 @@ define(['browser', 'dom', 'css!./emby-checkbox', 'registerElement'], function (b
function onKeyDown(e) {
// Don't submit form on enter
- if (e.keyCode === 13) {
+ // Real (non-emulator) Tizen does nothing on Space
+ if (e.keyCode === 13 || e.keyCode === 32) {
e.preventDefault();
this.checked = !this.checked;
diff --git a/src/elements/emby-collapse/emby-collapse.js b/src/elements/emby-collapse/emby-collapse.js
index 600af55516..fdd77adf0b 100644
--- a/src/elements/emby-collapse/emby-collapse.js
+++ b/src/elements/emby-collapse/emby-collapse.js
@@ -80,7 +80,7 @@ define(['browser', 'css!./emby-collapse', 'registerElement', 'emby-button'], fun
var title = this.getAttribute('title');
- var html = '
';
+ var html = '
';
this.insertAdjacentHTML('afterbegin', html);
diff --git a/src/elements/emby-radio/emby-radio.js b/src/elements/emby-radio/emby-radio.js
index 9b91591050..000c656822 100644
--- a/src/elements/emby-radio/emby-radio.js
+++ b/src/elements/emby-radio/emby-radio.js
@@ -6,10 +6,17 @@ define(['css!./emby-radio', 'registerElement'], function () {
function onKeyDown(e) {
// Don't submit form on enter
- if (e.keyCode === 13) {
+ // Real (non-emulator) Tizen does nothing on Space
+ if (e.keyCode === 13 || e.keyCode === 32) {
e.preventDefault();
- this.checked = true;
+ if (!this.checked) {
+ this.checked = true;
+
+ this.dispatchEvent(new CustomEvent('change', {
+ bubbles: true
+ }));
+ }
return false;
}
diff --git a/src/elements/emby-select/emby-select.css b/src/elements/emby-select/emby-select.css
index b508e5d0e3..9f07ca3f65 100644
--- a/src/elements/emby-select/emby-select.css
+++ b/src/elements/emby-select/emby-select.css
@@ -80,6 +80,10 @@
flex-shrink: 0;
}
+.trackSelections > .selectContainer {
+ margin: 0.4em 0;
+}
+
.emby-select-withcolor {
-webkit-appearance: none;
appearance: none;
diff --git a/src/index.html b/src/index.html
index 8e9c8db3b7..624be19360 100644
--- a/src/index.html
+++ b/src/index.html
@@ -105,5 +105,8 @@
+
+
+