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

Merge branch 'master' into fix-some-of-the-code-smells

This commit is contained in:
Yasin Silavi 2023-05-31 21:59:42 +03:30 committed by GitHub
commit 13786b082d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 851 additions and 448 deletions

View file

@ -0,0 +1,112 @@
import React, { useCallback, useEffect, useState } from 'react';
import CastConnected from '@mui/icons-material/CastConnected';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Cast from '@mui/icons-material/Cast';
import IconButton from '@mui/material/IconButton';
import { useTheme } from '@mui/material/styles';
import Tooltip from '@mui/material/Tooltip';
import { playbackManager } from 'components/playback/playbackmanager';
import globalize from 'scripts/globalize';
import Events from 'utils/events';
import RemotePlayMenu, { ID } from './menus/RemotePlayMenu';
import RemotePlayActiveMenu, { ID as ACTIVE_ID } from './menus/RemotePlayActiveMenu';
const RemotePlayButton = () => {
const theme = useTheme();
const [ playerInfo, setPlayerInfo ] = useState(playbackManager.getPlayerInfo());
const updatePlayerInfo = useCallback(() => {
setPlayerInfo(playbackManager.getPlayerInfo());
}, [ setPlayerInfo ]);
useEffect(() => {
Events.on(playbackManager, 'playerchange', updatePlayerInfo);
return () => {
Events.off(playbackManager, 'playerchange', updatePlayerInfo);
};
}, [ updatePlayerInfo ]);
const [ remotePlayMenuAnchorEl, setRemotePlayMenuAnchorEl ] = useState<null | HTMLElement>(null);
const isRemotePlayMenuOpen = Boolean(remotePlayMenuAnchorEl);
const onRemotePlayButtonClick = useCallback((event) => {
setRemotePlayMenuAnchorEl(event.currentTarget);
}, [ setRemotePlayMenuAnchorEl ]);
const onRemotePlayMenuClose = useCallback(() => {
setRemotePlayMenuAnchorEl(null);
}, [ setRemotePlayMenuAnchorEl ]);
const [ remotePlayActiveMenuAnchorEl, setRemotePlayActiveMenuAnchorEl ] = useState<null | HTMLElement>(null);
const isRemotePlayActiveMenuOpen = Boolean(remotePlayActiveMenuAnchorEl);
const onRemotePlayActiveButtonClick = useCallback((event) => {
setRemotePlayActiveMenuAnchorEl(event.currentTarget);
}, [ setRemotePlayActiveMenuAnchorEl ]);
const onRemotePlayActiveMenuClose = useCallback(() => {
setRemotePlayActiveMenuAnchorEl(null);
}, [ setRemotePlayActiveMenuAnchorEl ]);
return (
<>
{(playerInfo && !playerInfo.isLocalPlayer) ? (
<Box
sx={{
alignSelf: 'center'
}}
>
<Tooltip title={globalize.translate('ButtonCast')}>
<Button
variant='text'
size='large'
startIcon={<CastConnected />}
aria-label={globalize.translate('ButtonCast')}
aria-controls={ACTIVE_ID}
aria-haspopup='true'
onClick={onRemotePlayActiveButtonClick}
color='inherit'
sx={{
color: theme.palette.primary.main
}}
>
{playerInfo.deviceName || playerInfo.name}
</Button>
</Tooltip>
</Box>
) : (
<Tooltip title={globalize.translate('ButtonCast')}>
<IconButton
size='large'
aria-label={globalize.translate('ButtonCast')}
aria-controls={ID}
aria-haspopup='true'
onClick={onRemotePlayButtonClick}
color='inherit'
>
<Cast />
</IconButton>
</Tooltip>
)}
<RemotePlayMenu
open={isRemotePlayMenuOpen}
anchorEl={remotePlayMenuAnchorEl}
onMenuClose={onRemotePlayMenuClose}
/>
<RemotePlayActiveMenu
open={isRemotePlayActiveMenuOpen}
anchorEl={remotePlayActiveMenuAnchorEl}
onMenuClose={onRemotePlayActiveMenuClose}
playerInfo={playerInfo}
/>
</>
);
};
export default RemotePlayButton;

View file

@ -15,6 +15,7 @@ import globalize from 'scripts/globalize';
import AppTabs from '../tabs/AppTabs';
import { isDrawerPath } from '../drawers/AppDrawer';
import UserMenuButton from './UserMenuButton';
import RemotePlayButton from './RemotePlayButton';
interface AppToolbarProps {
isDrawerOpen: boolean
@ -89,6 +90,8 @@ const AppToolbar: FC<AppToolbarProps> = ({
{isUserLoggedIn && (
<>
<Box sx={{ display: 'flex', flexGrow: 1, justifyContent: 'flex-end' }}>
<RemotePlayButton />
<Tooltip title={globalize.translate('Search')}>
<IconButton
size='large'

View file

@ -0,0 +1,160 @@
import Check from '@mui/icons-material/Check';
import Close from '@mui/icons-material/Close';
import SettingsRemote from '@mui/icons-material/SettingsRemote';
import Divider from '@mui/material/Divider';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import Menu, { MenuProps } from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import dialog from 'components/dialog/dialog';
import { playbackManager } from 'components/playback/playbackmanager';
import React, { FC, useCallback, useState } from 'react';
import { Link } from 'react-router-dom';
import { enable, isEnabled, supported } from 'scripts/autocast';
import globalize from 'scripts/globalize';
interface RemotePlayActiveMenuProps extends MenuProps {
onMenuClose: () => void
playerInfo: {
name: string
isLocalPlayer: boolean
id?: string
deviceName?: string
playableMediaTypes?: string[]
supportedCommands?: string[]
} | null
}
export const ID = 'app-remote-play-active-menu';
const RemotePlayActiveMenu: FC<RemotePlayActiveMenuProps> = ({
anchorEl,
open,
onMenuClose,
playerInfo
}) => {
const [ isDisplayMirrorEnabled, setIsDisplayMirrorEnabled ] = useState(playbackManager.enableDisplayMirroring());
const isDisplayMirrorSupported = playerInfo?.supportedCommands && playerInfo.supportedCommands.indexOf('DisplayContent') !== -1;
const toggleDisplayMirror = useCallback(() => {
playbackManager.enableDisplayMirroring(!isDisplayMirrorEnabled);
setIsDisplayMirrorEnabled(!isDisplayMirrorEnabled);
}, [ isDisplayMirrorEnabled, setIsDisplayMirrorEnabled ]);
const [ isAutoCastEnabled, setIsAutoCastEnabled ] = useState(isEnabled());
const isAutoCastSupported = supported();
const toggleAutoCast = useCallback(() => {
enable(!isAutoCastEnabled);
setIsAutoCastEnabled(!isAutoCastEnabled);
}, [ isAutoCastEnabled, setIsAutoCastEnabled ]);
const remotePlayerName = playerInfo?.deviceName || playerInfo?.name;
const disconnectRemotePlayer = useCallback(() => {
if (playbackManager.getSupportedCommands().indexOf('EndSession') !== -1) {
dialog.show({
buttons: [
{
name: globalize.translate('Yes'),
id: 'yes'
}, {
name: globalize.translate('No'),
id: 'no'
}
],
text: globalize.translate('ConfirmEndPlayerSession', remotePlayerName)
}).then(id => {
onMenuClose();
if (id === 'yes') {
playbackManager.getCurrentPlayer().endSession();
}
playbackManager.setDefaultPlayerActive();
}).catch(() => {
// Dialog closed
});
} else {
onMenuClose();
playbackManager.setDefaultPlayerActive();
}
}, [ onMenuClose, remotePlayerName ]);
return (
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
id={ID}
keepMounted
open={open}
onClose={onMenuClose}
MenuListProps={{
'aria-labelledby': 'remote-play-active-subheader',
subheader: (
<ListSubheader component='div' id='remote-play-active-subheader'>
{remotePlayerName}
</ListSubheader>
)
}}
>
{isDisplayMirrorSupported && (
<MenuItem onClick={toggleDisplayMirror}>
{isDisplayMirrorEnabled && (
<ListItemIcon>
<Check />
</ListItemIcon>
)}
<ListItemText inset={!isDisplayMirrorEnabled}>
{globalize.translate('EnableDisplayMirroring')}
</ListItemText>
</MenuItem>
)}
{isAutoCastSupported && (
<MenuItem onClick={toggleAutoCast}>
{isAutoCastEnabled && (
<ListItemIcon>
<Check />
</ListItemIcon>
)}
<ListItemText inset={!isAutoCastEnabled}>
{globalize.translate('EnableAutoCast')}
</ListItemText>
</MenuItem>
)}
{(isDisplayMirrorSupported || isAutoCastSupported) && <Divider />}
<MenuItem
component={Link}
to='/queue'
onClick={onMenuClose}
>
<ListItemIcon>
<SettingsRemote />
</ListItemIcon>
<ListItemText>
{globalize.translate('HeaderRemoteControl')}
</ListItemText>
</MenuItem>
<Divider />
<MenuItem onClick={disconnectRemotePlayer}>
<ListItemIcon>
<Close />
</ListItemIcon>
<ListItemText>
{globalize.translate('Disconnect')}
</ListItemText>
</MenuItem>
</Menu>
);
};
export default RemotePlayActiveMenu;

View file

@ -0,0 +1,100 @@
import Warning from '@mui/icons-material/Warning';
import Divider from '@mui/material/Divider';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Menu, { type MenuProps } from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import React, { FC, useEffect, useState } from 'react';
import globalize from 'scripts/globalize';
import { playbackManager } from 'components/playback/playbackmanager';
import { pluginManager } from 'components/pluginManager';
import type { PlayTarget } from 'types/playTarget';
import PlayTargetIcon from '../../PlayTargetIcon';
interface RemotePlayMenuProps extends MenuProps {
onMenuClose: () => void
}
export const ID = 'app-remote-play-menu';
const RemotePlayMenu: FC<RemotePlayMenuProps> = ({
anchorEl,
open,
onMenuClose
}) => {
// TODO: Add other checks for support (Android app, secure context, etc)
const isChromecastPluginLoaded = !!pluginManager.plugins.find(plugin => plugin.id === 'chromecast');
const [ playbackTargets, setPlaybackTargets ] = useState<PlayTarget[]>([]);
const onPlayTargetClick = (target: PlayTarget) => {
playbackManager.trySetActivePlayer(target.playerName, target);
onMenuClose();
};
useEffect(() => {
const fetchPlaybackTargets = async () => {
setPlaybackTargets(
await playbackManager.getTargets()
);
};
if (open) {
fetchPlaybackTargets()
.catch(err => {
console.error('[AppRemotePlayMenu] unable to get playback targets', err);
});
}
}, [ open, setPlaybackTargets ]);
return (
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
id={ID}
keepMounted
open={open}
onClose={onMenuClose}
>
{!isChromecastPluginLoaded && ([
<MenuItem key='cast-unsupported-item' disabled>
<ListItemIcon>
<Warning />
</ListItemIcon>
<ListItemText>
{globalize.translate('GoogleCastUnsupported')}
</ListItemText>
</MenuItem>,
<Divider key='cast-unsupported-divider' />
])}
{playbackTargets.map(target => (
<MenuItem
key={target.id}
// Since we are looping over targets there is no good way to avoid creating a new function here
// eslint-disable-next-line react/jsx-no-bind
onClick={() => onPlayTargetClick(target)}
>
<ListItemIcon>
<PlayTargetIcon target={target} />
</ListItemIcon>
<ListItemText
primary={ target.appName ? `${target.name} - ${target.appName}` : target.name }
secondary={ target.user?.Name }
/>
</MenuItem>
))}
</Menu>
);
};
export default RemotePlayMenu;

View file

@ -0,0 +1,38 @@
import React from 'react';
import Cast from '@mui/icons-material/Cast';
import Computer from '@mui/icons-material/Computer';
import Devices from '@mui/icons-material/Devices';
import Smartphone from '@mui/icons-material/Smartphone';
import Tablet from '@mui/icons-material/Tablet';
import Tv from '@mui/icons-material/Tv';
import browser from 'scripts/browser';
import type { PlayTarget } from 'types/playTarget';
const PlayTargetIcon = ({ target }: { target: PlayTarget }) => {
if (!target.deviceType && target.isLocalPlayer) {
if (browser.tv) {
return <Tv />;
} else if (browser.mobile) {
return <Smartphone />;
}
return <Computer />;
}
switch (target.deviceType) {
case 'smartphone':
return <Smartphone />;
case 'tablet':
return <Tablet />;
case 'desktop':
return <Computer />;
case 'cast':
return <Cast />;
case 'tv':
return <Tv />;
default:
return <Devices />;
}
};
export default PlayTargetIcon;

View file

@ -1,4 +1,11 @@
import { Lan, VpnKey, Article, EditNotifications, Extension, Schedule, ExpandLess, ExpandMore } from '@mui/icons-material';
import Article from '@mui/icons-material/Article';
import EditNotifications from '@mui/icons-material/EditNotifications';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';
import Extension from '@mui/icons-material/Extension';
import Lan from '@mui/icons-material/Lan';
import Schedule from '@mui/icons-material/Schedule';
import VpnKey from '@mui/icons-material/VpnKey';
import Collapse from '@mui/material/Collapse';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
@ -63,7 +70,7 @@ const AdvancedDrawerSection = () => {
<ListItemIcon>
<EditNotifications />
</ListItemIcon>
<ListItemText primary={globalize.translate('TabNotifications')} />
<ListItemText primary={globalize.translate('Notifications')} />
</ListItemLink>
</ListItem>
<ListItem disablePadding>

View file

@ -1,6 +1,7 @@
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'notificationsettings.html', page: 'dashboard/notifications' },
{ path: 'usernew.html', page: 'user/usernew' },
{ path: 'userprofiles.html', page: 'user/userprofiles' },
{ path: 'useredit.html', page: 'user/useredit' },

View file

@ -103,18 +103,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
controller: 'dashboard/metadatanfo',
view: 'dashboard/metadatanfo.html'
}
}, {
path: 'notificationsetting.html',
pageProps: {
controller: 'dashboard/notifications/notification/index',
view: 'dashboard/notifications/notification/index.html'
}
}, {
path: 'notificationsettings.html',
pageProps: {
controller: 'dashboard/notifications/notifications/index',
view: 'dashboard/notifications/notifications/index.html'
}
}, {
path: 'playbackconfiguration.html',
pageProps: {

View file

@ -1,6 +1,7 @@
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'notificationsettings.html', page: 'dashboard/notifications' },
{ path: 'usernew.html', page: 'user/usernew' },
{ path: 'userprofiles.html', page: 'user/userprofiles' },
{ path: 'useredit.html', page: 'user/useredit' },

View file

@ -0,0 +1,36 @@
import React from 'react';
import Page from 'components/Page';
import globalize from 'scripts/globalize';
const PluginLink = () => (
<div
dangerouslySetInnerHTML={{
__html: `<a
is='emby-linkbutton'
class='button-link'
href='#/addplugin.html?name=Webhook&guid=71552a5a5c5c4350a2aeebe451a30173'
>
${globalize.translate('GetThePlugin')}
</a>`
}}
/>
);
const Notifications = () => (
<Page
id='notificationSettingPage'
title={globalize.translate('Notifications')}
className='mainAnimatedPage type-interior'
>
<div className='content-primary'>
<h2>{globalize.translate('Notifications')}</h2>
<p>
{globalize.translate('NotificationsMovedMessage')}
</p>
<PluginLink />
</div>
</Page>
);
export default Notifications;

View file

@ -103,18 +103,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
controller: 'dashboard/metadatanfo',
view: 'dashboard/metadatanfo.html'
}
}, {
path: 'notificationsetting.html',
pageProps: {
controller: 'dashboard/notifications/notification/index',
view: 'dashboard/notifications/notification/index.html'
}
}, {
path: 'notificationsettings.html',
pageProps: {
controller: 'dashboard/notifications/notifications/index',
view: 'dashboard/notifications/notifications/index.html'
}
}, {
path: 'playbackconfiguration.html',
pageProps: {