diff --git a/package-lock.json b/package-lock.json index 10f3e88359..1a14a2c72b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2083,7 +2083,6 @@ "version": "7.13.10", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz", "integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==", - "dev": true, "requires": { "regenerator-runtime": "^0.13.4" } @@ -7020,6 +7019,14 @@ "resolved": "https://registry.npmjs.org/headroom.js/-/headroom.js-0.12.0.tgz", "integrity": "sha512-iXnAafUm3FdzfJ91uixLws2hkKI1jC8bAKK/pt7XYr8Ie1jO7xbK48Ycpl9tUPyBgkzuj1p/PhJS0fy4E/5anA==" }, + "history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "requires": { + "@babel/runtime": "^7.7.6" + } + }, "hls.js": { "version": "0.14.17", "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-0.14.17.tgz", @@ -8902,14 +8909,6 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, - "page": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/page/-/page-1.11.6.tgz", - "integrity": "sha512-P6e2JfzkBrPeFCIPplLP7vDDiU84RUUZMrWdsH4ZBGJ8OosnwFkcUkBHp1DTIjuipLliw9yQn/ZJsXZvarsO+g==", - "requires": { - "path-to-regexp": "~1.2.1" - } - }, "pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -9016,21 +9015,6 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, - "path-to-regexp": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.2.1.tgz", - "integrity": "sha1-szcFwUAjTYc8hyHHuf2LVB7Tr/k=", - "requires": { - "isarray": "0.0.1" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - } - } - }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -10841,8 +10825,7 @@ "regenerator-runtime": { "version": "0.13.7", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", - "dev": true + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" }, "regenerator-transform": { "version": "0.14.5", diff --git a/package.json b/package.json index 1b80169014..ed39b1fcbf 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "fast-text-encoding": "1.0.3", "flv.js": "1.6.2", "headroom.js": "0.12.0", + "history": "5.3.0", "hls.js": "0.14.17", "intersection-observer": "0.12.0", "jellyfin-apiclient": "1.10.0", @@ -91,7 +92,6 @@ "marked": "4.0.10", "material-design-icons-iconfont": "6.1.1", "native-promise-only": "0.8.1", - "page": "1.11.6", "pdfjs-dist": "2.12.313", "react": "17.0.2", "react-dom": "17.0.2", diff --git a/src/components/appRouter.js b/src/components/appRouter.js index b834f1e830..fe2ad1166e 100644 --- a/src/components/appRouter.js +++ b/src/components/appRouter.js @@ -1,20 +1,22 @@ +import { Events } from 'jellyfin-apiclient'; +import { Action, createHashHistory } from 'history'; + import { appHost } from './apphost'; import appSettings from '../scripts/settings/appSettings'; import { clearBackdrop, setBackdropTransparency } from './backdrop/backdrop'; -import browser from '../scripts/browser'; -import { Events } from 'jellyfin-apiclient'; import globalize from '../scripts/globalize'; import itemHelper from './itemHelper'; import loading from './loading/loading'; -import page from 'page'; import viewManager from './viewManager/viewManager'; import Dashboard from '../utils/dashboard'; import ServerConnections from './ServerConnections'; import alert from './alert'; import reactControllerFactory from './reactControllerFactory'; +const history = createHashHistory(); + class AppRouter { - allRoutes = []; + allRoutes = new Map(); backdropContainer; backgroundContainer; currentRouteInfo; @@ -23,7 +25,6 @@ class AppRouter { forcedLogoutMsg; isDummyBackToHome; msgTimeout; - popstateOccurred = false; promiseShow; resolveOnNextShow; previousRoute = {}; @@ -33,58 +34,24 @@ class AppRouter { startPages = ['home', 'login', 'selectserver']; constructor() { - // WebKit fires a popstate event on document load - // Skip it using boolean - // For Tizen 2.x - // See `page` node module - let loaded = document.readyState === 'complete'; - if (!loaded) { - window.addEventListener('load', () => { - setTimeout(() => { - loaded = true; - }, 0); - }); - } - window.addEventListener('popstate', () => { - if (!loaded) return; - this.popstateOccurred = true; - }); - document.addEventListener('viewshow', () => this.onViewShow()); + // TODO: Can this baseRoute logic be simplified? 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); } + } - this.setBaseRoute(); - - // paths that start with a hashbang (i.e. /#!/page.html) get transformed to starting with // - // we need to strip one "/" for our routes to work - page('//*', (ctx) => { - page.redirect(ctx.path.substring(1)); + addRoute(path, route) { + this.allRoutes.set(path, { + route, + handler: this.#getHandler(route) }); } - /** - * @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); } @@ -124,7 +91,7 @@ class AppRouter { this.promiseShow = new Promise((resolve) => { this.resolveOnNextShow = resolve; - page.back(); + history.back(); }); return this.promiseShow; @@ -155,7 +122,7 @@ class AppRouter { this.promiseShow = new Promise((resolve) => { this.resolveOnNextShow = resolve; // Schedule a call to return the promise - setTimeout(() => page.show(path, options), 0); + setTimeout(() => history.push(path, options), 0); }); return this.promiseShow; @@ -167,27 +134,50 @@ class AppRouter { this.promiseShow = new Promise((resolve) => { this.resolveOnNextShow = resolve; // Schedule a call to return the promise - setTimeout(() => page.show(this.baseUrl() + path), 0); + setTimeout(() => history.push(this.baseUrl() + path), 0); }); return this.promiseShow; } - start(options) { + #goToRoute({ location, action }) { + // Strip the leading "!" if present + const normalizedPath = location.pathname.replace(/^!/, ''); + + if (this.allRoutes.has(normalizedPath)) { + console.debug('[appRouter] "%s" route found', normalizedPath, location, this.allRoutes.get(normalizedPath)); + this.allRoutes.get(normalizedPath) + .handler({ + // Recreate the default context used by page.js: https://github.com/visionmedia/page.js#context + path: normalizedPath + location.search, + pathname: normalizedPath, + querystring: location.search.replace(/^\?/, ''), + state: location.state, + // Custom context variables + isBack: action === Action.Pop + }); + } else { + console.warn('[appRouter] "%s" route not found', normalizedPath, location); + } + } + + start() { loading.show(); this.initApiClients(); - Events.on(appHost, 'beforeexit', this.onBeforeExit); Events.on(appHost, 'resume', this.onAppResume); ServerConnections.connect({ enableAutoLogin: appSettings.enableAutoLogin() }).then((result) => { this.firstConnectionResult = result; - options = options || {}; - page({ - click: options.click !== false, - hashbang: options.hashbang !== false + + // Handle the initial route + this.#goToRoute({ location: history.location }); + + // Handle route changes + history.listen(params => { + this.#goToRoute(params); }); }).catch().then(() => { loading.hide(); @@ -262,19 +252,6 @@ class AppRouter { setBackdropTransparency(level); } - 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': @@ -452,12 +429,6 @@ class AppRouter { } } - onBeforeExit() { - if (browser.web0s) { - page.restorePreviousState(); - } - } - normalizeImageOptions(options) { let setQuality; if (options.maxWidth || options.width || options.maxHeight || options.height || options.fillWidth || options.fillHeight) { @@ -544,12 +515,12 @@ class AppRouter { const apiClient = ServerConnections.currentApiClient(); const pathname = ctx.pathname.toLowerCase(); - console.debug('processing path request: ' + pathname); + console.debug('[appRouter] processing path request: ' + pathname); 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('route does not allow anonymous access: redirecting to login'); + console.debug('[appRouter] route does not allow anonymous access: redirecting to login'); this.beginConnectionWizard(); return; } @@ -563,10 +534,10 @@ class AppRouter { } if (apiClient && apiClient.isLoggedIn()) { - console.debug('user is authenticated'); + console.debug('[appRouter] user is authenticated'); if (route.isDefaultRoute) { - console.debug('loading home page'); + console.debug('[appRouter] loading home page'); this.goHome(); return; } else if (route.roles) { @@ -577,7 +548,7 @@ class AppRouter { } } - console.debug('proceeding to page: ' + pathname); + console.debug('[appRouter] proceeding to page: ' + pathname); callback(); } @@ -632,11 +603,8 @@ class AppRouter { return path; } - getHandler(route) { + #getHandler(route) { return (ctx, next) => { - ctx.isBack = this.popstateOccurred; - this.popstateOccurred = false; - const ignore = route.dummyRoute === true || this.previousRoute.dummyRoute === true; this.previousRoute = route; if (ignore) { diff --git a/src/scripts/routes.js b/src/scripts/routes.js index 62bd82e0b4..6fd437d1cd 100644 --- a/src/scripts/routes.js +++ b/src/scripts/routes.js @@ -566,13 +566,7 @@ import { appRouter } from '../components/appRouter'; }); defineRoute({ - path: '', - isDefaultRoute: true, - autoFocus: false - }); - - defineRoute({ - path: 'index.html', + path: '/', autoFocus: false, isDefaultRoute: true }); diff --git a/src/scripts/site.js b/src/scripts/site.js index e9a257f87d..6ec0e7709d 100644 --- a/src/scripts/site.js +++ b/src/scripts/site.js @@ -163,10 +163,7 @@ async function onAppReady() { import('../assets/css/ios.scss'); } - appRouter.start({ - click: false, - hashbang: true - }); + appRouter.start(); if (!browser.tv && !browser.xboxOne && !browser.ps4) { import('../components/nowPlayingBar/nowPlayingBar');