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

feat: (preferences) hook react display settings into user settings

This commit is contained in:
Grady Hallenbeck 2023-10-19 12:02:54 -07:00
parent ce4c7aed5e
commit 3dd26c7785
13 changed files with 530 additions and 46 deletions

View file

@ -3,15 +3,26 @@ import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
import FormHelperText from '@mui/material/FormHelperText'; import FormHelperText from '@mui/material/FormHelperText';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select'; import Select, { SelectChangeEvent } from '@mui/material/Select';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import React from 'react'; import React from 'react';
import globalize from 'scripts/globalize'; import globalize from 'scripts/globalize';
import { DisplaySettingsValues } from './types';
import { useScreensavers } from './hooks/useScreensavers';
import { useServerThemes } from './hooks/useServerThemes';
interface DisplayPreferencesProps {
onChange: (event: SelectChangeEvent | React.SyntheticEvent) => void;
values: DisplaySettingsValues;
}
export function DisplayPreferences({ onChange, values }: Readonly<DisplayPreferencesProps>) {
const { screensavers } = useScreensavers();
const { themes } = useServerThemes();
export function DisplayPreferences() {
return ( return (
<Stack spacing={2}> <Stack spacing={2}>
<Typography variant='h2'>{globalize.translate('Display')}</Typography> <Typography variant='h2'>{globalize.translate('Display')}</Typography>
@ -19,7 +30,12 @@ export function DisplayPreferences() {
<FormControl fullWidth> <FormControl fullWidth>
<Select <Select
aria-describedby='display-settings-layout-description' aria-describedby='display-settings-layout-description'
inputProps={{
name: 'layout'
}}
label={globalize.translate('LabelDisplayMode')} label={globalize.translate('LabelDisplayMode')}
onChange={onChange}
value={values.layout}
> >
<MenuItem value='auto'>{globalize.translate('Auto')}</MenuItem> <MenuItem value='auto'>{globalize.translate('Auto')}</MenuItem>
<MenuItem value='desktop'>{globalize.translate('Desktop')}</MenuItem> <MenuItem value='desktop'>{globalize.translate('Desktop')}</MenuItem>
@ -34,15 +50,31 @@ export function DisplayPreferences() {
</FormControl> </FormControl>
<FormControl fullWidth> <FormControl fullWidth>
<Select label={globalize.translate('LabelTheme')}> <Select
inputProps={{
name: 'theme'
}}
label={globalize.translate('LabelTheme')}
onChange={onChange}
value={values.theme}
>
{ ...themes.map(({ id, name }) => (
<MenuItem key={id} value={id}>{name}</MenuItem>
))}
</Select> </Select>
</FormControl> </FormControl>
<FormControl fullWidth> <FormControl fullWidth>
<FormControlLabel <FormControlLabel
aria-describedby='display-settings-disable-css-description' aria-describedby='display-settings-disable-css-description'
control={<Checkbox />} control={
<Checkbox
checked={values.disableCustomCss}
onChange={onChange}
/>
}
label={globalize.translate('DisableCustomCss')} label={globalize.translate('DisableCustomCss')}
name='disableCustomCss'
/> />
<FormHelperText id='display-settings-disable-css-description'> <FormHelperText id='display-settings-disable-css-description'>
{globalize.translate('LabelDisableCustomCss')} {globalize.translate('LabelDisableCustomCss')}
@ -52,8 +84,11 @@ export function DisplayPreferences() {
<FormControl fullWidth> <FormControl fullWidth>
<TextField <TextField
aria-describedby='display-settings-custom-css-description' aria-describedby='display-settings-custom-css-description'
defaultValue={values.customCss}
label={globalize.translate('LabelCustomCss')} label={globalize.translate('LabelCustomCss')}
multiline multiline
name='customCss'
onChange={onChange}
/> />
<FormHelperText id='display-settings-custom-css-description'> <FormHelperText id='display-settings-custom-css-description'>
{globalize.translate('LabelLocalCustomCss')} {globalize.translate('LabelLocalCustomCss')}
@ -64,7 +99,18 @@ export function DisplayPreferences() {
{/* Server Dashboard Theme */} {/* Server Dashboard Theme */}
<FormControl fullWidth> <FormControl fullWidth>
<Select label={globalize.translate('LabelScreensaver')}></Select> <Select
inputProps={{
name: 'screensaver'
}}
label={globalize.translate('LabelScreensaver')}
onChange={onChange}
value={values.screensaver}
>
{ ...screensavers.map(({ id, name }) => (
<MenuItem key={id} value={id}>{name}</MenuItem>
))}
</Select>
</FormControl> </FormControl>
{/* TODO: There are some extra options here related to screensavers */} {/* TODO: There are some extra options here related to screensavers */}
@ -72,8 +118,14 @@ export function DisplayPreferences() {
<FormControl fullWidth> <FormControl fullWidth>
<FormControlLabel <FormControlLabel
aria-describedby='display-settings-faster-animations-description' aria-describedby='display-settings-faster-animations-description'
control={<Checkbox />} control={
<Checkbox
checked={values.enableFasterAnimation}
onChange={onChange}
/>
}
label={globalize.translate('EnableFasterAnimations')} label={globalize.translate('EnableFasterAnimations')}
name='enableFasterAnimation'
/> />
<FormHelperText id='display-settings-faster-animations-description'> <FormHelperText id='display-settings-faster-animations-description'>
{globalize.translate('EnableFasterAnimationsHelp')} {globalize.translate('EnableFasterAnimationsHelp')}
@ -83,8 +135,14 @@ export function DisplayPreferences() {
<FormControl fullWidth> <FormControl fullWidth>
<FormControlLabel <FormControlLabel
aria-describedby='display-settings-blurhash-description' aria-describedby='display-settings-blurhash-description'
control={<Checkbox />} control={
<Checkbox
checked={values.enableBlurHash}
onChange={onChange}
/>
}
label={globalize.translate('EnableBlurHash')} label={globalize.translate('EnableBlurHash')}
name='enableBlurHash'
/> />
<FormHelperText id='display-settings-blurhash-description'> <FormHelperText id='display-settings-blurhash-description'>
{globalize.translate('EnableBlurHashHelp')} {globalize.translate('EnableBlurHashHelp')}

View file

@ -7,17 +7,29 @@ import Typography from '@mui/material/Typography';
import React from 'react'; import React from 'react';
import globalize from 'scripts/globalize'; import globalize from 'scripts/globalize';
import { DisplaySettingsValues } from './types';
export function ItemDetailPreferences() { interface ItemDetailPreferencesProps {
onChange: (event: React.SyntheticEvent) => void;
values: DisplaySettingsValues;
}
export function ItemDetailPreferences({ onChange, values }: Readonly<ItemDetailPreferencesProps>) {
return ( return (
<Stack spacing={3}> <Stack spacing={2}>
<Typography variant='h2'>{globalize.translate('ItemDetails')}</Typography> <Typography variant='h2'>{globalize.translate('ItemDetails')}</Typography>
<FormControl fullWidth> <FormControl fullWidth>
<FormControlLabel <FormControlLabel
aria-describedby='display-settings-item-details-banner-description' aria-describedby='display-settings-item-details-banner-description'
control={<Checkbox />} control={
<Checkbox
checked={values.enableItemDetailsBanner}
onChange={onChange}
/>
}
label={globalize.translate('EnableDetailsBanner')} label={globalize.translate('EnableDetailsBanner')}
name='enableItemDetailsBanner'
/> />
<FormHelperText id='display-settings-item-details-banner-description'> <FormHelperText id='display-settings-item-details-banner-description'>
{globalize.translate('EnableDetailsBannerHelp')} {globalize.translate('EnableDetailsBannerHelp')}

View file

@ -8,10 +8,16 @@ import Typography from '@mui/material/Typography';
import React from 'react'; import React from 'react';
import globalize from 'scripts/globalize'; import globalize from 'scripts/globalize';
import { DisplaySettingsValues } from './types';
export function LibraryPreferences() { interface LibraryPreferencesProps {
onChange: (event: React.SyntheticEvent) => void;
values: DisplaySettingsValues;
}
export function LibraryPreferences({ onChange, values }: Readonly<LibraryPreferencesProps>) {
return ( return (
<Stack spacing={3}> <Stack spacing={2}>
<Typography variant='h2'>{globalize.translate('HeaderLibraries')}</Typography> <Typography variant='h2'>{globalize.translate('HeaderLibraries')}</Typography>
<FormControl fullWidth> <FormControl fullWidth>
@ -26,7 +32,10 @@ export function LibraryPreferences() {
required: true, required: true,
step: '1' step: '1'
}} }}
defaultValue={values.libraryPageSize}
label={globalize.translate('LabelLibraryPageSize')} label={globalize.translate('LabelLibraryPageSize')}
name='libraryPageSize'
onChange={onChange}
/> />
<FormHelperText id='display-settings-lib-pagesize-description'> <FormHelperText id='display-settings-lib-pagesize-description'>
{globalize.translate('LabelLibraryPageSizeHelp')} {globalize.translate('LabelLibraryPageSizeHelp')}
@ -36,8 +45,14 @@ export function LibraryPreferences() {
<FormControl fullWidth> <FormControl fullWidth>
<FormControlLabel <FormControlLabel
aria-describedby='display-settings-lib-backdrops-description' aria-describedby='display-settings-lib-backdrops-description'
control={<Checkbox />} control={
<Checkbox
checked={values.enableLibraryBackdrops}
onChange={onChange}
/>
}
label={globalize.translate('Backdrops')} label={globalize.translate('Backdrops')}
name='enableLibraryBackdrops'
/> />
<FormHelperText id='display-settings-lib-backdrops-description'> <FormHelperText id='display-settings-lib-backdrops-description'>
{globalize.translate('EnableBackdropsHelp')} {globalize.translate('EnableBackdropsHelp')}
@ -47,8 +62,14 @@ export function LibraryPreferences() {
<FormControl fullWidth> <FormControl fullWidth>
<FormControlLabel <FormControlLabel
aria-describedby='display-settings-lib-theme-songs-description' aria-describedby='display-settings-lib-theme-songs-description'
control={<Checkbox />} control={
<Checkbox
checked={values.enableLibraryThemeSongs}
onChange={onChange}
/>
}
label={globalize.translate('ThemeSongs')} label={globalize.translate('ThemeSongs')}
name='enableLibraryThemeSongs'
/> />
<FormHelperText id='display-settings-lib-theme-songs-description'> <FormHelperText id='display-settings-lib-theme-songs-description'>
{globalize.translate('EnableThemeSongsHelp')} {globalize.translate('EnableThemeSongsHelp')}
@ -58,8 +79,14 @@ export function LibraryPreferences() {
<FormControl fullWidth> <FormControl fullWidth>
<FormControlLabel <FormControlLabel
aria-describedby='display-settings-lib-theme-videos-description' aria-describedby='display-settings-lib-theme-videos-description'
control={<Checkbox />} control={
<Checkbox
checked={values.enableLibraryThemeVideos}
onChange={onChange}
/>
}
label={globalize.translate('ThemeVideos')} label={globalize.translate('ThemeVideos')}
name='enableLibraryThemeVideos'
/> />
<FormHelperText id='display-settings-lib-theme-videos-description'> <FormHelperText id='display-settings-lib-theme-videos-description'>
{globalize.translate('EnableThemeVideosHelp')} {globalize.translate('EnableThemeVideosHelp')}
@ -69,8 +96,14 @@ export function LibraryPreferences() {
<FormControl fullWidth> <FormControl fullWidth>
<FormControlLabel <FormControlLabel
aria-describedby='display-settings-show-missing-episodes-description' aria-describedby='display-settings-show-missing-episodes-description'
control={<Checkbox />} control={
<Checkbox
checked={values.displayMissingEpisodes}
onChange={onChange}
/>
}
label={globalize.translate('DisplayMissingEpisodesWithinSeasons')} label={globalize.translate('DisplayMissingEpisodesWithinSeasons')}
name='displayMissingEpisodes'
/> />
<FormHelperText id='display-settings-show-missing-episodes-description'> <FormHelperText id='display-settings-show-missing-episodes-description'>
{globalize.translate('DisplayMissingEpisodesWithinSeasonsHelp')} {globalize.translate('DisplayMissingEpisodesWithinSeasonsHelp')}

View file

@ -2,23 +2,34 @@ import FormControl from '@mui/material/FormControl';
import FormHelperText from '@mui/material/FormHelperText'; import FormHelperText from '@mui/material/FormHelperText';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import Select from '@mui/material/Select'; import Select, { SelectChangeEvent } from '@mui/material/Select';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import React from 'react'; import React from 'react';
import globalize from 'scripts/globalize'; import globalize from 'scripts/globalize';
import { DATE_LOCALE_OPTIONS, LANGUAGE_OPTIONS } from './constants'; import { DATE_LOCALE_OPTIONS, LANGUAGE_OPTIONS } from './constants';
import { DisplaySettingsValues } from './types';
export function LocalizationPreferences() { interface LocalizationPreferencesProps {
onChange: (event: SelectChangeEvent) => void;
values: DisplaySettingsValues;
}
export function LocalizationPreferences({ onChange, values }: Readonly<LocalizationPreferencesProps>) {
return ( return (
<Stack spacing={3}> <Stack spacing={2}>
<Typography variant='h2'>{globalize.translate('Localization')}</Typography> <Typography variant='h2'>{globalize.translate('Localization')}</Typography>
<FormControl fullWidth> <FormControl fullWidth>
<Select <Select
aria-describedby='display-settings-language-description' aria-describedby='display-settings-language-description'
inputProps={{
name: 'language'
}}
label={globalize.translate('LabelDisplayLanguage')} label={globalize.translate('LabelDisplayLanguage')}
onChange={onChange}
value={values.language}
> >
{ ...LANGUAGE_OPTIONS.map(({ value, label }) => ( { ...LANGUAGE_OPTIONS.map(({ value, label }) => (
<MenuItem key={value } value={value}>{ label }</MenuItem> <MenuItem key={value } value={value}>{ label }</MenuItem>
@ -37,7 +48,14 @@ export function LocalizationPreferences() {
</FormControl> </FormControl>
<FormControl fullWidth> <FormControl fullWidth>
<Select label={globalize.translate('LabelDateTimeLocale')}> <Select
inputProps={{
name: 'dateTimeLocale'
}}
label={globalize.translate('LabelDateTimeLocale')}
onChange={onChange}
value={values.dateTimeLocale}
>
{...DATE_LOCALE_OPTIONS.map(({ value, label }) => ( {...DATE_LOCALE_OPTIONS.map(({ value, label }) => (
<MenuItem key={value} value={value}>{label}</MenuItem> <MenuItem key={value} value={value}>{label}</MenuItem>
))} ))}

View file

@ -8,15 +8,22 @@ import Typography from '@mui/material/Typography';
import React from 'react'; import React from 'react';
import globalize from 'scripts/globalize'; import globalize from 'scripts/globalize';
import { DisplaySettingsValues } from './types';
export function NextUpPreferences() { interface NextUpPreferencesProps {
onChange: (event: React.SyntheticEvent) => void;
values: DisplaySettingsValues;
}
export function NextUpPreferences({ onChange, values }: Readonly<NextUpPreferencesProps>) {
return ( return (
<Stack spacing={3}> <Stack spacing={2}>
<Typography variant='h2'>{globalize.translate('NextUp')}</Typography> <Typography variant='h2'>{globalize.translate('NextUp')}</Typography>
<FormControl fullWidth> <FormControl fullWidth>
<TextField <TextField
aria-describedby='display-settings-max-days-next-up-description' aria-describedby='display-settings-max-days-next-up-description'
defaultValue={values.maxDaysForNextUp}
inputProps={{ inputProps={{
type: 'number', type: 'number',
inputMode: 'numeric', inputMode: 'numeric',
@ -27,6 +34,8 @@ export function NextUpPreferences() {
step: '1' step: '1'
}} }}
label={globalize.translate('LabelMaxDaysForNextUp')} label={globalize.translate('LabelMaxDaysForNextUp')}
name='maxDaysForNextUp'
onChange={onChange}
/> />
<FormHelperText id='display-settings-max-days-next-up-description'> <FormHelperText id='display-settings-max-days-next-up-description'>
{globalize.translate('LabelMaxDaysForNextUpHelp')} {globalize.translate('LabelMaxDaysForNextUpHelp')}
@ -36,8 +45,14 @@ export function NextUpPreferences() {
<FormControl fullWidth> <FormControl fullWidth>
<FormControlLabel <FormControlLabel
aria-describedby='display-settings-next-up-rewatching-description' aria-describedby='display-settings-next-up-rewatching-description'
control={<Checkbox />} control={
<Checkbox
checked={values.enableRewatchingInNextUp}
onChange={onChange}
/>
}
label={globalize.translate('EnableRewatchingNextUp')} label={globalize.translate('EnableRewatchingNextUp')}
name='enableRewatchingInNextUp'
/> />
<FormHelperText id='display-settings-next-up-rewatching-description'> <FormHelperText id='display-settings-next-up-rewatching-description'>
{globalize.translate('EnableRewatchingNextUpHelp')} {globalize.translate('EnableRewatchingNextUpHelp')}
@ -47,8 +62,14 @@ export function NextUpPreferences() {
<FormControl fullWidth> <FormControl fullWidth>
<FormControlLabel <FormControlLabel
aria-describedby='display-settings-next-up-images-description' aria-describedby='display-settings-next-up-images-description'
control={<Checkbox />} control={
<Checkbox
checked={values.episodeImagesInNextUp}
onChange={onChange}
/>
}
label={globalize.translate('UseEpisodeImagesInNextUp')} label={globalize.translate('UseEpisodeImagesInNextUp')}
name='episodeImagesInNextUp'
/> />
<FormHelperText id='display-settings-next-up-images-description'> <FormHelperText id='display-settings-next-up-images-description'>
{globalize.translate('UseEpisodeImagesInNextUpHelp')} {globalize.translate('UseEpisodeImagesInNextUpHelp')}

View file

@ -0,0 +1,46 @@
import { useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import toast from 'components/toast/toast';
import globalize from 'scripts/globalize';
import { DisplaySettingsValues } from '../types';
import { useDisplaySettings } from './useDisplaySettings';
export function useDisplaySettingForm() {
const [urlParams] = useSearchParams();
const {
displaySettings,
loading,
saveDisplaySettings
} = useDisplaySettings({ userId: urlParams.get('userId') });
const [formValues, setFormValues] = useState<DisplaySettingsValues>();
useEffect(() => {
if (!loading && displaySettings && !formValues) {
setFormValues(displaySettings);
}
}, [formValues, loading, displaySettings]);
const updateField = useCallback(({ name, value }) => {
if (formValues) {
setFormValues({
...formValues,
[name]: value
});
}
}, [formValues, setFormValues]);
const submitChanges = useCallback(async () => {
if (formValues) {
await saveDisplaySettings(formValues);
toast(globalize.translate('SettingsSaved'));
}
}, [formValues, saveDisplaySettings]);
return {
loading,
values: formValues,
submitChanges,
updateField
};
}

View file

@ -0,0 +1,159 @@
import { UserDto } from '@jellyfin/sdk/lib/generated-client';
import { ApiClient } from 'jellyfin-apiclient';
import { useCallback, useEffect, useState } from 'react';
import { appHost } from 'components/apphost';
import layoutManager from 'components/layoutManager';
import { useApi } from 'hooks/useApi';
import themeManager from 'scripts/themeManager';
import { currentSettings, UserSettings } from 'scripts/settings/userSettings';
import { DisplaySettingsValues } from '../types';
interface UseDisplaySettingsParams {
userId?: string | null;
}
export function useDisplaySettings({ userId }: UseDisplaySettingsParams) {
const [loading, setLoading] = useState(true);
const [userSettings, setUserSettings] = useState<UserSettings>();
const [displaySettings, setDisplaySettings] = useState<DisplaySettingsValues>();
const { __legacyApiClient__, user: currentUser } = useApi();
useEffect(() => {
if (!userId || !currentUser || !__legacyApiClient__) {
return;
}
setLoading(true);
void (async () => {
const loadedSettings = await loadDisplaySettings({ api: __legacyApiClient__, currentUser, userId });
setDisplaySettings(loadedSettings.displaySettings);
setUserSettings(loadedSettings.userSettings);
setLoading(false);
})();
return () => {
setLoading(false);
};
}, [__legacyApiClient__, currentUser, userId]);
const saveSettings = useCallback(async (newSettings: DisplaySettingsValues) => {
if (!userId || !userSettings || !__legacyApiClient__) {
return;
}
return saveDisplaySettings({
api: __legacyApiClient__,
newDisplaySettings: newSettings,
userSettings,
userId
});
}, [__legacyApiClient__, userSettings, userId]);
return {
displaySettings,
loading,
saveDisplaySettings: saveSettings
};
}
interface LoadDisplaySettingsParams {
currentUser: UserDto;
userId?: string;
api: ApiClient;
}
async function loadDisplaySettings({
currentUser,
userId,
api
}: LoadDisplaySettingsParams) {
const settings = (!userId || userId === currentUser?.Id) ? currentSettings : new UserSettings();
const user = (!userId || userId === currentUser?.Id) ? currentUser : await api.getUser(userId);
await settings.setUserInfo(userId, api);
const displaySettings = {
customCss: settings.customCss(),
dashboardTheme: settings.dashboardTheme() || 'auto',
dateTimeLocale: settings.dateTimeLocale() || 'auto',
disableCustomCss: Boolean(settings.disableCustomCss()),
displayMissingEpisodes: user?.Configuration?.DisplayMissingEpisodes ?? false,
enableBlurHash: Boolean(settings.enableBlurhash()),
enableFasterAnimation: Boolean(settings.enableFastFadein()),
enableItemDetailsBanner: Boolean(settings.detailsBanner()),
enableLibraryBackdrops: Boolean(settings.enableBackdrops()),
enableLibraryThemeSongs: Boolean(settings.enableThemeSongs()),
enableLibraryThemeVideos: Boolean(settings.enableThemeVideos()),
enableRewatchingInNextUp: Boolean(settings.enableRewatchingInNextUp()),
episodeImagesInNextUp: Boolean(settings.useEpisodeImagesInNextUpAndResume()),
language: settings.language() || 'auto',
layout: layoutManager.getSavedLayout() || 'auto',
libraryPageSize: settings.libraryPageSize(),
maxDaysForNextUp: settings.maxDaysForNextUp(),
screensaver: settings.screensaver() || 'none',
screensaverInterval: settings.backdropScreensaverInterval(),
theme: settings.theme()
};
return {
displaySettings,
userSettings: settings
};
}
interface SaveDisplaySettingsParams {
api: ApiClient;
newDisplaySettings: DisplaySettingsValues
userSettings: UserSettings;
userId: string;
}
async function saveDisplaySettings({
api,
newDisplaySettings,
userSettings,
userId
}: SaveDisplaySettingsParams) {
const user = await api.getUser(userId);
if (appHost.supports('displaylanguage')) {
userSettings.language(normalizeValue(newDisplaySettings.language));
}
userSettings.customCss(normalizeValue(newDisplaySettings.customCss));
userSettings.dashboardTheme(normalizeValue(newDisplaySettings.dashboardTheme));
userSettings.dateTimeLocale(normalizeValue(newDisplaySettings.dateTimeLocale));
userSettings.disableCustomCss(newDisplaySettings.disableCustomCss);
userSettings.enableBlurhash(newDisplaySettings.enableBlurHash);
userSettings.enableFastFadein(newDisplaySettings.enableFasterAnimation);
userSettings.detailsBanner(newDisplaySettings.enableItemDetailsBanner);
userSettings.enableBackdrops(newDisplaySettings.enableLibraryBackdrops);
userSettings.enableThemeSongs(newDisplaySettings.enableLibraryThemeSongs);
userSettings.enableThemeVideos(newDisplaySettings.enableLibraryThemeVideos);
userSettings.enableRewatchingInNextUp(newDisplaySettings.enableRewatchingInNextUp);
userSettings.useEpisodeImagesInNextUpAndResume(newDisplaySettings.episodeImagesInNextUp);
userSettings.libraryPageSize(newDisplaySettings.libraryPageSize);
userSettings.maxDaysForNextUp(newDisplaySettings.maxDaysForNextUp);
userSettings.screensaver(normalizeValue(newDisplaySettings.screensaver));
userSettings.backdropScreensaverInterval(newDisplaySettings.screensaverInterval);
userSettings.theme(newDisplaySettings.theme);
layoutManager.setLayout(normalizeValue(newDisplaySettings.layout));
const promises = [
themeManager.setTheme(userSettings.theme())
];
if (user.Id && user.Configuration) {
user.Configuration.DisplayMissingEpisodes = newDisplaySettings.displayMissingEpisodes;
promises.push(api.updateUserConfiguration(user.Id, user.Configuration));
}
await Promise.all(promises);
}
function normalizeValue(value: string) {
return /^(auto|none)$/.test(value) ? '' : value;
}

View file

@ -0,0 +1,29 @@
import { useMemo } from 'react';
import { pluginManager } from 'components/pluginManager';
import { Plugin, PluginType } from 'types/plugin';
import globalize from 'scripts/globalize';
export function useScreensavers() {
const screensavers = useMemo<Plugin[]>(() => {
const installedScreensaverPlugins = pluginManager
.ofType(PluginType.Screensaver)
.map((plugin: Plugin) => ({
...plugin,
name: globalize.translate(plugin.name) as string
}));
return [
{
id: 'none',
name: globalize.translate('None') as string,
type: PluginType.Screensaver
},
...installedScreensaverPlugins
];
}, []);
return {
screensavers: screensavers ?? []
};
}

View file

@ -0,0 +1,32 @@
import { useEffect, useMemo, useState } from 'react';
import themeManager from 'scripts/themeManager';
import { Theme } from 'types/webConfig';
export function useServerThemes() {
const [themes, setThemes] = useState<Theme[]>();
useEffect(() => {
async function getServerThemes() {
const loadedThemes = await themeManager.getThemes();
setThemes(loadedThemes ?? []);
}
if (!themes) {
void getServerThemes();
}
// We've intentionally left the dependency array here to ensure that the effect happens only once.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const defaultTheme = useMemo(() => {
if (!themes) return null;
return themes.find((theme) => theme.default);
}, [themes]);
return {
themes: themes ?? [],
defaultTheme
};
}

View file

@ -1,16 +1,50 @@
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { SelectChangeEvent } from '@mui/material/Select';
import Stack from '@mui/material/Stack'; import Stack from '@mui/material/Stack';
import React from 'react'; import React, { useCallback } from 'react';
import Page from 'components/Page'; import Page from 'components/Page';
import globalize from 'scripts/globalize'; import globalize from 'scripts/globalize';
import theme from 'themes/theme'; import theme from 'themes/theme';
import { DisplayPreferences } from './DisplayPreferences'; import { DisplayPreferences } from './DisplayPreferences';
import { ItemDetailPreferences } from './ItemDetailPreferences';
import { LibraryPreferences } from './LibraryPreferences'; import { LibraryPreferences } from './LibraryPreferences';
import { LocalizationPreferences } from './LocalizationPreferences'; import { LocalizationPreferences } from './LocalizationPreferences';
import { NextUpPreferences } from './NextUpPreferences'; import { NextUpPreferences } from './NextUpPreferences';
import { useDisplaySettingForm } from './hooks/useDisplaySettingForm';
import { DisplaySettingsValues } from './types';
import LoadingComponent from 'components/loading/LoadingComponent';
export default function UserDisplayPreferences() { export default function UserDisplayPreferences() {
const {
loading,
submitChanges,
updateField,
values
} = useDisplaySettingForm();
const handleSubmitForm = useCallback((e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
void submitChanges();
}, [submitChanges]);
const handleFieldChange = useCallback((e: SelectChangeEvent | React.SyntheticEvent) => {
const target = e.target as HTMLInputElement;
const fieldName = target.name as keyof DisplaySettingsValues;
const fieldValue = target.checked ?? target.value;
if (values?.[fieldName] !== fieldValue) {
updateField({
name: fieldName,
value: fieldValue
});
}
}, [updateField, values]);
if (loading || !values) {
return <LoadingComponent />;
}
return ( return (
<Page <Page
className='libraryPage userPreferencesPage noSecondaryNavPage' className='libraryPage userPreferencesPage noSecondaryNavPage'
@ -18,12 +52,31 @@ export default function UserDisplayPreferences() {
title={globalize.translate('Display')} title={globalize.translate('Display')}
> >
<div className='settingsContainer padded-left padded-right padded-bottom-page'> <div className='settingsContainer padded-left padded-right padded-bottom-page'>
<form style={{ margin: 'auto' }}> <form
onSubmit={handleSubmitForm}
style={{ margin: 'auto' }}
>
<Stack spacing={4}> <Stack spacing={4}>
<LocalizationPreferences /> <LocalizationPreferences
<DisplayPreferences /> onChange={handleFieldChange}
<LibraryPreferences /> values={values}
<NextUpPreferences /> />
<DisplayPreferences
onChange={handleFieldChange}
values={values}
/>
<LibraryPreferences
onChange={handleFieldChange}
values={values}
/>
<NextUpPreferences
onChange={handleFieldChange}
values={values}
/>
<ItemDetailPreferences
onChange={handleFieldChange}
values={values}
/>
<Button <Button
type='submit' type='submit'

View file

@ -0,0 +1,22 @@
export interface DisplaySettingsValues {
customCss: string;
dashboardTheme: string;
dateTimeLocale: string;
disableCustomCss: boolean;
displayMissingEpisodes: boolean;
enableBlurHash: boolean;
enableFasterAnimation: boolean;
enableItemDetailsBanner: boolean;
enableLibraryBackdrops: boolean;
enableLibraryThemeSongs: boolean;
enableLibraryThemeVideos: boolean;
enableRewatchingInNextUp: boolean;
episodeImagesInNextUp: boolean;
language: string;
layout: string;
libraryPageSize: number;
maxDaysForNextUp: number;
screensaver: string;
screensaverInterval: number;
theme: string;
}

View file

@ -199,7 +199,7 @@ export class UserSettings {
/** /**
* Get or set 'Theme Songs' state. * Get or set 'Theme Songs' state.
* @param {boolean|undefined} val - Flag to enable 'Theme Songs' or undefined. * @param {boolean|undefined} [val] - Flag to enable 'Theme Songs' or undefined.
* @return {boolean} 'Theme Songs' state. * @return {boolean} 'Theme Songs' state.
*/ */
enableThemeSongs(val) { enableThemeSongs(val) {
@ -212,7 +212,7 @@ export class UserSettings {
/** /**
* Get or set 'Theme Videos' state. * Get or set 'Theme Videos' state.
* @param {boolean|undefined} val - Flag to enable 'Theme Videos' or undefined. * @param {boolean|undefined} [val] - Flag to enable 'Theme Videos' or undefined.
* @return {boolean} 'Theme Videos' state. * @return {boolean} 'Theme Videos' state.
*/ */
enableThemeVideos(val) { enableThemeVideos(val) {
@ -225,7 +225,7 @@ export class UserSettings {
/** /**
* Get or set 'Fast Fade-in' state. * Get or set 'Fast Fade-in' state.
* @param {boolean|undefined} val - Flag to enable 'Fast Fade-in' or undefined. * @param {boolean|undefined} [val] - Flag to enable 'Fast Fade-in' or undefined.
* @return {boolean} 'Fast Fade-in' state. * @return {boolean} 'Fast Fade-in' state.
*/ */
enableFastFadein(val) { enableFastFadein(val) {
@ -238,7 +238,7 @@ export class UserSettings {
/** /**
* Get or set 'Blurhash' state. * Get or set 'Blurhash' state.
* @param {boolean|undefined} val - Flag to enable 'Blurhash' or undefined. * @param {boolean|undefined} [val] - Flag to enable 'Blurhash' or undefined.
* @return {boolean} 'Blurhash' state. * @return {boolean} 'Blurhash' state.
*/ */
enableBlurhash(val) { enableBlurhash(val) {
@ -251,7 +251,7 @@ export class UserSettings {
/** /**
* Get or set 'Backdrops' state. * Get or set 'Backdrops' state.
* @param {boolean|undefined} val - Flag to enable 'Backdrops' or undefined. * @param {boolean|undefined} [val] - Flag to enable 'Backdrops' or undefined.
* @return {boolean} 'Backdrops' state. * @return {boolean} 'Backdrops' state.
*/ */
enableBackdrops(val) { enableBackdrops(val) {
@ -264,7 +264,7 @@ export class UserSettings {
/** /**
* Get or set 'disableCustomCss' state. * Get or set 'disableCustomCss' state.
* @param {boolean|undefined} val - Flag to enable 'disableCustomCss' or undefined. * @param {boolean|undefined} [val] - Flag to enable 'disableCustomCss' or undefined.
* @return {boolean} 'disableCustomCss' state. * @return {boolean} 'disableCustomCss' state.
*/ */
disableCustomCss(val) { disableCustomCss(val) {
@ -277,7 +277,7 @@ export class UserSettings {
/** /**
* Get or set customCss. * Get or set customCss.
* @param {string|undefined} val - Language. * @param {string|undefined} [val] - Language.
* @return {string} Language. * @return {string} Language.
*/ */
customCss(val) { customCss(val) {
@ -290,7 +290,7 @@ export class UserSettings {
/** /**
* Get or set 'Details Banner' state. * Get or set 'Details Banner' state.
* @param {boolean|undefined} val - Flag to enable 'Details Banner' or undefined. * @param {boolean|undefined} [val] - Flag to enable 'Details Banner' or undefined.
* @return {boolean} 'Details Banner' state. * @return {boolean} 'Details Banner' state.
*/ */
detailsBanner(val) { detailsBanner(val) {
@ -316,7 +316,7 @@ export class UserSettings {
/** /**
* Get or set language. * Get or set language.
* @param {string|undefined} val - Language. * @param {string|undefined} [val] - Language.
* @return {string} Language. * @return {string} Language.
*/ */
language(val) { language(val) {
@ -329,7 +329,7 @@ export class UserSettings {
/** /**
* Get or set datetime locale. * Get or set datetime locale.
* @param {string|undefined} val - Datetime locale. * @param {string|undefined} [val] - Datetime locale.
* @return {string} Datetime locale. * @return {string} Datetime locale.
*/ */
dateTimeLocale(val) { dateTimeLocale(val) {
@ -368,7 +368,7 @@ export class UserSettings {
/** /**
* Get or set theme for Dashboard. * Get or set theme for Dashboard.
* @param {string|undefined} val - Theme for Dashboard. * @param {string|undefined} [val] - Theme for Dashboard.
* @return {string} Theme for Dashboard. * @return {string} Theme for Dashboard.
*/ */
dashboardTheme(val) { dashboardTheme(val) {
@ -394,7 +394,7 @@ export class UserSettings {
/** /**
* Get or set main theme. * Get or set main theme.
* @param {string|undefined} val - Main theme. * @param {string|undefined} [val] - Main theme.
* @return {string} Main theme. * @return {string} Main theme.
*/ */
theme(val) { theme(val) {
@ -407,7 +407,7 @@ export class UserSettings {
/** /**
* Get or set screensaver. * Get or set screensaver.
* @param {string|undefined} val - Screensaver. * @param {string|undefined} [val] - Screensaver.
* @return {string} Screensaver. * @return {string} Screensaver.
*/ */
screensaver(val) { screensaver(val) {
@ -420,7 +420,7 @@ export class UserSettings {
/** /**
* Get or set the interval between backdrops when using the backdrop screensaver. * Get or set the interval between backdrops when using the backdrop screensaver.
* @param {number|undefined} val - The interval between backdrops in seconds. * @param {number|undefined} [val] - The interval between backdrops in seconds.
* @return {number} The interval between backdrops in seconds. * @return {number} The interval between backdrops in seconds.
*/ */
backdropScreensaverInterval(val) { backdropScreensaverInterval(val) {
@ -433,7 +433,7 @@ export class UserSettings {
/** /**
* Get or set library page size. * Get or set library page size.
* @param {number|undefined} val - Library page size. * @param {number|undefined} [val] - Library page size.
* @return {number} Library page size. * @return {number} Library page size.
*/ */
libraryPageSize(val) { libraryPageSize(val) {

View file

@ -1,5 +1,6 @@
interface Theme { export interface Theme {
name: string name: string
default?: boolean;
id: string id: string
color: string color: string
} }