diff --git a/src/apps/experimental/routes/asyncRoutes/user.ts b/src/apps/experimental/routes/asyncRoutes/user.ts index 59a88990b8..997182a6a3 100644 --- a/src/apps/experimental/routes/asyncRoutes/user.ts +++ b/src/apps/experimental/routes/asyncRoutes/user.ts @@ -2,14 +2,15 @@ import { AsyncRoute } from 'components/router/AsyncRoute'; import { AppType } from 'constants/appType'; export const ASYNC_USER_ROUTES: AsyncRoute[] = [ - { path: 'home', page: 'home', type: AppType.Experimental }, - { path: 'quickconnect', page: 'quickConnect' }, - { path: 'search', page: 'search' }, - { path: 'userprofile', page: 'user/userprofile' }, - { path: 'movies', page: 'movies', type: AppType.Experimental }, - { path: 'tv', page: 'shows', type: AppType.Experimental }, - { path: 'music', page: 'music', type: AppType.Experimental }, - { path: 'livetv', page: 'livetv', type: AppType.Experimental }, + { path: 'home', type: AppType.Experimental }, + { path: 'homevideos', type: AppType.Experimental }, + { path: 'livetv', type: AppType.Experimental }, + { path: 'movies', type: AppType.Experimental }, + { path: 'music', type: AppType.Experimental }, { path: 'mypreferencesdisplay', page: 'user/display', type: AppType.Experimental }, - { path: 'homevideos', page: 'homevideos', type: AppType.Experimental } + { path: 'mypreferencesmenu', page: 'user/settings' }, + { path: 'quickconnect', page: 'quickConnect' }, + { path: 'search' }, + { path: 'tv', page: 'shows', type: AppType.Experimental }, + { path: 'userprofile', page: 'user/userprofile' } ]; diff --git a/src/apps/experimental/routes/legacyRoutes/user.ts b/src/apps/experimental/routes/legacyRoutes/user.ts index 7ba85448e0..2dd81a4b65 100644 --- a/src/apps/experimental/routes/legacyRoutes/user.ts +++ b/src/apps/experimental/routes/legacyRoutes/user.ts @@ -19,12 +19,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [ controller: 'lyrics', view: 'lyrics.html' } - }, { - path: 'mypreferencesmenu', - pageProps: { - controller: 'user/menu/index', - view: 'user/menu/index.html' - } }, { path: 'mypreferencescontrols', pageProps: { diff --git a/src/apps/stable/routes/asyncRoutes/user.ts b/src/apps/stable/routes/asyncRoutes/user.ts index 1074053e61..8cdfde959d 100644 --- a/src/apps/stable/routes/asyncRoutes/user.ts +++ b/src/apps/stable/routes/asyncRoutes/user.ts @@ -1,6 +1,7 @@ import { AsyncRoute } from '../../../../components/router/AsyncRoute'; export const ASYNC_USER_ROUTES: AsyncRoute[] = [ + { path: 'mypreferencesmenu', page: 'user/settings' }, { path: 'quickconnect', page: 'quickConnect' }, { path: 'search', page: 'search' }, { path: 'userprofile', page: 'user/userprofile' } diff --git a/src/apps/stable/routes/legacyRoutes/user.ts b/src/apps/stable/routes/legacyRoutes/user.ts index 9abbefefee..612572a865 100644 --- a/src/apps/stable/routes/legacyRoutes/user.ts +++ b/src/apps/stable/routes/legacyRoutes/user.ts @@ -31,12 +31,6 @@ export const LEGACY_USER_ROUTES: LegacyRoute[] = [ controller: 'music/musicrecommended', view: 'music/music.html' } - }, { - path: 'mypreferencesmenu', - pageProps: { - controller: 'user/menu/index', - view: 'user/menu/index.html' - } }, { path: 'mypreferencescontrols', pageProps: { diff --git a/src/apps/stable/routes/user/settings/index.tsx b/src/apps/stable/routes/user/settings/index.tsx new file mode 100644 index 0000000000..e392118bd5 --- /dev/null +++ b/src/apps/stable/routes/user/settings/index.tsx @@ -0,0 +1,356 @@ +import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; +import React, { useEffect, useMemo, useState, type FC } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +import { appHost } from 'components/apphost'; +import layoutManager from 'components/layoutManager'; +import Page from 'components/Page'; +import LinkButton from 'elements/emby-button/LinkButton'; +import { useApi } from 'hooks/useApi'; +import { useQuickConnectEnabled } from 'hooks/useQuickConnect'; +import { useUsers } from 'hooks/useUsers'; +import globalize from 'lib/globalize'; +import browser from 'scripts/browser'; +import Dashboard from 'utils/dashboard'; +import shell from 'scripts/shell'; + +const UserSettingsPage: FC = () => { + const { user: currentUser } = useApi(); + const [ searchParams ] = useSearchParams(); + const { + data: isQuickConnectEnabled, + isPending: isQuickConnectEnabledPending + } = useQuickConnectEnabled(); + const { data: users } = useUsers(); + const [ user, setUser ] = useState(); + + const userId = useMemo(() => ( + searchParams.get('userId') || currentUser?.Id + ), [ currentUser, searchParams ]); + const isLoggedInUser = useMemo(() => ( + userId && userId === currentUser?.Id + ), [ currentUser, userId ]); + + useEffect(() => { + if (userId) { + if (userId === currentUser?.Id) setUser(currentUser); + else setUser(users?.find(({ Id }) => userId === Id)); + } + }, [ currentUser, userId, users ]); + + if (!userId || !user || isQuickConnectEnabledPending) return null; + + return ( + +
+
+
+

+ {user.Name} +

+ + +
+
+
+ + {isQuickConnectEnabled && ( + +
+
+
+ )} + + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + {appHost.supports('clientsettings') && ( + +
+
+
+ )} + + {isLoggedInUser && !browser.mobile && ( + +
+
+
+ )} +
+ + {isLoggedInUser && user.Policy?.IsAdministrator && !layoutManager.tv && ( +
+

+ {globalize.translate('HeaderAdmin')} +

+ + +
+
+
+ + +
+
+
+
+ )} + + {isLoggedInUser && ( +
+

+ {globalize.translate('HeaderUser')} +

+ + {appHost.supports('multiserver') && ( + +
+
+
+ )} + + +
+
+
+ + {appHost.supports('exitmenu') && ( + +
+
+
+ )} +
+ )} +
+
+
+ ); +}; + +export default UserSettingsPage; diff --git a/src/components/Page.tsx b/src/components/Page.tsx index f3f9db7249..af31ace0ba 100644 --- a/src/components/Page.tsx +++ b/src/components/Page.tsx @@ -1,6 +1,7 @@ import React, { type FC, type PropsWithChildren, type HTMLAttributes, useEffect, useRef, StrictMode } from 'react'; -import viewManager from './viewManager/viewManager'; +import autoFocuser from 'components/autoFocuser'; +import viewManager from 'components/viewManager/viewManager'; type CustomPageProps = { id: string, // id is required for libraryMenu @@ -9,6 +10,7 @@ type CustomPageProps = { isMenuButtonEnabled?: boolean, isNowPlayingBarEnabled?: boolean, isThemeMediaSupported?: boolean, + shouldAutoFocus?: boolean, backDropType?: string, }; @@ -27,6 +29,7 @@ const Page: FC> = ({ isMenuButtonEnabled = false, isNowPlayingBarEnabled = true, isThemeMediaSupported = false, + shouldAutoFocus = false, backDropType }) => { const element = useRef(null); @@ -58,6 +61,12 @@ const Page: FC> = ({ element.current?.dispatchEvent(new CustomEvent('pageshow', event)); }, [ element, isNowPlayingBarEnabled, isThemeMediaSupported ]); + useEffect(() => { + if (shouldAutoFocus) { + autoFocuser.autoFocus(element.current); + } + }, [ shouldAutoFocus ]); + return (
= ({ const { data: isQuickConnectEnabled } = useQuickConnectEnabled(); const onClientSettingsClick = useCallback(() => { - window.NativeShell?.openClientSettings(); + shell.openClientSettings(); onMenuClose(); }, [ onMenuClose ]); diff --git a/src/controllers/user/menu/index.html b/src/controllers/user/menu/index.html deleted file mode 100644 index a6b0a2d045..0000000000 --- a/src/controllers/user/menu/index.html +++ /dev/null @@ -1,127 +0,0 @@ - diff --git a/src/controllers/user/menu/index.js b/src/controllers/user/menu/index.js deleted file mode 100644 index d02bb8d1b1..0000000000 --- a/src/controllers/user/menu/index.js +++ /dev/null @@ -1,75 +0,0 @@ -import { appHost } from '../../../components/apphost'; -import '../../../components/listview/listview.scss'; -import '../../../elements/emby-button/emby-button'; -import layoutManager from '../../../components/layoutManager'; -import Dashboard from '../../../utils/dashboard'; - -export default function (view, params) { - view.querySelector('.btnLogout').addEventListener('click', function () { - Dashboard.logout(); - }); - - view.querySelector('.selectServer').addEventListener('click', function () { - Dashboard.selectServer(); - }); - - view.querySelector('.clientSettings').addEventListener('click', function () { - window.NativeShell.openClientSettings(); - }); - - view.querySelector('.exitApp').addEventListener('click', function () { - appHost.exit(); - }); - - view.addEventListener('viewshow', function () { - // this page can also be used by admins to change user preferences from the user edit page - const userId = params.userId || Dashboard.getCurrentUserId(); - const page = this; - - page.querySelector('.lnkUserProfile').setAttribute('href', '#/userprofile?userId=' + userId); - page.querySelector('.lnkDisplayPreferences').setAttribute('href', '#/mypreferencesdisplay?userId=' + userId); - page.querySelector('.lnkHomePreferences').setAttribute('href', '#/mypreferenceshome?userId=' + userId); - page.querySelector('.lnkPlaybackPreferences').setAttribute('href', '#/mypreferencesplayback?userId=' + userId); - page.querySelector('.lnkSubtitlePreferences').setAttribute('href', '#/mypreferencessubtitles?userId=' + userId); - page.querySelector('.lnkQuickConnectPreferences').setAttribute('href', '#/quickconnect?userId=' + userId); - page.querySelector('.lnkControlsPreferences').setAttribute('href', '#/mypreferencescontrols?userId=' + userId); - - const supportsClientSettings = appHost.supports('clientsettings'); - page.querySelector('.clientSettings').classList.toggle('hide', !supportsClientSettings); - - const supportsExitMenu = appHost.supports('exitmenu'); - page.querySelector('.exitApp').classList.toggle('hide', !supportsExitMenu); - - const supportsMultiServer = appHost.supports('multiserver'); - page.querySelector('.selectServer').classList.toggle('hide', !supportsMultiServer); - - page.querySelector('.lnkControlsPreferences').classList.toggle('hide', layoutManager.mobile); - - ApiClient.getQuickConnect('Enabled') - .then(enabled => { - if (enabled === true) { - page.querySelector('.lnkQuickConnectPreferences').classList.remove('hide'); - } - }) - .catch(() => { - console.debug('Failed to get QuickConnect status'); - }); - ApiClient.getUser(userId).then(function (user) { - page.querySelector('.headerUsername').innerText = user.Name; - if (user.Policy.IsAdministrator && !layoutManager.tv) { - page.querySelector('.adminSection').classList.remove('hide'); - } - }); - - // Hide the actions if user preferences are being edited for a different user - if (params.userId && params.userId !== Dashboard.getCurrentUserId) { - page.querySelector('.userSection').classList.add('hide'); - page.querySelector('.adminSection').classList.add('hide'); - page.querySelector('.lnkControlsPreferences').classList.add('hide'); - } - - import('../../../components/autoFocuser').then(({ default: autoFocuser }) => { - autoFocuser.autoFocus(view); - }); - }); -} diff --git a/src/elements/emby-button/LinkButton.tsx b/src/elements/emby-button/LinkButton.tsx index 77bf39a3f0..359bc0d6b0 100644 --- a/src/elements/emby-button/LinkButton.tsx +++ b/src/elements/emby-button/LinkButton.tsx @@ -18,7 +18,7 @@ interface LinkButtonProps extends DetailedHTMLProps = ({ className, isAutoHideEnabled, - href, + href = '#', // The href must have a value to be focusable in the TV layout target, onClick, children, diff --git a/src/scripts/shell.js b/src/scripts/shell.js index cbcd9f8859..979db02c86 100644 --- a/src/scripts/shell.js +++ b/src/scripts/shell.js @@ -10,6 +10,11 @@ export default { window.NativeShell.disableFullscreen(); } }, + openClientSettings: () => { + if (window.NativeShell?.openClientSettings) { + window.NativeShell.openClientSettings(); + } + }, openUrl: function(url, target) { if (window.NativeShell?.openUrl) { window.NativeShell.openUrl(url, target);