Skip to content

Commit b45e406

Browse files
committed
WIP - new topbar
1 parent e9fc4c4 commit b45e406

19 files changed

+1067
-212
lines changed

packages/webui/src/client/styles/_colorScheme.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,9 @@ $ui-button-primary--translucent: var(--ui-button-primary--translucent);
3737

3838
$ui-dark-color: var(--ui-dark-color);
3939
$ui-dark-color-brighter: var(--ui-dark-color-brighter);
40+
41+
$color-interactive-highlight: var(--color-interactive-highlight);
42+
43+
$color-header-inactive: var(--color-header-inactive);
44+
$color-header-rehearsal: var(--color-header-rehearsal);
45+
$color-header-on-air: var(--color-header-on-air);

packages/webui/src/client/styles/defaultColors.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,11 @@
4141
--ui-dark-color: #252627;
4242
--ui-dark-color-brighter: #5f6164;
4343

44+
--color-interactive-highlight: #40b8fa99;
45+
46+
--color-header-inactive: rgb(38, 137, 186);
47+
--color-header-rehearsal: #666600;
48+
--color-header-on-air: #000000;
49+
4450
--segment-timeline-background-color: #{$segment-timeline-background-color};
4551
}

packages/webui/src/client/styles/notifications.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@
490490
.rundown-view {
491491
&.notification-center-open {
492492
padding-right: 25vw !important;
493-
> .rundown-header .rundown-overview {
493+
> .rundown-header_OLD .rundown-overview {
494494
padding-right: calc(25vw + 1.5em) !important;
495495
}
496496
}

packages/webui/src/client/styles/rundownView.scss

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -142,19 +142,19 @@ $break-width: 35rem;
142142
}
143143
}
144144

145-
.rundown-header .notification-pop-ups {
145+
.rundown-header_OLD .notification-pop-ups {
146146
top: 65px;
147147
}
148148

149-
> .rundown-header .rundown-overview {
149+
> .rundown-header_OLD .rundown-overview {
150150
transition: 0s padding-right 0.5s;
151151
}
152152

153153
&.notification-center-open {
154154
padding-right: $notification-center-width;
155155
transition: 0s padding-right 1s;
156156

157-
> .rundown-header .rundown-overview {
157+
> .rundown-header_OLD .rundown-overview {
158158
padding-right: calc(#{$notification-center-width} + 1.5em);
159159
transition: 0s padding-right 1s;
160160
}
@@ -164,7 +164,7 @@ $break-width: 35rem;
164164
padding-right: $properties-panel-width;
165165
transition: 0s padding-right 1s;
166166

167-
> .rundown-header .rundown-overview {
167+
> .rundown-header_OLD .rundown-overview {
168168
padding-right: calc(#{$properties-panel-width} + 1.5em);
169169
transition: 0s padding-right 1s;
170170
}
@@ -245,7 +245,7 @@ body.no-overflow {
245245
}
246246
}
247247

248-
.rundown-header {
248+
.rundown-header_OLD {
249249
padding: 0;
250250

251251
.header-row {
@@ -488,17 +488,17 @@ body.no-overflow {
488488
cursor: default;
489489
}
490490

491-
.rundown-header.not-active .first-row {
491+
.rundown-header_OLD.not-active .first-row {
492492
background-color: rgb(38, 137, 186);
493493
}
494-
.rundown-header.not-active .first-row .timing-clock,
495-
.rundown-header.not-active .first-row .timing-clock-label {
494+
.rundown-header_OLD.not-active .first-row .timing-clock,
495+
.rundown-header_OLD.not-active .first-row .timing-clock-label {
496496
color: #fff !important;
497497
}
498-
// .rundown-header.active .first-row {
498+
// .rundown-header_OLD.active .first-row {
499499
// background-color: #600
500500
// }
501-
.rundown-header.active.rehearsal .first-row {
501+
.rundown-header_OLD.active.rehearsal .first-row {
502502
background-color: #660;
503503
}
504504

@@ -3585,7 +3585,7 @@ svg.icon {
35853585

35863586
@import 'rundownOverview';
35873587

3588-
.rundown-header .timing__header_t-timers {
3588+
.rundown-header_OLD .timing__header_t-timers {
35893589
position: absolute;
35903590
right: 100%;
35913591
top: 50%;

packages/webui/src/client/ui/RundownList/util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { doModalDialog } from '../../lib/ModalDialog.js'
44
import { doUserAction, UserAction } from '../../lib/clientUserAction.js'
55
import { MeteorCall } from '../../lib/meteorApi.js'
66
import { TFunction } from 'i18next'
7-
import { handleRundownReloadResponse } from '../RundownView/RundownHeader/RundownReloadResponse.js'
7+
import { handleRundownReloadResponse } from '../RundownView/RundownHeader_old/RundownReloadResponse.js'
88
import {
99
RundownId,
1010
RundownLayoutId,

packages/webui/src/client/ui/RundownView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ export function RundownView(props: Readonly<IProps>): JSX.Element {
291291
return (
292292
<div
293293
className={classNames('container-fluid', 'header-clear', {
294-
'header-clear--no-rundown-header': hideRundownHeader,
294+
'header-clear--no-rundown-header_OLD': hideRundownHeader,
295295
})}
296296
>
297297
<RundownViewContent
@@ -1399,6 +1399,7 @@ const RundownViewContent = translateWithTracker<IPropsWithReady & ITrackedProps,
13991399
onActivate={this.onActivate}
14001400
inActiveRundownView={this.props.inActiveRundownView}
14011401
currentRundown={currentRundown}
1402+
rundownCount={this.props.rundowns.length}
14021403
layout={this.props.selectedHeaderLayout}
14031404
showStyleBase={showStyleBase}
14041405
showStyleVariant={showStyleVariant}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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

Comments
 (0)