mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
517 lines
15 KiB
JavaScript
517 lines
15 KiB
JavaScript
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
|
|
|
import { setBackdropTransparency } from '../backdrop/backdrop';
|
|
import globalize from '../../lib/globalize';
|
|
import itemHelper from '../itemHelper';
|
|
import loading from '../loading/loading';
|
|
import ServerConnections from '../ServerConnections';
|
|
import alert from '../alert';
|
|
|
|
import { queryClient } from 'utils/query/queryClient';
|
|
import { getItemQuery } from 'hooks/useItem';
|
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
|
import { history } from 'RootAppRouter';
|
|
|
|
/** Pages of "no return" (when "Go back" should behave differently, probably quitting the application). */
|
|
const START_PAGE_PATHS = ['/home', '/login', '/selectserver'];
|
|
|
|
/** Pages that do not require a user to be logged in to view. */
|
|
const PUBLIC_PATHS = [
|
|
'/addserver',
|
|
'/selectserver',
|
|
'/login',
|
|
'/forgotpassword',
|
|
'/forgotpasswordpin',
|
|
'/wizardremoteaccess',
|
|
'/wizardfinish',
|
|
'/wizardlibrary',
|
|
'/wizardsettings',
|
|
'/wizardstart',
|
|
'/wizarduser'
|
|
];
|
|
|
|
class AppRouter {
|
|
forcedLogoutMsg;
|
|
msgTimeout;
|
|
promiseShow;
|
|
resolveOnNextShow;
|
|
|
|
constructor() {
|
|
document.addEventListener('viewshow', () => this.onViewShow());
|
|
|
|
this.lastPath = history.location.pathname + history.location.search;
|
|
this.listen();
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
ready() {
|
|
return this.promiseShow || Promise.resolve();
|
|
}
|
|
|
|
async back() {
|
|
if (this.promiseShow) await this.promiseShow;
|
|
|
|
this.promiseShow = new Promise((resolve) => {
|
|
const unlisten = history.listen(() => {
|
|
unlisten();
|
|
this.promiseShow = null;
|
|
resolve();
|
|
});
|
|
history.back();
|
|
});
|
|
|
|
return this.promiseShow;
|
|
}
|
|
|
|
async show(path, options) {
|
|
if (this.promiseShow) await this.promiseShow;
|
|
|
|
// ensure the path does not start with '#' since the router adds this
|
|
if (path.startsWith('#')) {
|
|
path = path.substring(1);
|
|
}
|
|
// Support legacy '#!' routes since people may have old bookmarks, etc.
|
|
if (path.startsWith('!')) {
|
|
path = path.substring(1);
|
|
}
|
|
|
|
if (path.indexOf('/') !== 0 && path.indexOf('://') === -1) {
|
|
path = '/' + path;
|
|
}
|
|
|
|
path = path.replace(this.baseUrl(), '');
|
|
|
|
// can't use this with home right now due to the back menu
|
|
if (history.location.pathname === path && path !== '/home') {
|
|
loading.hide();
|
|
return Promise.resolve();
|
|
}
|
|
|
|
this.promiseShow = new Promise((resolve) => {
|
|
this.resolveOnNextShow = resolve;
|
|
// Schedule a call to return the promise
|
|
setTimeout(() => history.push(path, options), 0);
|
|
});
|
|
|
|
return this.promiseShow;
|
|
}
|
|
|
|
listen() {
|
|
history.listen(({ location }) => {
|
|
const normalizedPath = location.pathname.replace(/^!/, '');
|
|
const fullPath = normalizedPath + location.search;
|
|
|
|
if (fullPath === this.lastPath) {
|
|
console.debug('[appRouter] path did not change, resolving promise');
|
|
this.onViewShow();
|
|
}
|
|
|
|
this.lastPath = fullPath;
|
|
});
|
|
}
|
|
|
|
baseUrl() {
|
|
return this.baseRoute;
|
|
}
|
|
|
|
canGoBack() {
|
|
const path = history.location.pathname;
|
|
|
|
if (
|
|
!document.querySelector('.dialogContainer')
|
|
&& START_PAGE_PATHS.includes(path)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return window.history.length > 1;
|
|
}
|
|
|
|
showItem(item, serverId, options) {
|
|
// TODO: Refactor this so it only gets items, not strings.
|
|
if (typeof item === 'string') {
|
|
const apiClient = serverId ? ServerConnections.getApiClient(serverId) : ServerConnections.currentApiClient();
|
|
const api = toApi(apiClient);
|
|
const userId = apiClient.getCurrentUserId();
|
|
|
|
queryClient
|
|
.fetchQuery(getItemQuery(api, item, userId))
|
|
.then(itemObject => {
|
|
this.showItem(itemObject, options);
|
|
})
|
|
.catch(err => {
|
|
console.error('[AppRouter] Failed to fetch item', err);
|
|
});
|
|
} else {
|
|
if (arguments.length === 2) {
|
|
options = arguments[1];
|
|
}
|
|
|
|
const url = this.getRouteUrl(item, options);
|
|
this.show(url);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the backdrop, background, and document transparency
|
|
* @deprecated use Dashboard.setBackdropTransparency
|
|
*/
|
|
setTransparency(level) {
|
|
// TODO: Remove this after JMP is updated to not use this function
|
|
console.warn('Deprecated! Use Dashboard.setBackdropTransparency');
|
|
setBackdropTransparency(level);
|
|
}
|
|
|
|
onViewShow() {
|
|
const resolve = this.resolveOnNextShow;
|
|
if (resolve) {
|
|
this.promiseShow = null;
|
|
this.resolveOnNextShow = null;
|
|
resolve();
|
|
}
|
|
}
|
|
|
|
onForcedLogoutMessageTimeout() {
|
|
const msg = this.forcedLogoutMsg;
|
|
this.forcedLogoutMsg = null;
|
|
|
|
if (msg) {
|
|
alert(msg);
|
|
}
|
|
}
|
|
|
|
showForcedLogoutMessage(msg) {
|
|
this.forcedLogoutMsg = msg;
|
|
if (this.msgTimeout) {
|
|
clearTimeout(this.msgTimeout);
|
|
}
|
|
|
|
this.msgTimeout = setTimeout(this.onForcedLogoutMessageTimeout, 100);
|
|
}
|
|
|
|
onRequestFail(_e, data) {
|
|
const apiClient = this;
|
|
|
|
if (data.status === 403 && data.errorCode === 'ParentalControl') {
|
|
const isPublicPage = PUBLIC_PATHS.includes(history.location.pathname);
|
|
|
|
// Bounce to the login screen, but not if a password entry fails, obviously
|
|
if (!isPublicPage) {
|
|
appRouter.showForcedLogoutMessage(globalize.translate('AccessRestrictedTryAgainLater'));
|
|
appRouter.showLocalLogin(apiClient.serverId());
|
|
}
|
|
}
|
|
}
|
|
|
|
#getRequestFile() {
|
|
let path = window.location.pathname || '';
|
|
|
|
const index = path.lastIndexOf('/');
|
|
if (index !== -1) {
|
|
path = path.substring(index);
|
|
} else {
|
|
path = '/' + path;
|
|
}
|
|
|
|
if (!path || path === '/') {
|
|
path = '/index.html';
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
getRouteUrl(item, options) {
|
|
if (!item) {
|
|
throw new Error('item cannot be null');
|
|
}
|
|
|
|
if (item.url) {
|
|
return item.url;
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
if (item === 'wizard') {
|
|
return '#/wizardstart';
|
|
}
|
|
|
|
if (item === 'manageserver') {
|
|
return '#/dashboard';
|
|
}
|
|
|
|
if (item === 'recordedtv') {
|
|
return '#/livetv?tab=3&serverId=' + options.serverId;
|
|
}
|
|
|
|
if (item === 'nextup') {
|
|
return '#/list?type=nextup&serverId=' + options.serverId;
|
|
}
|
|
|
|
if (item === 'list') {
|
|
let urlForList = '#/list?serverId=' + options.serverId + '&type=' + options.itemTypes;
|
|
|
|
if (options.isFavorite) {
|
|
urlForList += '&IsFavorite=true';
|
|
}
|
|
|
|
if (options.isAiring) {
|
|
urlForList += '&IsAiring=true';
|
|
}
|
|
|
|
if (options.isMovie) {
|
|
urlForList += '&IsMovie=true';
|
|
}
|
|
|
|
if (options.isSeries) {
|
|
urlForList += '&IsSeries=true&IsMovie=false&IsNews=false';
|
|
}
|
|
|
|
if (options.isSports) {
|
|
urlForList += '&IsSports=true';
|
|
}
|
|
|
|
if (options.isKids) {
|
|
urlForList += '&IsKids=true';
|
|
}
|
|
|
|
if (options.isNews) {
|
|
urlForList += '&IsNews=true';
|
|
}
|
|
|
|
return urlForList;
|
|
}
|
|
|
|
if (item === 'livetv') {
|
|
if (options.section === 'programs') {
|
|
return '#/livetv?tab=0&serverId=' + options.serverId;
|
|
}
|
|
if (options.section === 'guide') {
|
|
return '#/livetv?tab=1&serverId=' + options.serverId;
|
|
}
|
|
|
|
if (options.section === 'movies') {
|
|
return '#/list?type=Programs&IsMovie=true&serverId=' + options.serverId;
|
|
}
|
|
|
|
if (options.section === 'shows') {
|
|
return '#/list?type=Programs&IsSeries=true&IsMovie=false&IsNews=false&serverId=' + options.serverId;
|
|
}
|
|
|
|
if (options.section === 'sports') {
|
|
return '#/list?type=Programs&IsSports=true&serverId=' + options.serverId;
|
|
}
|
|
|
|
if (options.section === 'kids') {
|
|
return '#/list?type=Programs&IsKids=true&serverId=' + options.serverId;
|
|
}
|
|
|
|
if (options.section === 'news') {
|
|
return '#/list?type=Programs&IsNews=true&serverId=' + options.serverId;
|
|
}
|
|
|
|
if (options.section === 'onnow') {
|
|
return '#/list?type=Programs&IsAiring=true&serverId=' + options.serverId;
|
|
}
|
|
|
|
if (options.section === 'channels') {
|
|
return '#/livetv?tab=2&serverId=' + options.serverId;
|
|
}
|
|
|
|
if (options.section === 'dvrschedule') {
|
|
return '#/livetv?tab=4&serverId=' + options.serverId;
|
|
}
|
|
|
|
if (options.section === 'seriesrecording') {
|
|
return '#/livetv?tab=5&serverId=' + options.serverId;
|
|
}
|
|
|
|
return '#/livetv?serverId=' + options.serverId;
|
|
}
|
|
|
|
if (itemType == 'SeriesTimer') {
|
|
return '#/details?seriesTimerId=' + id + '&serverId=' + serverId;
|
|
}
|
|
|
|
if (item.CollectionType == CollectionType.Livetv) {
|
|
return `#/livetv?collectionType=${item.CollectionType}`;
|
|
}
|
|
|
|
if (item.Type === 'Genre') {
|
|
url = '#/list?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?musicGenreId=' + item.Id + '&serverId=' + serverId;
|
|
|
|
if (options.parentId) {
|
|
url += '&parentId=' + options.parentId;
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
if (item.Type === 'Studio') {
|
|
url = '#/list?studioId=' + item.Id + '&serverId=' + serverId;
|
|
|
|
if (options.parentId) {
|
|
url += '&parentId=' + options.parentId;
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
if (item === 'tag') {
|
|
url = `#/list?type=tag&tag=${encodeURIComponent(options.tag)}&serverId=${serverId}`;
|
|
|
|
if (options.parentId) {
|
|
url += '&parentId=' + options.parentId;
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
if (context !== 'folders' && !itemHelper.isLocalItem(item)) {
|
|
if (item.CollectionType == CollectionType.Movies) {
|
|
url = `#/movies?topParentId=${item.Id}&collectionType=${item.CollectionType}`;
|
|
|
|
if (options && options.section === 'latest') {
|
|
url += '&tab=1';
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
if (item.CollectionType == CollectionType.Tvshows) {
|
|
url = `#/tv?topParentId=${item.Id}&collectionType=${item.CollectionType}`;
|
|
|
|
if (options && options.section === 'latest') {
|
|
url += '&tab=1';
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
if (item.CollectionType == CollectionType.Music) {
|
|
url = `#/music?topParentId=${item.Id}&collectionType=${item.CollectionType}`;
|
|
|
|
if (options?.section === 'latest') {
|
|
url += '&tab=1';
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
const layoutMode = localStorage.getItem('layout');
|
|
|
|
if (layoutMode === 'experimental' && item.CollectionType == CollectionType.Homevideos) {
|
|
url = '#/homevideos?topParentId=' + item.Id;
|
|
|
|
return url;
|
|
}
|
|
}
|
|
|
|
const itemTypes = ['Playlist', 'TvChannel', 'Program', 'BoxSet', 'MusicAlbum', 'MusicGenre', 'Person', 'Recording', 'MusicArtist'];
|
|
|
|
if (itemTypes.indexOf(itemType) >= 0) {
|
|
return '#/details?id=' + id + '&serverId=' + serverId;
|
|
}
|
|
|
|
const contextSuffix = context ? '&context=' + context : '';
|
|
|
|
if (itemType == 'Series' || itemType == 'Season' || itemType == 'Episode') {
|
|
return '#/details?id=' + id + contextSuffix + '&serverId=' + serverId;
|
|
}
|
|
|
|
if (item.IsFolder) {
|
|
if (id) {
|
|
return '#/list?parentId=' + id + '&serverId=' + serverId;
|
|
}
|
|
|
|
return '#';
|
|
}
|
|
|
|
return '#/details?id=' + id + '&serverId=' + serverId;
|
|
}
|
|
|
|
showLocalLogin(serverId) {
|
|
return this.show('login?serverid=' + serverId);
|
|
}
|
|
|
|
showVideoOsd() {
|
|
return this.show('video');
|
|
}
|
|
|
|
showSelectServer() {
|
|
return this.show('selectserver');
|
|
}
|
|
|
|
showSettings() {
|
|
return this.show('mypreferencesmenu');
|
|
}
|
|
|
|
showNowPlaying() {
|
|
return this.show('queue');
|
|
}
|
|
|
|
showGuide() {
|
|
return this.show('livetv?tab=1');
|
|
}
|
|
|
|
goHome() {
|
|
return this.show('home');
|
|
}
|
|
|
|
showSearch() {
|
|
return this.show('search');
|
|
}
|
|
|
|
showLiveTV() {
|
|
return this.show('livetv');
|
|
}
|
|
|
|
showRecordedTV() {
|
|
return this.show('livetv?tab=3');
|
|
}
|
|
|
|
showFavorites() {
|
|
return this.show('home?tab=1');
|
|
}
|
|
}
|
|
|
|
export const appRouter = new AppRouter();
|
|
|
|
export const isLyricsPage = () => history.location.pathname.toLowerCase() === '/lyrics';
|
|
|
|
window.Emby = window.Emby || {};
|
|
window.Emby.Page = appRouter;
|