mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2024-11-15 18:08:17 -07:00
feat: (preferences) hook react display settings into user settings
This commit is contained in:
parent
ce4c7aed5e
commit
3dd26c7785
@ -3,15 +3,26 @@ import FormControl from '@mui/material/FormControl';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
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 TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import React from 'react';
|
||||
|
||||
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 (
|
||||
<Stack spacing={2}>
|
||||
<Typography variant='h2'>{globalize.translate('Display')}</Typography>
|
||||
@ -19,7 +30,12 @@ export function DisplayPreferences() {
|
||||
<FormControl fullWidth>
|
||||
<Select
|
||||
aria-describedby='display-settings-layout-description'
|
||||
inputProps={{
|
||||
name: 'layout'
|
||||
}}
|
||||
label={globalize.translate('LabelDisplayMode')}
|
||||
onChange={onChange}
|
||||
value={values.layout}
|
||||
>
|
||||
<MenuItem value='auto'>{globalize.translate('Auto')}</MenuItem>
|
||||
<MenuItem value='desktop'>{globalize.translate('Desktop')}</MenuItem>
|
||||
@ -34,15 +50,31 @@ export function DisplayPreferences() {
|
||||
</FormControl>
|
||||
|
||||
<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>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
aria-describedby='display-settings-disable-css-description'
|
||||
control={<Checkbox />}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.disableCustomCss}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('DisableCustomCss')}
|
||||
name='disableCustomCss'
|
||||
/>
|
||||
<FormHelperText id='display-settings-disable-css-description'>
|
||||
{globalize.translate('LabelDisableCustomCss')}
|
||||
@ -52,8 +84,11 @@ export function DisplayPreferences() {
|
||||
<FormControl fullWidth>
|
||||
<TextField
|
||||
aria-describedby='display-settings-custom-css-description'
|
||||
defaultValue={values.customCss}
|
||||
label={globalize.translate('LabelCustomCss')}
|
||||
multiline
|
||||
name='customCss'
|
||||
onChange={onChange}
|
||||
/>
|
||||
<FormHelperText id='display-settings-custom-css-description'>
|
||||
{globalize.translate('LabelLocalCustomCss')}
|
||||
@ -64,7 +99,18 @@ export function DisplayPreferences() {
|
||||
{/* Server Dashboard Theme */}
|
||||
|
||||
<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>
|
||||
|
||||
{/* TODO: There are some extra options here related to screensavers */}
|
||||
@ -72,8 +118,14 @@ export function DisplayPreferences() {
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
aria-describedby='display-settings-faster-animations-description'
|
||||
control={<Checkbox />}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.enableFasterAnimation}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('EnableFasterAnimations')}
|
||||
name='enableFasterAnimation'
|
||||
/>
|
||||
<FormHelperText id='display-settings-faster-animations-description'>
|
||||
{globalize.translate('EnableFasterAnimationsHelp')}
|
||||
@ -83,8 +135,14 @@ export function DisplayPreferences() {
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
aria-describedby='display-settings-blurhash-description'
|
||||
control={<Checkbox />}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.enableBlurHash}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('EnableBlurHash')}
|
||||
name='enableBlurHash'
|
||||
/>
|
||||
<FormHelperText id='display-settings-blurhash-description'>
|
||||
{globalize.translate('EnableBlurHashHelp')}
|
||||
|
@ -7,17 +7,29 @@ import Typography from '@mui/material/Typography';
|
||||
import React from 'react';
|
||||
|
||||
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 (
|
||||
<Stack spacing={3}>
|
||||
<Stack spacing={2}>
|
||||
<Typography variant='h2'>{globalize.translate('ItemDetails')}</Typography>
|
||||
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
aria-describedby='display-settings-item-details-banner-description'
|
||||
control={<Checkbox />}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.enableItemDetailsBanner}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('EnableDetailsBanner')}
|
||||
name='enableItemDetailsBanner'
|
||||
/>
|
||||
<FormHelperText id='display-settings-item-details-banner-description'>
|
||||
{globalize.translate('EnableDetailsBannerHelp')}
|
||||
|
@ -8,10 +8,16 @@ import Typography from '@mui/material/Typography';
|
||||
import React from 'react';
|
||||
|
||||
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 (
|
||||
<Stack spacing={3}>
|
||||
<Stack spacing={2}>
|
||||
<Typography variant='h2'>{globalize.translate('HeaderLibraries')}</Typography>
|
||||
|
||||
<FormControl fullWidth>
|
||||
@ -26,7 +32,10 @@ export function LibraryPreferences() {
|
||||
required: true,
|
||||
step: '1'
|
||||
}}
|
||||
defaultValue={values.libraryPageSize}
|
||||
label={globalize.translate('LabelLibraryPageSize')}
|
||||
name='libraryPageSize'
|
||||
onChange={onChange}
|
||||
/>
|
||||
<FormHelperText id='display-settings-lib-pagesize-description'>
|
||||
{globalize.translate('LabelLibraryPageSizeHelp')}
|
||||
@ -36,8 +45,14 @@ export function LibraryPreferences() {
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
aria-describedby='display-settings-lib-backdrops-description'
|
||||
control={<Checkbox />}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.enableLibraryBackdrops}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('Backdrops')}
|
||||
name='enableLibraryBackdrops'
|
||||
/>
|
||||
<FormHelperText id='display-settings-lib-backdrops-description'>
|
||||
{globalize.translate('EnableBackdropsHelp')}
|
||||
@ -47,8 +62,14 @@ export function LibraryPreferences() {
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
aria-describedby='display-settings-lib-theme-songs-description'
|
||||
control={<Checkbox />}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.enableLibraryThemeSongs}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('ThemeSongs')}
|
||||
name='enableLibraryThemeSongs'
|
||||
/>
|
||||
<FormHelperText id='display-settings-lib-theme-songs-description'>
|
||||
{globalize.translate('EnableThemeSongsHelp')}
|
||||
@ -58,8 +79,14 @@ export function LibraryPreferences() {
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
aria-describedby='display-settings-lib-theme-videos-description'
|
||||
control={<Checkbox />}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.enableLibraryThemeVideos}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('ThemeVideos')}
|
||||
name='enableLibraryThemeVideos'
|
||||
/>
|
||||
<FormHelperText id='display-settings-lib-theme-videos-description'>
|
||||
{globalize.translate('EnableThemeVideosHelp')}
|
||||
@ -69,8 +96,14 @@ export function LibraryPreferences() {
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
aria-describedby='display-settings-show-missing-episodes-description'
|
||||
control={<Checkbox />}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.displayMissingEpisodes}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('DisplayMissingEpisodesWithinSeasons')}
|
||||
name='displayMissingEpisodes'
|
||||
/>
|
||||
<FormHelperText id='display-settings-show-missing-episodes-description'>
|
||||
{globalize.translate('DisplayMissingEpisodesWithinSeasonsHelp')}
|
||||
|
@ -2,23 +2,34 @@ import FormControl from '@mui/material/FormControl';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import Link from '@mui/material/Link';
|
||||
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 Typography from '@mui/material/Typography';
|
||||
import React from 'react';
|
||||
|
||||
import globalize from 'scripts/globalize';
|
||||
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 (
|
||||
<Stack spacing={3}>
|
||||
<Stack spacing={2}>
|
||||
<Typography variant='h2'>{globalize.translate('Localization')}</Typography>
|
||||
|
||||
<FormControl fullWidth>
|
||||
<Select
|
||||
aria-describedby='display-settings-language-description'
|
||||
inputProps={{
|
||||
name: 'language'
|
||||
}}
|
||||
label={globalize.translate('LabelDisplayLanguage')}
|
||||
onChange={onChange}
|
||||
value={values.language}
|
||||
>
|
||||
{ ...LANGUAGE_OPTIONS.map(({ value, label }) => (
|
||||
<MenuItem key={value } value={value}>{ label }</MenuItem>
|
||||
@ -37,7 +48,14 @@ export function LocalizationPreferences() {
|
||||
</FormControl>
|
||||
|
||||
<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 }) => (
|
||||
<MenuItem key={value} value={value}>{label}</MenuItem>
|
||||
))}
|
||||
|
@ -8,15 +8,22 @@ import Typography from '@mui/material/Typography';
|
||||
import React from 'react';
|
||||
|
||||
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 (
|
||||
<Stack spacing={3}>
|
||||
<Stack spacing={2}>
|
||||
<Typography variant='h2'>{globalize.translate('NextUp')}</Typography>
|
||||
|
||||
<FormControl fullWidth>
|
||||
<TextField
|
||||
aria-describedby='display-settings-max-days-next-up-description'
|
||||
defaultValue={values.maxDaysForNextUp}
|
||||
inputProps={{
|
||||
type: 'number',
|
||||
inputMode: 'numeric',
|
||||
@ -27,6 +34,8 @@ export function NextUpPreferences() {
|
||||
step: '1'
|
||||
}}
|
||||
label={globalize.translate('LabelMaxDaysForNextUp')}
|
||||
name='maxDaysForNextUp'
|
||||
onChange={onChange}
|
||||
/>
|
||||
<FormHelperText id='display-settings-max-days-next-up-description'>
|
||||
{globalize.translate('LabelMaxDaysForNextUpHelp')}
|
||||
@ -36,8 +45,14 @@ export function NextUpPreferences() {
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
aria-describedby='display-settings-next-up-rewatching-description'
|
||||
control={<Checkbox />}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.enableRewatchingInNextUp}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('EnableRewatchingNextUp')}
|
||||
name='enableRewatchingInNextUp'
|
||||
/>
|
||||
<FormHelperText id='display-settings-next-up-rewatching-description'>
|
||||
{globalize.translate('EnableRewatchingNextUpHelp')}
|
||||
@ -47,8 +62,14 @@ export function NextUpPreferences() {
|
||||
<FormControl fullWidth>
|
||||
<FormControlLabel
|
||||
aria-describedby='display-settings-next-up-images-description'
|
||||
control={<Checkbox />}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={values.episodeImagesInNextUp}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('UseEpisodeImagesInNextUp')}
|
||||
name='episodeImagesInNextUp'
|
||||
/>
|
||||
<FormHelperText id='display-settings-next-up-images-description'>
|
||||
{globalize.translate('UseEpisodeImagesInNextUpHelp')}
|
||||
|
@ -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
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
@ -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 ?? []
|
||||
};
|
||||
}
|
@ -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
|
||||
};
|
||||
}
|
@ -1,16 +1,50 @@
|
||||
import Button from '@mui/material/Button';
|
||||
import { SelectChangeEvent } from '@mui/material/Select';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import Page from 'components/Page';
|
||||
import globalize from 'scripts/globalize';
|
||||
import theme from 'themes/theme';
|
||||
import { DisplayPreferences } from './DisplayPreferences';
|
||||
import { ItemDetailPreferences } from './ItemDetailPreferences';
|
||||
import { LibraryPreferences } from './LibraryPreferences';
|
||||
import { LocalizationPreferences } from './LocalizationPreferences';
|
||||
import { NextUpPreferences } from './NextUpPreferences';
|
||||
import { useDisplaySettingForm } from './hooks/useDisplaySettingForm';
|
||||
import { DisplaySettingsValues } from './types';
|
||||
import LoadingComponent from 'components/loading/LoadingComponent';
|
||||
|
||||
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 (
|
||||
<Page
|
||||
className='libraryPage userPreferencesPage noSecondaryNavPage'
|
||||
@ -18,12 +52,31 @@ export default function UserDisplayPreferences() {
|
||||
title={globalize.translate('Display')}
|
||||
>
|
||||
<div className='settingsContainer padded-left padded-right padded-bottom-page'>
|
||||
<form style={{ margin: 'auto' }}>
|
||||
<form
|
||||
onSubmit={handleSubmitForm}
|
||||
style={{ margin: 'auto' }}
|
||||
>
|
||||
<Stack spacing={4}>
|
||||
<LocalizationPreferences />
|
||||
<DisplayPreferences />
|
||||
<LibraryPreferences />
|
||||
<NextUpPreferences />
|
||||
<LocalizationPreferences
|
||||
onChange={handleFieldChange}
|
||||
values={values}
|
||||
/>
|
||||
<DisplayPreferences
|
||||
onChange={handleFieldChange}
|
||||
values={values}
|
||||
/>
|
||||
<LibraryPreferences
|
||||
onChange={handleFieldChange}
|
||||
values={values}
|
||||
/>
|
||||
<NextUpPreferences
|
||||
onChange={handleFieldChange}
|
||||
values={values}
|
||||
/>
|
||||
<ItemDetailPreferences
|
||||
onChange={handleFieldChange}
|
||||
values={values}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type='submit'
|
||||
|
22
src/apps/experimental/routes/user/display/types.ts
Normal file
22
src/apps/experimental/routes/user/display/types.ts
Normal 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;
|
||||
}
|
@ -199,7 +199,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
enableThemeSongs(val) {
|
||||
@ -212,7 +212,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
enableThemeVideos(val) {
|
||||
@ -225,7 +225,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
enableFastFadein(val) {
|
||||
@ -238,7 +238,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
enableBlurhash(val) {
|
||||
@ -251,7 +251,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
enableBackdrops(val) {
|
||||
@ -264,7 +264,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
disableCustomCss(val) {
|
||||
@ -277,7 +277,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* Get or set customCss.
|
||||
* @param {string|undefined} val - Language.
|
||||
* @param {string|undefined} [val] - Language.
|
||||
* @return {string} Language.
|
||||
*/
|
||||
customCss(val) {
|
||||
@ -290,7 +290,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
detailsBanner(val) {
|
||||
@ -316,7 +316,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* Get or set language.
|
||||
* @param {string|undefined} val - Language.
|
||||
* @param {string|undefined} [val] - Language.
|
||||
* @return {string} Language.
|
||||
*/
|
||||
language(val) {
|
||||
@ -329,7 +329,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* Get or set datetime locale.
|
||||
* @param {string|undefined} val - Datetime locale.
|
||||
* @param {string|undefined} [val] - Datetime locale.
|
||||
* @return {string} Datetime locale.
|
||||
*/
|
||||
dateTimeLocale(val) {
|
||||
@ -368,7 +368,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
dashboardTheme(val) {
|
||||
@ -394,7 +394,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* Get or set main theme.
|
||||
* @param {string|undefined} val - Main theme.
|
||||
* @param {string|undefined} [val] - Main theme.
|
||||
* @return {string} Main theme.
|
||||
*/
|
||||
theme(val) {
|
||||
@ -407,7 +407,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* Get or set screensaver.
|
||||
* @param {string|undefined} val - Screensaver.
|
||||
* @param {string|undefined} [val] - Screensaver.
|
||||
* @return {string} Screensaver.
|
||||
*/
|
||||
screensaver(val) {
|
||||
@ -420,7 +420,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
backdropScreensaverInterval(val) {
|
||||
@ -433,7 +433,7 @@ export class UserSettings {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
libraryPageSize(val) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
interface Theme {
|
||||
export interface Theme {
|
||||
name: string
|
||||
default?: boolean;
|
||||
id: string
|
||||
color: string
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user