mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Add react-router
This commit is contained in:
parent
6534c0a596
commit
b2372a96e2
16 changed files with 176 additions and 59 deletions
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -10760,6 +10760,23 @@
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"dev": true
|
"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": {
|
"read-file-stdin": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/read-file-stdin/-/read-file-stdin-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/read-file-stdin/-/read-file-stdin-0.2.1.tgz",
|
||||||
|
|
|
@ -96,6 +96,7 @@
|
||||||
"pdfjs-dist": "2.12.313",
|
"pdfjs-dist": "2.12.313",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
|
"react-router-dom": "6.3.0",
|
||||||
"resize-observer-polyfill": "1.5.1",
|
"resize-observer-polyfill": "1.5.1",
|
||||||
"screenfull": "6.0.0",
|
"screenfull": "6.0.0",
|
||||||
"sortablejs": "1.14.0",
|
"sortablejs": "1.14.0",
|
||||||
|
|
22
src/components/HistoryRouter.tsx
Normal file
22
src/components/HistoryRouter.tsx
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import React, { useLayoutEffect } from 'react';
|
||||||
|
import { HistoryRouterProps, Router } from 'react-router-dom';
|
||||||
|
|
||||||
|
export function HistoryRouter({ basename, children, history }: HistoryRouterProps) {
|
||||||
|
const [state, setState] = React.useState({
|
||||||
|
action: history.action,
|
||||||
|
location: history.location
|
||||||
|
});
|
||||||
|
|
||||||
|
useLayoutEffect(() => history.listen(setState), [history]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Router
|
||||||
|
basename={basename}
|
||||||
|
// eslint-disable-next-line react/no-children-prop
|
||||||
|
children={children}
|
||||||
|
location={state.location}
|
||||||
|
navigationType={state.action}
|
||||||
|
navigator={history}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
50
src/components/Page.tsx
Normal file
50
src/components/Page.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import viewManager from './viewManager/viewManager';
|
||||||
|
|
||||||
|
type PageProps = {
|
||||||
|
title?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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> = ({ children, title }) => {
|
||||||
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// hide active non-react views
|
||||||
|
viewManager.hideView();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const event = {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: false,
|
||||||
|
detail: {
|
||||||
|
isRestored: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// pagebeforeshow - hides tabs on tabless 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 ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={element}
|
||||||
|
data-role='page'
|
||||||
|
className='mainAnimatedPage page libraryPage allLibraryPage noSecondaryNavPage'
|
||||||
|
data-title={title}
|
||||||
|
data-backbutton='true'
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -122,7 +122,7 @@ class AppRouter {
|
||||||
isBack: action === Action.Pop
|
isBack: action === Action.Pop
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn('[appRouter] "%s" route not found', normalizedPath, location);
|
console.info('[appRouter] "%s" route not found', normalizedPath, location);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ class AppRouter {
|
||||||
Events.on(apiClient, 'requestfail', this.onRequestFail);
|
Events.on(apiClient, 'requestfail', this.onRequestFail);
|
||||||
});
|
});
|
||||||
|
|
||||||
ServerConnections.connect().then(result => {
|
return ServerConnections.connect().then(result => {
|
||||||
this.firstConnectionResult = result;
|
this.firstConnectionResult = result;
|
||||||
|
|
||||||
// Handle the initial route
|
// 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 = {
|
type LiveTVSearchResultsProps = {
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
parentId?: string;
|
parentId?: string | null;
|
||||||
collectionType?: string;
|
collectionType?: string | null;
|
||||||
query?: string;
|
query?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ import SearchResultsRow from './SearchResultsRow';
|
||||||
|
|
||||||
type SearchResultsProps = {
|
type SearchResultsProps = {
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
parentId?: string;
|
parentId?: string | null;
|
||||||
collectionType?: string;
|
collectionType?: string | null;
|
||||||
query?: string;
|
query?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ const createSuggestionLink = ({ name, href }: { name: string, href: string }) =>
|
||||||
|
|
||||||
type SearchSuggestionsProps = {
|
type SearchSuggestionsProps = {
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
parentId?: string;
|
parentId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ serverId = window.ApiClient.serverId(), parentId }: SearchSuggestionsProps) => {
|
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) {
|
tryRestoreView(options, onViewChanging) {
|
||||||
if (options.cancel) {
|
if (options.cancel) {
|
||||||
return Promise.reject({ cancelled: true });
|
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="mainAnimatedPages skinBody">
|
||||||
<div class="splashLogo"></div>
|
<div class="splashLogo"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="reactRoot"></div>
|
||||||
<div class="mainDrawerHandle"></div>
|
<div class="mainDrawerHandle"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
16
src/routes/index.tsx
Normal file
16
src/routes/index.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
|
import SearchPage from './search';
|
||||||
|
|
||||||
|
const AppRoutes = () => (
|
||||||
|
<Routes>
|
||||||
|
<Route path='/'>
|
||||||
|
<Route path='search.html' element={<SearchPage />} />
|
||||||
|
{/* Suppress warnings for unhandled routes */}
|
||||||
|
<Route path='*' element={null} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AppRoutes;
|
40
src/routes/search.tsx
Normal file
40
src/routes/search.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
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 title={globalize.translate('Search')}>
|
||||||
|
<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'
|
type: 'home'
|
||||||
});
|
});
|
||||||
|
|
||||||
defineRoute({
|
|
||||||
alias: '/search.html',
|
|
||||||
path: 'search.html',
|
|
||||||
pageComponent: 'SearchPage'
|
|
||||||
});
|
|
||||||
|
|
||||||
defineRoute({
|
defineRoute({
|
||||||
alias: '/list.html',
|
alias: '/list.html',
|
||||||
path: 'list.html',
|
path: 'list.html',
|
||||||
|
|
|
@ -7,6 +7,8 @@ import 'classlist.js';
|
||||||
import 'whatwg-fetch';
|
import 'whatwg-fetch';
|
||||||
import 'resize-observer-polyfill';
|
import 'resize-observer-polyfill';
|
||||||
import '../assets/css/site.scss';
|
import '../assets/css/site.scss';
|
||||||
|
import React from 'react';
|
||||||
|
import * as ReactDOM from 'react-dom';
|
||||||
import { Events } from 'jellyfin-apiclient';
|
import { Events } from 'jellyfin-apiclient';
|
||||||
import ServerConnections from '../components/ServerConnections';
|
import ServerConnections from '../components/ServerConnections';
|
||||||
import globalize from './globalize';
|
import globalize from './globalize';
|
||||||
|
@ -18,7 +20,7 @@ import { appHost } from '../components/apphost';
|
||||||
import { getPlugins } from './settings/webSettings';
|
import { getPlugins } from './settings/webSettings';
|
||||||
import { pluginManager } from '../components/pluginManager';
|
import { pluginManager } from '../components/pluginManager';
|
||||||
import packageManager from '../components/packageManager';
|
import packageManager from '../components/packageManager';
|
||||||
import { appRouter } from '../components/appRouter';
|
import { appRouter, history } from '../components/appRouter';
|
||||||
import '../elements/emby-button/emby-button';
|
import '../elements/emby-button/emby-button';
|
||||||
import './autoThemes';
|
import './autoThemes';
|
||||||
import './libraryMenu';
|
import './libraryMenu';
|
||||||
|
@ -40,6 +42,8 @@ import SyncPlayHtmlVideoPlayer from '../components/syncPlay/ui/players/HtmlVideo
|
||||||
import SyncPlayHtmlAudioPlayer from '../components/syncPlay/ui/players/HtmlAudioPlayer';
|
import SyncPlayHtmlAudioPlayer from '../components/syncPlay/ui/players/HtmlAudioPlayer';
|
||||||
import { currentSettings } from './settings/userSettings';
|
import { currentSettings } from './settings/userSettings';
|
||||||
import taskButton from './taskbutton';
|
import taskButton from './taskbutton';
|
||||||
|
import { HistoryRouter } from '../components/HistoryRouter.tsx';
|
||||||
|
import AppRoutes from '../routes/index.tsx';
|
||||||
|
|
||||||
function loadCoreDictionary() {
|
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'];
|
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();
|
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) {
|
if (!browser.tv && !browser.xboxOne && !browser.ps4) {
|
||||||
import('../components/nowPlayingBar/nowPlayingBar');
|
import('../components/nowPlayingBar/nowPlayingBar');
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue