diff --git a/src/apps/experimental/routes/asyncRoutes/user.ts b/src/apps/experimental/routes/asyncRoutes/user.ts index 9f0a74e272..e653bf172d 100644 --- a/src/apps/experimental/routes/asyncRoutes/user.ts +++ b/src/apps/experimental/routes/asyncRoutes/user.ts @@ -8,5 +8,6 @@ export const ASYNC_USER_ROUTES: AsyncRoute[] = [ { path: 'movies.html', page: 'movies', type: AsyncRouteType.Experimental }, { path: 'tv.html', page: 'shows', type: AsyncRouteType.Experimental }, { path: 'music.html', page: 'music', type: AsyncRouteType.Experimental }, - { path: 'livetv.html', page: 'livetv', type: AsyncRouteType.Experimental } + { path: 'livetv.html', page: 'livetv', type: AsyncRouteType.Experimental }, + { path: 'mypreferencesdisplay.html', page: 'user/display', type: AsyncRouteType.Experimental } ]; diff --git a/src/apps/experimental/routes/legacyRoutes/user.ts b/src/apps/experimental/routes/legacyRoutes/user.ts index e6e3fcdfcb..1547f68359 100644 --- a/src/apps/experimental/routes/legacyRoutes/user.ts +++ b/src/apps/experimental/routes/legacyRoutes/user.ts @@ -25,12 +25,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [ controller: 'user/controls/index', view: 'user/controls/index.html' } - }, { - path: 'mypreferencesdisplay.html', - pageProps: { - controller: 'user/display/index', - view: 'user/display/index.html' - } }, { path: 'mypreferenceshome.html', pageProps: { diff --git a/src/apps/experimental/routes/user/display/DisplayPreferences.tsx b/src/apps/experimental/routes/user/display/DisplayPreferences.tsx new file mode 100644 index 0000000000..0645edbd4f --- /dev/null +++ b/src/apps/experimental/routes/user/display/DisplayPreferences.tsx @@ -0,0 +1,203 @@ +import Checkbox from '@mui/material/Checkbox'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +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, { Fragment } from 'react'; + +import { appHost } from 'components/apphost'; +import { useApi } from 'hooks/useApi'; +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) { + const { user } = useApi(); + const { screensavers } = useScreensavers(); + const { themes } = useServerThemes(); + + return ( + + {globalize.translate('Display')} + + { appHost.supports('displaymode') && ( + + {globalize.translate('LabelDisplayMode')} + + + {globalize.translate('DisplayModeHelp')} + {globalize.translate('LabelPleaseRestart')} + + + ) } + + { themes.length > 0 && ( + + {globalize.translate('LabelTheme')} + + + ) } + + + + } + label={globalize.translate('DisableCustomCss')} + name='disableCustomCss' + /> + + {globalize.translate('LabelDisableCustomCss')} + + + + + + + {globalize.translate('LabelLocalCustomCss')} + + + + { themes.length > 0 && user?.Policy?.IsAdministrator && ( + + {globalize.translate('LabelDashboardTheme')} + + + ) } + + { screensavers.length > 0 && appHost.supports('screensaver') && ( + + + {globalize.translate('LabelScreensaver')} + + + + + + + {globalize.translate('LabelBackdropScreensaverIntervalHelp')} + + + + ) } + + + + } + label={globalize.translate('EnableFasterAnimations')} + name='enableFasterAnimation' + /> + + {globalize.translate('EnableFasterAnimationsHelp')} + + + + + + } + label={globalize.translate('EnableBlurHash')} + name='enableBlurHash' + /> + + {globalize.translate('EnableBlurHashHelp')} + + + + ); +} diff --git a/src/apps/experimental/routes/user/display/ItemDetailPreferences.tsx b/src/apps/experimental/routes/user/display/ItemDetailPreferences.tsx new file mode 100644 index 0000000000..00da9439de --- /dev/null +++ b/src/apps/experimental/routes/user/display/ItemDetailPreferences.tsx @@ -0,0 +1,40 @@ +import Checkbox from '@mui/material/Checkbox'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormHelperText from '@mui/material/FormHelperText'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import React from 'react'; + +import globalize from 'scripts/globalize'; +import { DisplaySettingsValues } from './types'; + +interface ItemDetailPreferencesProps { + onChange: (event: React.SyntheticEvent) => void; + values: DisplaySettingsValues; +} + +export function ItemDetailPreferences({ onChange, values }: Readonly) { + return ( + + {globalize.translate('ItemDetails')} + + + + } + label={globalize.translate('EnableDetailsBanner')} + name='enableItemDetailsBanner' + /> + + {globalize.translate('EnableDetailsBannerHelp')} + + + + ); +} diff --git a/src/apps/experimental/routes/user/display/LibraryPreferences.tsx b/src/apps/experimental/routes/user/display/LibraryPreferences.tsx new file mode 100644 index 0000000000..a73fa7138c --- /dev/null +++ b/src/apps/experimental/routes/user/display/LibraryPreferences.tsx @@ -0,0 +1,114 @@ +import Checkbox from '@mui/material/Checkbox'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormHelperText from '@mui/material/FormHelperText'; +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'; + +interface LibraryPreferencesProps { + onChange: (event: React.SyntheticEvent) => void; + values: DisplaySettingsValues; +} + +export function LibraryPreferences({ onChange, values }: Readonly) { + return ( + + {globalize.translate('HeaderLibraries')} + + + + + {globalize.translate('LabelLibraryPageSizeHelp')} + + + + + + } + label={globalize.translate('Backdrops')} + name='enableLibraryBackdrops' + /> + + {globalize.translate('EnableBackdropsHelp')} + + + + + + } + label={globalize.translate('ThemeSongs')} + name='enableLibraryThemeSongs' + /> + + {globalize.translate('EnableThemeSongsHelp')} + + + + + + } + label={globalize.translate('ThemeVideos')} + name='enableLibraryThemeVideos' + /> + + {globalize.translate('EnableThemeVideosHelp')} + + + + + + } + label={globalize.translate('DisplayMissingEpisodesWithinSeasons')} + name='displayMissingEpisodes' + /> + + {globalize.translate('DisplayMissingEpisodesWithinSeasonsHelp')} + + + + ); +} diff --git a/src/apps/experimental/routes/user/display/LocalizationPreferences.tsx b/src/apps/experimental/routes/user/display/LocalizationPreferences.tsx new file mode 100644 index 0000000000..e406d0e38e --- /dev/null +++ b/src/apps/experimental/routes/user/display/LocalizationPreferences.tsx @@ -0,0 +1,80 @@ +import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import Link from '@mui/material/Link'; +import MenuItem from '@mui/material/MenuItem'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import React from 'react'; + +import { appHost } from 'components/apphost'; +import datetime from 'scripts/datetime'; +import globalize from 'scripts/globalize'; +import { DATE_LOCALE_OPTIONS, LANGUAGE_OPTIONS } from './constants'; +import { DisplaySettingsValues } from './types'; + +interface LocalizationPreferencesProps { + onChange: (event: SelectChangeEvent) => void; + values: DisplaySettingsValues; +} + +export function LocalizationPreferences({ onChange, values }: Readonly) { + if (!appHost.supports('displaylanguage') && !datetime.supportsLocalization()) { + return null; + } + return ( + + {globalize.translate('Localization')} + + { appHost.supports('displaylanguage') && ( + + {globalize.translate('LabelDisplayLanguage')} + + + {globalize.translate('LabelDisplayLanguageHelp')} + { appHost.supports('externallinks') && ( + + {globalize.translate('LearnHowYouCanContribute')} + + ) } + + + ) } + + { datetime.supportsLocalization() && ( + + {globalize.translate('LabelDateTimeLocale')} + + + ) } + + ); +} diff --git a/src/apps/experimental/routes/user/display/NextUpPreferences.tsx b/src/apps/experimental/routes/user/display/NextUpPreferences.tsx new file mode 100644 index 0000000000..1c21012a1f --- /dev/null +++ b/src/apps/experimental/routes/user/display/NextUpPreferences.tsx @@ -0,0 +1,80 @@ +import Checkbox from '@mui/material/Checkbox'; +import FormControl from '@mui/material/FormControl'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormHelperText from '@mui/material/FormHelperText'; +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'; + +interface NextUpPreferencesProps { + onChange: (event: React.SyntheticEvent) => void; + values: DisplaySettingsValues; +} + +export function NextUpPreferences({ onChange, values }: Readonly) { + return ( + + {globalize.translate('NextUp')} + + + + + {globalize.translate('LabelMaxDaysForNextUpHelp')} + + + + + + } + label={globalize.translate('EnableRewatchingNextUp')} + name='enableRewatchingInNextUp' + /> + + {globalize.translate('EnableRewatchingNextUpHelp')} + + + + + + } + label={globalize.translate('UseEpisodeImagesInNextUp')} + name='episodeImagesInNextUp' + /> + + {globalize.translate('UseEpisodeImagesInNextUpHelp')} + + + + ); +} diff --git a/src/apps/experimental/routes/user/display/constants.ts b/src/apps/experimental/routes/user/display/constants.ts new file mode 100644 index 0000000000..7ece9aa9c7 --- /dev/null +++ b/src/apps/experimental/routes/user/display/constants.ts @@ -0,0 +1,79 @@ +import globalize from 'scripts/globalize'; + +export const LANGUAGE_OPTIONS = [ + { value: 'auto', label: globalize.translate('Auto') }, + { value: 'af', label: 'Afrikaans' }, + { value: 'ar', label: 'العربية' }, + { value: 'be-BY', label: 'Беларуская' }, + { value: 'bg-BG', label: 'Български' }, + { value: 'bn_BD', label: 'বাংলা (বাংলাদেশ)' }, + { value: 'ca', label: 'Català' }, + { value: 'cs', label: 'Čeština' }, + { value: 'cy', label: 'Cymraeg' }, + { value: 'da', label: 'Dansk' }, + { value: 'de', label: 'Deutsch' }, + { value: 'el', label: 'Ελληνικά' }, + { value: 'en-GB', label: 'English (United Kingdom)' }, + { value: 'en-US', label: 'English' }, + { value: 'eo', label: 'Esperanto' }, + { value: 'es', label: 'Español' }, + { value: 'es_419', label: 'Español americano' }, + { value: 'es-AR', label: 'Español (Argentina)' }, + { value: 'es_DO', label: 'Español (Dominicana)' }, + { value: 'es-MX', label: 'Español (México)' }, + { value: 'et', label: 'Eesti' }, + { value: 'eu', label: 'Euskara' }, + { value: 'fa', label: 'فارسی' }, + { value: 'fi', label: 'Suomi' }, + { value: 'fil', label: 'Filipino' }, + { value: 'fr', label: 'Français' }, + { value: 'fr-CA', label: 'Français (Canada)' }, + { value: 'gl', label: 'Galego' }, + { value: 'gsw', label: 'Schwiizerdütsch' }, + { value: 'he', label: 'עִבְרִית' }, + { value: 'hi-IN', label: 'हिन्दी' }, + { value: 'hr', label: 'Hrvatski' }, + { value: 'hu', label: 'Magyar' }, + { value: 'id', label: 'Bahasa Indonesia' }, + { value: 'is-IS', label: 'Íslenska' }, + { value: 'it', label: 'Italiano' }, + { value: 'ja', label: '日本語' }, + { value: 'kk', label: 'Qazaqşa' }, + { value: 'ko', label: '한국어' }, + { value: 'lt-LT', label: 'Lietuvių' }, + { value: 'lv', label: 'Latviešu' }, + { value: 'mk', label: 'Македонски' }, + { value: 'ml', label: 'മലയാളം' }, + { value: 'mr', label: 'मराठी' }, + { value: 'ms', label: 'Bahasa Melayu' }, + { value: 'nb', label: 'Norsk bokmål' }, + { value: 'ne', label: 'नेपाली' }, + { value: 'nl', label: 'Nederlands' }, + { value: 'nn', label: 'Norsk nynorsk' }, + { value: 'pa', label: 'ਪੰਜਾਬੀ' }, + { value: 'pl', label: 'Polski' }, + { value: 'pr', label: 'Pirate' }, + { value: 'pt', label: 'Português' }, + { value: 'pt-BR', label: 'Português (Brasil)' }, + { value: 'pt-PT', label: 'Português (Portugal)' }, + { value: 'ro', label: 'Românește' }, + { value: 'ru', label: 'Русский' }, + { value: 'sk', label: 'Slovenčina' }, + { value: 'sl-SI', label: 'Slovenščina' }, + { value: 'sq', label: 'Shqip' }, + { value: 'sr', label: 'Српски' }, + { value: 'sv', label: 'Svenska' }, + { value: 'ta', label: 'தமிழ்' }, + { value: 'te', label: 'తెలుగు' }, + { value: 'th', label: 'ภาษาไทย' }, + { value: 'tr', label: 'Türkçe' }, + { value: 'uk', label: 'Українська' }, + { value: 'ur_PK', label: ' اُردُو' }, + { value: 'vi', label: 'Tiếng Việt' }, + { value: 'zh-CN', label: '汉语 (简化字)' }, + { value: 'zh-TW', label: '漢語 (繁体字)' }, + { value: 'zh-HK', label: '廣東話 (香港)' } +]; + +// NOTE: Option `Euskara` (eu) does not exist in legacy date locale options. +export const DATE_LOCALE_OPTIONS = LANGUAGE_OPTIONS.filter(({ value }) => value !== 'eu'); diff --git a/src/apps/experimental/routes/user/display/hooks/useDisplaySettingForm.ts b/src/apps/experimental/routes/user/display/hooks/useDisplaySettingForm.ts new file mode 100644 index 0000000000..d7986fa42c --- /dev/null +++ b/src/apps/experimental/routes/user/display/hooks/useDisplaySettingForm.ts @@ -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(); + + 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 + }; +} diff --git a/src/apps/experimental/routes/user/display/hooks/useDisplaySettings.ts b/src/apps/experimental/routes/user/display/hooks/useDisplaySettings.ts new file mode 100644 index 0000000000..3e4ca6455a --- /dev/null +++ b/src/apps/experimental/routes/user/display/hooks/useDisplaySettings.ts @@ -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(); + const [displaySettings, setDisplaySettings] = useState(); + 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; +} diff --git a/src/apps/experimental/routes/user/display/hooks/useScreensavers.ts b/src/apps/experimental/routes/user/display/hooks/useScreensavers.ts new file mode 100644 index 0000000000..8d9342552c --- /dev/null +++ b/src/apps/experimental/routes/user/display/hooks/useScreensavers.ts @@ -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(() => { + 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 ?? [] + }; +} diff --git a/src/apps/experimental/routes/user/display/hooks/useServerThemes.ts b/src/apps/experimental/routes/user/display/hooks/useServerThemes.ts new file mode 100644 index 0000000000..4a1cde3a1e --- /dev/null +++ b/src/apps/experimental/routes/user/display/hooks/useServerThemes.ts @@ -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(); + + 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 + }; +} diff --git a/src/apps/experimental/routes/user/display/index.tsx b/src/apps/experimental/routes/user/display/index.tsx new file mode 100644 index 0000000000..f30b48f5bc --- /dev/null +++ b/src/apps/experimental/routes/user/display/index.tsx @@ -0,0 +1,96 @@ +import Button from '@mui/material/Button'; +import { SelectChangeEvent } from '@mui/material/Select'; +import Stack from '@mui/material/Stack'; +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) => { + 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.type === 'checkbox' ? target.checked : target.value; + + if (values?.[fieldName] !== fieldValue) { + updateField({ + name: fieldName, + value: fieldValue + }); + } + }, [updateField, values]); + + if (loading || !values) { + return ; + } + + return ( + +
+
+ + + + + + + + + +
+
+
+ ); +} diff --git a/src/apps/experimental/routes/user/display/types.ts b/src/apps/experimental/routes/user/display/types.ts new file mode 100644 index 0000000000..a5e08d2dd9 --- /dev/null +++ b/src/apps/experimental/routes/user/display/types.ts @@ -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; +} diff --git a/src/scripts/settings/userSettings.js b/src/scripts/settings/userSettings.js index 525a77a4d2..b72bbd5627 100644 --- a/src/scripts/settings/userSettings.js +++ b/src/scripts/settings/userSettings.js @@ -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) { diff --git a/src/styles/site.scss b/src/styles/site.scss index f3f1a699b7..0e9c66c901 100644 --- a/src/styles/site.scss +++ b/src/styles/site.scss @@ -39,6 +39,7 @@ body { right: 0; bottom: 0; contain: strict; + z-index: -1; } .layout-mobile, diff --git a/src/themes/theme.ts b/src/themes/theme.ts index 70e060dd74..4a17f6ade4 100644 --- a/src/themes/theme.ts +++ b/src/themes/theme.ts @@ -62,6 +62,13 @@ const theme = createTheme({ variant: 'filled' } }, + MuiFormHelperText: { + styleOverrides: { + root: { + fontSize: '1rem' + } + } + }, MuiTextField: { defaultProps: { variant: 'filled' diff --git a/src/types/webConfig.ts b/src/types/webConfig.ts index 5ef164c247..b6c2adc3a4 100644 --- a/src/types/webConfig.ts +++ b/src/types/webConfig.ts @@ -1,5 +1,6 @@ -interface Theme { +export interface Theme { name: string + default?: boolean; id: string color: string }