diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 000000000..79828b6bd --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,18 @@ +import { History } from '@remix-run/router'; +import React from 'react'; + +import { HistoryRouter } from './components/HistoryRouter'; +import { ApiProvider } from './hooks/useApi'; +import AppRoutes from './routes/index'; + +const App = ({ history }: { history: History }) => { + return ( + + + + + + ); +}; + +export default App; diff --git a/src/apiclient.d.ts b/src/apiclient.d.ts index 9ad794dd6..33f3752ca 100644 --- a/src/apiclient.d.ts +++ b/src/apiclient.d.ts @@ -117,6 +117,7 @@ declare module 'jellyfin-apiclient' { getCountries(): Promise; getCriticReviews(itemId: string, options?: any): Promise; getCultures(): Promise; + getCurrentUser(cache?: boolean): Promise; getCurrentUserId(): string; getDateParamValue(date: Date): string; getDefaultImageQuality(imageType: ImageType): number; diff --git a/src/components/ConnectionRequired.tsx b/src/components/ConnectionRequired.tsx index 9de9db0ec..aa67a257e 100644 --- a/src/components/ConnectionRequired.tsx +++ b/src/components/ConnectionRequired.tsx @@ -128,8 +128,8 @@ const ConnectionRequired: FunctionComponent = ({ // If this is an admin route, ensure the user has access if (isAdminRequired) { try { - const user = await client.getCurrentUser(); - if (!user.Policy.IsAdministrator) { + const user = await client?.getCurrentUser(); + if (!user?.Policy?.IsAdministrator) { console.warn('[ConnectionRequired] normal user attempted to access admin route'); bounce(await ServerConnections.connect()); return; diff --git a/src/components/ServerConnections.js b/src/components/ServerConnections.js index 5f7a7ef67..9bdb82fc6 100644 --- a/src/components/ServerConnections.js +++ b/src/components/ServerConnections.js @@ -86,6 +86,10 @@ class ServerConnections extends ConnectionManager { return this.localApiClient; } + /** + * Gets the ApiClient that is currently connected. + * @returns {ApiClient|undefined} apiClient + */ currentApiClient() { let apiClient = this.getLocalApiClient(); diff --git a/src/components/ServerContentPage.tsx b/src/components/ServerContentPage.tsx index f5c705cf6..7473c856e 100644 --- a/src/components/ServerContentPage.tsx +++ b/src/components/ServerContentPage.tsx @@ -34,7 +34,7 @@ const ServerContentPage: FunctionComponent = ({ view }) const apiClient = ServerConnections.currentApiClient(); // Fetch the view html from the server and translate it - const viewHtml = await apiClient.get(apiClient.getUrl(view + location.search)) + const viewHtml = await apiClient?.get(apiClient.getUrl(view + location.search)) .then((html: string) => globalize.translateHtml(html)); viewManager.loadView({ diff --git a/src/components/search/SearchSuggestions.tsx b/src/components/search/SearchSuggestions.tsx index 244286189..d16cdca8e 100644 --- a/src/components/search/SearchSuggestions.tsx +++ b/src/components/search/SearchSuggestions.tsx @@ -1,10 +1,13 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'; +import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind'; +import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api'; +import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by'; import escapeHtml from 'escape-html'; import React, { FunctionComponent, useEffect, useState } from 'react'; import { appRouter } from '../appRouter'; +import { useApi } from '../../hooks/useApi'; import globalize from '../../scripts/globalize'; -import ServerConnections from '../ServerConnections'; import '../../elements/emby-button/emby-button'; @@ -21,27 +24,30 @@ const createSuggestionLink = ({ name, href }: { name: string, href: string }) => }); type SearchSuggestionsProps = { - serverId?: string; parentId?: string | null; } -const SearchSuggestions: FunctionComponent = ({ serverId = window.ApiClient.serverId(), parentId }: SearchSuggestionsProps) => { +const SearchSuggestions: FunctionComponent = ({ parentId }: SearchSuggestionsProps) => { const [ suggestions, setSuggestions ] = useState([]); + const { api, user } = useApi(); useEffect(() => { - const apiClient = ServerConnections.getApiClient(serverId); - - apiClient.getItems(apiClient.getCurrentUserId(), { - SortBy: 'IsFavoriteOrLiked,Random', - IncludeItemTypes: 'Movie,Series,MusicArtist', - Limit: 20, - Recursive: true, - ImageTypeLimit: 0, - EnableImages: false, - ParentId: parentId, - EnableTotalRecordCount: false - }).then(result => setSuggestions(result.Items || [])); - }, [parentId, serverId]); + if (api && user?.Id) { + getItemsApi(api) + .getItemsByUserId({ + userId: user.Id, + sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random], + includeItemTypes: [BaseItemKind.Movie, BaseItemKind.Series, BaseItemKind.MusicArtist], + limit: 20, + recursive: true, + imageTypeLimit: 0, + enableImages: false, + parentId: parentId || undefined, + enableTotalRecordCount: false + }) + .then(result => setSuggestions(result.data.Items || [])); + } + }, [ api, parentId, user ]); return (
({}); +export const useApi = () => useContext(ApiContext); + +export const ApiProvider: FC = ({ children }) => { + const [ legacyApiClient, setLegacyApiClient ] = useState(); + const [ api, setApi ] = useState(); + const [ user, setUser ] = useState(); + + useEffect(() => { + ServerConnections.currentApiClient() + ?.getCurrentUser() + .then(newUser => updateApiUser(undefined, newUser)) + .catch(err => { + console.info('[ApiProvider] Could not get current user', err); + }); + + const updateApiUser = (_e: Event | undefined, newUser: UserDto) => { + setUser(newUser); + + if (newUser.ServerId) { + setLegacyApiClient(ServerConnections.getApiClient(newUser.ServerId)); + } + }; + + const resetApiUser = () => { + setLegacyApiClient(undefined); + setUser(undefined); + }; + + events.on(ServerConnections, 'localusersignedin', updateApiUser); + events.on(ServerConnections, 'localusersignedout', resetApiUser); + + return () => { + events.off(ServerConnections, 'localusersignedin', updateApiUser); + events.off(ServerConnections, 'localusersignedout', resetApiUser); + }; + }, [ setLegacyApiClient, setUser ]); + + useEffect(() => { + setApi(legacyApiClient ? toApi(legacyApiClient) : undefined); + }, [ legacyApiClient, setApi ]); + + return ( + + {children} + + ); +}; diff --git a/src/index.jsx b/src/index.jsx index 2c786b736..b0de802d5 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -37,8 +37,7 @@ import './legacy/htmlMediaElement'; import './legacy/vendorStyles'; import { currentSettings } from './scripts/settings/userSettings'; import taskButton from './scripts/taskbutton'; -import { HistoryRouter } from './components/HistoryRouter.tsx'; -import AppRoutes from './routes/index.tsx'; +import App from './App.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', 'eu', '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']; @@ -146,9 +145,7 @@ async function onAppReady() { ReactDOM.render( - - - + , document.getElementById('reactRoot') ); diff --git a/src/plugins/syncPlay/core/Manager.js b/src/plugins/syncPlay/core/Manager.js index af39d422d..740e0709d 100644 --- a/src/plugins/syncPlay/core/Manager.js +++ b/src/plugins/syncPlay/core/Manager.js @@ -71,7 +71,7 @@ class Manager { /** * Update active ApiClient. - * @param {Object} apiClient The ApiClient. + * @param {ApiClient|undefined} apiClient The ApiClient. */ updateApiClient(apiClient) { if (!apiClient) { diff --git a/src/routes/search.tsx b/src/routes/search.tsx index 059a81783..d154018b4 100644 --- a/src/routes/search.tsx +++ b/src/routes/search.tsx @@ -21,7 +21,6 @@ const Search: FunctionComponent = () => { {!query && } diff --git a/src/utils/jellyfin-apiclient/compat.ts b/src/utils/jellyfin-apiclient/compat.ts new file mode 100644 index 000000000..444dbee45 --- /dev/null +++ b/src/utils/jellyfin-apiclient/compat.ts @@ -0,0 +1,23 @@ +import { Api, Jellyfin } from '@jellyfin/sdk'; +import { ApiClient } from 'jellyfin-apiclient'; + +/** + * Returns an SDK Api instance using the same parameters as the provided ApiClient. + * @param {ApiClient} apiClient The (legacy) ApiClient. + * @returns {Api} An equivalent SDK Api instance. + */ +export const toApi = (apiClient: ApiClient): Api => { + return (new Jellyfin({ + clientInfo: { + name: apiClient.appName(), + version: apiClient.appVersion() + }, + deviceInfo: { + name: apiClient.deviceName(), + id: apiClient.deviceId() + } + })).createApi( + apiClient.serverAddress(), + apiClient.accessToken() + ); +}; diff --git a/webpack.common.js b/webpack.common.js index 4c15134b5..2548961b8 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -146,7 +146,7 @@ const config = { }, { test: /\.(js|jsx)$/, - exclude: /node_modules[\\/](?!@uupaa[\\/]dynamic-import-polyfill|@remix-run[\\/]router|blurhash|date-fns|dom7|epubjs|flv.js|libarchive.js|marked|react-router|screenfull|ssr-window|swiper)/, + exclude: /node_modules[\\/](?!@uupaa[\\/]dynamic-import-polyfill|@remix-run[\\/]router|blurhash|compare-versions|date-fns|dom7|epubjs|flv.js|libarchive.js|marked|react-router|screenfull|ssr-window|swiper)/, use: [{ loader: 'babel-loader', options: {