1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00
This commit is contained in:
Bill Thornton 2025-03-30 11:03:15 -04:00 committed by GitHub
commit 384d322474
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 388 additions and 229 deletions

View file

@ -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' }
];

View file

@ -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: {

View file

@ -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' }

View file

@ -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: {

View file

@ -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<UserDto>();
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 (
<Page
id='myPreferencesMenuPage'
className='libraryPage userPreferencesPage noSecondaryNavPage mainAnimatedPage'
title={globalize.translate('Settings')}
shouldAutoFocus
>
<div className='padded-left padded-right padded-bottom-page padded-top'>
<div
className='readOnlyContent'
style={{
margin: '0 auto'
}}
>
<div className='verticalSection verticalSection-extrabottompadding'>
<h2
className='sectionTitle headerUsername'
style={{
paddingLeft: '0.25em'
}}
>
{user.Name}
</h2>
<LinkButton
href={`#/userprofile?userId=${userId}`}
className='lnkUserProfile listItem-border'
style={{
display: 'block',
margin: 0,
padding: 0
}}
>
<div className='listItem'>
<span className='material-icons listItemIcon listItemIcon-transparent person' aria-hidden='true' />
<div className='listItemBody'>
<div className='listItemBodyText'>
{globalize.translate('Profile')}
</div>
</div>
</div>
</LinkButton>
{isQuickConnectEnabled && (
<LinkButton
href={`#/quickconnect?userId=${userId}`}
className='lnkQuickConnectPreferences listItem-border'
style={{
display: 'block',
margin: 0,
padding: 0
}}
>
<div className='listItem'>
<span className='material-icons listItemIcon listItemIcon-transparent phonelink_lock' aria-hidden='true' />
<div className='listItemBody'>
<div className='listItemBodyText'>
{globalize.translate('QuickConnect')}
</div>
</div>
</div>
</LinkButton>
)}
<LinkButton
href={`#/mypreferencesdisplay?userId=${userId}`}
className='lnkDisplayPreferences listItem-border'
style={{
display: 'block',
margin: 0,
padding: 0
}}
>
<div className='listItem'>
<span className='material-icons listItemIcon listItemIcon-transparent tv' aria-hidden='true' />
<div className='listItemBody'>
<div className='listItemBodyText'>
{globalize.translate('Display')}
</div>
</div>
</div>
</LinkButton>
<LinkButton
href={`#/mypreferenceshome?userId=${userId}`}
className='lnkHomePreferences listItem-border'
style={{
display: 'block',
margin: 0,
padding: 0
}}
>
<div className='listItem'>
<span className='material-icons listItemIcon listItemIcon-transparent home' aria-hidden='true' />
<div className='listItemBody'>
<div className='listItemBodyText'>
{globalize.translate('Home')}
</div>
</div>
</div>
</LinkButton>
<LinkButton
href={`#/mypreferencesplayback?userId=${userId}`}
className='lnkPlaybackPreferences listItem-border'
style={{
display: 'block',
margin: 0,
padding: 0
}}
>
<div className='listItem'>
<span className='material-icons listItemIcon listItemIcon-transparent play_circle_filled' aria-hidden='true' />
<div className='listItemBody'>
<div className='listItemBodyText'>
{globalize.translate('TitlePlayback')}
</div>
</div>
</div>
</LinkButton>
<LinkButton
href={`#/mypreferencessubtitles?userId=${userId}`}
className='lnkSubtitlePreferences listItem-border'
style={{
display: 'block',
margin: 0,
padding: 0
}}
>
<div className='listItem'>
<span className='material-icons listItemIcon listItemIcon-transparent closed_caption' aria-hidden='true' />
<div className='listItemBody'>
<div className='listItemBodyText'>
{globalize.translate('Subtitles')}
</div>
</div>
</div>
</LinkButton>
{appHost.supports('clientsettings') && (
<LinkButton
onClick={shell.openClientSettings}
className='clientSettings listItem-border'
style={{
display: 'block',
margin: 0,
padding: 0
}}
>
<div className='listItem'>
<span className='material-icons listItemIcon listItemIcon-transparent devices_other' aria-hidden='true' />
<div className='listItemBody'>
<div className='listItemBodyText'>
{globalize.translate('ClientSettings')}
</div>
</div>
</div>
</LinkButton>
)}
{isLoggedInUser && !browser.mobile && (
<LinkButton
href={`#/mypreferencescontrols?userId=${userId}`}
className='lnkControlsPreferences listItem-border'
style={{
display: 'block',
margin: 0,
padding: 0
}}
>
<div className='listItem'>
<span className='material-icons listItemIcon listItemIcon-transparent keyboard' aria-hidden='true' />
<div className='listItemBody'>
<div className='listItemBodyText'>
{globalize.translate('Controls')}
</div>
</div>
</div>
</LinkButton>
)}
</div>
{isLoggedInUser && user.Policy?.IsAdministrator && !layoutManager.tv && (
<div className='adminSection verticalSection verticalSection-extrabottompadding'>
<h2
className='sectionTitle headerUsername'
style={{
paddingLeft: '0.25em'
}}
>
{globalize.translate('HeaderAdmin')}
</h2>
<LinkButton
href='#/dashboard'
className='listItem-border'
style={{
display: 'block',
margin: 0,
padding: 0
}}
>
<div className='listItem'>
<span className='material-icons listItemIcon listItemIcon-transparent dashboard' aria-hidden='true' />
<div className='listItemBody'>
<div className='listItemBodyText'>
{globalize.translate('TabDashboard')}
</div>
</div>
</div>
</LinkButton>
<LinkButton
href='#/metadata'
className='listItem-border'
style={{
display: 'block',
margin: 0,
padding: 0
}}
>
<div className='listItem'>
<span className='material-icons listItemIcon listItemIcon-transparent mode_edit' aria-hidden='true' />
<div className='listItemBody'>
<div className='listItemBodyText'>
{globalize.translate('MetadataManager')}
</div>
</div>
</div>
</LinkButton>
</div>
)}
{isLoggedInUser && (
<div className='userSection verticalSection verticalSection-extrabottompadding'>
<h2
className='sectionTitle headerUsername'
style={{
paddingLeft: '0.25em'
}}
>
{globalize.translate('HeaderUser')}
</h2>
{appHost.supports('multiserver') && (
<LinkButton
onClick={Dashboard.selectServer}
className='selectServer listItem-border'
style={{
display: 'block',
margin: 0,
padding: 0
}}
>
<div className='listItem'>
<span className='material-icons listItemIcon listItemIcon-transparent storage' aria-hidden='true' />
<div className='listItemBody'>
<div className='listItemBodyText'>
{globalize.translate('SelectServer')}
</div>
</div>
</div>
</LinkButton>
)}
<LinkButton
onClick={Dashboard.logout}
className='btnLogout listItem-border'
style={{
display: 'block',
margin: 0,
padding: 0
}}
>
<div className='listItem'>
<span className='material-icons listItemIcon listItemIcon-transparent exit_to_app' aria-hidden='true' />
<div className='listItemBody'>
<div className='listItemBodyText'>
{globalize.translate('ButtonSignOut')}
</div>
</div>
</div>
</LinkButton>
{appHost.supports('exitmenu') && (
<LinkButton
onClick={appHost.exit}
className='exitApp listItem-border'
style={{
display: 'block',
margin: 0,
padding: 0
}}
>
<div className='listItem'>
<span className='material-icons listItemIcon listItemIcon-transparent close' aria-hidden='true' />
<div className='listItemBody'>
<div className='listItemBodyText'>
{globalize.translate('ButtonExitApp')}
</div>
</div>
</div>
</LinkButton>
)}
</div>
)}
</div>
</div>
</Page>
);
};
export default UserSettingsPage;

View file

@ -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<PropsWithChildren<PageProps>> = ({
isMenuButtonEnabled = false,
isNowPlayingBarEnabled = true,
isThemeMediaSupported = false,
shouldAutoFocus = false,
backDropType
}) => {
const element = useRef<HTMLDivElement>(null);
@ -58,6 +61,12 @@ const Page: FC<PropsWithChildren<PageProps>> = ({
element.current?.dispatchEvent(new CustomEvent('pageshow', event));
}, [ element, isNowPlayingBarEnabled, isThemeMediaSupported ]);
useEffect(() => {
if (shouldAutoFocus) {
autoFocuser.autoFocus(element.current);
}
}, [ shouldAutoFocus ]);
return (
<StrictMode>
<div

View file

@ -35,7 +35,7 @@ export function enable() {
/**
* Set focus on a suitable element, taking into account the previously selected.
* @param {HTMLElement} [container] - Element to limit scope.
* @param {HTMLElement | null} [container] - Element to limit scope.
* @returns {HTMLElement} Focused element.
*/
export function autoFocus(container) {

View file

@ -17,9 +17,10 @@ import { Link } from 'react-router-dom';
import { appHost } from 'components/apphost';
import { useApi } from 'hooks/useApi';
import globalize from 'lib/globalize';
import Dashboard from 'utils/dashboard';
import { useQuickConnectEnabled } from 'hooks/useQuickConnect';
import globalize from 'lib/globalize';
import shell from 'scripts/shell';
import Dashboard from 'utils/dashboard';
export const ID = 'app-user-menu';
@ -36,7 +37,7 @@ const AppUserMenu: FC<AppUserMenuProps> = ({
const { data: isQuickConnectEnabled } = useQuickConnectEnabled();
const onClientSettingsClick = useCallback(() => {
window.NativeShell?.openClientSettings();
shell.openClientSettings();
onMenuClose();
}, [ onMenuClose ]);

View file

@ -1,127 +0,0 @@
<div id="myPreferencesMenuPage" data-role="page" class="page libraryPage userPreferencesPage noSecondaryNavPage" data-title="${Settings}" data-backbutton="true">
<div class="padded-left padded-right padded-bottom-page padded-top">
<div class="readOnlyContent" style="margin: 0 auto;">
<div class="verticalSection verticalSection-extrabottompadding">
<h2 class="sectionTitle headerUsername" style="padding-left:.25em;"></h2>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkUserProfile listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent person" aria-hidden="true"></span>
<div class="listItemBody">
<div class="listItemBodyText">${Profile}</div>
</div>
</div>
</a>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkQuickConnectPreferences listItem-border hide">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent phonelink_lock" aria-hidden="true"></span>
<div class="listItemBody">
<div class="listItemBodyText">${QuickConnect}</div>
</div>
</div>
</a>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkDisplayPreferences listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent tv" aria-hidden="true"></span>
<div class="listItemBody">
<div class="listItemBodyText">${Display}</div>
</div>
</div>
</a>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkHomePreferences listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent home" aria-hidden="true"></span>
<div class="listItemBody">
<div class="listItemBodyText">${Home}</div>
</div>
</div>
</a>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkPlaybackPreferences listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent play_circle_filled" aria-hidden="true"></span>
<div class="listItemBody">
<div class="listItemBodyText">${TitlePlayback}</div>
</div>
</div>
</a>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkSubtitlePreferences listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent closed_caption" aria-hidden="true"></span>
<div class="listItemBody">
<div class="listItemBodyText">${Subtitles}</div>
</div>
</div>
</a>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="clientSettings listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent devices_other" aria-hidden="true"></span>
<div class="listItemBody">
<div class="listItemBodyText">${ClientSettings}</div>
</div>
</div>
</a>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="lnkControlsPreferences listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent keyboard" aria-hidden="true"></span>
<div class="listItemBody">
<div class="listItemBodyText">${Controls}</div>
</div>
</div>
</a>
</div>
<div class="adminSection verticalSection verticalSection-extrabottompadding hide">
<h2 class="sectionTitle" style="padding-left:.25em;">${HeaderAdmin}</h2>
<a is="emby-linkbutton" href="#/dashboard" style="display:block;padding:0;margin:0;" class="listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent dashboard" aria-hidden="true"></span>
<div class="listItemBody">
<div class="listItemBodyText">${TabDashboard}</div>
</div>
</div>
</a>
<a is="emby-linkbutton" href="#/metadata" style="display:block;padding:0;margin:0;" class="listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent mode_edit" aria-hidden="true"></span>
<div class="listItemBody">
<div class="listItemBodyText">${MetadataManager}</div>
</div>
</div>
</a>
</div>
<div class="userSection verticalSection verticalSection-extrabottompadding">
<h2 class="sectionTitle" style="padding-left:.25em;">${HeaderUser}</h2>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="selectServer hide listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent storage" aria-hidden="true"></span>
<div class="listItemBody">
<div class="listItemBodyText">${SelectServer}</div>
</div>
</div>
</a>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="btnLogout listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent exit_to_app" aria-hidden="true"></span>
<div class="listItemBody">
<div class="listItemBodyText">${ButtonSignOut}</div>
</div>
</div>
</a>
<a is="emby-linkbutton" data-ripple="false" href="#" style="display:block;padding:0;margin:0;" class="exitApp listItem-border">
<div class="listItem">
<span class="material-icons listItemIcon listItemIcon-transparent close" aria-hidden="true"></span>
<div class="listItemBody">
<div class="listItemBodyText">${ButtonExitApp}</div>
</div>
</div>
</a>
</div>
</div>
</div>
</div>

View file

@ -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);
});
});
}

View file

@ -18,7 +18,7 @@ interface LinkButtonProps extends DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnc
const LinkButton: React.FC<LinkButtonProps> = ({
className,
isAutoHideEnabled,
href,
href = '#', // The href must have a value to be focusable in the TV layout
target,
onClick,
children,

View file

@ -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);