308 lines
10 KiB
TypeScript
308 lines
10 KiB
TypeScript
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<SyncPlayMenuProps> = ({
|
|
anchorEl,
|
|
open,
|
|
onMenuClose
|
|
}) => {
|
|
const [ syncPlay, setSyncPlay ] = useState<SyncPlayInstance>();
|
|
const { __legacyApiClient__, api, user } = useApi();
|
|
const [ groups, setGroups ] = useState<GroupInfoDto[]>([]);
|
|
const [ currentGroup, setCurrentGroup ] = useState<GroupInfoDto>();
|
|
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(
|
|
<MenuItem
|
|
key='sync-play-start-playback'
|
|
onClick={onStartGroupPlaybackClick}
|
|
>
|
|
<ListItemIcon>
|
|
<PlayCircle />
|
|
</ListItemIcon>
|
|
<ListItemText primary={globalize.translate('LabelSyncPlayResumePlayback')} />
|
|
</MenuItem>
|
|
);
|
|
} else if (syncPlay?.Manager.isPlaybackActive()) {
|
|
menuItems.push(
|
|
<MenuItem
|
|
key='sync-play-stop-playback'
|
|
onClick={onStopGroupPlaybackClick}
|
|
>
|
|
<ListItemIcon>
|
|
<StopCircle />
|
|
</ListItemIcon>
|
|
<ListItemText primary={globalize.translate('LabelSyncPlayHaltPlayback')} />
|
|
</MenuItem>
|
|
);
|
|
}
|
|
|
|
menuItems.push(
|
|
<MenuItem
|
|
key='sync-play-settings'
|
|
onClick={onGroupSettingsClick}
|
|
>
|
|
<ListItemIcon>
|
|
<Tune />
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary={globalize.translate('Settings')}
|
|
/>
|
|
</MenuItem>
|
|
);
|
|
|
|
menuItems.push(
|
|
<Divider key='sync-play-controls-divider' />
|
|
);
|
|
|
|
menuItems.push(
|
|
<MenuItem
|
|
key='sync-play-exit'
|
|
onClick={onGroupLeaveClick}
|
|
>
|
|
<ListItemIcon>
|
|
<PersonRemove />
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary={globalize.translate('LabelSyncPlayLeaveGroup')}
|
|
/>
|
|
</MenuItem>
|
|
);
|
|
} else if (groups.length === 0 && user?.Policy?.SyncPlayAccess !== SyncPlayUserAccessType.CreateAndJoinGroups) {
|
|
menuItems.push(
|
|
<MenuItem key='sync-play-unavailable' disabled>
|
|
<ListItemIcon>
|
|
<PersonOff />
|
|
</ListItemIcon>
|
|
<ListItemText primary={globalize.translate('LabelSyncPlayNoGroups')} />
|
|
</MenuItem>
|
|
);
|
|
} else {
|
|
if (groups.length > 0) {
|
|
groups.forEach(group => {
|
|
menuItems.push(
|
|
<MenuItem
|
|
key={group.GroupId}
|
|
// Since we are looping over groups there is no good way to avoid creating a new function here
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onClick={() => group.GroupId && onGroupJoinClick(group.GroupId)}
|
|
>
|
|
<ListItemIcon>
|
|
<PersonAdd />
|
|
</ListItemIcon>
|
|
<ListItemText
|
|
primary={group.GroupName}
|
|
secondary={group.Participants?.join(', ')}
|
|
/>
|
|
</MenuItem>
|
|
);
|
|
});
|
|
|
|
menuItems.push(
|
|
<Divider key='sync-play-groups-divider' />
|
|
);
|
|
}
|
|
|
|
if (user?.Policy?.SyncPlayAccess === SyncPlayUserAccessType.CreateAndJoinGroups) {
|
|
menuItems.push(
|
|
<MenuItem
|
|
key='sync-play-new-group'
|
|
onClick={onGroupAddClick}
|
|
>
|
|
<ListItemIcon>
|
|
<GroupAdd />
|
|
</ListItemIcon>
|
|
<ListItemText primary={globalize.translate('LabelSyncPlayNewGroupDescription')} />
|
|
</MenuItem>
|
|
);
|
|
}
|
|
}
|
|
|
|
const MenuListProps = isSyncPlayEnabled ? {
|
|
'aria-labelledby': 'sync-play-active-subheader',
|
|
subheader: (
|
|
<ListSubheader component='div' id='sync-play-active-subheader'>
|
|
{currentGroup?.GroupName}
|
|
</ListSubheader>
|
|
)
|
|
} : undefined;
|
|
|
|
return (
|
|
<Menu
|
|
anchorEl={anchorEl}
|
|
anchorOrigin={{
|
|
vertical: 'bottom',
|
|
horizontal: 'right'
|
|
}}
|
|
transformOrigin={{
|
|
vertical: 'top',
|
|
horizontal: 'right'
|
|
}}
|
|
id={ID}
|
|
keepMounted
|
|
open={open}
|
|
onClose={onMenuClose}
|
|
MenuListProps={MenuListProps}
|
|
>
|
|
{menuItems}
|
|
</Menu>
|
|
);
|
|
};
|
|
|
|
export default SyncPlayMenu;
|