|
| 1 | +import React, { useCallback, useContext, useEffect, useRef } from 'react' |
| 2 | +import { useTranslation } from 'react-i18next' |
| 3 | +import Escape from '../../../lib/Escape' |
| 4 | +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' |
| 5 | +import { Rundown, getRundownNrcsName } from '@sofie-automation/corelib/dist/dataModel/Rundown' |
| 6 | +import { ContextMenu, MenuItem, ContextMenuTrigger } from '@jstarpl/react-contextmenu' |
| 7 | +import { contextMenuHoldToDisplayTime, useRundownViewEventBusListener } from '../../../lib/lib' |
| 8 | +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' |
| 9 | +import { faBars } from '@fortawesome/free-solid-svg-icons' |
| 10 | +import { |
| 11 | + ActivateRundownPlaylistEvent, |
| 12 | + DeactivateRundownPlaylistEvent, |
| 13 | + IEventContext, |
| 14 | + RundownViewEvents, |
| 15 | +} from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' |
| 16 | +import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' |
| 17 | +import { UserPermissionsContext } from '../../UserPermissions' |
| 18 | +import * as RundownResolver from '../../../lib/RundownResolver' |
| 19 | +import { checkRundownTimes, useRundownPlaylistOperations } from '../RundownHeader_old/useRundownPlaylistOperations' |
| 20 | +import { reloadRundownPlaylistClick } from '../RundownNotifier' |
| 21 | + |
| 22 | +export const RUNDOWN_CONTEXT_MENU_ID = 'rundown-context-menu' |
| 23 | + |
| 24 | +interface RundownContextMenuProps { |
| 25 | + playlist: DBRundownPlaylist |
| 26 | + studio: UIStudio |
| 27 | + firstRundown: Rundown | undefined |
| 28 | +} |
| 29 | + |
| 30 | +/** |
| 31 | + * The RundownContextMenu component renders both the context menu definition and the right-click |
| 32 | + * trigger area. It also registers event bus listeners for playlist operations (activate, |
| 33 | + * deactivate, take, reset, etc.) since these are tightly coupled to the menu actions. |
| 34 | + */ |
| 35 | +export function RundownContextMenu({ playlist, studio, firstRundown }: Readonly<RundownContextMenuProps>): JSX.Element { |
| 36 | + const { t } = useTranslation() |
| 37 | + const userPermissions = useContext(UserPermissionsContext) |
| 38 | + const operations = useRundownPlaylistOperations() |
| 39 | + |
| 40 | + const canClearQuickLoop = |
| 41 | + !!studio.settings.enableQuickLoop && |
| 42 | + !RundownResolver.isLoopLocked(playlist) && |
| 43 | + RundownResolver.isAnyLoopMarkerDefined(playlist) |
| 44 | + |
| 45 | + const rundownTimesInfo = checkRundownTimes(playlist.timing) |
| 46 | + |
| 47 | + // --- Event bus listeners for playlist operations --- |
| 48 | + const eventActivate = useCallback( |
| 49 | + (e: ActivateRundownPlaylistEvent) => { |
| 50 | + if (e.rehearsal) { |
| 51 | + operations.activateRehearsal(e.context) |
| 52 | + } else { |
| 53 | + operations.activate(e.context) |
| 54 | + } |
| 55 | + }, |
| 56 | + [operations] |
| 57 | + ) |
| 58 | + const eventDeactivate = useCallback( |
| 59 | + (e: DeactivateRundownPlaylistEvent) => operations.deactivate(e.context), |
| 60 | + [operations] |
| 61 | + ) |
| 62 | + const eventResync = useCallback((e: IEventContext) => operations.reloadRundownPlaylist(e.context), [operations]) |
| 63 | + const eventTake = useCallback((e: IEventContext) => operations.take(e.context), [operations]) |
| 64 | + const eventResetRundownPlaylist = useCallback((e: IEventContext) => operations.resetRundown(e.context), [operations]) |
| 65 | + const eventCreateSnapshot = useCallback((e: IEventContext) => operations.takeRundownSnapshot(e.context), [operations]) |
| 66 | + |
| 67 | + useRundownViewEventBusListener(RundownViewEvents.ACTIVATE_RUNDOWN_PLAYLIST, eventActivate) |
| 68 | + useRundownViewEventBusListener(RundownViewEvents.DEACTIVATE_RUNDOWN_PLAYLIST, eventDeactivate) |
| 69 | + useRundownViewEventBusListener(RundownViewEvents.RESYNC_RUNDOWN_PLAYLIST, eventResync) |
| 70 | + useRundownViewEventBusListener(RundownViewEvents.TAKE, eventTake) |
| 71 | + useRundownViewEventBusListener(RundownViewEvents.RESET_RUNDOWN_PLAYLIST, eventResetRundownPlaylist) |
| 72 | + useRundownViewEventBusListener(RundownViewEvents.CREATE_SNAPSHOT_FOR_DEBUG, eventCreateSnapshot) |
| 73 | + |
| 74 | + useEffect(() => { |
| 75 | + reloadRundownPlaylistClick.set(operations.reloadRundownPlaylist) |
| 76 | + }, [operations.reloadRundownPlaylist]) |
| 77 | + |
| 78 | + return ( |
| 79 | + <Escape to="document"> |
| 80 | + <ContextMenu id={RUNDOWN_CONTEXT_MENU_ID}> |
| 81 | + <div className="react-contextmenu-label">{playlist && playlist.name}</div> |
| 82 | + {userPermissions.studio ? ( |
| 83 | + <React.Fragment> |
| 84 | + {!(playlist.activationId && playlist.rehearsal) ? ( |
| 85 | + !rundownTimesInfo.shouldHaveStarted && !playlist.activationId ? ( |
| 86 | + <MenuItem onClick={operations.activateRehearsal}> |
| 87 | + {t('Prepare Studio and Activate (Rehearsal)')} |
| 88 | + </MenuItem> |
| 89 | + ) : ( |
| 90 | + <MenuItem onClick={operations.activateRehearsal}>{t('Activate (Rehearsal)')}</MenuItem> |
| 91 | + ) |
| 92 | + ) : ( |
| 93 | + <MenuItem onClick={operations.activate}>{t('Activate (On-Air)')}</MenuItem> |
| 94 | + )} |
| 95 | + {rundownTimesInfo.willShortlyStart && !playlist.activationId && ( |
| 96 | + <MenuItem onClick={operations.activate}>{t('Activate (On-Air)')}</MenuItem> |
| 97 | + )} |
| 98 | + {playlist.activationId ? <MenuItem onClick={operations.deactivate}>{t('Deactivate')}</MenuItem> : null} |
| 99 | + {studio.settings.allowAdlibTestingSegment && playlist.activationId ? ( |
| 100 | + <MenuItem onClick={operations.activateAdlibTesting}>{t('AdLib Testing')}</MenuItem> |
| 101 | + ) : null} |
| 102 | + {playlist.activationId ? <MenuItem onClick={operations.take}>{t('Take')}</MenuItem> : null} |
| 103 | + {studio.settings.allowHold && playlist.activationId ? ( |
| 104 | + <MenuItem onClick={operations.hold}>{t('Hold')}</MenuItem> |
| 105 | + ) : null} |
| 106 | + {playlist.activationId && canClearQuickLoop ? ( |
| 107 | + <MenuItem onClick={operations.clearQuickLoop}>{t('Clear QuickLoop')}</MenuItem> |
| 108 | + ) : null} |
| 109 | + {!(playlist.activationId && !playlist.rehearsal && !studio.settings.allowRundownResetOnAir) ? ( |
| 110 | + <MenuItem onClick={operations.resetRundown}>{t('Reset Rundown')}</MenuItem> |
| 111 | + ) : null} |
| 112 | + <MenuItem onClick={operations.reloadRundownPlaylist}> |
| 113 | + {t('Reload {{nrcsName}} Data', { |
| 114 | + nrcsName: getRundownNrcsName(firstRundown), |
| 115 | + })} |
| 116 | + </MenuItem> |
| 117 | + <MenuItem onClick={operations.takeRundownSnapshot}>{t('Store Snapshot')}</MenuItem> |
| 118 | + </React.Fragment> |
| 119 | + ) : ( |
| 120 | + <React.Fragment> |
| 121 | + <MenuItem>{t('No actions available')}</MenuItem> |
| 122 | + </React.Fragment> |
| 123 | + )} |
| 124 | + </ContextMenu> |
| 125 | + </Escape> |
| 126 | + ) |
| 127 | +} |
| 128 | + |
| 129 | +interface RundownContextMenuTriggerProps { |
| 130 | + children: React.ReactNode |
| 131 | +} |
| 132 | + |
| 133 | +export function RundownHeaderContextMenuTrigger({ children }: Readonly<RundownContextMenuTriggerProps>): JSX.Element { |
| 134 | + return ( |
| 135 | + <ContextMenuTrigger |
| 136 | + id={RUNDOWN_CONTEXT_MENU_ID} |
| 137 | + attributes={{ |
| 138 | + className: 'rundown-header__trigger', |
| 139 | + }} |
| 140 | + holdToDisplay={contextMenuHoldToDisplayTime()} |
| 141 | + > |
| 142 | + {children} |
| 143 | + </ContextMenuTrigger> |
| 144 | + ) |
| 145 | +} |
| 146 | + |
| 147 | +/** |
| 148 | + * A hamburger button that opens the context menu on left-click. |
| 149 | + */ |
| 150 | +export function RundownHamburgerButton(): JSX.Element { |
| 151 | + const { t } = useTranslation() |
| 152 | + const buttonRef = useRef<HTMLButtonElement | null>(null) |
| 153 | + |
| 154 | + const handleClick = useCallback((e: React.MouseEvent) => { |
| 155 | + e.preventDefault() |
| 156 | + e.stopPropagation() |
| 157 | + |
| 158 | + // Dispatch a custom contextmenu event |
| 159 | + if (buttonRef.current) { |
| 160 | + const rect = buttonRef.current.getBoundingClientRect() |
| 161 | + const event = new MouseEvent('contextmenu', { |
| 162 | + view: globalThis as unknown as Window, |
| 163 | + bubbles: true, |
| 164 | + cancelable: true, |
| 165 | + clientX: rect.left, |
| 166 | + clientY: rect.bottom + 5, |
| 167 | + button: 2, |
| 168 | + buttons: 2, |
| 169 | + }) |
| 170 | + buttonRef.current.dispatchEvent(event) |
| 171 | + } |
| 172 | + }, []) |
| 173 | + |
| 174 | + return ( |
| 175 | + <button ref={buttonRef} className="rundown-header__hamburger-btn" onClick={handleClick} title={t('Menu')}> |
| 176 | + <FontAwesomeIcon icon={faBars} /> |
| 177 | + </button> |
| 178 | + ) |
| 179 | +} |
0 commit comments