1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Merge pull request #4544 from thornbill/fix-eslint-warnings-ts

This commit is contained in:
Bill Thornton 2023-04-26 23:56:33 -04:00 committed by GitHub
commit f5e292bf29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 342 additions and 364 deletions

View file

@ -1,4 +1,4 @@
import React, { FunctionComponent, useEffect, useState } from 'react';
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import type { ConnectResponse } from 'jellyfin-apiclient';
@ -35,116 +35,119 @@ const ConnectionRequired: FunctionComponent<ConnectionRequiredProps> = ({
const [ isLoading, setIsLoading ] = useState(true);
useEffect(() => {
const bounce = async (connectionResponse: ConnectResponse) => {
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
if (location.pathname === BounceRoutes.Login) {
setIsLoading(false);
} else {
console.debug('[ConnectionRequired] not logged in, redirecting to login page');
navigate(`${BounceRoutes.Login}?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) {
// Update the current ApiClient
// TODO: Is there a better place to handle this?
ServerConnections.setLocalApiClient(firstConnection.ApiClient);
// 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;
}
const bounce = useCallback(async (connectionResponse: ConnectResponse) => {
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
if (location.pathname === BounceRoutes.Login) {
setIsLoading(false);
} else {
console.debug('[ConnectionRequired] not logged in, redirecting to login page');
navigate(`${BounceRoutes.Login}?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;
}
// Bounce to the correct page in the login flow
bounce(firstConnection);
console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State);
}, [location.pathname, navigate]);
const handleIncompleteWizard = useCallback(async (firstConnection: ConnectResponse) => {
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) {
// Update the current ApiClient
// TODO: Is there a better place to handle this?
ServerConnections.setLocalApiClient(firstConnection.ApiClient);
// 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;
}
}
// 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.
// Bounce to the correct page in the login flow
bounce(firstConnection);
}, [bounce, navigate]);
const client = ServerConnections.currentApiClient();
const validateUserAccess = useCallback(async () => {
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');
// 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());
} 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;
}
} catch (ex) {
console.warn('[ConnectionRequired] error bouncing from admin route', ex);
return;
}
}
setIsLoading(false);
};
setIsLoading(false);
}, [bounce, isAdminRequired, isUserRequired]);
validateConnection();
}, [ isAdminRequired, isUserRequired, location.pathname, navigate ]);
useEffect(() => {
// 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.
// Check connection status on initial page load
const firstConnection = appRouter.firstConnectionResult;
appRouter.firstConnectionResult = null;
if (firstConnection && firstConnection.State !== ConnectionState.SignedIn) {
handleIncompleteWizard(firstConnection);
} else {
validateUserAccess();
}
}, [handleIncompleteWizard, validateUserAccess]);
if (isLoading) {
return <Loading />;

View file

@ -4,6 +4,7 @@ import { useLocation } from 'react-router-dom';
import ServerConnections from './ServerConnections';
import viewManager from './viewManager/viewManager';
import globalize from '../scripts/globalize';
import type { RestoreViewFailResponse } from '../types/viewManager';
interface ServerContentPageProps {
view: string
@ -29,7 +30,7 @@ const ServerContentPage: FunctionComponent<ServerContentPageProps> = ({ view })
};
viewManager.tryRestoreView(viewOptions)
.catch(async (result?: any) => {
.catch(async (result?: RestoreViewFailResponse) => {
if (!result || !result.cancelled) {
const apiClient = ServerConnections.currentApiClient();
@ -46,12 +47,13 @@ const ServerContentPage: FunctionComponent<ServerContentPageProps> = ({ view })
};
loadPage();
}, [
},
// location.state is NOT included as a dependency here since dialogs will update state while the current view stays the same
// eslint-disable-next-line react-hooks/exhaustive-deps
[
view,
location.pathname,
location.search
// location.state is NOT included as a dependency here since dialogs will update state while the current view
// stays the same
]);
return <></>;

View file

@ -1,4 +1,4 @@
import type { BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
import { type BaseItemDtoQueryResult, ItemFields, ItemFilter } from '@jellyfin/sdk/lib/generated-client';
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
import loading from '../loading/loading';
@ -33,6 +33,41 @@ const getDefaultSortBy = () => {
return 'SortName';
};
const getFields = (viewQuerySettings: ViewQuerySettings) => {
const fields: ItemFields[] = [
ItemFields.BasicSyncInfo,
ItemFields.MediaSourceCount
];
if (viewQuerySettings.imageType === 'primary') {
fields.push(ItemFields.PrimaryImageAspectRatio);
}
return fields.join(',');
};
const getFilters = (viewQuerySettings: ViewQuerySettings) => {
const filters: ItemFilter[] = [];
if (viewQuerySettings.IsPlayed) {
filters.push(ItemFilter.IsPlayed);
}
if (viewQuerySettings.IsUnplayed) {
filters.push(ItemFilter.IsUnplayed);
}
if (viewQuerySettings.IsFavorite) {
filters.push(ItemFilter.IsFavorite);
}
if (viewQuerySettings.IsResumable) {
filters.push(ItemFilter.IsResumable);
}
return filters;
};
const getVisibleViewSettings = () => {
return [
'showTitle',
@ -228,33 +263,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
}, [getCardOptions, getContext, itemsResult.Items, getNoItemsMessage, viewQuerySettings.imageType]);
const getQuery = useCallback(() => {
let fields = 'BasicSyncInfo,MediaSourceCount';
if (viewQuerySettings.imageType === 'primary') {
fields += ',PrimaryImageAspectRatio';
}
if (viewQuerySettings.showYear) {
fields += ',ProductionYear';
}
const queryFilters: string[] = [];
if (viewQuerySettings.IsPlayed) {
queryFilters.push('IsPlayed');
}
if (viewQuerySettings.IsUnplayed) {
queryFilters.push('IsUnplayed');
}
if (viewQuerySettings.IsFavorite) {
queryFilters.push('IsFavorite');
}
if (viewQuerySettings.IsResumable) {
queryFilters.push('IsResumable');
}
const queryFilters = getFilters(viewQuerySettings);
let queryIsHD;
@ -271,7 +280,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
SortOrder: viewQuerySettings.SortOrder,
IncludeItemTypes: getItemTypes().join(','),
Recursive: true,
Fields: fields,
Fields: getFields(viewQuerySettings),
ImageTypeLimit: 1,
EnableImageTypes: 'Primary,Backdrop,Banner,Thumb,Disc,Logo',
Limit: userSettings.libraryPageSize(undefined) || undefined,
@ -293,28 +302,7 @@ const ViewItemsContainer: FC<ViewItemsContainerProps> = ({
ParentId: topParentId
};
}, [
viewQuerySettings.imageType,
viewQuerySettings.showYear,
viewQuerySettings.IsPlayed,
viewQuerySettings.IsUnplayed,
viewQuerySettings.IsFavorite,
viewQuerySettings.IsResumable,
viewQuerySettings.IsHD,
viewQuerySettings.IsSD,
viewQuerySettings.SortBy,
viewQuerySettings.SortOrder,
viewQuerySettings.VideoTypes,
viewQuerySettings.GenreIds,
viewQuerySettings.Is4K,
viewQuerySettings.Is3D,
viewQuerySettings.HasSubtitles,
viewQuerySettings.HasTrailer,
viewQuerySettings.HasSpecialFeature,
viewQuerySettings.HasThemeSong,
viewQuerySettings.HasThemeVideo,
viewQuerySettings.StartIndex,
viewQuerySettings.NameLessThan,
viewQuerySettings.NameStartsWith,
viewQuerySettings,
getItemTypes,
getBasekey,
topParentId

View file

@ -1,4 +1,3 @@
import type { UserDto } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
import Dashboard from '../../../utils/dashboard';
import globalize from '../../../scripts/globalize';
@ -17,7 +16,7 @@ type IProps = {
const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
const element = useRef<HTMLDivElement>(null);
const loadUser = useCallback(() => {
const loadUser = useCallback(async () => {
const page = element.current;
if (!page) {
@ -25,61 +24,48 @@ const UserPasswordForm: FunctionComponent<IProps> = ({ userId }: IProps) => {
return;
}
window.ApiClient.getUser(userId).then(function (user) {
Dashboard.getCurrentUser().then(function (loggedInUser: UserDto) {
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
}
const user = await window.ApiClient.getUser(userId);
const loggedInUser = await Dashboard.getCurrentUser();
if (!user.Configuration) {
throw new Error('Unexpected null user.Configuration');
}
if (!user.Policy || !user.Configuration) {
throw new Error('Unexpected null user policy or configuration');
}
LibraryMenu.setTitle(user.Name);
LibraryMenu.setTitle(user.Name);
let showLocalAccessSection = false;
let showLocalAccessSection = false;
if (user.HasConfiguredPassword) {
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide');
showLocalAccessSection = true;
} else {
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide');
}
if (user.HasConfiguredPassword) {
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.remove('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.remove('hide');
showLocalAccessSection = true;
} else {
(page.querySelector('#btnResetPassword') as HTMLDivElement).classList.add('hide');
(page.querySelector('#fldCurrentPassword') as HTMLDivElement).classList.add('hide');
}
if (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess) {
(page.querySelector('.passwordSection') as HTMLDivElement).classList.remove('hide');
} else {
(page.querySelector('.passwordSection') as HTMLDivElement).classList.add('hide');
}
const canChangePassword = loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess;
(page.querySelector('.passwordSection') as HTMLDivElement).classList.toggle('hide', !canChangePassword);
(page.querySelector('.localAccessSection') as HTMLDivElement).classList.toggle('hide', !(showLocalAccessSection && canChangePassword));
if (showLocalAccessSection && (loggedInUser?.Policy?.IsAdministrator || user.Policy.EnableUserPreferenceAccess)) {
(page.querySelector('.localAccessSection') as HTMLDivElement).classList.remove('hide');
} else {
(page.querySelector('.localAccessSection') as HTMLDivElement).classList.add('hide');
}
const txtEasyPassword = page.querySelector('#txtEasyPassword') as HTMLInputElement;
txtEasyPassword.value = '';
const txtEasyPassword = page.querySelector('#txtEasyPassword') as HTMLInputElement;
txtEasyPassword.value = '';
if (user.HasConfiguredEasyPassword) {
txtEasyPassword.placeholder = '******';
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
} else {
txtEasyPassword.removeAttribute('placeholder');
txtEasyPassword.placeholder = '';
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
}
if (user.HasConfiguredEasyPassword) {
txtEasyPassword.placeholder = '******';
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.remove('hide');
} else {
txtEasyPassword.removeAttribute('placeholder');
txtEasyPassword.placeholder = '';
(page.querySelector('#btnResetEasyPassword') as HTMLDivElement).classList.add('hide');
}
const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement;
const chkEnableLocalEasyPassword = page.querySelector('.chkEnableLocalEasyPassword') as HTMLInputElement;
chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false;
chkEnableLocalEasyPassword.checked = user.Configuration.EnableLocalPassword || false;
import('../../autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
});
import('../../autoFocuser').then(({ default: autoFocuser }) => {
autoFocuser.autoFocus(page);
});
(page.querySelector('#txtCurrentPassword') as HTMLInputElement).value = '';

View file

@ -1,7 +1,7 @@
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
import type { ApiClient } from 'jellyfin-apiclient';
import classNames from 'classnames';
import React, { FunctionComponent, useEffect, useState } from 'react';
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import globalize from '../../scripts/globalize';
import ServerConnections from '../ServerConnections';
@ -14,6 +14,17 @@ type SearchResultsProps = {
query?: string;
}
const ensureNonNullItems = (result: BaseItemDtoQueryResult) => ({
...result,
Items: result.Items || []
});
const isMovies = (collectionType: string) => collectionType === 'movies';
const isMusic = (collectionType: string) => collectionType === 'music';
const isTVShows = (collectionType: string) => collectionType === 'tvshows' || collectionType === 'tv';
/*
* React component to display search result rows for global search and non-live tv library search
*/
@ -35,55 +46,55 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
const [ people, setPeople ] = useState<BaseItemDto[]>([]);
const [ collections, setCollections ] = useState<BaseItemDto[]>([]);
useEffect(() => {
const getDefaultParameters = () => ({
ParentId: parentId,
searchTerm: query,
Limit: 24,
Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount',
Recursive: true,
EnableTotalRecordCount: false,
ImageTypeLimit: 1,
IncludePeople: false,
IncludeMedia: false,
IncludeGenres: false,
IncludeStudios: false,
IncludeArtists: false
});
const getDefaultParameters = useCallback(() => ({
ParentId: parentId,
searchTerm: query,
Limit: 24,
Fields: 'PrimaryImageAspectRatio,CanDelete,BasicSyncInfo,MediaSourceCount',
Recursive: true,
EnableTotalRecordCount: false,
ImageTypeLimit: 1,
IncludePeople: false,
IncludeMedia: false,
IncludeGenres: false,
IncludeStudios: false,
IncludeArtists: false
}), [parentId, query]);
const fetchArtists = (apiClient: ApiClient, params = {}) => apiClient?.getArtists(
apiClient?.getCurrentUserId(),
const fetchArtists = useCallback((apiClient: ApiClient, params = {}) => (
apiClient?.getArtists(
apiClient.getCurrentUserId(),
{
...getDefaultParameters(),
IncludeArtists: true,
...params
}
);
).then(ensureNonNullItems)
), [getDefaultParameters]);
const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems(
apiClient?.getCurrentUserId(),
const fetchItems = useCallback((apiClient: ApiClient, params = {}) => (
apiClient?.getItems(
apiClient.getCurrentUserId(),
{
...getDefaultParameters(),
IncludeMedia: true,
...params
}
);
).then(ensureNonNullItems)
), [getDefaultParameters]);
const fetchPeople = (apiClient: ApiClient, params = {}) => apiClient?.getPeople(
apiClient?.getCurrentUserId(),
const fetchPeople = useCallback((apiClient: ApiClient, params = {}) => (
apiClient?.getPeople(
apiClient.getCurrentUserId(),
{
...getDefaultParameters(),
IncludePeople: true,
...params
}
);
const isMovies = () => collectionType === 'movies';
const isMusic = () => collectionType === 'music';
const isTVShows = () => collectionType === 'tvshows' || collectionType === 'tv';
).then(ensureNonNullItems)
), [getDefaultParameters]);
useEffect(() => {
// Reset state
setMovies([]);
setShows([]);
@ -102,78 +113,80 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
setPeople([]);
setCollections([]);
if (query) {
const apiClient = ServerConnections.getApiClient(serverId);
// Movie libraries
if (!collectionType || isMovies()) {
// Movies row
fetchItems(apiClient, { IncludeItemTypes: 'Movie' })
.then(result => setMovies(result.Items || []));
}
// TV Show libraries
if (!collectionType || isTVShows()) {
// Shows row
fetchItems(apiClient, { IncludeItemTypes: 'Series' })
.then(result => setShows(result.Items || []));
// Episodes row
fetchItems(apiClient, { IncludeItemTypes: 'Episode' })
.then(result => setEpisodes(result.Items || []));
}
// People are included for Movies and TV Shows
if (!collectionType || isMovies() || isTVShows()) {
// People row
fetchPeople(apiClient).then(result => setPeople(result.Items || []));
}
// Music libraries
if (!collectionType || isMusic()) {
// Playlists row
fetchItems(apiClient, { IncludeItemTypes: 'Playlist' })
.then(results => setPlaylists(results.Items || []));
// Artists row
fetchArtists(apiClient).then(result => setArtists(result.Items || []));
// Albums row
fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' })
.then(result => setAlbums(result.Items || []));
// Songs row
fetchItems(apiClient, { IncludeItemTypes: 'Audio' })
.then(result => setSongs(result.Items || []));
}
// Other libraries do not support in-library search currently
if (!collectionType) {
// Videos row
fetchItems(apiClient, {
MediaTypes: 'Video',
ExcludeItemTypes: 'Movie,Episode,TvChannel'
}).then(result => setVideos(result.Items || []));
// Programs row
fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' })
.then(result => setPrograms(result.Items || []));
// Channels row
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
.then(result => setChannels(result.Items || []));
// Photo Albums row
fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' })
.then(results => setPhotoAlbums(results.Items || []));
// Photos row
fetchItems(apiClient, { IncludeItemTypes: 'Photo' })
.then(results => setPhotos(results.Items || []));
// Audio Books row
fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' })
.then(results => setAudioBooks(results.Items || []));
// Books row
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
.then(results => setBooks(results.Items || []));
// Collections row
fetchItems(apiClient, { IncludeItemTypes: 'BoxSet' })
.then(result => setCollections(result.Items || []));
}
if (!query) {
return;
}
}, [collectionType, parentId, query, serverId]);
const apiClient = ServerConnections.getApiClient(serverId);
// Movie libraries
if (!collectionType || isMovies(collectionType)) {
// Movies row
fetchItems(apiClient, { IncludeItemTypes: 'Movie' })
.then(result => setMovies(result.Items));
}
// TV Show libraries
if (!collectionType || isTVShows(collectionType)) {
// Shows row
fetchItems(apiClient, { IncludeItemTypes: 'Series' })
.then(result => setShows(result.Items));
// Episodes row
fetchItems(apiClient, { IncludeItemTypes: 'Episode' })
.then(result => setEpisodes(result.Items));
}
// People are included for Movies and TV Shows
if (!collectionType || isMovies(collectionType) || isTVShows(collectionType)) {
// People row
fetchPeople(apiClient).then(result => setPeople(result.Items));
}
// Music libraries
if (!collectionType || isMusic(collectionType)) {
// Playlists row
fetchItems(apiClient, { IncludeItemTypes: 'Playlist' })
.then(results => setPlaylists(results.Items));
// Artists row
fetchArtists(apiClient).then(result => setArtists(result.Items));
// Albums row
fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' })
.then(result => setAlbums(result.Items));
// Songs row
fetchItems(apiClient, { IncludeItemTypes: 'Audio' })
.then(result => setSongs(result.Items));
}
// Other libraries do not support in-library search currently
if (!collectionType) {
// Videos row
fetchItems(apiClient, {
MediaTypes: 'Video',
ExcludeItemTypes: 'Movie,Episode,TvChannel'
}).then(result => setVideos(result.Items));
// Programs row
fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' })
.then(result => setPrograms(result.Items));
// Channels row
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
.then(result => setChannels(result.Items));
// Photo Albums row
fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' })
.then(result => setPhotoAlbums(result.Items));
// Photos row
fetchItems(apiClient, { IncludeItemTypes: 'Photo' })
.then(result => setPhotos(result.Items));
// Audio Books row
fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' })
.then(result => setAudioBooks(result.Items));
// Books row
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
.then(result => setBooks(result.Items));
// Collections row
fetchItems(apiClient, { IncludeItemTypes: 'BoxSet' })
.then(result => setCollections(result.Items));
}
}, [collectionType, fetchArtists, fetchItems, fetchPeople, query, serverId]);
return (
<div

View file

@ -2,6 +2,7 @@ import React, { FunctionComponent, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import globalize from '../../scripts/globalize';
import type { RestoreViewFailResponse } from '../../types/viewManager';
import viewManager from './viewManager';
export interface ViewManagerPageProps {
@ -45,7 +46,7 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
};
viewManager.tryRestoreView(viewOptions)
.catch(async (result?: any) => {
.catch(async (result?: RestoreViewFailResponse) => {
if (!result || !result.cancelled) {
const [ controllerFactory, viewHtml ] = await Promise.all([
import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`),
@ -63,7 +64,10 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
};
loadPage();
}, [
},
// location.state is NOT included as a dependency here since dialogs will update state while the current view stays the same
// eslint-disable-next-line react-hooks/exhaustive-deps
[
controller,
view,
type,
@ -73,8 +77,6 @@ const ViewManagerPage: FunctionComponent<ViewManagerPageProps> = ({
transition,
location.pathname,
location.search
// location.state is NOT included as a dependency here since dialogs will update state while the current view
// stays the same
]);
return <></>;

View file

@ -126,7 +126,7 @@ const Scroller: FC<ScrollerProps> = ({
}, [getScrollPosition, getScrollSize, getScrollWidth]);
const initCenterFocus = useCallback((elem: EventTarget, scrollerInstance: scrollerFactory) => {
dom.addEventListener(elem, 'focus', function (e: { target: any; }) {
dom.addEventListener(elem, 'focus', function (e: FocusEvent) {
const focused = focusManager.focusableParent(e.target);
if (focused) {
scrollerInstance.toCenter(focused, false);

View file

@ -25,6 +25,17 @@ type AuthProvider = {
Id?: string;
}
const getCheckedElementDataIds = (elements: NodeListOf<Element>) => (
Array.prototype.filter.call(elements, e => e.checked)
.map(e => e.getAttribute('data-id'))
);
function onSaveComplete() {
Dashboard.navigate('userprofiles.html');
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
const UserEdit: FunctionComponent = () => {
const [ userName, setUserName ] = useState('');
const [ deleteFoldersAccess, setDeleteFoldersAccess ] = useState<ResetProvider[]>([]);
@ -56,7 +67,7 @@ const UserEdit: FunctionComponent = () => {
}
const fldSelectLoginProvider = page.querySelector('.fldSelectLoginProvider') as HTMLDivElement;
providers.length > 1 ? fldSelectLoginProvider.classList.remove('hide') : fldSelectLoginProvider.classList.add('hide');
fldSelectLoginProvider.classList.toggle('hide', providers.length <= 1);
setAuthProviders(providers);
@ -73,7 +84,7 @@ const UserEdit: FunctionComponent = () => {
}
const fldSelectPasswordResetProvider = page.querySelector('.fldSelectPasswordResetProvider') as HTMLDivElement;
providers.length > 1 ? fldSelectPasswordResetProvider.classList.remove('hide') : fldSelectPasswordResetProvider.classList.add('hide');
fldSelectPasswordResetProvider.classList.toggle('hide', providers.length <= 1);
setPasswordResetProviders(providers);
@ -145,7 +156,7 @@ const UserEdit: FunctionComponent = () => {
});
const disabledUserBanner = page.querySelector('.disabledUserBanner') as HTMLDivElement;
user.Policy.IsDisabled ? disabledUserBanner.classList.remove('hide') : disabledUserBanner.classList.add('hide');
disabledUserBanner.classList.toggle('hide', !user.Policy.IsDisabled);
const txtUserName = page.querySelector('#txtUserName') as HTMLInputElement;
txtUserName.disabled = false;
@ -198,19 +209,9 @@ const UserEdit: FunctionComponent = () => {
loadData();
function onSaveComplete() {
Dashboard.navigate('userprofiles.html');
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
const saveUser = (user: UserDto) => {
if (!user.Id) {
throw new Error('Unexpected null user.Id');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user id or policy');
}
user.Name = (page.querySelector('#txtUserName') as HTMLInputElement).value;
@ -235,19 +236,15 @@ const UserEdit: FunctionComponent = () => {
user.Policy.AuthenticationProviderId = (page.querySelector('#selectLoginProvider') as HTMLSelectElement).value;
user.Policy.PasswordResetProviderId = (page.querySelector('#selectPasswordResetProvider') as HTMLSelectElement).value;
user.Policy.EnableContentDeletion = (page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).checked;
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : Array.prototype.filter.call(page.querySelectorAll('.chkFolder'), function (c) {
return c.checked;
}).map(function (c) {
return c.getAttribute('data-id');
});
if (window.ApiClient.isMinServerVersion('10.6.0')) {
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
}
window.ApiClient.updateUser(user).then(function () {
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {}).then(function () {
user.Policy.EnableContentDeletionFromFolders = user.Policy.EnableContentDeletion ? [] : getCheckedElementDataIds(page.querySelectorAll('.chkFolder'));
user.Policy.SyncPlayAccess = (page.querySelector('#selectSyncPlayAccess') as HTMLSelectElement).value as SyncPlayUserAccessType;
window.ApiClient.updateUser(user)
.then(() => (
window.ApiClient.updateUserPolicy(user.Id || '', user.Policy || {})
)).then(() => {
onSaveComplete();
});
});
};
const onSubmit = (e: Event) => {
@ -261,16 +258,11 @@ const UserEdit: FunctionComponent = () => {
};
(page.querySelector('.chkEnableDeleteAllFolders') as HTMLInputElement).addEventListener('change', function (this: HTMLInputElement) {
if (this.checked) {
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.add('hide');
} else {
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.remove('hide');
}
(page.querySelector('.deleteAccess') as HTMLDivElement).classList.toggle('hide', this.checked);
});
window.ApiClient.getNamedConfiguration('network').then(function (config) {
const fldRemoteAccess = page.querySelector('.fldRemoteAccess') as HTMLDivElement;
config.EnableRemoteAccess ? fldRemoteAccess.classList.remove('hide') : fldRemoteAccess.classList.add('hide');
(page.querySelector('.fldRemoteAccess') as HTMLDivElement).classList.toggle('hide', !config.EnableRemoteAccess);
});
(page.querySelector('.editUserProfileForm') as HTMLFormElement).addEventListener('submit', onSubmit);

View file

@ -111,12 +111,8 @@ const UserNew: FunctionComponent = () => {
userInput.Name = (page.querySelector('#txtUsername') as HTMLInputElement).value;
userInput.Password = (page.querySelector('#txtPassword') as HTMLInputElement).value;
window.ApiClient.createUser(userInput).then(function (user) {
if (!user.Id) {
throw new Error('Unexpected null user.Id');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user id or policy');
}
user.Policy.EnableAllFolders = (page.querySelector('.chkEnableAllFolders') as HTMLInputElement).checked;

View file

@ -215,12 +215,8 @@ const UserParentalControl: FunctionComponent = () => {
};
const saveUser = (user: UserDto) => {
if (!user.Id) {
throw new Error('Unexpected null user.Id');
}
if (!user.Policy) {
throw new Error('Unexpected null user.Policy');
if (!user.Id || !user.Policy) {
throw new Error('Unexpected null user id or policy');
}
const parentalRating = parseInt((page.querySelector('#selectMaxParentalRating') as HTMLSelectElement).value, 10);

View file

@ -30,12 +30,8 @@ const UserProfile: FunctionComponent = () => {
loading.show();
window.ApiClient.getUser(userId).then(function (user) {
if (!user.Name) {
throw new Error('Unexpected null user.Name');
}
if (!user.Id) {
throw new Error('Unexpected null user.Id');
if (!user.Name || !user.Id) {
throw new Error('Unexpected null user name or id');
}
setUserName(user.Name);

3
src/types/viewManager.ts Normal file
View file

@ -0,0 +1,3 @@
export interface RestoreViewFailResponse {
cancelled?: boolean
}

View file

@ -31,25 +31,26 @@ function getItemsSplit(apiClient: ApiClient, userId: string, options: GetItemsRe
function mergeResults(results: BaseItemDtoQueryResult[]) {
const merged: BaseItemDtoQueryResult = {
Items: [],
TotalRecordCount: 0,
StartIndex: 0
};
// set TotalRecordCount separately so TS knows it is defined
merged.TotalRecordCount = 0;
for (const result of results) {
if (result.Items == null) {
if (!result.Items) {
console.log('[getItems] Retrieved Items array is invalid', result.Items);
continue;
}
if (result.TotalRecordCount == null) {
if (!result.TotalRecordCount) {
console.log('[getItems] Retrieved TotalRecordCount is invalid', result.TotalRecordCount);
continue;
}
if (result.StartIndex == null) {
if (typeof result.StartIndex === 'undefined') {
console.log('[getItems] Retrieved StartIndex is invalid', result.StartIndex);
continue;
}
merged.Items = merged.Items?.concat(result.Items);
merged.TotalRecordCount! += result.TotalRecordCount;
merged.TotalRecordCount += result.TotalRecordCount;
merged.StartIndex = Math.min(merged.StartIndex || 0, result.StartIndex);
}
return merged;