mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #3706 from thornbill/react-router-3
Add react router and migrate search page
This commit is contained in:
commit
c31e8968dd
18 changed files with 407 additions and 60 deletions
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -10760,6 +10760,23 @@
|
|||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true
|
||||
},
|
||||
"react-router": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.3.0.tgz",
|
||||
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
|
||||
"requires": {
|
||||
"history": "^5.2.0"
|
||||
}
|
||||
},
|
||||
"react-router-dom": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.3.0.tgz",
|
||||
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
|
||||
"requires": {
|
||||
"history": "^5.2.0",
|
||||
"react-router": "6.3.0"
|
||||
}
|
||||
},
|
||||
"read-file-stdin": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/read-file-stdin/-/read-file-stdin-0.2.1.tgz",
|
||||
|
|
|
@ -96,6 +96,7 @@
|
|||
"pdfjs-dist": "2.12.313",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-router-dom": "6.3.0",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"screenfull": "6.0.0",
|
||||
"sortablejs": "1.14.0",
|
||||
|
|
169
src/components/ConnectionRequired.tsx
Normal file
169
src/components/ConnectionRequired.tsx
Normal file
|
@ -0,0 +1,169 @@
|
|||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import alert from './alert';
|
||||
import { appRouter } from './appRouter';
|
||||
import loading from './loading/loading';
|
||||
import ServerConnections from './ServerConnections';
|
||||
import globalize from '../scripts/globalize';
|
||||
|
||||
enum BounceRoutes {
|
||||
Home = '/home.html',
|
||||
Login = '/login.html',
|
||||
SelectServer = '/selectserver.html',
|
||||
StartWizard = '/wizardstart.html'
|
||||
}
|
||||
|
||||
// TODO: This should probably be in the SDK
|
||||
enum ConnectionState {
|
||||
SignedIn = 'SignedIn',
|
||||
ServerSignIn = 'ServerSignIn',
|
||||
ServerSelection = 'ServerSelection',
|
||||
ServerUpdateNeeded = 'ServerUpdateNeeded'
|
||||
}
|
||||
|
||||
type ConnectionRequiredProps = {
|
||||
isAdminRequired?: boolean,
|
||||
isUserRequired?: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
* A component that ensures a server connection has been established.
|
||||
* Additional parameters exist to verify a user or admin have authenticated.
|
||||
* If a condition fails, this component will navigate to the appropriate page.
|
||||
*/
|
||||
const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
|
||||
children,
|
||||
isAdminRequired = false,
|
||||
isUserRequired = true
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [ isLoading, setIsLoading ] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const bounce = async (connectionResponse: any) => {
|
||||
switch (connectionResponse.State) {
|
||||
case ConnectionState.SignedIn:
|
||||
// Already logged in, bounce to the home page
|
||||
console.debug('[ConnectionRequired] already logged in, redirecting to home');
|
||||
navigate(BounceRoutes.Home);
|
||||
return;
|
||||
case ConnectionState.ServerSignIn:
|
||||
// Bounce to the login page
|
||||
console.debug('[ConnectionRequired] not logged in, redirecting to login page');
|
||||
navigate(BounceRoutes.Login, {
|
||||
state: {
|
||||
serverid: connectionResponse.ApiClient.serverId()
|
||||
}
|
||||
});
|
||||
return;
|
||||
case ConnectionState.ServerSelection:
|
||||
// Bounce to select server page
|
||||
console.debug('[ConnectionRequired] redirecting to select server page');
|
||||
navigate(BounceRoutes.SelectServer);
|
||||
return;
|
||||
case ConnectionState.ServerUpdateNeeded:
|
||||
// Show update needed message and bounce to select server page
|
||||
try {
|
||||
await alert({
|
||||
text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'),
|
||||
html: globalize.translate('ServerUpdateNeeded', '<a href="https://github.com/jellyfin/jellyfin">https://github.com/jellyfin/jellyfin</a>')
|
||||
});
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] failed to show alert', ex);
|
||||
}
|
||||
console.debug('[ConnectionRequired] server update required, redirecting to select server page');
|
||||
navigate(BounceRoutes.SelectServer);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State);
|
||||
};
|
||||
|
||||
const validateConnection = async () => {
|
||||
// Check connection status on initial page load
|
||||
const firstConnection = appRouter.firstConnectionResult;
|
||||
appRouter.firstConnectionResult = null;
|
||||
|
||||
if (firstConnection && firstConnection.State !== ConnectionState.SignedIn) {
|
||||
if (firstConnection.State === ConnectionState.ServerSignIn) {
|
||||
// Verify the wizard is complete
|
||||
try {
|
||||
const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`);
|
||||
if (!infoResponse.ok) {
|
||||
throw new Error('Public system info request failed');
|
||||
}
|
||||
const systemInfo = await infoResponse.json();
|
||||
if (!systemInfo?.StartupWizardCompleted) {
|
||||
// Bounce to the wizard
|
||||
console.info('[ConnectionRequired] startup wizard is not complete, redirecting there');
|
||||
navigate(BounceRoutes.StartWizard);
|
||||
return;
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error('[ConnectionRequired] checking wizard status failed', ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Bounce to the correct page in the login flow
|
||||
bounce(firstConnection);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: appRouter will call appHost.exit() if navigating back when you are already at the default route.
|
||||
// This case will need to be handled elsewhere before appRouter can be killed.
|
||||
|
||||
const client = ServerConnections.currentApiClient();
|
||||
|
||||
// If this is a user route, ensure a user is logged in
|
||||
if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) {
|
||||
try {
|
||||
console.warn('[ConnectionRequired] unauthenticated user attempted to access user route');
|
||||
bounce(await ServerConnections.connect());
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] error bouncing from user route', ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is an admin route, ensure the user has access
|
||||
if (isAdminRequired) {
|
||||
try {
|
||||
const user = await client.getCurrentUser();
|
||||
if (!user.Policy.IsAdministrator) {
|
||||
console.warn('[ConnectionRequired] normal user attempted to access admin route');
|
||||
bounce(await ServerConnections.connect());
|
||||
return;
|
||||
}
|
||||
} catch (ex) {
|
||||
console.warn('[ConnectionRequired] error bouncing from admin route', ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
loading.show();
|
||||
validateConnection();
|
||||
}, [ isAdminRequired, isUserRequired, navigate ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
loading.hide();
|
||||
}
|
||||
}, [ isLoading ]);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>{children}</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionRequired;
|
48
src/components/HistoryRouter.tsx
Normal file
48
src/components/HistoryRouter.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import React, { useLayoutEffect } from 'react';
|
||||
import { HistoryRouterProps, Router } from 'react-router-dom';
|
||||
import { Update } from 'history';
|
||||
|
||||
/** Strips leading "!" from paths */
|
||||
const normalizePath = (pathname: string) => pathname.replace(/^!/, '');
|
||||
|
||||
/**
|
||||
* A slightly customized version of the HistoryRouter from react-router-dom.
|
||||
* We need to use HistoryRouter to have a shared history state between react-router and appRouter, but it does not seem
|
||||
* to be properly exported in the upstream package.
|
||||
* We also needed some customizations to handle #! routes.
|
||||
* Refs: https://github.com/remix-run/react-router/blob/v6.3.0/packages/react-router-dom/index.tsx#L222
|
||||
*/
|
||||
export function HistoryRouter({ basename, children, history }: HistoryRouterProps) {
|
||||
const [state, setState] = React.useState<Update>({
|
||||
action: history.action,
|
||||
location: history.location
|
||||
});
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const onHistoryChange = (update: Update) => {
|
||||
if (update.location.pathname.startsWith('!')) {
|
||||
// When the location changes, we need to check for #! paths and replace the location with the "!" stripped
|
||||
history.replace(normalizePath(update.location.pathname), update.location.state);
|
||||
} else {
|
||||
setState(update);
|
||||
}
|
||||
};
|
||||
|
||||
history.listen(onHistoryChange);
|
||||
}, [ history ]);
|
||||
|
||||
return (
|
||||
<Router
|
||||
basename={basename}
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
children={children}
|
||||
location={{
|
||||
...state.location,
|
||||
// The original location does not get replaced with the normalized version, so we need to strip it here
|
||||
pathname: normalizePath(state.location.pathname)
|
||||
}}
|
||||
navigationType={state.action}
|
||||
navigator={history}
|
||||
/>
|
||||
);
|
||||
}
|
69
src/components/Page.tsx
Normal file
69
src/components/Page.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import React, { FunctionComponent, HTMLAttributes, useEffect, useRef } from 'react';
|
||||
|
||||
import viewManager from './viewManager/viewManager';
|
||||
|
||||
type PageProps = {
|
||||
id: string, // id is required for libraryMenu
|
||||
title?: string,
|
||||
isBackButtonEnabled?: boolean,
|
||||
isNowPlayingBarEnabled?: boolean,
|
||||
isThemeMediaSupported?: boolean
|
||||
};
|
||||
|
||||
/**
|
||||
* Page component that handles hiding active non-react views, triggering the required events for
|
||||
* navigation and appRouter state updates, and setting the correct classes and data attributes.
|
||||
*/
|
||||
const Page: FunctionComponent<PageProps & HTMLAttributes<HTMLDivElement>> = ({
|
||||
children,
|
||||
id,
|
||||
className = '',
|
||||
title,
|
||||
isBackButtonEnabled = true,
|
||||
isNowPlayingBarEnabled = true,
|
||||
isThemeMediaSupported = false
|
||||
}) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// hide active non-react views
|
||||
viewManager.hideView();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const event = {
|
||||
bubbles: true,
|
||||
cancelable: false,
|
||||
detail: {
|
||||
isRestored: false,
|
||||
options: {
|
||||
enableMediaControl: isNowPlayingBarEnabled,
|
||||
supportsThemeMedia: isThemeMediaSupported
|
||||
}
|
||||
}
|
||||
};
|
||||
// viewbeforeshow - switches between the admin dashboard and standard themes
|
||||
element.current?.dispatchEvent(new CustomEvent('viewbeforeshow', event));
|
||||
// pagebeforeshow - hides tabs on tables pages in libraryMenu
|
||||
element.current?.dispatchEvent(new CustomEvent('pagebeforeshow', event));
|
||||
// viewshow - updates state of appRouter
|
||||
element.current?.dispatchEvent(new CustomEvent('viewshow', event));
|
||||
// pageshow - updates header/navigation in libraryMenu
|
||||
element.current?.dispatchEvent(new CustomEvent('pageshow', event));
|
||||
}, [ element, isNowPlayingBarEnabled, isThemeMediaSupported ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={element}
|
||||
id={id}
|
||||
data-role='page'
|
||||
className={`page ${className}`}
|
||||
data-title={title}
|
||||
data-backbutton={`${isBackButtonEnabled}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
|
@ -122,7 +122,11 @@ class AppRouter {
|
|||
isBack: action === Action.Pop
|
||||
});
|
||||
} else {
|
||||
console.warn('[appRouter] "%s" route not found', normalizedPath, location);
|
||||
console.info('[appRouter] "%s" route not found', normalizedPath, location);
|
||||
this.currentRouteInfo = {
|
||||
route: {},
|
||||
path: normalizedPath + location.search
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,7 +143,7 @@ class AppRouter {
|
|||
Events.on(apiClient, 'requestfail', this.onRequestFail);
|
||||
});
|
||||
|
||||
ServerConnections.connect().then(result => {
|
||||
return ServerConnections.connect().then(result => {
|
||||
this.firstConnectionResult = result;
|
||||
|
||||
// Handle the initial route
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
import React, { FunctionComponent, useState } from 'react';
|
||||
|
||||
import SearchFields from '../search/SearchFields';
|
||||
import SearchResults from '../search/SearchResults';
|
||||
import SearchSuggestions from '../search/SearchSuggestions';
|
||||
import LiveTVSearchResults from '../search/LiveTVSearchResults';
|
||||
|
||||
type SearchProps = {
|
||||
serverId?: string,
|
||||
parentId?: string,
|
||||
collectionType?: string
|
||||
};
|
||||
|
||||
const SearchPage: FunctionComponent<SearchProps> = ({ serverId, parentId, collectionType }: SearchProps) => {
|
||||
const [ query, setQuery ] = useState<string>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchFields onSearch={setQuery} />
|
||||
{!query &&
|
||||
<SearchSuggestions
|
||||
serverId={serverId || window.ApiClient.serverId()}
|
||||
parentId={parentId}
|
||||
/>
|
||||
}
|
||||
<SearchResults
|
||||
serverId={serverId || window.ApiClient.serverId()}
|
||||
parentId={parentId}
|
||||
collectionType={collectionType}
|
||||
query={query}
|
||||
/>
|
||||
<LiveTVSearchResults
|
||||
serverId={serverId || window.ApiClient.serverId()}
|
||||
parentId={parentId}
|
||||
collectionType={collectionType}
|
||||
query={query}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchPage;
|
|
@ -21,8 +21,8 @@ const CARD_OPTIONS = {
|
|||
|
||||
type LiveTVSearchResultsProps = {
|
||||
serverId?: string;
|
||||
parentId?: string;
|
||||
collectionType?: string;
|
||||
parentId?: string | null;
|
||||
collectionType?: string | null;
|
||||
query?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,8 @@ import SearchResultsRow from './SearchResultsRow';
|
|||
|
||||
type SearchResultsProps = {
|
||||
serverId?: string;
|
||||
parentId?: string;
|
||||
collectionType?: string;
|
||||
parentId?: string | null;
|
||||
collectionType?: string | null;
|
||||
query?: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ const createSuggestionLink = ({ name, href }: { name: string, href: string }) =>
|
|||
|
||||
type SearchSuggestionsProps = {
|
||||
serverId?: string;
|
||||
parentId?: string;
|
||||
parentId?: string | null;
|
||||
}
|
||||
|
||||
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ serverId = window.ApiClient.serverId(), parentId }: SearchSuggestionsProps) => {
|
||||
|
|
|
@ -147,6 +147,15 @@ class ViewManager {
|
|||
});
|
||||
}
|
||||
|
||||
hideView() {
|
||||
if (currentView) {
|
||||
dispatchViewEvent(currentView, null, 'viewbeforehide');
|
||||
dispatchViewEvent(currentView, null, 'viewhide');
|
||||
currentView.classList.add('hide');
|
||||
currentView = null;
|
||||
}
|
||||
}
|
||||
|
||||
tryRestoreView(options, onViewChanging) {
|
||||
if (options.cancel) {
|
||||
return Promise.reject({ cancelled: true });
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
<div id="searchPage" data-role="page" class="page libraryPage allLibraryPage noSecondaryNavPage" data-title="${Search}" data-backbutton="true">
|
||||
</div>
|
|
@ -158,6 +158,7 @@
|
|||
<div class="mainAnimatedPages skinBody">
|
||||
<div class="splashLogo"></div>
|
||||
</div>
|
||||
<div id="reactRoot"></div>
|
||||
<div class="mainDrawerHandle"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
24
src/routes/index.tsx
Normal file
24
src/routes/index.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import ConnectionRequired from '../components/ConnectionRequired';
|
||||
import SearchPage from './search';
|
||||
|
||||
const AppRoutes = () => (
|
||||
<Routes>
|
||||
<Route path='/'>
|
||||
<Route
|
||||
path='search.html'
|
||||
element={
|
||||
<ConnectionRequired>
|
||||
<SearchPage />
|
||||
</ConnectionRequired>
|
||||
}
|
||||
/>
|
||||
{/* Suppress warnings for unhandled routes */}
|
||||
<Route path='*' element={null} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
|
||||
export default AppRoutes;
|
44
src/routes/search.tsx
Normal file
44
src/routes/search.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import Page from '../components/Page';
|
||||
import SearchFields from '../components/search/SearchFields';
|
||||
import SearchResults from '../components/search/SearchResults';
|
||||
import SearchSuggestions from '../components/search/SearchSuggestions';
|
||||
import LiveTVSearchResults from '../components/search/LiveTVSearchResults';
|
||||
import globalize from '../scripts/globalize';
|
||||
|
||||
const SearchPage: FunctionComponent = () => {
|
||||
const [ query, setQuery ] = useState<string>();
|
||||
const [ searchParams ] = useSearchParams();
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='searchPage'
|
||||
title={globalize.translate('Search')}
|
||||
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
|
||||
>
|
||||
<SearchFields onSearch={setQuery} />
|
||||
{!query &&
|
||||
<SearchSuggestions
|
||||
serverId={searchParams.get('serverId') || window.ApiClient.serverId()}
|
||||
parentId={searchParams.get('parentId')}
|
||||
/>
|
||||
}
|
||||
<SearchResults
|
||||
serverId={searchParams.get('serverId') || window.ApiClient.serverId()}
|
||||
parentId={searchParams.get('parentId')}
|
||||
collectionType={searchParams.get('collectionType')}
|
||||
query={query}
|
||||
/>
|
||||
<LiveTVSearchResults
|
||||
serverId={searchParams.get('serverId') || window.ApiClient.serverId()}
|
||||
parentId={searchParams.get('parentId')}
|
||||
collectionType={searchParams.get('collectionType')}
|
||||
query={query}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchPage;
|
|
@ -308,12 +308,6 @@ import { appRouter } from '../components/appRouter';
|
|||
type: 'home'
|
||||
});
|
||||
|
||||
defineRoute({
|
||||
alias: '/search.html',
|
||||
path: 'search.html',
|
||||
pageComponent: 'SearchPage'
|
||||
});
|
||||
|
||||
defineRoute({
|
||||
alias: '/list.html',
|
||||
path: 'list.html',
|
||||
|
|
|
@ -7,6 +7,8 @@ import 'classlist.js';
|
|||
import 'whatwg-fetch';
|
||||
import 'resize-observer-polyfill';
|
||||
import '../assets/css/site.scss';
|
||||
import React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { Events } from 'jellyfin-apiclient';
|
||||
import ServerConnections from '../components/ServerConnections';
|
||||
import globalize from './globalize';
|
||||
|
@ -18,7 +20,7 @@ import { appHost } from '../components/apphost';
|
|||
import { getPlugins } from './settings/webSettings';
|
||||
import { pluginManager } from '../components/pluginManager';
|
||||
import packageManager from '../components/packageManager';
|
||||
import { appRouter } from '../components/appRouter';
|
||||
import { appRouter, history } from '../components/appRouter';
|
||||
import '../elements/emby-button/emby-button';
|
||||
import './autoThemes';
|
||||
import './libraryMenu';
|
||||
|
@ -40,6 +42,8 @@ import SyncPlayHtmlVideoPlayer from '../components/syncPlay/ui/players/HtmlVideo
|
|||
import SyncPlayHtmlAudioPlayer from '../components/syncPlay/ui/players/HtmlAudioPlayer';
|
||||
import { currentSettings } from './settings/userSettings';
|
||||
import taskButton from './taskbutton';
|
||||
import { HistoryRouter } from '../components/HistoryRouter.tsx';
|
||||
import AppRoutes from '../routes/index.tsx';
|
||||
|
||||
function loadCoreDictionary() {
|
||||
const languages = ['af', 'ar', 'be-by', 'bg-bg', 'bn_bd', 'ca', 'cs', 'cy', 'da', 'de', 'el', 'en-gb', 'en-us', 'eo', 'es', 'es-419', 'es-ar', 'es_do', 'es-mx', 'et', 'fa', 'fi', 'fil', 'fr', 'fr-ca', 'gl', 'gsw', 'he', 'hi-in', 'hr', 'hu', 'id', 'it', 'ja', 'kk', 'ko', 'lt-lt', 'lv', 'mr', 'ms', 'nb', 'nl', 'nn', 'pl', 'pr', 'pt', 'pt-br', 'pt-pt', 'ro', 'ru', 'sk', 'sl-si', 'sq', 'sv', 'ta', 'th', 'tr', 'uk', 'ur_pk', 'vi', 'zh-cn', 'zh-hk', 'zh-tw'];
|
||||
|
@ -167,7 +171,14 @@ async function onAppReady() {
|
|||
ServerConnections.currentApiClient()?.ensureWebSocket();
|
||||
});
|
||||
|
||||
appRouter.start();
|
||||
await appRouter.start();
|
||||
|
||||
ReactDOM.render(
|
||||
<HistoryRouter history={history}>
|
||||
<AppRoutes />
|
||||
</HistoryRouter>,
|
||||
document.getElementById('reactRoot')
|
||||
);
|
||||
|
||||
if (!browser.tv && !browser.xboxOne && !browser.ps4) {
|
||||
import('../components/nowPlayingBar/nowPlayingBar');
|
||||
|
|
|
@ -96,7 +96,7 @@ module.exports = {
|
|||
},
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules[\\/](?!@uupaa[\\/]dynamic-import-polyfill|blurhash|date-fns|epubjs|flv.js|libarchive.js|marked|screenfull)/,
|
||||
exclude: /node_modules[\\/](?!@uupaa[\\/]dynamic-import-polyfill|blurhash|date-fns|epubjs|flv.js|libarchive.js|marked|react-router|screenfull)/,
|
||||
use: [{
|
||||
loader: 'babel-loader'
|
||||
}]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue