import type { GroupInfoDto } from '@jellyfin/sdk/lib/generated-client/models/group-info-dto'; import { SyncPlayUserAccessType } from '@jellyfin/sdk/lib/generated-client/models/sync-play-user-access-type'; import { getSyncPlayApi } from '@jellyfin/sdk/lib/utils/api/sync-play-api'; import GroupAdd from '@mui/icons-material/GroupAdd'; import PersonAdd from '@mui/icons-material/PersonAdd'; import PersonOff from '@mui/icons-material/PersonOff'; import PersonRemove from '@mui/icons-material/PersonRemove'; import PlayCircle from '@mui/icons-material/PlayCircle'; import StopCircle from '@mui/icons-material/StopCircle'; import Tune from '@mui/icons-material/Tune'; 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 type { ApiClient } from 'jellyfin-apiclient'; import React, { FC, useCallback, useEffect, useState } from 'react'; import { pluginManager } from 'components/pluginManager'; import { useApi } from 'hooks/useApi'; import globalize from 'scripts/globalize'; import { PluginType } from 'types/plugin'; import Events from 'utils/events'; export const ID = 'app-sync-play-menu'; interface SyncPlayMenuProps extends MenuProps { onMenuClose: () => void } interface SyncPlayInstance { Manager: { getGroupInfo: () => GroupInfoDto | null | undefined getTimeSyncCore: () => object isPlaybackActive: () => boolean isPlaylistEmpty: () => boolean haltGroupPlayback: (apiClient: ApiClient) => void resumeGroupPlayback: (apiClient: ApiClient) => void } } const SyncPlayMenu: FC = ({ anchorEl, open, onMenuClose }) => { const [ syncPlay, setSyncPlay ] = useState(); const { __legacyApiClient__, api, user } = useApi(); const [ groups, setGroups ] = useState([]); const [ currentGroup, setCurrentGroup ] = useState(); const isSyncPlayEnabled = Boolean(currentGroup); useEffect(() => { setSyncPlay(pluginManager.firstOfType(PluginType.SyncPlay)?.instance); }, []); useEffect(() => { const fetchGroups = async () => { if (api) { setGroups((await getSyncPlayApi(api).syncPlayGetGroups()).data); } }; fetchGroups() .catch(err => { console.error('[SyncPlayMenu] unable to fetch SyncPlay groups', err); }); }, [ api ]); const onGroupAddClick = useCallback(() => { if (api && user) { getSyncPlayApi(api) .syncPlayCreateGroup({ newGroupRequestDto: { GroupName: globalize.translate('SyncPlayGroupDefaultTitle', user.Name) } }) .catch(err => { console.error('[SyncPlayMenu] failed to create a SyncPlay group', err); }); onMenuClose(); } }, [ api, onMenuClose, user ]); const onGroupLeaveClick = useCallback(() => { if (api) { getSyncPlayApi(api) .syncPlayLeaveGroup() .catch(err => { console.error('[SyncPlayMenu] failed to leave SyncPlay group', err); }); onMenuClose(); } }, [ api, onMenuClose ]); const onGroupJoinClick = useCallback((GroupId: string) => { if (api) { getSyncPlayApi(api) .syncPlayJoinGroup({ joinGroupRequestDto: { GroupId } }) .catch(err => { console.error('[SyncPlayMenu] failed to join SyncPlay group', err); }); onMenuClose(); } }, [ api, onMenuClose ]); const onGroupSettingsClick = useCallback(async () => { if (!syncPlay) return; // TODO: Rewrite settings UI const SyncPlaySettingsEditor = (await import('../../../../../plugins/syncPlay/ui/settings/SettingsEditor')).default; new SyncPlaySettingsEditor( __legacyApiClient__, syncPlay.Manager.getTimeSyncCore(), { groupInfo: currentGroup }) .embed() .catch(err => { if (err) { console.error('[SyncPlayMenu] Error creating SyncPlay settings editor', err); } }); onMenuClose(); }, [ __legacyApiClient__, currentGroup, onMenuClose, syncPlay ]); const onStartGroupPlaybackClick = useCallback(() => { if (__legacyApiClient__) { syncPlay?.Manager.resumeGroupPlayback(__legacyApiClient__); onMenuClose(); } }, [ __legacyApiClient__, onMenuClose, syncPlay ]); const onStopGroupPlaybackClick = useCallback(() => { if (__legacyApiClient__) { syncPlay?.Manager.haltGroupPlayback(__legacyApiClient__); onMenuClose(); } }, [ __legacyApiClient__, onMenuClose, syncPlay ]); const updateSyncPlayGroup = useCallback((_e, enabled) => { if (syncPlay && enabled) { setCurrentGroup(syncPlay.Manager.getGroupInfo() ?? undefined); } else { setCurrentGroup(undefined); } }, [ syncPlay ]); useEffect(() => { if (!syncPlay) return; Events.on(syncPlay.Manager, 'enabled', updateSyncPlayGroup); return () => { Events.off(syncPlay.Manager, 'enabled', updateSyncPlayGroup); }; }, [ updateSyncPlayGroup, syncPlay ]); const menuItems = []; if (isSyncPlayEnabled) { if (!syncPlay?.Manager.isPlaylistEmpty() && !syncPlay?.Manager.isPlaybackActive()) { menuItems.push( ); } else if (syncPlay?.Manager.isPlaybackActive()) { menuItems.push( ); } menuItems.push( ); menuItems.push( ); menuItems.push( ); } else if (groups.length === 0 && user?.Policy?.SyncPlayAccess !== SyncPlayUserAccessType.CreateAndJoinGroups) { menuItems.push( ); } else { if (groups.length > 0) { groups.forEach(group => { menuItems.push( group.GroupId && onGroupJoinClick(group.GroupId)} > ); }); menuItems.push( ); } if (user?.Policy?.SyncPlayAccess === SyncPlayUserAccessType.CreateAndJoinGroups) { menuItems.push( ); } } const MenuListProps = isSyncPlayEnabled ? { 'aria-labelledby': 'sync-play-active-subheader', subheader: ( {currentGroup?.GroupName} ) } : undefined; return ( {menuItems} ); }; export default SyncPlayMenu;