Merge pull request #5092 from thornbill/home-screen-queries
Use react-query for UserViews queries
This commit is contained in:
commit
94ed946459
8 changed files with 196 additions and 89 deletions
|
@ -1,19 +1,18 @@
|
||||||
import loadable from '@loadable/component';
|
import loadable from '@loadable/component';
|
||||||
import { ThemeProvider } from '@mui/material/styles';
|
import { ThemeProvider } from '@mui/material/styles';
|
||||||
import { History } from '@remix-run/router';
|
import { History } from '@remix-run/router';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { ApiProvider } from 'hooks/useApi';
|
import { ApiProvider } from 'hooks/useApi';
|
||||||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||||
import theme from 'themes/theme';
|
import theme from 'themes/theme';
|
||||||
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
|
|
||||||
const StableAppRouter = loadable(() => import('./apps/stable/AppRouter'));
|
const StableAppRouter = loadable(() => import('./apps/stable/AppRouter'));
|
||||||
const RootAppRouter = loadable(() => import('./RootAppRouter'));
|
const RootAppRouter = loadable(() => import('./RootAppRouter'));
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
const RootApp = ({ history }: Readonly<{ history: History }>) => {
|
const RootApp = ({ history }: Readonly<{ history: History }>) => {
|
||||||
const layoutMode = localStorage.getItem('layout');
|
const layoutMode = localStorage.getItem('layout');
|
||||||
const isExperimentalLayout = layoutMode === 'experimental';
|
const isExperimentalLayout = layoutMode === 'experimental';
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto';
|
|
||||||
import { getUserViewsApi } from '@jellyfin/sdk/lib/utils/api/user-views-api';
|
|
||||||
import Dashboard from '@mui/icons-material/Dashboard';
|
import Dashboard from '@mui/icons-material/Dashboard';
|
||||||
import Edit from '@mui/icons-material/Edit';
|
import Edit from '@mui/icons-material/Edit';
|
||||||
import Favorite from '@mui/icons-material/Favorite';
|
import Favorite from '@mui/icons-material/Favorite';
|
||||||
|
@ -12,12 +10,13 @@ import ListItemButton from '@mui/material/ListItemButton';
|
||||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import ListSubheader from '@mui/material/ListSubheader';
|
import ListSubheader from '@mui/material/ListSubheader';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import ListItemLink from 'components/ListItemLink';
|
import ListItemLink from 'components/ListItemLink';
|
||||||
import { appRouter } from 'components/router/appRouter';
|
import { appRouter } from 'components/router/appRouter';
|
||||||
import { useApi } from 'hooks/useApi';
|
import { useApi } from 'hooks/useApi';
|
||||||
|
import { useUserViews } from 'hooks/useUserViews';
|
||||||
import { useWebConfig } from 'hooks/useWebConfig';
|
import { useWebConfig } from 'hooks/useWebConfig';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
|
@ -25,29 +24,14 @@ import LibraryIcon from '../LibraryIcon';
|
||||||
import DrawerHeaderLink from './DrawerHeaderLink';
|
import DrawerHeaderLink from './DrawerHeaderLink';
|
||||||
|
|
||||||
const MainDrawerContent = () => {
|
const MainDrawerContent = () => {
|
||||||
const { api, user } = useApi();
|
const { user } = useApi();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [ userViews, setUserViews ] = useState<BaseItemDto[]>([]);
|
const { data: userViewsData } = useUserViews(user?.Id);
|
||||||
|
const userViews = userViewsData?.Items || [];
|
||||||
const webConfig = useWebConfig();
|
const webConfig = useWebConfig();
|
||||||
|
|
||||||
const isHomeSelected = location.pathname === '/home.html' && (!location.search || location.search === '?tab=0');
|
const isHomeSelected = location.pathname === '/home.html' && (!location.search || location.search === '?tab=0');
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (api && user?.Id) {
|
|
||||||
getUserViewsApi(api)
|
|
||||||
.getUserViews({ userId: user.Id })
|
|
||||||
.then(({ data }) => {
|
|
||||||
setUserViews(data.Items || []);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.warn('[MainDrawer] failed to fetch user views', err);
|
|
||||||
setUserViews([]);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setUserViews([]);
|
|
||||||
}
|
|
||||||
}, [ api, user?.Id ]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* MAIN LINKS */}
|
{/* MAIN LINKS */}
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
|
|
||||||
import escapeHtml from 'escape-html';
|
import escapeHtml from 'escape-html';
|
||||||
|
|
||||||
|
import { getUserViewsQuery } from 'hooks/useUserViews';
|
||||||
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||||
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
|
|
||||||
import layoutManager from '../layoutManager';
|
import layoutManager from '../layoutManager';
|
||||||
import focusManager from '../focusManager';
|
import focusManager from '../focusManager';
|
||||||
import globalize from '../../scripts/globalize';
|
import globalize from '../../scripts/globalize';
|
||||||
|
@ -291,7 +296,12 @@ function loadForm(context, user, userSettings, apiClient) {
|
||||||
|
|
||||||
updateHomeSectionValues(context, userSettings);
|
updateHomeSectionValues(context, userSettings);
|
||||||
|
|
||||||
const promise1 = apiClient.getUserViews({ IncludeHidden: true }, user.Id);
|
const promise1 = queryClient
|
||||||
|
.fetchQuery(getUserViewsQuery(
|
||||||
|
toApi(apiClient),
|
||||||
|
user.Id,
|
||||||
|
{ includeHidden: true }
|
||||||
|
));
|
||||||
const promise2 = apiClient.getJSON(apiClient.getUrl(`Users/${user.Id}/GroupingOptions`));
|
const promise2 = apiClient.getJSON(apiClient.getUrl(`Users/${user.Id}/GroupingOptions`));
|
||||||
|
|
||||||
Promise.all([promise1, promise2]).then(responses => {
|
Promise.all([promise1, promise2]).then(responses => {
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import layoutManager from 'components/layoutManager';
|
import layoutManager from 'components/layoutManager';
|
||||||
|
import { getUserViewsQuery } from 'hooks/useUserViews';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
import { DEFAULT_SECTIONS, HomeSectionType } from 'types/homeSectionType';
|
import { DEFAULT_SECTIONS, HomeSectionType } from 'types/homeSectionType';
|
||||||
import Dashboard from 'utils/dashboard';
|
import Dashboard from 'utils/dashboard';
|
||||||
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||||
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
|
|
||||||
import { loadRecordings } from './sections/activeRecordings';
|
import { loadRecordings } from './sections/activeRecordings';
|
||||||
import { loadLibraryButtons } from './sections/libraryButtons';
|
import { loadLibraryButtons } from './sections/libraryButtons';
|
||||||
|
@ -50,7 +53,11 @@ function getAllSectionsToShow(userSettings, sectionCount) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadSections(elem, apiClient, user, userSettings) {
|
export function loadSections(elem, apiClient, user, userSettings) {
|
||||||
return getUserViews(apiClient, user.Id).then(function (userViews) {
|
const userId = user.Id || apiClient.getCurrentUserId();
|
||||||
|
return queryClient
|
||||||
|
.fetchQuery(getUserViewsQuery(toApi(apiClient), userId))
|
||||||
|
.then(result => result.Items || [])
|
||||||
|
.then(function (userViews) {
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
if (userViews.length) {
|
if (userViews.length) {
|
||||||
|
@ -167,12 +174,6 @@ function loadSection(page, apiClient, user, userSettings, userViews, allSections
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserViews(apiClient, userId) {
|
|
||||||
return apiClient.getUserViews({}, userId || apiClient.getCurrentUserId()).then(function (result) {
|
|
||||||
return result.Items;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableScrollX() {
|
function enableScrollX() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
45
src/hooks/useUserViews.ts
Normal file
45
src/hooks/useUserViews.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import type { Api } from '@jellyfin/sdk/lib/api';
|
||||||
|
import type { UserViewsApiGetUserViewsRequest } from '@jellyfin/sdk/lib/generated-client/api/user-views-api';
|
||||||
|
import { getUserViewsApi } from '@jellyfin/sdk/lib/utils/api/user-views-api';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
|
|
||||||
|
import { queryOptions } from 'utils/query/queryOptions';
|
||||||
|
|
||||||
|
import { useApi } from './useApi';
|
||||||
|
|
||||||
|
const fetchUserViews = async (
|
||||||
|
api?: Api,
|
||||||
|
userId?: string,
|
||||||
|
params?: UserViewsApiGetUserViewsRequest,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
if (!api) throw new Error('No API instance available');
|
||||||
|
if (!userId) throw new Error('No User ID provided');
|
||||||
|
|
||||||
|
const response = await getUserViewsApi(api)
|
||||||
|
.getUserViews({ ...params, userId }, options);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserViewsQuery = (
|
||||||
|
api?: Api,
|
||||||
|
userId?: string,
|
||||||
|
params?: UserViewsApiGetUserViewsRequest
|
||||||
|
) => queryOptions({
|
||||||
|
queryKey: [ 'User', userId, 'Views', params ],
|
||||||
|
queryFn: ({ signal }) => fetchUserViews(api, userId, params, { signal }),
|
||||||
|
// On initial page load we request user views 3x. Setting a 1 second stale time
|
||||||
|
// allows a single request to be made to resolve all 3.
|
||||||
|
staleTime: 1000, // 1 second
|
||||||
|
enabled: !!api && !!userId
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useUserViews = (
|
||||||
|
userId?: string,
|
||||||
|
params?: UserViewsApiGetUserViewsRequest
|
||||||
|
) => {
|
||||||
|
const apiContext = useApi();
|
||||||
|
const { api } = apiContext;
|
||||||
|
return useQuery(getUserViewsQuery(api, userId, params));
|
||||||
|
};
|
|
@ -1,6 +1,10 @@
|
||||||
import escapeHtml from 'escape-html';
|
import escapeHtml from 'escape-html';
|
||||||
import Headroom from 'headroom.js';
|
import Headroom from 'headroom.js';
|
||||||
|
|
||||||
|
import { getUserViewsQuery } from 'hooks/useUserViews';
|
||||||
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||||
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
|
|
||||||
import dom from './dom';
|
import dom from './dom';
|
||||||
import layoutManager from '../components/layoutManager';
|
import layoutManager from '../components/layoutManager';
|
||||||
import inputManager from './inputManager';
|
import inputManager from './inputManager';
|
||||||
|
@ -383,7 +387,9 @@ function onSidebarLinkClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserViews(apiClient, userId) {
|
function getUserViews(apiClient, userId) {
|
||||||
return apiClient.getUserViews({}, userId).then(function (result) {
|
return queryClient
|
||||||
|
.fetchQuery(getUserViewsQuery(toApi(apiClient), userId))
|
||||||
|
.then(function (result) {
|
||||||
const items = result.Items;
|
const items = result.Items;
|
||||||
const list = [];
|
const list = [];
|
||||||
|
|
||||||
|
|
3
src/utils/query/queryClient.ts
Normal file
3
src/utils/query/queryClient.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient();
|
59
src/utils/query/queryOptions.ts
Normal file
59
src/utils/query/queryOptions.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
// Copyright (c) 2021-2024 Tanner Linsley
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backport of the `queryOptions` utility function for react-query v4.
|
||||||
|
* Upgrading to v5 requires React 18.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { QueryKey, UseQueryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export type UndefinedInitialDataOptions<
|
||||||
|
TQueryFnData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
TData = TQueryFnData,
|
||||||
|
TQueryKey extends QueryKey = QueryKey,
|
||||||
|
> = UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
|
||||||
|
initialData?: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
type NonUndefinedGuard<T> = T extends undefined ? never : T;
|
||||||
|
|
||||||
|
export type DefinedInitialDataOptions<
|
||||||
|
TQueryFnData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
TData = TQueryFnData,
|
||||||
|
TQueryKey extends QueryKey = QueryKey,
|
||||||
|
> = UseQueryOptions<TQueryFnData, TError, TData, TQueryKey> & {
|
||||||
|
initialData:
|
||||||
|
| NonUndefinedGuard<TQueryFnData>
|
||||||
|
| (() => NonUndefinedGuard<TQueryFnData>)
|
||||||
|
};
|
||||||
|
|
||||||
|
export function queryOptions<
|
||||||
|
TQueryFnData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
TData = TQueryFnData,
|
||||||
|
TQueryKey extends QueryKey = QueryKey,
|
||||||
|
>(
|
||||||
|
options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||||
|
): UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> & {
|
||||||
|
queryKey: TQueryKey
|
||||||
|
};
|
||||||
|
|
||||||
|
export function queryOptions<
|
||||||
|
TQueryFnData = unknown,
|
||||||
|
TError = unknown,
|
||||||
|
TData = TQueryFnData,
|
||||||
|
TQueryKey extends QueryKey = QueryKey,
|
||||||
|
>(
|
||||||
|
options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||||
|
): DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> & {
|
||||||
|
queryKey: TQueryKey
|
||||||
|
};
|
||||||
|
|
||||||
|
export function queryOptions(options: unknown) {
|
||||||
|
return options;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue