diff --git a/.eslintrc.js b/.eslintrc.js
index a4e972c83e..7de812ea6e 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,3 +1,5 @@
+const restrictedGlobals = require('confusing-browser-globals');
+
module.exports = {
root: true,
plugins: [
@@ -39,14 +41,15 @@ module.exports = {
'no-floating-decimal': ['error'],
'no-multi-spaces': ['error'],
'no-multiple-empty-lines': ['error', { 'max': 1 }],
+ 'no-restricted-globals': ['error'].concat(restrictedGlobals),
'no-trailing-spaces': ['error'],
- 'no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
- 'no-unused-vars': ['error', { 'vars': 'all', 'args': 'none', 'ignoreRestSiblings': true }],
+ '@babel/no-unused-expressions': ['error', { 'allowShortCircuit': true, 'allowTernary': true, 'allowTaggedTemplates': true }],
+ //'no-unused-vars': ['error', { 'vars': 'all', 'args': 'none', 'ignoreRestSiblings': true }],
'one-var': ['error', 'never'],
'padded-blocks': ['error', 'never'],
//'prefer-const': ['error', {'destructuring': 'all'}],
'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': false }],
- 'semi': ['error'],
+ '@babel/semi': ['error'],
'space-before-blocks': ['error'],
'space-infix-ops': 'error',
'yoda': 'error'
@@ -106,6 +109,7 @@ module.exports = {
// TODO: Fix warnings and remove these rules
'no-redeclare': ['off'],
'no-useless-escape': ['off'],
+ 'no-unused-vars': ['off'],
// TODO: Remove after ES6 migration is complete
'import/no-unresolved': ['off']
},
diff --git a/package.json b/package.json
index fb3c2dbd30..d6e8c1941f 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,8 @@
"repository": "https://github.com/jellyfin/jellyfin-web",
"license": "GPL-2.0-or-later",
"devDependencies": {
- "@babel/core": "^7.11.1",
- "@babel/eslint-parser": "^7.11.3",
+ "@babel/core": "^7.11.4",
+ "@babel/eslint-parser": "^7.11.4",
"@babel/eslint-plugin": "^7.11.3",
"@babel/plugin-proposal-class-properties": "^7.10.1",
"@babel/plugin-proposal-private-methods": "^7.10.1",
@@ -16,8 +16,9 @@
"autoprefixer": "^9.8.6",
"babel-loader": "^8.0.6",
"browser-sync": "^2.26.12",
+ "confusing-browser-globals": "^1.0.9",
"copy-webpack-plugin": "^5.1.1",
- "css-loader": "^4.2.1",
+ "css-loader": "^4.2.2",
"cssnano": "^4.1.10",
"del": "^5.1.0",
"eslint": "^7.7.0",
@@ -38,7 +39,7 @@
"gulp-postcss": "^8.0.0",
"gulp-sass": "^4.0.2",
"gulp-sourcemaps": "^2.6.5",
- "gulp-terser": "^1.3.2",
+ "gulp-terser": "^1.4.0",
"html-webpack-plugin": "^4.3.0",
"lazypipe": "^1.0.2",
"node-sass": "^4.13.1",
@@ -63,7 +64,7 @@
"fast-text-encoding": "^1.0.3",
"flv.js": "^1.5.0",
"headroom.js": "^0.11.0",
- "hls.js": "^0.14.8",
+ "hls.js": "^0.14.9",
"howler": "^2.2.0",
"intersection-observer": "^0.11.0",
"jellyfin-apiclient": "^1.4.1",
@@ -96,6 +97,7 @@
"src/components/alphaPicker/alphaPicker.js",
"src/components/appFooter/appFooter.js",
"src/components/apphost.js",
+ "src/components/appRouter.js",
"src/components/autoFocuser.js",
"src/components/backdrop/backdrop.js",
"src/components/cardbuilder/cardBuilder.js",
@@ -144,6 +146,7 @@
"src/components/multiSelect/multiSelect.js",
"src/components/notifications/notifications.js",
"src/components/nowPlayingBar/nowPlayingBar.js",
+ "src/components/packageManager.js",
"src/components/playback/brightnessosd.js",
"src/components/playback/mediasession.js",
"src/components/playback/nowplayinghelper.js",
@@ -159,16 +162,21 @@
"src/components/playerstats/playerstats.js",
"src/components/playlisteditor/playlisteditor.js",
"src/components/playmenu.js",
+ "src/components/pluginManager.js",
"src/components/prompt/prompt.js",
"src/components/recordingcreator/recordingbutton.js",
"src/components/recordingcreator/recordingcreator.js",
"src/components/recordingcreator/seriesrecordingeditor.js",
"src/components/recordingcreator/recordinghelper.js",
"src/components/refreshdialog/refreshdialog.js",
+ "src/components/recordingcreator/recordingeditor.js",
+ "src/components/recordingcreator/recordingfields.js",
"src/components/qualityOptions.js",
"src/components/remotecontrol/remotecontrol.js",
"src/components/sanatizefilename.js",
"src/components/scrollManager.js",
+ "src/plugins/experimentalWarnings/plugin.js",
+ "src/plugins/sessionPlayer/plugin.js",
"src/plugins/htmlAudioPlayer/plugin.js",
"src/plugins/chromecastPlayer/plugin.js",
"src/components/slideshow/slideshow.js",
@@ -313,11 +321,13 @@
"src/plugins/backdropScreensaver/plugin.js",
"src/plugins/bookPlayer/plugin.js",
"src/plugins/bookPlayer/tableOfContents.js",
+ "src/plugins/chromecastPlayer/chromecastHelper.js",
"src/plugins/photoPlayer/plugin.js",
"src/plugins/youtubePlayer/plugin.js",
"src/scripts/alphanumericshortcuts.js",
"src/scripts/autoBackdrops.js",
"src/scripts/browser.js",
+ "src/scripts/clientUtils.js",
"src/scripts/datetime.js",
"src/scripts/deleteHelper.js",
"src/scripts/dfnshelper.js",
diff --git a/src/components/appRouter.js b/src/components/appRouter.js
index e6bd86336b..f986e71357 100644
--- a/src/components/appRouter.js
+++ b/src/components/appRouter.js
@@ -1,68 +1,298 @@
-define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdrop', 'browser', 'page', 'appSettings', 'apphost', 'connectionManager'], function (loading, globalize, events, viewManager, skinManager, backdrop, browser, page, appSettings, appHost, connectionManager) {
- 'use strict';
+import appHost from 'apphost';
+import appSettings from 'appSettings';
+import backdrop from 'backdrop';
+import browser from 'browser';
+import connectionManager from 'connectionManager';
+import events from 'events';
+import globalize from 'globalize';
+import itemHelper from 'itemHelper';
+import loading from 'loading';
+import page from 'page';
+import viewManager from 'viewManager';
- appHost = appHost.default || appHost;
- viewManager = viewManager.default || viewManager;
- browser = browser.default || browser;
- loading = loading.default || loading;
+class AppRouter {
+ allRoutes = [];
+ backdropContainer;
+ backgroundContainer;
+ currentRouteInfo;
+ currentViewLoadRequest;
+ firstConnectionResult;
+ forcedLogoutMsg;
+ handleAnchorClick = page.clickHandler;
+ isDummyBackToHome;
+ msgTimeout;
+ popstateOccurred = false;
+ resolveOnNextShow;
+ /**
+ * Pages of "no return" (when "Go back" should behave differently, probably quitting the application).
+ */
+ startPages = ['home', 'login', 'selectserver'];
- var appRouter = {
- showLocalLogin: function (serverId, manualLogin) {
- var pageName = manualLogin ? 'manuallogin' : 'login';
- show('/startup/' + pageName + '.html?serverid=' + serverId);
- },
- showSelectServer: function () {
- show('/startup/selectserver.html');
- },
- showWelcome: function () {
- show('/startup/welcome.html');
- },
- showSettings: function () {
- show('/settings/settings.html');
- },
- showNowPlaying: function () {
- show('queue');
+ constructor() {
+ window.addEventListener('popstate', () => {
+ this.popstateOccurred = true;
+ });
+
+ document.addEventListener('viewshow', () => {
+ const resolve = this.resolveOnNextShow;
+ if (resolve) {
+ this.resolveOnNextShow = null;
+ resolve();
+ }
+ });
+
+ this.baseRoute = window.location.href.split('?')[0].replace(this.getRequestFile(), '');
+ // support hashbang
+ this.baseRoute = this.baseRoute.split('#')[0];
+ if (this.baseRoute.endsWith('/') && !this.baseRoute.endsWith('://')) {
+ this.baseRoute = this.baseRoute.substring(0, this.baseRoute.length - 1);
}
- };
- function beginConnectionWizard() {
+ this.setBaseRoute();
+ }
+
+ /**
+ * @private
+ */
+ setBaseRoute() {
+ let baseRoute = window.location.pathname.replace(this.getRequestFile(), '');
+ if (baseRoute.lastIndexOf('/') === baseRoute.length - 1) {
+ baseRoute = baseRoute.substring(0, baseRoute.length - 1);
+ }
+ console.debug('setting page base to ' + baseRoute);
+ page.base(baseRoute);
+ }
+
+ addRoute(path, newRoute) {
+ page(path, this.getHandler(newRoute));
+ this.allRoutes.push(newRoute);
+ }
+
+ showLocalLogin(serverId) {
+ Dashboard.navigate('login.html?serverid=' + serverId);
+ }
+
+ showVideoOsd() {
+ return Dashboard.navigate('video');
+ }
+
+ showSelectServer() {
+ Dashboard.navigate(AppInfo.isNativeApp ? 'selectserver.html' : 'login.html');
+ }
+
+ showWelcome() {
+ Dashboard.navigate(AppInfo.isNativeApp ? 'selectserver.html' : 'login.html');
+ }
+
+ showSettings() {
+ Dashboard.navigate('mypreferencesmenu.html');
+ }
+
+ showNowPlaying() {
+ this.show('queue');
+ }
+
+ beginConnectionWizard() {
backdrop.clearBackdrop();
loading.show();
connectionManager.connect({
enableAutoLogin: appSettings.enableAutoLogin()
- }).then(function (result) {
- handleConnectionResult(result);
+ }).then((result) => {
+ this.handleConnectionResult(result);
});
}
- function handleConnectionResult(result) {
+ param(name, url) {
+ name = name.replace(/[[]/, '\\[').replace(/[\]]/, '\\]');
+ const regexS = '[\\?&]' + name + '=([^]*)';
+ const regex = new RegExp(regexS, 'i');
+
+ const results = regex.exec(url || getWindowLocationSearch());
+ if (results == null) {
+ return '';
+ } else {
+ return decodeURIComponent(results[1].replace(/\+/g, ' '));
+ }
+ }
+
+ back() {
+ page.back();
+ }
+
+ show(path, options) {
+ if (path.indexOf('/') !== 0 && path.indexOf('://') === -1) {
+ path = '/' + path;
+ }
+
+ path = path.replace(this.baseUrl(), '');
+
+ if (this.currentRouteInfo && this.currentRouteInfo.path === path) {
+ // can't use this with home right now due to the back menu
+ if (this.currentRouteInfo.route.type !== 'home') {
+ loading.hide();
+ return Promise.resolve();
+ }
+ }
+
+ return new Promise((resolve) => {
+ this.resolveOnNextShow = resolve;
+ page.show(path, options);
+ });
+ }
+
+ showDirect(path) {
+ return new Promise(function(resolve) {
+ this.resolveOnNextShow = resolve;
+ page.show(this.baseUrl() + path);
+ });
+ }
+
+ start(options) {
+ loading.show();
+ this.initApiClients();
+
+ events.on(appHost, 'beforeexit', this.onBeforeExit);
+ events.on(appHost, 'resume', this.onAppResume);
+
+ connectionManager.connect({
+ enableAutoLogin: appSettings.enableAutoLogin()
+ }).then((result) => {
+ this.firstConnectionResult = result;
+ options = options || {};
+ page({
+ click: options.click !== false,
+ hashbang: options.hashbang !== false
+ });
+ }).catch().then(() => {
+ loading.hide();
+ });
+ }
+
+ baseUrl() {
+ return this.baseRoute;
+ }
+
+ canGoBack() {
+ const curr = this.current();
+ if (!curr) {
+ return false;
+ }
+
+ if (!document.querySelector('.dialogContainer') && this.startPages.indexOf(curr.type) !== -1) {
+ return false;
+ }
+
+ return window.history.length > 1;
+ }
+
+ current() {
+ return this.currentRouteInfo ? this.currentRouteInfo.route : null;
+ }
+
+ invokeShortcut(id) {
+ if (id.indexOf('library-') === 0) {
+ id = id.replace('library-', '');
+ id = id.split('_');
+
+ this.showItem(id[0], id[1]);
+ } else if (id.indexOf('item-') === 0) {
+ id = id.replace('item-', '');
+ id = id.split('_');
+ this.showItem(id[0], id[1]);
+ } else {
+ id = id.split('_');
+ this.show(this.getRouteUrl(id[0], {
+ serverId: id[1]
+ }));
+ }
+ }
+
+ showItem(item, serverId, options) {
+ // TODO: Refactor this so it only gets items, not strings.
+ if (typeof (item) === 'string') {
+ const apiClient = serverId ? connectionManager.getApiClient(serverId) : connectionManager.currentApiClient();
+ apiClient.getItem(apiClient.getCurrentUserId(), item).then((itemObject) => {
+ this.showItem(itemObject, options);
+ });
+ } else {
+ if (arguments.length === 2) {
+ options = arguments[1];
+ }
+
+ const url = this.getRouteUrl(item, options);
+ this.show(url, {
+ item: item
+ });
+ }
+ }
+
+ setTransparency(level) {
+ if (!this.backdropContainer) {
+ this.backdropContainer = document.querySelector('.backdropContainer');
+ }
+ if (!this.backgroundContainer) {
+ this.backgroundContainer = document.querySelector('.backgroundContainer');
+ }
+
+ if (level === 'full' || level === 2) {
+ backdrop.clearBackdrop(true);
+ document.documentElement.classList.add('transparentDocument');
+ this.backgroundContainer.classList.add('backgroundContainer-transparent');
+ this.backdropContainer.classList.add('hide');
+ } else if (level === 'backdrop' || level === 1) {
+ backdrop.externalBackdrop(true);
+ document.documentElement.classList.add('transparentDocument');
+ this.backgroundContainer.classList.add('backgroundContainer-transparent');
+ this.backdropContainer.classList.add('hide');
+ } else {
+ backdrop.externalBackdrop(false);
+ document.documentElement.classList.remove('transparentDocument');
+ this.backgroundContainer.classList.remove('backgroundContainer-transparent');
+ this.backdropContainer.classList.remove('hide');
+ }
+ }
+
+ getRoutes() {
+ return this.allRoutes;
+ }
+
+ pushState(state, title, url) {
+ state.navigate = false;
+ window.history.pushState(state, title, url);
+ }
+
+ enableNativeHistory() {
+ return false;
+ }
+
+ handleConnectionResult(result) {
switch (result.State) {
case 'SignedIn':
loading.hide();
Emby.Page.goHome();
break;
case 'ServerSignIn':
- result.ApiClient.getPublicUsers().then(function (users) {
+ result.ApiClient.getPublicUsers().then((users) => {
if (users.length) {
- appRouter.showLocalLogin(result.Servers[0].Id);
+ this.showLocalLogin(result.Servers[0].Id);
} else {
- appRouter.showLocalLogin(result.Servers[0].Id, true);
+ this.showLocalLogin(result.Servers[0].Id, true);
}
});
break;
case 'ServerSelection':
- appRouter.showSelectServer();
+ this.showSelectServer();
break;
case 'ConnectSignIn':
- appRouter.showWelcome();
+ this.showWelcome();
break;
case 'ServerUpdateNeeded':
- require(['alert'], function (alert) {
- alert.default({
+ import('alert').then(({default: alert}) =>{
+ alert({
text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'),
html: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin')
- }).then(function () {
- appRouter.showSelectServer();
+ }).then(() => {
+ this.showSelectServer();
});
});
break;
@@ -71,8 +301,8 @@ define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdro
}
}
- function loadContentUrl(ctx, next, route, request) {
- var url;
+ loadContentUrl(ctx, next, route, request) {
+ let url;
if (route.contentPath && typeof (route.contentPath) === 'function') {
url = route.contentPath(ctx.querystring);
} else {
@@ -85,55 +315,54 @@ define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdro
url = '/' + url;
}
- url = baseUrl() + url;
+ url = this.baseUrl() + url;
}
if (ctx.querystring && route.enableContentQueryString) {
url += '?' + ctx.querystring;
}
- require(['text!' + url], function (html) {
- loadContent(ctx, route, html, request);
+ import('text!' + url).then(({default: html}) => {
+ this.loadContent(ctx, route, html, request);
});
}
- function handleRoute(ctx, next, route) {
- authenticate(ctx, route, function () {
- initRoute(ctx, next, route);
+ handleRoute(ctx, next, route) {
+ this.authenticate(ctx, route, () => {
+ this.initRoute(ctx, next, route);
});
}
- function initRoute(ctx, next, route) {
- var onInitComplete = function (controllerFactory) {
- sendRouteToViewManager(ctx, next, route, controllerFactory);
+ initRoute(ctx, next, route) {
+ const onInitComplete = (controllerFactory) => {
+ this.sendRouteToViewManager(ctx, next, route, controllerFactory);
};
if (route.controller) {
- require(['controllers/' + route.controller], onInitComplete);
+ import('controllers/' + route.controller).then(onInitComplete);
} else {
onInitComplete();
}
}
- function cancelCurrentLoadRequest() {
- var currentRequest = currentViewLoadRequest;
+ cancelCurrentLoadRequest() {
+ const currentRequest = this.currentViewLoadRequest;
if (currentRequest) {
currentRequest.cancel = true;
}
}
- var currentViewLoadRequest;
- function sendRouteToViewManager(ctx, next, route, controllerFactory) {
- if (isDummyBackToHome && route.type === 'home') {
- isDummyBackToHome = false;
+ sendRouteToViewManager(ctx, next, route, controllerFactory) {
+ if (this.isDummyBackToHome && route.type === 'home') {
+ this.isDummyBackToHome = false;
return;
}
- cancelCurrentLoadRequest();
- var isBackNav = ctx.isBack;
+ this.cancelCurrentLoadRequest();
+ const isBackNav = ctx.isBack;
- var currentRequest = {
- url: baseUrl() + ctx.path,
+ const currentRequest = {
+ url: this.baseUrl() + ctx.path,
transition: route.transition,
isBack: isBackNav,
state: ctx.state,
@@ -146,11 +375,11 @@ define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdro
},
autoFocus: route.autoFocus
};
- currentViewLoadRequest = currentRequest;
+ this.currentViewLoadRequest = currentRequest;
- var onNewViewNeeded = function () {
+ const onNewViewNeeded = () => {
if (typeof route.path === 'string') {
- loadContentUrl(ctx, next, route, currentRequest);
+ this.loadContentUrl(ctx, next, route, currentRequest);
} else {
next();
}
@@ -160,64 +389,62 @@ define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdro
onNewViewNeeded();
return;
}
- viewManager.tryRestoreView(currentRequest, function () {
- currentRouteInfo = {
+ viewManager.tryRestoreView(currentRequest, () => {
+ this.currentRouteInfo = {
route: route,
path: ctx.path
};
- }).catch(function (result) {
+ }).catch((result) => {
if (!result || !result.cancelled) {
onNewViewNeeded();
}
});
}
- var msgTimeout;
- var forcedLogoutMsg;
- function onForcedLogoutMessageTimeout() {
- var msg = forcedLogoutMsg;
- forcedLogoutMsg = null;
+ onForcedLogoutMessageTimeout() {
+ const msg = this.forcedLogoutMsg;
+ this.forcedLogoutMsg = null;
if (msg) {
- require(['alert'], function (alert) {
+ import('alert').then((alert) => {
alert(msg);
});
}
}
- function showForcedLogoutMessage(msg) {
- forcedLogoutMsg = msg;
- if (msgTimeout) {
- clearTimeout(msgTimeout);
+ showForcedLogoutMessage(msg) {
+ this.forcedLogoutMsg = msg;
+ if (this.msgTimeout) {
+ clearTimeout(this.msgTimeout);
}
- msgTimeout = setTimeout(onForcedLogoutMessageTimeout, 100);
+ this.msgTimeout = setTimeout(this.onForcedLogoutMessageTimeout, 100);
}
- function onRequestFail(e, data) {
- var apiClient = this;
+ onRequestFail(e, data) {
+ const apiClient = this;
if (data.status === 403) {
if (data.errorCode === 'ParentalControl') {
- var isCurrentAllowed = currentRouteInfo ? (currentRouteInfo.route.anonymous || currentRouteInfo.route.startup) : true;
+ const isCurrentAllowed = this.currentRouteInfo ? (this.currentRouteInfo.route.anonymous || this.currentRouteInfo.route.startup) : true;
// Bounce to the login screen, but not if a password entry fails, obviously
if (!isCurrentAllowed) {
- showForcedLogoutMessage(globalize.translate('AccessRestrictedTryAgainLater'));
- appRouter.showLocalLogin(apiClient.serverId());
+ this.showForcedLogoutMessage(globalize.translate('AccessRestrictedTryAgainLater'));
+ this.showLocalLogin(apiClient.serverId());
}
}
}
}
- function onBeforeExit(e) {
+ onBeforeExit() {
if (browser.web0s) {
page.restorePreviousState();
}
}
- function normalizeImageOptions(options) {
- var setQuality;
+ normalizeImageOptions(options) {
+ let setQuality;
if (options.maxWidth || options.width || options.maxHeight || options.height) {
setQuality = true;
}
@@ -227,10 +454,10 @@ define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdro
}
}
- function getMaxBandwidth() {
+ getMaxBandwidth() {
/* eslint-disable compat/compat */
if (navigator.connection) {
- var max = navigator.connection.downlinkMax;
+ let max = navigator.connection.downlinkMax;
if (max && max > 0 && max < Number.POSITIVE_INFINITY) {
max /= 8;
max *= 1000000;
@@ -243,90 +470,65 @@ define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdro
return null;
}
- function getMaxBandwidthIOS() {
+ getMaxBandwidthIOS() {
return 800000;
}
- function onApiClientCreated(e, newApiClient) {
- newApiClient.normalizeImageOptions = normalizeImageOptions;
+ onApiClientCreated(e, newApiClient) {
+ newApiClient.normalizeImageOptions = this.normalizeImageOptions;
if (browser.iOS) {
- newApiClient.getMaxBandwidth = getMaxBandwidthIOS;
+ newApiClient.getMaxBandwidth = this.getMaxBandwidthIOS;
} else {
- newApiClient.getMaxBandwidth = getMaxBandwidth;
+ newApiClient.getMaxBandwidth = this.getMaxBandwidth;
}
- events.off(newApiClient, 'requestfail', onRequestFail);
- events.on(newApiClient, 'requestfail', onRequestFail);
+ events.off(newApiClient, 'requestfail', this.onRequestFail);
+ events.on(newApiClient, 'requestfail', this.onRequestFail);
}
- function initApiClient(apiClient) {
- onApiClientCreated({}, apiClient);
+ initApiClient(apiClient, instance) {
+ instance.onApiClientCreated({}, apiClient);
}
- function initApiClients() {
- connectionManager.getApiClients().forEach(initApiClient);
+ initApiClients() {
+ connectionManager.getApiClients().forEach((apiClient) => {
+ this.initApiClient(apiClient, this);
+ });
- events.on(connectionManager, 'apiclientcreated', onApiClientCreated);
+ events.on(connectionManager, 'apiclientcreated', this.onApiClientCreated);
}
- function onAppResume() {
- var apiClient = connectionManager.currentApiClient();
+ onAppResume() {
+ const apiClient = connectionManager.currentApiClient();
if (apiClient) {
apiClient.ensureWebSocket();
}
}
- var firstConnectionResult;
- function start(options) {
- loading.show();
-
- initApiClients();
-
- events.on(appHost, 'beforeexit', onBeforeExit);
- events.on(appHost, 'resume', onAppResume);
-
- connectionManager.connect({
- enableAutoLogin: appSettings.enableAutoLogin()
- }).then(function (result) {
- firstConnectionResult = result;
- options = options || {};
- page({
- click: options.click !== false,
- hashbang: options.hashbang !== false
- });
- }).catch().then(function() {
- loading.hide();
- });
- }
-
- function enableNativeHistory() {
- return false;
- }
-
- function authenticate(ctx, route, callback) {
- var firstResult = firstConnectionResult;
+ authenticate(ctx, route, callback) {
+ const firstResult = this.firstConnectionResult;
if (firstResult) {
- firstConnectionResult = null;
+ this.firstConnectionResult = null;
if (firstResult.State !== 'SignedIn' && !route.anonymous) {
- handleConnectionResult(firstResult);
+ this.handleConnectionResult(firstResult);
return;
}
}
- var apiClient = connectionManager.currentApiClient();
- var pathname = ctx.pathname.toLowerCase();
+ const apiClient = connectionManager.currentApiClient();
+ const pathname = ctx.pathname.toLowerCase();
console.debug('appRouter - processing path request ' + pathname);
- var isCurrentRouteStartup = currentRouteInfo ? currentRouteInfo.route.startup : true;
- var shouldExitApp = ctx.isBack && route.isDefaultRoute && isCurrentRouteStartup;
+ const isCurrentRouteStartup = this.currentRouteInfo ? this.currentRouteInfo.route.startup : true;
+ const shouldExitApp = ctx.isBack && route.isDefaultRoute && isCurrentRouteStartup;
if (!shouldExitApp && (!apiClient || !apiClient.isLoggedIn()) && !route.anonymous) {
console.debug('appRouter - route does not allow anonymous access, redirecting to login');
- beginConnectionWizard();
+ this.beginConnectionWizard();
return;
}
@@ -346,9 +548,9 @@ define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdro
Emby.Page.goHome();
return;
} else if (route.roles) {
- validateRoles(apiClient, route.roles).then(function () {
+ this.validateRoles(apiClient, route.roles).then(() => {
callback();
- }, beginConnectionWizard);
+ }, this.beginConnectionWizard);
return;
}
}
@@ -357,15 +559,15 @@ define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdro
callback();
}
- function validateRoles(apiClient, roles) {
- return Promise.all(roles.split(',').map(function (role) {
- return validateRole(apiClient, role);
+ validateRoles(apiClient, roles) {
+ return Promise.all(roles.split(',').map((role) => {
+ return this.validateRole(apiClient, role);
}));
}
- function validateRole(apiClient, role) {
+ validateRole(apiClient, role) {
if (role === 'admin') {
- return apiClient.getCurrentUser().then(function (user) {
+ return apiClient.getCurrentUser().then((user) => {
if (user.Policy.IsAdministrator) {
return Promise.resolve();
}
@@ -377,15 +579,13 @@ define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdro
return Promise.resolve();
}
- var isDummyBackToHome;
-
- function loadContent(ctx, route, html, request) {
+ loadContent(ctx, route, html, request) {
html = globalize.translateHtml(html, route.dictionary);
request.view = html;
viewManager.loadView(request);
- currentRouteInfo = {
+ this.currentRouteInfo = {
route: route,
path: ctx.path
};
@@ -393,10 +593,10 @@ define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdro
ctx.handled = true;
}
- function getRequestFile() {
- var path = self.location.pathname || '';
+ getRequestFile() {
+ let path = window.location.pathname || '';
- var index = path.lastIndexOf('/');
+ const index = path.lastIndexOf('/');
if (index !== -1) {
path = path.substring(index);
} else {
@@ -410,39 +610,19 @@ define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdro
return path;
}
- function endsWith(str, srch) {
- return str.lastIndexOf(srch) === srch.length - 1;
- }
-
- var baseRoute = self.location.href.split('?')[0].replace(getRequestFile(), '');
- // support hashbang
- baseRoute = baseRoute.split('#')[0];
- if (endsWith(baseRoute, '/') && !endsWith(baseRoute, '://')) {
- baseRoute = baseRoute.substring(0, baseRoute.length - 1);
- }
-
- function baseUrl() {
- 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;
+ getHandler(route) {
+ return (ctx, next) => {
+ ctx.isBack = this.popstateOccurred;
+ this.handleRoute(ctx, next, route);
+ this.popstateOccurred = false;
};
}
- function getWindowLocationSearch(win) {
- var currentPath = currentRouteInfo ? (currentRouteInfo.path || '') : '';
+ getWindowLocationSearch() {
+ const currentPath = this.currentRouteInfo ? (this.currentRouteInfo.path || '') : '';
- var index = currentPath.indexOf('?');
- var search = '';
+ const index = currentPath.indexOf('?');
+ let search = '';
if (index !== -1) {
search = currentPath.substring(index);
@@ -451,199 +631,218 @@ define(['loading', 'globalize', 'events', 'viewManager', 'skinManager', 'backdro
return search || '';
}
- function param(name, url) {
- name = name.replace(/[\[]/, '\\\[').replace(/[\]]/, '\\\]');
- var regexS = '[\\?&]' + name + '=([^]*)';
- var regex = new RegExp(regexS, 'i');
-
- var results = regex.exec(url || getWindowLocationSearch());
- if (results == null) {
- return '';
- } else {
- return decodeURIComponent(results[1].replace(/\+/g, ' '));
- }
+ showGuide() {
+ Dashboard.navigate('livetv.html?tab=1');
}
- function back() {
- page.back();
+ goHome() {
+ Dashboard.navigate('home.html');
}
- /**
- * Pages of "no return" (when "Go back" should behave differently, probably quitting the application).
- */
- var startPages = ['home', 'login', 'selectserver'];
-
- function canGoBack() {
- var curr = current();
- if (!curr) {
- return false;
- }
-
- if (!document.querySelector('.dialogContainer') && startPages.indexOf(curr.type) !== -1) {
- return false;
- }
-
- return (page.len || 0) > 0;
+ showSearch() {
+ Dashboard.navigate('search.html');
}
- function showDirect(path) {
- return new Promise(function(resolve, reject) {
- resolveOnNextShow = resolve;
- page.show(baseUrl() + path);
- });
+ showLiveTV() {
+ Dashboard.navigate('livetv.html');
}
- function show(path, options) {
- if (path.indexOf('/') !== 0 && path.indexOf('://') === -1) {
- path = '/' + path;
+ showRecordedTV() {
+ Dashboard.navigate('livetv.html?tab=3');
+ }
+
+ showFavorites() {
+ Dashboard.navigate('home.html?tab=1');
+ }
+
+ setTitle(title) {
+ LibraryMenu.setTitle(title);
+ }
+
+ getRouteUrl(item, options) {
+ if (!item) {
+ throw new Error('item cannot be null');
}
- path = path.replace(baseUrl(), '');
+ if (item.url) {
+ return item.url;
+ }
- if (currentRouteInfo && currentRouteInfo.path === path) {
- // can't use this with home right now due to the back menu
- if (currentRouteInfo.route.type !== 'home') {
- loading.hide();
- return Promise.resolve();
+ const context = options ? options.context : null;
+ const id = item.Id || item.ItemId;
+
+ if (!options) {
+ options = {};
+ }
+
+ let url;
+ // TODO: options will never be false. Replace condition with lodash's isEmpty()
+ const itemType = item.Type || (options ? options.itemType : null);
+ const serverId = item.ServerId || options.serverId;
+
+ if (item === 'settings') {
+ return 'mypreferencesmenu.html';
+ }
+
+ if (item === 'wizard') {
+ return 'wizardstart.html';
+ }
+
+ if (item === 'manageserver') {
+ return 'dashboard.html';
+ }
+
+ if (item === 'recordedtv') {
+ return 'livetv.html?tab=3&serverId=' + options.serverId;
+ }
+
+ if (item === 'nextup') {
+ return 'list.html?type=nextup&serverId=' + options.serverId;
+ }
+
+ if (item === 'list') {
+ let url = 'list.html?serverId=' + options.serverId + '&type=' + options.itemTypes;
+
+ if (options.isFavorite) {
+ url += '&IsFavorite=true';
+ }
+
+ return url;
+ }
+
+ if (item === 'livetv') {
+ if (options.section === 'programs') {
+ return 'livetv.html?tab=0&serverId=' + options.serverId;
+ }
+ if (options.section === 'guide') {
+ return 'livetv.html?tab=1&serverId=' + options.serverId;
+ }
+
+ if (options.section === 'movies') {
+ return 'list.html?type=Programs&IsMovie=true&serverId=' + options.serverId;
+ }
+
+ if (options.section === 'shows') {
+ return 'list.html?type=Programs&IsSeries=true&IsMovie=false&IsNews=false&serverId=' + options.serverId;
+ }
+
+ if (options.section === 'sports') {
+ return 'list.html?type=Programs&IsSports=true&serverId=' + options.serverId;
+ }
+
+ if (options.section === 'kids') {
+ return 'list.html?type=Programs&IsKids=true&serverId=' + options.serverId;
+ }
+
+ if (options.section === 'news') {
+ return 'list.html?type=Programs&IsNews=true&serverId=' + options.serverId;
+ }
+
+ if (options.section === 'onnow') {
+ return 'list.html?type=Programs&IsAiring=true&serverId=' + options.serverId;
+ }
+
+ if (options.section === 'dvrschedule') {
+ return 'livetv.html?tab=4&serverId=' + options.serverId;
+ }
+
+ if (options.section === 'seriesrecording') {
+ return 'livetv.html?tab=5&serverId=' + options.serverId;
+ }
+
+ return 'livetv.html?serverId=' + options.serverId;
+ }
+
+ if (itemType == 'SeriesTimer') {
+ return 'details?seriesTimerId=' + id + '&serverId=' + serverId;
+ }
+
+ if (item.CollectionType == 'livetv') {
+ return 'livetv.html';
+ }
+
+ if (item.Type === 'Genre') {
+ url = 'list.html?genreId=' + item.Id + '&serverId=' + serverId;
+
+ if (context === 'livetv') {
+ url += '&type=Programs';
+ }
+
+ if (options.parentId) {
+ url += '&parentId=' + options.parentId;
+ }
+
+ return url;
+ }
+
+ if (item.Type === 'MusicGenre') {
+ url = 'list.html?musicGenreId=' + item.Id + '&serverId=' + serverId;
+
+ if (options.parentId) {
+ url += '&parentId=' + options.parentId;
+ }
+
+ return url;
+ }
+
+ if (item.Type === 'Studio') {
+ url = 'list.html?studioId=' + item.Id + '&serverId=' + serverId;
+
+ if (options.parentId) {
+ url += '&parentId=' + options.parentId;
+ }
+
+ return url;
+ }
+
+ if (context !== 'folders' && !itemHelper.isLocalItem(item)) {
+ if (item.CollectionType == 'movies') {
+ url = 'movies.html?topParentId=' + item.Id;
+
+ if (options && options.section === 'latest') {
+ url += '&tab=1';
+ }
+
+ return url;
+ }
+
+ if (item.CollectionType == 'tvshows') {
+ url = 'tv.html?topParentId=' + item.Id;
+
+ if (options && options.section === 'latest') {
+ url += '&tab=2';
+ }
+
+ return url;
+ }
+
+ if (item.CollectionType == 'music') {
+ return 'music.html?topParentId=' + item.Id;
}
}
- return new Promise(function (resolve, reject) {
- resolveOnNextShow = resolve;
- page.show(path, options);
- });
- }
+ const itemTypes = ['Playlist', 'TvChannel', 'Program', 'BoxSet', 'MusicAlbum', 'MusicGenre', 'Person', 'Recording', 'MusicArtist'];
- var resolveOnNextShow;
- document.addEventListener('viewshow', function () {
- var resolve = resolveOnNextShow;
- if (resolve) {
- resolveOnNextShow = null;
- resolve();
+ if (itemTypes.indexOf(itemType) >= 0) {
+ return 'details?id=' + id + '&serverId=' + serverId;
}
- });
- var currentRouteInfo;
- function current() {
- return currentRouteInfo ? currentRouteInfo.route : null;
- }
+ const contextSuffix = context ? '&context=' + context : '';
- function showItem(item, serverId, options) {
- // TODO: Refactor this so it only gets items, not strings.
- if (typeof (item) === 'string') {
- var apiClient = serverId ? connectionManager.getApiClient(serverId) : connectionManager.currentApiClient();
- apiClient.getItem(apiClient.getCurrentUserId(), item).then(function (itemObject) {
- appRouter.showItem(itemObject, options);
- });
- } else {
- if (arguments.length === 2) {
- options = arguments[1];
+ if (itemType == 'Series' || itemType == 'Season' || itemType == 'Episode') {
+ return 'details?id=' + id + contextSuffix + '&serverId=' + serverId;
+ }
+
+ if (item.IsFolder) {
+ if (id) {
+ return 'list.html?parentId=' + id + '&serverId=' + serverId;
}
- var url = appRouter.getRouteUrl(item, options);
- appRouter.show(url, {
- item: item
- });
- }
- }
-
- var allRoutes = [];
-
- function addRoute(path, newRoute) {
- page(path, getHandler(newRoute));
- allRoutes.push(newRoute);
- }
-
- function getRoutes() {
- return allRoutes;
- }
-
- var backdropContainer;
- var backgroundContainer;
- function setTransparency(level) {
- if (!backdropContainer) {
- backdropContainer = document.querySelector('.backdropContainer');
- }
- if (!backgroundContainer) {
- backgroundContainer = document.querySelector('.backgroundContainer');
+ return '#';
}
- if (level === 'full' || level === 2) {
- backdrop.clearBackdrop(true);
- document.documentElement.classList.add('transparentDocument');
- backgroundContainer.classList.add('backgroundContainer-transparent');
- backdropContainer.classList.add('hide');
- } else if (level === 'backdrop' || level === 1) {
- backdrop.externalBackdrop(true);
- document.documentElement.classList.add('transparentDocument');
- backgroundContainer.classList.add('backgroundContainer-transparent');
- backdropContainer.classList.add('hide');
- } else {
- backdrop.externalBackdrop(false);
- document.documentElement.classList.remove('transparentDocument');
- backgroundContainer.classList.remove('backgroundContainer-transparent');
- backdropContainer.classList.remove('hide');
- }
+ return 'details?id=' + id + '&serverId=' + serverId;
}
+}
- function pushState(state, title, url) {
- state.navigate = false;
- history.pushState(state, title, url);
- }
-
- function setBaseRoute() {
- var baseRoute = self.location.pathname.replace(getRequestFile(), '');
- if (baseRoute.lastIndexOf('/') === baseRoute.length - 1) {
- baseRoute = baseRoute.substring(0, baseRoute.length - 1);
- }
-
- console.debug('setting page base to ' + baseRoute);
- page.base(baseRoute);
- }
-
- setBaseRoute();
-
- function invokeShortcut(id) {
- if (id.indexOf('library-') === 0) {
- id = id.replace('library-', '');
- id = id.split('_');
-
- appRouter.showItem(id[0], id[1]);
- } else if (id.indexOf('item-') === 0) {
- id = id.replace('item-', '');
- id = id.split('_');
-
- appRouter.showItem(id[0], id[1]);
- } else {
- id = id.split('_');
- appRouter.show(appRouter.getRouteUrl(id[0], {
- serverId: id[1]
- }));
- }
- }
-
- appRouter.addRoute = addRoute;
- appRouter.param = param;
- appRouter.back = back;
- appRouter.show = show;
- appRouter.showDirect = showDirect;
- appRouter.start = start;
- appRouter.baseUrl = baseUrl;
- appRouter.canGoBack = canGoBack;
- appRouter.current = current;
- appRouter.beginConnectionWizard = beginConnectionWizard;
- appRouter.invokeShortcut = invokeShortcut;
- appRouter.showItem = showItem;
- appRouter.setTransparency = setTransparency;
- appRouter.getRoutes = getRoutes;
- appRouter.pushState = pushState;
- appRouter.enableNativeHistory = enableNativeHistory;
- appRouter.handleAnchorClick = page.clickHandler;
- appRouter.TransparencyLevel = {
- None: 0,
- Backdrop: 1,
- Full: 2
- };
-
- return appRouter;
-});
+export default new AppRouter();
diff --git a/src/components/apphost.js b/src/components/apphost.js
index c3e9342827..33ca5a0b79 100644
--- a/src/components/apphost.js
+++ b/src/components/apphost.js
@@ -55,7 +55,7 @@ function replaceAll(originalString, strReplace, strWith) {
function generateDeviceId() {
const keys = [];
- if (keys.push(navigator.userAgent), keys.push(new Date().getTime()), self.btoa) {
+ if (keys.push(navigator.userAgent), keys.push(new Date().getTime()), window.btoa) {
const result = replaceAll(btoa(keys.join('|')), '=', '1');
return Promise.resolve(result);
}
@@ -404,9 +404,9 @@ document.addEventListener(visibilityChange, function () {
}
}, false);
-if (self.addEventListener) {
- self.addEventListener('focus', onAppVisible);
- self.addEventListener('blur', onAppHidden);
+if (window.addEventListener) {
+ window.addEventListener('focus', onAppVisible);
+ window.addEventListener('blur', onAppHidden);
}
export default appHost;
diff --git a/src/components/cardbuilder/card.css b/src/components/cardbuilder/card.css
index ef5ea6604c..74c376e85b 100644
--- a/src/components/cardbuilder/card.css
+++ b/src/components/cardbuilder/card.css
@@ -239,33 +239,13 @@ button::-moz-focus-inner {
border: none;
}
-.cardImage-img {
- max-height: 100%;
- max-width: 100%;
-
- /* This is simply for lazy image purposes, to ensure the image is visible sooner when scrolling */
- min-height: 70%;
- min-width: 70%;
- margin: auto;
-}
-
-.coveredImage-img {
- width: 100%;
- height: 100%;
-}
-
-.coveredImage-noscale-img {
- max-height: none;
- max-width: none;
-}
-
.coveredImage {
background-size: cover;
background-position: center center;
}
-.coveredImage-noScale {
- background-size: cover;
+.coveredImage.coveredImage-contain {
+ background-size: contain;
}
.cardFooter {
@@ -372,6 +352,8 @@ button::-moz-focus-inner {
.cardDefaultText {
white-space: normal;
text-align: center;
+ font-size: 2em;
+ font-weight: bold;
}
.cardImageContainer .cardImageIcon {
diff --git a/src/components/cardbuilder/cardBuilder.js b/src/components/cardbuilder/cardBuilder.js
index e644365906..63eaf2bdfe 100644
--- a/src/components/cardbuilder/cardBuilder.js
+++ b/src/components/cardbuilder/cardBuilder.js
@@ -986,6 +986,10 @@ import 'programStyles';
lines = [];
}
+ if (overlayText && showTitle) {
+ lines = [item.Name];
+ }
+
const addRightTextMargin = isOuterFooter && options.cardLayout && !options.centerText && options.cardFooterAside !== 'none' && layoutManager.mobile;
html += getCardTextLines(lines, cssClass, !options.overlayText, isOuterFooter, options.cardLayout, addRightTextMargin, options.lines);
@@ -1117,7 +1121,7 @@ import 'programStyles';
function importRefreshIndicator() {
if (!refreshIndicatorLoaded) {
refreshIndicatorLoaded = true;
- /* eslint-disable-next-line no-unused-expressions */
+ /* eslint-disable-next-line @babel/no-unused-expressions */
import('emby-itemrefreshindicator');
}
}
@@ -1212,8 +1216,8 @@ import 'programStyles';
if (coveredImage) {
cardImageContainerClass += ' coveredImage';
- if (item.MediaType === 'Photo' || item.Type === 'PhotoAlbum' || item.Type === 'Folder' || item.ProgramInfo || item.Type === 'Program' || item.Type === 'Recording') {
- cardImageContainerClass += ' coveredImage-noScale';
+ if (item.Type === 'TvChannel') {
+ cardImageContainerClass += ' coveredImage-contain';
}
}
@@ -1449,7 +1453,7 @@ import 'programStyles';
const userData = item.UserData || {};
if (itemHelper.canMarkPlayed(item)) {
- /* eslint-disable-next-line no-unused-expressions */
+ /* eslint-disable-next-line @babel/no-unused-expressions */
import('emby-playstatebutton');
html += '';
}
@@ -1457,7 +1461,7 @@ import 'programStyles';
if (itemHelper.canRate(item)) {
const likes = userData.Likes == null ? '' : userData.Likes;
- /* eslint-disable-next-line no-unused-expressions */
+ /* eslint-disable-next-line @babel/no-unused-expressions */
import('emby-ratingbutton');
html += '';
}
diff --git a/src/components/confirm/confirm.js b/src/components/confirm/confirm.js
index 0670816a53..eca612ccb8 100644
--- a/src/components/confirm/confirm.js
+++ b/src/components/confirm/confirm.js
@@ -19,7 +19,7 @@ export default (() => {
}
const text = replaceAll(options.text || '', '
', '\n');
- const result = confirm(text);
+ const result = window.confirm(text);
if (result) {
return Promise.resolve();
diff --git a/src/components/dialogHelper/dialogHelper.js b/src/components/dialogHelper/dialogHelper.js
index a6f664149c..e1a267fed2 100644
--- a/src/components/dialogHelper/dialogHelper.js
+++ b/src/components/dialogHelper/dialogHelper.js
@@ -85,9 +85,9 @@ import 'scrollStyles';
}
if (!self.closedByBack && isHistoryEnabled(dlg)) {
- const state = history.state || {};
+ const state = window.history.state || {};
if (state.dialogId === hash) {
- history.back();
+ window.history.back();
}
}
@@ -213,7 +213,7 @@ import 'scrollStyles';
export function close(dlg) {
if (isOpened(dlg)) {
if (isHistoryEnabled(dlg)) {
- history.back();
+ window.history.back();
} else {
closeDialog(dlg);
}
@@ -375,7 +375,7 @@ import 'scrollStyles';
dlg.setAttribute('data-lockscroll', 'true');
}
- if (options.enableHistory !== false && appRouter.enableNativeHistory()) {
+ if (options.enableHistory !== false) {
dlg.setAttribute('data-history', 'true');
}
diff --git a/src/components/directorybrowser/directorybrowser.js b/src/components/directorybrowser/directorybrowser.js
index 5c44db3b15..4205e04a4f 100644
--- a/src/components/directorybrowser/directorybrowser.js
+++ b/src/components/directorybrowser/directorybrowser.js
@@ -125,7 +125,7 @@ import 'emby-button';
html += ``;
html += '';
if (!readOnlyAttribute) {
- html += ``;
+ html += ``;
}
html += '';
if (!readOnlyAttribute) {
diff --git a/src/components/displaySettings/displaySettings.template.html b/src/components/displaySettings/displaySettings.template.html
index e751ce56c4..1b9bf00376 100644
--- a/src/components/displaySettings/displaySettings.template.html
+++ b/src/components/displaySettings/displaySettings.template.html
@@ -170,7 +170,7 @@