27 |
28 |
29 |
30 | {Object.entries(bridgeStatus?.devices).filter(([id]) => {
31 | return props.bridge.settings.devices[id]
32 | }).length > 0 && (
33 |
34 |
Device statuses:
35 | {Object.entries(bridgeStatus?.devices)
36 | /**
37 | * Temporary fix - just like in DevicesList.tsx.
38 | * TODO - fix this bug on the backend side.
39 | */
40 | .filter(([id]) => {
41 | return props.bridge.settings.devices[id]
42 | })
43 | .map(([deviceId, device]) => {
44 | const deviceSettings = props.bridge.settings.devices[deviceId]
45 |
46 | return
47 | })}
48 |
49 | )}
50 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/bridgeItem/style.scss:
--------------------------------------------------------------------------------
1 | .bridge-item-header {
2 | display: flex;
3 | align-items: center;
4 |
5 | .device-statuses {
6 | display: flex;
7 | align-items: center;
8 |
9 | > .label {
10 | font-size: 1.2rem;
11 | color: rgba(255, 255, 255, 0.7);
12 | margin-right: 1.5rem;
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/deviceIcon/style.scss:
--------------------------------------------------------------------------------
1 | .device-icon {
2 | display: flex;
3 | align-items: center;
4 | width: 40px;
5 |
6 | img {
7 | max-height: 2rem;
8 | max-width: 3rem;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/deviceItem/DeviceItemHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Bridge, BridgeDevice } from '../../../../../models/project/Bridge'
3 | import { AtemOptions, CasparCGOptions, DeviceType } from 'timeline-state-resolver-types'
4 |
5 | import './style.scss'
6 | import { DeviceShortcut } from '../deviceShorcut/DeviceShortcut'
7 | import { ScListItemLabel } from '../scList/ScListItemLabel'
8 |
9 | export const DeviceItemHeader: React.FC<{
10 | bridge: Bridge
11 | deviceId: string
12 | device: BridgeDevice
13 | deviceName?: string
14 | }> = (props) => {
15 | const deviceSettings = props.bridge.settings.devices[props.deviceId]
16 |
17 | if (!deviceSettings || !deviceSettings.options) return <>>
18 |
19 | const deviceOptions = deviceSettings.options as CasparCGOptions | AtemOptions
20 |
21 | if (!deviceOptions) {
22 | return null
23 | }
24 | let deviceAddress = `${deviceOptions.host}:${deviceOptions.port}`
25 | if (deviceSettings.type === DeviceType.HTTPSEND) {
26 | deviceAddress = ''
27 | }
28 |
29 | return (
30 | = ({ mapping }) => {
14 | const ipcServer = useContext(IPCServerContext)
15 | const project = useContext(ProjectContext)
16 | const { handleError } = useContext(ErrorHandlerContext)
17 |
18 | const handleMappingTypeChange = useCallback(
19 | (newMappingType: MappingAtemType) => {
20 | mapping.mappingType = newMappingType
21 | ipcServer.updateProject({ id: project.id, project }).catch(handleError)
22 | },
23 | [handleError, ipcServer, mapping, project]
24 | )
25 |
26 | const handleIndexChange = useCallback(
27 | (newIndex: MappingAtem['index']) => {
28 | mapping.index = newIndex
29 | ipcServer.updateProject({ id: project.id, project }).catch(handleError)
30 | },
31 | [handleError, ipcServer, mapping, project]
32 | )
33 |
34 | return (
35 | <>
36 |
37 |
44 |
45 |
46 |
47 |
56 |
57 | >
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/layersPage/device-specific-settings/CasparCGMappingSettings.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { IntInput } from '../../../../inputs/IntInput'
3 | import { MappingCasparCG } from 'timeline-state-resolver-types'
4 |
5 | export const CasparCGMappingSettings: React.FC<{
6 | mapping: MappingCasparCG
7 | onUpdate: (mappingUpdate: MappingCasparCG) => void
8 | }> = (props) => {
9 | return (
10 | <>
11 |
12 | {
19 | props.onUpdate({ ...props.mapping, channel: v })
20 | }}
21 | caps={[0, 999]}
22 | />
23 |
24 |
25 |
26 | {
33 | props.onUpdate({ ...props.mapping, layer: v })
34 | }}
35 | caps={[0, 999]}
36 | />
37 |
38 | >
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/message/Message.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { IoIosHelpCircleOutline } from 'react-icons/io'
3 | import { IoClose } from 'react-icons/io5'
4 |
5 | import './style.scss'
6 |
7 | export const Message: React.FC<{
8 | content?: React.ReactNode
9 | type: 'help' | 'warning'
10 | onClose?: () => void
11 | children?: React.ReactNode
12 | }> = (props) => {
13 | return (
14 |
15 |
16 |
17 |
18 |
{props.content || props.children}
19 | {props.onClose && (
20 |
23 | )}
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/message/style.scss:
--------------------------------------------------------------------------------
1 | .message {
2 | background: linear-gradient(180deg, #383a45 0%, #393a45 100%);
3 | font-size: 1.2rem;
4 | padding: 1rem 2rem;
5 | display: flex;
6 | border-radius: 0.5rem;
7 | align-items: center;
8 |
9 | .icon {
10 | flex-grow: 0;
11 | margin-right: 1rem;
12 | display: flex;
13 | align-items: center;
14 |
15 | svg {
16 | width: 2rem;
17 | height: 2rem;
18 | }
19 | }
20 | .content {
21 | flex-grow: 1;
22 | // margin-top: 0.2rem;
23 | }
24 |
25 | button.close {
26 | background: none;
27 | padding: 0;
28 | border: 0;
29 | color: white;
30 | opacity: 0.7;
31 | display: flex;
32 | align-items: center;
33 | margin-left: 1rem;
34 |
35 | &:hover {
36 | opacity: 1;
37 | }
38 |
39 | svg {
40 | width: 1.5rem;
41 | height: 1.5rem;
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/peripheralsList/PeripheralsList.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react-lite'
2 | import React from 'react'
3 | import { Bridge, BridgeStatus } from '../../../../../models/project/Bridge'
4 | import { ScList } from '../scList/ScList'
5 | import { store } from '../../../../mobx/store'
6 | import { PeripheralStatus } from '../../../../../models/project/Peripheral'
7 | import { PeripheralItemHeader } from '../peripheralItem/PeripheralItemHeader'
8 |
9 | export const PeripheralsList: React.FC<{
10 | autoConnectToAllPeripherals: boolean
11 | bridgeId: string
12 | statuses: BridgeStatus['peripherals']
13 | settings: Bridge['settings']['peripherals']
14 | }> = observer(function PeripheralsList(props) {
15 | const appStore = store.appStore
16 |
17 | if (Object.keys(props.statuses).length === 0) {
18 | return (
19 |
20 | No panels connected.
21 |
22 | Connected Streamdeck or X-keys panels will appear here.
23 |
24 | )
25 | }
26 |
27 | return (
28 |
29 |
{
31 | const peripheralSettings = props.settings[peripheralId]
32 | const otherStatus = appStore.peripherals[`${props.bridgeId}-${peripheralId}`] as
33 | | PeripheralStatus
34 | | undefined
35 | if (!peripheralSettings)
36 | return {
37 | id: peripheralId,
38 | header: null,
39 | content: null,
40 | }
41 |
42 | return {
43 | id: peripheralId,
44 | header: (
45 |
52 | ),
53 | }
54 | })}
55 | />
56 | {}
57 |
58 | )
59 | })
60 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/projectPage/style.scss:
--------------------------------------------------------------------------------
1 | .rundown-header-item {
2 | display: flex;
3 | align-items: center;
4 | width: 100%;
5 |
6 | .header-label {
7 | flex-grow: 1;
8 | border: 0;
9 | }
10 |
11 | .controls {
12 | display: flex;
13 | align-items: center;
14 |
15 | button {
16 | &:not(:last-child) {
17 | margin-right: 2rem;
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/projectPageLayout/ProjectPageLayout.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { HelpButton } from '../../../inputs/HelpButton/HelpButton'
3 | import { Message } from '../message/Message'
4 | import './style.scss'
5 |
6 | export const ProjectPageLayout: React.FC<{
7 | title: string
8 | subtitle?: string
9 | help?: React.ReactNode
10 | controls?: React.ReactNode
11 | children: React.ReactNode
12 | }> = (props) => {
13 | const [showHelp, setShowHelp] = useState(false)
14 |
15 | return (
16 |
17 |
18 |
19 |
{props.subtitle}
20 |
{props.title}
21 |
22 | {props.help && (
23 |
{
26 | setShowHelp(!showHelp)
27 | }}
28 | />
29 | )}
30 | {props.controls && {props.controls}
}
31 |
32 | {showHelp && props.help &&
setShowHelp(false)} />}
33 | {props.children}
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/projectPageLayout/style.scss:
--------------------------------------------------------------------------------
1 | @import '../../../../styles/foundation/all';
2 |
3 | .dialog-form {
4 | .form-control {
5 | .MuiFormControl-root {
6 | width: 100%;
7 | }
8 | }
9 | }
10 | .form-control {
11 | margin-right: 1rem;
12 | display: flex;
13 | align-items: center;
14 | }
15 |
16 | .project-page-layout {
17 | display: flex;
18 | min-height: 0;
19 |
20 | .main {
21 | flex-grow: 1;
22 | overflow: auto;
23 | padding: 2rem 2.5rem;
24 |
25 | > .header {
26 | display: flex;
27 | align-items: flex-end;
28 | margin-bottom: 4rem;
29 |
30 | .section {
31 | margin: 0 1rem;
32 | }
33 |
34 | .titles {
35 | flex-grow: 0;
36 |
37 | .title {
38 | font-size: 2.8rem;
39 | font-weight: 700;
40 | }
41 |
42 | .subtitle {
43 | font-size: 1.4rem;
44 | font-weight: 500;
45 | margin-bottom: -0.5rem;
46 | }
47 | }
48 |
49 | .controls {
50 | display: flex;
51 | align-items: center;
52 | margin-left: 3rem;
53 | margin-bottom: 0.7rem;
54 | }
55 | }
56 |
57 | > .message {
58 | margin-bottom: 4rem;
59 | margin-top: -2rem;
60 | }
61 |
62 | > .content {
63 | > .note {
64 | text-align: center;
65 | opacity: 0.7;
66 | padding: 2rem 0;
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/projectPageMenubar/ProjectPageMenubar.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import React from 'react'
3 |
4 | import './style.scss'
5 |
6 | export const ProjectPageMenubar: React.FC<{
7 | menubar: {
8 | groupId: string
9 | items: {
10 | label: string
11 | id: string
12 | icon?: React.ReactNode
13 | }[]
14 | }[]
15 | activeItemId?: string
16 | onItemClick: (itemId: string) => void
17 | }> = (props) => {
18 | return (
19 |
20 | {props.menubar.map((group) => {
21 | return (
22 |
23 | {group.items.map((item) => {
24 | const isActive = item.id === props.activeItemId
25 | return (
26 |
props.onItemClick(item.id)}
30 | >
31 | {item.icon &&
{item.icon}
}
32 |
{item.label}
33 |
34 | )
35 | })}
36 |
37 |
38 | )
39 | })}
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/projectPageMenubar/style.scss:
--------------------------------------------------------------------------------
1 | .menubar {
2 | flex-grow: 0;
3 | flex-shrink: 0;
4 | width: 20rem;
5 | background: linear-gradient(180deg, #191b23 0%, #252733 19.83%);
6 |
7 | &__group {
8 | margin-top: 1rem;
9 | .item {
10 | padding: 1rem 1rem;
11 | font-size: 1.3rem;
12 | font-weight: 500;
13 | display: flex;
14 | align-items: center;
15 | cursor: pointer;
16 |
17 | .icon {
18 | margin-right: 1rem;
19 | display: flex;
20 | align-items: center;
21 |
22 | svg {
23 | height: 1.8rem;
24 | width: 1.8rem;
25 | }
26 | }
27 |
28 | &:hover {
29 | background-color: rgba(255, 255, 255, 0.05);
30 | }
31 |
32 | &.active {
33 | background: linear-gradient(90deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0) 159.04%), #3a4152;
34 | }
35 | }
36 |
37 | &:not(:last-child) {
38 | .separator {
39 | margin: 1rem 1rem 0 1rem;
40 | height: 0.2rem;
41 | box-sizing: border-box;
42 | width: auto;
43 | background: linear-gradient(90deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0) 159.04%), #3a4152;
44 | }
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/roundedSection/RoundedSection.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { HelpButton } from '../../../inputs/HelpButton/HelpButton'
3 | import { Message } from '../message/Message'
4 |
5 | import './style.scss'
6 |
7 | export const RoundedSection: React.FC<{
8 | title: React.ReactNode
9 | controls?: React.ReactNode
10 | help?: string
11 | children: React.ReactNode
12 | }> = (props) => {
13 | const [showHelp, setShowHelp] = useState(false)
14 |
15 | return (
16 |
17 |
18 |
{props.title}
19 | {props.controls &&
{props.controls}
}
20 |
21 | {props.help && (
22 |
{
25 | setShowHelp(!showHelp)
26 | }}
27 | />
28 | )}
29 |
30 |
31 |
32 | {showHelp && props.help && (
33 |
34 | setShowHelp(false)} />
35 |
36 | )}
37 | {props.children}
38 |
39 |
40 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/scList/ScList.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import React, { useState } from 'react'
3 | import { MdKeyboardArrowDown } from 'react-icons/md'
4 | import './style.scss'
5 |
6 | export const ScList: React.FC<{
7 | list: { id: string; header: React.ReactNode; content?: React.ReactNode }[]
8 | /** List of IDs that should be open by default */
9 | openByDefault?: string[]
10 | }> = (props) => {
11 | return (
12 |
13 | {props.list.map((item) => {
14 | return (
15 |
22 | )
23 | })}
24 |
25 | )
26 | }
27 |
28 | export const ScListItem: React.FC<{
29 | id: string
30 | header: React.ReactNode
31 | content?: React.ReactNode
32 | openByDefault?: boolean
33 | }> = (props) => {
34 | const [isOpen, setOpen] = useState(props.openByDefault ?? false)
35 |
36 | return (
37 |
38 | {
41 | if (props.content) setOpen(!isOpen)
42 | }}
43 | >
44 | {props.content && (
45 |
46 |
47 |
48 | )}
49 |
{props.header}
50 |
51 |
52 | {props.content && isOpen && {props.content}
}
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/scList/ScListItemLabel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const ScListItemLabel: React.FC<{ title: string; subtitle?: string }> = (props) => {
4 | return (
5 |
6 |
{props.title}
7 | {props.subtitle &&
{props.subtitle}
}
8 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/homePage/scList/StatusCircle.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import React from 'react'
3 |
4 | export const StatusCircle: React.FC<{ status: 'connected' | 'disconnected' }> = (props) => {
5 | return
6 | }
7 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/newRundownPage/ImportRundownIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const ImportRundownIcon = (): JSX.Element => (
4 |
13 | )
14 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/newRundownPage/NewGropIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const NewGroupIcon = (): JSX.Element => (
4 |
17 | )
18 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/newRundownPage/NewRundownOption.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react-lite'
2 | import React from 'react'
3 |
4 | import './newRundownOption.scss'
5 |
6 | export const NewRundownOption: React.FC<{ label: string; icon: React.ReactNode; onClick: () => void }> = observer(
7 | function NewRundownOption(props) {
8 | return (
9 |
13 | )
14 | }
15 | )
16 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/newRundownPage/newRundownOption.scss:
--------------------------------------------------------------------------------
1 | .new-rundown-option {
2 | background: transparent;
3 | border-radius: 1rem;
4 | border: 2px solid #4e4f59;
5 | color: white;
6 | padding: 1.5rem 2.5rem;
7 | display: flex;
8 | align-items: center;
9 | width: 35rem;
10 |
11 | cursor: pointer;
12 |
13 | &:not(:last-child) {
14 | margin-bottom: 2rem;
15 | }
16 |
17 | &:hover {
18 | background: rgba(255, 255, 255, 0.05);
19 | }
20 |
21 | .icon {
22 | svg {
23 | height: 3.8rem;
24 | width: 3.8rem;
25 |
26 | path {
27 | stroke: #a8aaba;
28 | }
29 | }
30 | }
31 | .label {
32 | margin-left: 2rem;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/pages/newRundownPage/newRundownPage.scss:
--------------------------------------------------------------------------------
1 | @import '../../../styles/foundation/all';
2 |
3 | .new-rundown-page {
4 | display: flex;
5 | align-items: center;
6 |
7 | .title {
8 | flex-basis: 50%;
9 | text-align: right;
10 | font-size: 2.4rem;
11 | font-weight: 700;
12 | color: #cfd1dc;
13 | padding-right: 5rem;
14 | }
15 |
16 | .options {
17 | flex-basis: 50%;
18 | padding-left: 5rem;
19 | border-left: 2px solid #4e4f59;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/rundown/GroupView/PartSubmenu.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@mui/material'
2 | import React, { useContext, useState } from 'react'
3 | import { MdOutlineEditNote } from 'react-icons/md'
4 | import { PartGUI } from '../../../../models/rundown/Part'
5 | import { ErrorHandlerContext } from '../../../contexts/ErrorHandler'
6 | import { IPCServerContext } from '../../../contexts/IPCServer'
7 | import { PartPropertiesDialog } from '../PartPropertiesDialog'
8 |
9 | export const PartSubmenu: React.FC<{
10 | rundownId: string
11 | groupId: string
12 | part: PartGUI
13 | /** Part or group locked */
14 | locked: boolean
15 | }> = ({ rundownId, groupId, part, locked }) => {
16 | const ipcServer = useContext(IPCServerContext)
17 | const { handleError } = useContext(ErrorHandlerContext)
18 |
19 | const [partPropertiesDialogOpen, setPartPropertiesDialogOpen] = useState(false)
20 |
21 | return (
22 |
23 |
24 |
35 |
36 |
37 |
{
43 | ipcServer
44 | .updatePart({
45 | rundownId,
46 | groupId,
47 | partId: part.id,
48 | part: {
49 | ...part,
50 | name,
51 | },
52 | })
53 | .catch(handleError)
54 | setPartPropertiesDialogOpen(false)
55 | }}
56 | onDiscarded={() => {
57 | setPartPropertiesDialogOpen(false)
58 | }}
59 | />
60 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/rundown/GroupView/PlayHead.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { observer } from 'mobx-react-lite'
3 | import { store } from '../../../mobx/store'
4 | import { useMemoComputedValue } from '../../../mobx/lib'
5 |
6 | type PropsType = {
7 | groupId: string
8 | partId: string
9 | partViewDuration: number
10 | }
11 |
12 | export const PlayHead = observer(function PlayHead(props: PropsType) {
13 | const percentage: number | null = useMemoComputedValue(() => {
14 | const playhead = store.groupPlayDataStore.groups.get(props.groupId)?.playheads[props.partId]
15 |
16 | if (!playhead) return null
17 | if (!props.partViewDuration) {
18 | // The part is infinitely long
19 | if (playhead.partPauseTime !== undefined) return 0
20 | else return 100
21 | }
22 |
23 | return Math.min(1, playhead.playheadTime / props.partViewDuration) * 100
24 | }, [props.groupId, props.partId, props.partViewDuration])
25 | if (percentage === null) return null
26 |
27 | return (
28 |
32 | )
33 | })
34 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/rundown/GroupView/part/CountdownHeads/CountdownHead.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const CountDownHead: React.FC<{ timeUntilStart: number }> = ({ timeUntilStart }) => {
4 | // The time where the countdown should start to show
5 | const TIME_MAX = 30 * 1000
6 |
7 | const percentage = Math.min(100, (timeUntilStart / TIME_MAX) * 100)
8 |
9 | return (
10 |
11 | {/* {percentage} */}
12 | {/*
*/}
13 |
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/rundown/GroupView/part/CountdownHeads/CountdownHeads.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { observer } from 'mobx-react-lite'
3 | import { store } from '../../../../../mobx/store'
4 | import { CountDownHead } from './CountdownHead'
5 | import { useMemoComputedObject } from '../../../../../mobx/lib'
6 |
7 | type PropsType = {
8 | groupId: string
9 | partId: string
10 | }
11 |
12 | export const CountdownHeads = observer(function CountdownHeads(props: PropsType) {
13 | const timesUntilStart = useMemoComputedObject(() => {
14 | const playData = store.groupPlayDataStore.groups.get(props.groupId)
15 |
16 | if (!playData) return null
17 |
18 | return playData.countdowns[props.partId] || null
19 | }, [props.groupId, props.partId])
20 |
21 | return (
22 | <>
23 | {timesUntilStart &&
24 | timesUntilStart.map((timeUntilStart, index) => (
25 |
26 | ))}
27 | >
28 | )
29 | })
30 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/rundown/GroupView/part/CurrentTime/CurrentTime.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { observer } from 'mobx-react-lite'
3 | import { store } from '../../../../../mobx/store'
4 | import { useMemoComputedObject } from '../../../../../mobx/lib'
5 | import { formatDuration } from '../../../../../../lib/timeLib'
6 | import { DISPLAY_DECIMAL_COUNT } from '../../../../../constants'
7 |
8 | type PropsType = {
9 | groupId: string
10 | partId: string
11 | }
12 |
13 | export const CurrentTime = observer(function CurrentTime(props: PropsType) {
14 | // Memoize this, to avoid recalculating it every time the playhead is calculated
15 | const { value, label } = useMemoComputedObject(
16 | () => {
17 | const playData = store.groupPlayDataStore.groups.get(props.groupId)
18 | if (playData) {
19 | const playhead = playData.playheads[props.partId]
20 | const countDowns = playData.countdowns[props.partId] ?? []
21 | if (playhead) {
22 | const playheadTime = playhead.playheadTime
23 | if (typeof playheadTime === 'number') {
24 | return {
25 | label: 'ELAPSED',
26 | value: formatDuration(playheadTime, DISPLAY_DECIMAL_COUNT),
27 | }
28 | }
29 | } else if (countDowns.length > 0) {
30 | const countDown = countDowns[0]
31 |
32 | return {
33 | label: 'TO START',
34 | value: formatDuration(countDown.duration, DISPLAY_DECIMAL_COUNT, true),
35 | }
36 | }
37 | }
38 | // else:
39 | return {
40 | label: '',
41 | value: null,
42 | }
43 | },
44 | [props.groupId, props.partId],
45 | true
46 | )
47 |
48 | if (!value) return null
49 | if (!label) return null
50 |
51 | return (
52 | <>
53 | {label}{' '}
54 | {value}
55 | >
56 | )
57 | })
58 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/rundown/GroupView/part/RemainingTime/RemainingTime.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { observer } from 'mobx-react-lite'
3 | import { store } from '../../../../../mobx/store'
4 | import { useMemoComputedValue } from '../../../../../mobx/lib'
5 | import { formatDuration } from '../../../../../../lib/timeLib'
6 | import { DISPLAY_DECIMAL_COUNT } from '../../../../../constants'
7 |
8 | type PropsType = {
9 | groupId: string
10 | partId: string
11 | }
12 |
13 | export const RemainingTime = observer(function RemainingTime(props: PropsType) {
14 | const countDownTimeString = useMemoComputedValue(() => {
15 | const playhead = store.groupPlayDataStore.groups.get(props.groupId)?.playheads[props.partId]
16 | if (!playhead) return null
17 |
18 | if (playhead.partDuration === null) return null
19 |
20 | const countDownTime = playhead.partDuration - playhead.playheadTime
21 | if (!countDownTime) return null
22 | return formatDuration(countDownTime, DISPLAY_DECIMAL_COUNT, true)
23 | }, [props.groupId, props.partId])
24 |
25 | if (!countDownTimeString) return null
26 |
27 | return (
28 | <>
29 | REMAINING{' '}
30 | {countDownTimeString}
31 | >
32 | )
33 | })
34 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/rundown/ScrollWatcher/style.scss:
--------------------------------------------------------------------------------
1 | .scroll-watch-container {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | width: 100%;
6 | height: 100%;
7 |
8 | overflow-y: auto;
9 | overflow-x: hidden;
10 | }
11 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/DataRow/DataRow.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useContext } from 'react'
2 | import { useSnackbar } from 'notistack'
3 | import { ErrorHandlerContext } from '../../../contexts/ErrorHandler'
4 |
5 | import './style.scss'
6 |
7 | export const DataRow = (props: { label: string; value: any }): JSX.Element => {
8 | const { handleError } = useContext(ErrorHandlerContext)
9 | const { enqueueSnackbar } = useSnackbar()
10 |
11 | const copyValueToClipboard = useCallback(async () => {
12 | try {
13 | await navigator.clipboard.writeText(props.value)
14 | enqueueSnackbar(`Value copied to clipboard.`, { variant: 'success' })
15 | } catch (error) {
16 | handleError(error)
17 | }
18 | }, [enqueueSnackbar, handleError, props.value])
19 |
20 | return (
21 |
22 |
23 | {props.label}
24 |
25 |
{
29 | void copyValueToClipboard()
30 | }}
31 | >
32 | {props.value}
33 |
34 |
35 | )
36 | }
37 |
38 | export const FormRow = (props: { children: React.ReactNode }): JSX.Element => {
39 | return {props.children}
40 | }
41 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/DataRow/style.scss:
--------------------------------------------------------------------------------
1 | .data-row {
2 | .copy-to-clipboard {
3 | cursor: pointer;
4 |
5 | &:hover {
6 | text-decoration: underline;
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/SidebarContent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import classNames from 'classnames'
3 |
4 | export const SidebarContent: React.FC<{
5 | title: string | React.ReactNode
6 | className: string
7 | children: React.ReactNode
8 | }> = (props) => {
9 | return (
10 |
11 |
12 | {typeof props.title === 'string' ? {props.title} : props.title}
13 |
14 |
{props.children}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/resource/ResourceLibraryItemThumbnail.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { FaFile, FaFileAudio, FaFileVideo } from 'react-icons/fa'
3 | import { CasparCGMedia } from '@shared/models'
4 |
5 | export const ResourceLibraryItemThumbnail: React.FC<{ resource: CasparCGMedia }> = (props) => {
6 | const { resource } = props
7 |
8 | if (resource.thumbnail) {
9 | return
10 | }
11 |
12 | if (resource.type === 'video' && !resource.thumbnail) {
13 | return
14 | }
15 |
16 | if (resource.type === 'audio') {
17 | return
18 | }
19 |
20 | return
21 | }
22 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/GDD/GDDTypes/select.tsx:
--------------------------------------------------------------------------------
1 | import { assertNever } from '@shared/lib'
2 | import { GDDTypeIntegerSelect, GDDTypeStringSelect, GDDTypeNumberSelect } from 'graphics-data-definition'
3 | import React from 'react'
4 | import { SelectEnum } from '../../../../inputs/SelectEnum'
5 | import { EditProperty, getEditPropertyMeta, PropertyProps } from '../lib'
6 |
7 | export const gddTypeSelect: React.FC<
8 | PropertyProps
9 | > = (props) => {
10 | const data = props.data || ''
11 | const { label, description } = getEditPropertyMeta(props)
12 |
13 | const options: { [key: string]: any } = {}
14 |
15 | for (const enumValue of props.schema.enum) {
16 | const label = props.schema.gddOptions?.labels[enumValue] as string | undefined
17 | options[label || enumValue] = enumValue
18 | }
19 |
20 | return (
21 |
22 | {
28 | if (props.schema.type === 'string') {
29 | props.setData(String(value))
30 | props.onSave()
31 | } else if (props.schema.type === 'integer') {
32 | props.setData(parseInt(value, 10))
33 | props.onSave()
34 | } else if (props.schema.type === 'number') {
35 | props.setData(parseFloat(value))
36 | props.onSave()
37 | } else assertNever(props.schema)
38 | }}
39 | allowUndefined={true}
40 | options={options}
41 | />
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/GDD/GDDTypes/string-color-rrggbb.tsx:
--------------------------------------------------------------------------------
1 | import { GDDTypeColorRRGGBB } from 'graphics-data-definition'
2 | import React from 'react'
3 | import { EditProperty, PropertyProps, WithLabel } from '../lib'
4 |
5 | export const gddTypeColorRRGGBB: React.FC> = (props) => {
6 | const data = props.data || ''
7 | return (
8 |
9 |
10 | {
14 | props.setData(e.target.value)
15 | }}
16 | onBlur={props.onSave}
17 | />
18 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/GDD/GDDTypes/string-multi-line.tsx:
--------------------------------------------------------------------------------
1 | import { GDDTypeMultiLine } from 'graphics-data-definition'
2 | import React from 'react'
3 | import { EditProperty, PropertyProps } from '../lib'
4 |
5 | export const gddTypeMultiLine: React.FC> = (props) => {
6 | const data = props.data || ''
7 | return (
8 |
9 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/GDD/GDDTypes/string-single-line.tsx:
--------------------------------------------------------------------------------
1 | import { GDDTypeSingleLine } from 'graphics-data-definition'
2 | import React from 'react'
3 | import { TextInput } from '../../../../inputs/TextInput'
4 | import { EditProperty, getEditPropertyMeta, PropertyProps } from '../lib'
5 |
6 | export const gddTypeSingleLine: React.FC> = (props) => {
7 | const data = props.data || ''
8 | const { label, description } = getEditPropertyMeta(props)
9 | return (
10 |
11 | {
17 | props.setData(v || undefined)
18 | props.onSave()
19 | }}
20 | allowUndefined={true}
21 | />
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/GDD/gddEdit.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { GDDSchema } from 'graphics-data-definition'
3 | import { TSRTimelineObj } from 'timeline-state-resolver-types'
4 |
5 | import './style.scss'
6 | import { componentAny } from './componentAny'
7 | import { deepClone } from '@shared/lib'
8 | import { OnSave } from '../timelineObjs/lib'
9 |
10 | export const EditGDDData: React.FC<{
11 | objs: TSRTimelineObj[]
12 |
13 | schema: GDDSchema
14 | data: any
15 | onSaveObj: OnSave
16 | onSaveContentData: (newData: any) => void
17 | }> = ({ objs, schema, data, onSaveContentData, onSaveObj }) => {
18 | // clone, since the data is edited internally:
19 | const [currentData, setCurrentData] = useState(JSON.parse(JSON.stringify(data)))
20 |
21 | useEffect(() => {
22 | // clone, since the data is edited internally:
23 | setCurrentData(JSON.parse(JSON.stringify(data)))
24 | }, [data])
25 |
26 | const updateCurrentData = (data: any) => {
27 | setCurrentData(deepClone(data))
28 | }
29 | const onSave = () => {
30 | onSaveContentData(currentData)
31 | }
32 |
33 | return (
34 |
35 | {componentAny({
36 | objs: objs,
37 | fullPath: [],
38 | onSaveObj,
39 | schema: schema,
40 | data: currentData,
41 | setData: updateCurrentData,
42 | onSave,
43 | property: '',
44 | })}
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/GDD/style.scss:
--------------------------------------------------------------------------------
1 | .gdd-edit-data {
2 | --padding: 0.2em;
3 |
4 | display: block;
5 | border: 1px solid grey;
6 | border-radius: var(--padding);
7 | padding: var(--padding);
8 |
9 | overflow-x: auto;
10 |
11 | .gdd-edit-data__label {
12 | font-weight: bold;
13 | }
14 | .gdd-edit-data__description {
15 | font-weight: normal;
16 | font-style: italic;
17 | font-size: 80%;
18 | }
19 | .gdd-edit-data__edit {
20 | overflow-x: auto;
21 | }
22 | .gdd-edit-data__data-validation {
23 | background-color: rgba(255,50,50,0.3);
24 | border: rgba(255,0,0,0.7);
25 | }
26 | .gdd-edit-data__gdd-property {
27 | margin: 0.1em;
28 | }
29 | .gdd-edit-data__gdd-property-array>.items {
30 | display: flex;
31 | flex-direction: column;
32 | }
33 | .gdd-edit-data__gdd-property-array table td {
34 | vertical-align: top;
35 | }
36 | .gdd-edit-data__gdd-property-object {
37 | border: 1px solid grey;
38 | border-radius: var(--padding);
39 | padding: var(--padding);
40 | box-shadow: 0px 2px 10px rgba(0,0,0, 0.2);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/abstract.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TimelineObjAbstractAny } from 'timeline-state-resolver-types'
3 | import { EditWrapper, OnSave } from './lib'
4 |
5 | export const EditTimelineObjAbstractAny: React.FC<{ objs: TimelineObjAbstractAny[]; onSave: OnSave }> = ({
6 | objs,
7 | onSave,
8 | }) => {
9 | return (
10 |
11 | No settings available
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/casparcg.scss:
--------------------------------------------------------------------------------
1 | .casparcg-template-data {
2 | table {
3 | td {
4 | padding: 0;
5 | }
6 | td.key {
7 | width: 5em;
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/empty.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TimelineObjEmpty } from 'timeline-state-resolver-types'
3 | import { EditWrapper, OnSave } from './lib'
4 |
5 | export const EditTimelineObjEmpty: React.FC<{ objs: TimelineObjEmpty[]; onSave: OnSave }> = ({ objs, onSave }) => {
6 | return (
7 |
8 | No settings
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/lawo.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TimelineObjLawoAny } from 'timeline-state-resolver-types'
3 | import { EditWrapper, NOT_IMPLEMENTED_SETTINGS, OnSave } from './lib'
4 |
5 | export const EditTimelineObjLawoAny: React.FC<{ objs: TimelineObjLawoAny[]; onSave: OnSave }> = ({ objs, onSave }) => {
6 | return (
7 |
8 | {NOT_IMPLEMENTED_SETTINGS}
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/panasonic.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TimelineObjPanasonicPtzAny } from 'timeline-state-resolver-types'
3 | import { EditWrapper, NOT_IMPLEMENTED_SETTINGS, OnSave } from './lib'
4 |
5 | export const EditTimelineObjPanasonicPtzAny: React.FC<{ objs: TimelineObjPanasonicPtzAny[]; onSave: OnSave }> = ({
6 | objs,
7 | onSave,
8 | }) => {
9 | return (
10 |
11 | {NOT_IMPLEMENTED_SETTINGS}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/pharos.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TimelineObjPharosAny } from 'timeline-state-resolver-types'
3 | import { EditWrapper, NOT_IMPLEMENTED_SETTINGS, OnSave } from './lib'
4 |
5 | export const EditTimelineObjPharosAny: React.FC<{ objs: TimelineObjPharosAny[]; onSave: OnSave }> = ({
6 | objs,
7 | onSave,
8 | }) => {
9 | return (
10 |
11 | {NOT_IMPLEMENTED_SETTINGS}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/quantel.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TimelineObjQuantelAny } from 'timeline-state-resolver-types'
3 | import { EditWrapper, NOT_IMPLEMENTED_SETTINGS, OnSave } from './lib'
4 |
5 | export const EditTimelineObjQuantelAny: React.FC<{ objs: TimelineObjQuantelAny[]; onSave: OnSave }> = ({
6 | objs,
7 | onSave,
8 | }) => {
9 | return (
10 |
11 | {NOT_IMPLEMENTED_SETTINGS}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/shotoku.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TimelineObjShotoku } from 'timeline-state-resolver-types'
3 | import { EditWrapper, NOT_IMPLEMENTED_SETTINGS, OnSave } from './lib'
4 |
5 | export const EditTimelineObjShotoku: React.FC<{ objs: TimelineObjShotoku[]; onSave: OnSave }> = ({ objs, onSave }) => {
6 | return (
7 |
8 | {NOT_IMPLEMENTED_SETTINGS}
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/singularLive.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TimelineObjSingularLiveAny } from 'timeline-state-resolver-types'
3 | import { EditWrapper, NOT_IMPLEMENTED_SETTINGS, OnSave } from './lib'
4 |
5 | export const EditTimelineObjSingularLiveAny: React.FC<{ objs: TimelineObjSingularLiveAny[]; onSave: OnSave }> = ({
6 | objs,
7 | onSave,
8 | }) => {
9 | return (
10 |
11 | {NOT_IMPLEMENTED_SETTINGS}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/sisyfos.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TimelineObjSisyfosAny } from 'timeline-state-resolver-types'
3 | import { EditWrapper, NOT_IMPLEMENTED_SETTINGS, OnSave } from './lib'
4 |
5 | export const EditTimelineObjSisyfosAny: React.FC<{ objs: TimelineObjSisyfosAny[]; onSave: OnSave }> = ({
6 | objs,
7 | onSave,
8 | }) => {
9 | return (
10 |
11 | {NOT_IMPLEMENTED_SETTINGS}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/sofieChef.tsx:
--------------------------------------------------------------------------------
1 | import { assertNever } from '@shared/lib'
2 | import React from 'react'
3 | import { TimelineContentTypeSofieChef, TimelineObjSofieChefAny } from 'timeline-state-resolver-types'
4 | import { firstValue, inputValue } from '../../../../lib/multipleEdit'
5 | import { SelectEnum } from '../../../inputs/SelectEnum'
6 | import { TextInput } from '../../../inputs/TextInput'
7 | import { EditWrapper, OnSave, OnSaveType } from './lib'
8 |
9 | export const EditTimelineObjSofieChefAny: React.FC<{ objs: TimelineObjSofieChefAny[]; onSave: OnSave }> = ({
10 | objs,
11 | onSave: onSave0,
12 | }) => {
13 | const onSave = onSave0 as OnSaveType
14 | let settings: JSX.Element = <>>
15 |
16 | const contentType = firstValue(objs, (obj) => obj.content.type)
17 | if (!contentType) return null
18 |
19 | if (contentType === TimelineContentTypeSofieChef.URL) {
20 | settings = (
21 | <>
22 |
23 | obj.content.url, '')}
27 | onChange={(v) => {
28 | onSave({ content: { url: v } })
29 | }}
30 | allowUndefined={false}
31 | />
32 |
33 | >
34 | )
35 | } else {
36 | assertNever(contentType)
37 | }
38 |
39 | return (
40 |
41 |
42 | obj.content.type, '')}
46 | options={TimelineContentTypeSofieChef}
47 | onChange={(newValue) => {
48 | onSave({ content: { type: newValue } })
49 | }}
50 | />
51 |
52 | {settings}
53 |
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/tcpSend.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { IntInput } from '../../../inputs/IntInput'
3 | import { TextInput } from '../../../inputs/TextInput'
4 | import { TimelineObjTCPSendAny } from 'timeline-state-resolver-types'
5 | import { EditWrapper, OnSave } from './lib'
6 | import { inputValue } from '../../../../lib/multipleEdit'
7 |
8 | export const EditTimelineObjTCPSendAny: React.FC<{ objs: TimelineObjTCPSendAny[]; onSave: OnSave }> = ({
9 | objs,
10 | onSave,
11 | }) => {
12 | return (
13 |
14 |
15 | obj.content.message, '')}
19 | onChange={(v) => {
20 | onSave({ content: { message: v } })
21 | }}
22 | allowUndefined={false}
23 | />
24 |
25 |
26 |
27 | obj.content.temporalPriority, undefined)}
31 | onChange={(v) => {
32 | onSave({ content: { temporalPriority: v } })
33 | }}
34 | allowUndefined={true}
35 | />
36 |
37 |
38 |
39 | obj.content.queueId, undefined)}
43 | onChange={(v) => {
44 | onSave({ content: { queueId: v } })
45 | }}
46 | allowUndefined={true}
47 | />
48 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/telemetrics.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TimelineObjTelemetricsAny } from 'timeline-state-resolver-types'
3 | import { EditWrapper, NOT_IMPLEMENTED_SETTINGS, OnSave } from './lib'
4 |
5 | export const EditTimelineObjTelemetricsAny: React.FC<{ objs: TimelineObjTelemetricsAny[]; onSave: OnSave }> = ({
6 | objs,
7 | onSave,
8 | }) => {
9 | return (
10 |
11 | {NOT_IMPLEMENTED_SETTINGS}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/unknown.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TSRTimelineObj } from 'timeline-state-resolver-types'
3 | import { EditWrapper, OnSave } from './lib'
4 |
5 | export const EditTimelineObjUnknown: React.FC<{ objs: TSRTimelineObj[]; onSave: OnSave }> = ({ objs, onSave }) => {
6 | return (
7 |
8 | Unknown/Unsupported timeline-object.
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/sidebar/timelineObj/timelineObjs/vizMSE.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { TimelineObjVIZMSEAny } from 'timeline-state-resolver-types'
3 | import { EditWrapper, NOT_IMPLEMENTED_SETTINGS, OnSave } from './lib'
4 |
5 | export const EditTimelineObjVIZMSEAny: React.FC<{ objs: TimelineObjVIZMSEAny[]; onSave: OnSave }> = ({
6 | objs,
7 | onSave,
8 | }) => {
9 | return (
10 |
11 | {NOT_IMPLEMENTED_SETTINGS}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/util/AntiWiggle/style.scss:
--------------------------------------------------------------------------------
1 | .anti-wiggle {
2 | display: inline-block;
3 |
4 | position: relative;
5 | }
6 | .anti-wiggle__inner {
7 | position: absolute;
8 | top: 0;
9 | left: 0;
10 |
11 | // So that the content don't wrap in its container:
12 | width: max-content;
13 | }
14 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/util/ConfirmationDialog.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material'
3 |
4 | interface IProps {
5 | open: boolean
6 | onAccepted: () => void
7 | onDiscarded: () => void
8 | acceptLabel: string
9 | title: string
10 | children: JSX.Element
11 | }
12 |
13 | export function ConfirmationDialog({
14 | open,
15 | title,
16 | children,
17 | acceptLabel,
18 | onAccepted,
19 | onDiscarded,
20 | }: IProps): JSX.Element {
21 | return (
22 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/util/Debug.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 |
3 | export function DebugTestErrors(): JSX.Element {
4 | const els: JSX.Element[] = []
5 |
6 | for (let i = 0; i < 10; i++) {
7 | // These elements are missing a "key"-prop, which should trigger a React warning
8 | els.push(A
)
9 | }
10 |
11 | const [wait, setWait] = useState(false)
12 | useEffect(() => {
13 | setTimeout(() => {
14 | setWait(true)
15 | }, 200)
16 | }, [])
17 |
18 | useEffect(() => {
19 | if (wait) {
20 | throw new Error('This is a client error thrown in a React component')
21 | }
22 | }, [wait])
23 |
24 | return <>{els}>
25 | }
26 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/util/DropZone.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import React from 'react'
3 |
4 | interface IDropZoneProps extends React.HTMLAttributes {
5 | isOver?: boolean
6 | label?: string
7 | }
8 |
9 | export const DropZone = React.forwardRef(function DropZone(
10 | { isOver, label, children, className, ...restProps },
11 | ref
12 | ) {
13 | return (
14 |
15 | {children}
16 |
{label}
17 |
18 | )
19 | })
20 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/util/SmallCheckbox.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Checkbox, CheckboxProps } from '@mui/material'
3 |
4 | export function SmallCheckbox(props: CheckboxProps): JSX.Element {
5 | return
6 | }
7 |
--------------------------------------------------------------------------------
/apps/app/src/react/components/util/Spinner.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { CircularProgress } from '@mui/material'
3 |
4 | export function Spinner({ heavyOperation }: { heavyOperation?: boolean }): JSX.Element {
5 | return (
6 | <>
7 |
8 | >
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/apps/app/src/react/constants.ts:
--------------------------------------------------------------------------------
1 | export let DISPLAY_DECIMAL_COUNT = 0
2 |
3 | export function setConstants(constants: { decimalCount?: number }): void {
4 | if (constants.decimalCount !== undefined) DISPLAY_DECIMAL_COUNT = constants.decimalCount
5 | }
6 |
--------------------------------------------------------------------------------
/apps/app/src/react/contexts/ErrorHandler.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | export const ErrorHandlerContext = React.createContext<{ handleError: (error: unknown) => void }>({
4 | handleError: (error) => {
5 | // Note: this is never-used default, as it is overridden in App.tsx
6 | // eslint-disable-next-line no-console
7 | console.error(error)
8 | },
9 | })
10 |
--------------------------------------------------------------------------------
/apps/app/src/react/contexts/Hotkey.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ActiveTriggers } from '../../models/rundown/Trigger'
3 | import { EventEmitter } from 'events'
4 | /** Used to communicate with the backend */
5 |
6 | export interface IHotkeyContext {
7 | triggers: TriggersEmitter
8 | }
9 | export class TriggersEmitter extends EventEmitter {
10 | private peripheralTriggers: ActiveTriggers = []
11 | private activeKeys: ActiveTriggers = []
12 |
13 | setPeripheralTriggers(peripheralTriggers: ActiveTriggers): void {
14 | this.peripheralTriggers = peripheralTriggers
15 | this.emit('peripheralTriggers', this.peripheralTriggers)
16 | this.handleUpdates()
17 | }
18 | setActiveKeys(activeKeys: ActiveTriggers): void {
19 | this.activeKeys = activeKeys
20 | this.emit('activeKeys', this.activeKeys)
21 | this.handleUpdates()
22 | }
23 | getAllTriggers(): ActiveTriggers {
24 | // All triggers combined, both from peripherals and keyboard:
25 | return [...this.activeKeys, ...this.peripheralTriggers]
26 | }
27 | handleUpdates(): void {
28 | // Emit them, so that the GUI can listen to them and tie them to triggers:
29 | this.emit('trigger', this.getAllTriggers())
30 | }
31 | isAnyoneListening(): boolean {
32 | return this.listenerCount('trigger') > 0
33 | }
34 | }
35 | export const HotkeyContext = React.createContext({
36 | triggers: new TriggersEmitter(),
37 | })
38 |
--------------------------------------------------------------------------------
/apps/app/src/react/contexts/IPCServer.ts:
--------------------------------------------------------------------------------
1 | import { IPCServer } from '../api/IPCServer'
2 | import React from 'react'
3 | /** Used to communicate with the backend */
4 | export const IPCServerContext = React.createContext({} as IPCServer)
5 |
--------------------------------------------------------------------------------
/apps/app/src/react/contexts/Logger.ts:
--------------------------------------------------------------------------------
1 | import { LoggerLike } from '@shared/api'
2 | import React from 'react'
3 |
4 | /** Used to send logs to the backend where they are logged to the server console and written to disk. */
5 | export const LoggerContext = React.createContext({
6 | /* eslint-disable no-console */
7 | error: (...args: any[]) => {
8 | console.error(...args)
9 | },
10 | warn: (...args: any[]) => {
11 | console.warn(...args)
12 | },
13 | info: (...args: any[]) => {
14 | console.info(...args)
15 | },
16 | http: (...args: any[]) => {
17 | console.debug(...args)
18 | },
19 | verbose: (...args: any[]) => {
20 | console.debug(...args)
21 | },
22 | debug: (...args: any[]) => {
23 | console.debug(...args)
24 | },
25 | silly: (...args: any[]) => {
26 | console.debug(...args)
27 | },
28 | /* eslint-enable no-console */
29 | })
30 |
--------------------------------------------------------------------------------
/apps/app/src/react/contexts/Project.ts:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Project } from '../../models/project/Project'
3 |
4 | export const ProjectContext = React.createContext({} as Project)
5 |
--------------------------------------------------------------------------------
/apps/app/src/react/lib/errorHandling.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Wraps a callback function, and improves any errors thrown inside it.
3 | * This is intended to be used for functions executed in setTimeout, event handlers etc,
4 | * where a thrown error would be uncaught
5 | */
6 | export function CB any>(cb: T): T {
7 | return ((...args: any[]) => {
8 | try {
9 | return cb(...args)
10 | } catch (error) {
11 | // @ts-expect-error hack
12 | const handleError = window.handleError as undefined | ((error: any) => void)
13 | if (handleError) {
14 | handleError(error)
15 | } else {
16 | throw error
17 | }
18 | }
19 | }) as any
20 | }
21 |
--------------------------------------------------------------------------------
/apps/app/src/react/lib/useDebounce.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 |
3 | /** Debounce an input value for `delay` amount of time, until `value` settles. */
4 | export function useDebounce(value: T, delay: number): T {
5 | const [debouncedValue, setDebouncedValue] = useState(value)
6 |
7 | /** This will run any time value or delay changes */
8 | useEffect(() => {
9 | const handler = setTimeout(() => {
10 | setDebouncedValue(value)
11 | }, delay)
12 |
13 | /** Clear the timeout if value changes or the component is unmounted */
14 | return () => {
15 | clearTimeout(handler)
16 | }
17 | }, [value, delay])
18 |
19 | return debouncedValue
20 | }
21 |
--------------------------------------------------------------------------------
/apps/app/src/react/lib/useFrame.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef, useCallback } from 'react'
2 |
3 | /**
4 | * Execute callback on every frame
5 | * Callback function returns true to evaluate again on next frame. If it return false, the computation will pause until next deps change.
6 | */
7 | export function useFrame(fcn: (nowTime: number) => boolean, deps: React.DependencyList): void {
8 | const [time, setTime] = useState(0)
9 |
10 | const isRunning = useRef(true)
11 |
12 | const updateFrame = useCallback(() => {
13 | if (!isRunning.current) return
14 |
15 | setTime(Date.now())
16 |
17 | window.requestAnimationFrame(() => {
18 | updateFrame()
19 | })
20 | }, [])
21 |
22 | useEffect(() => {
23 | isRunning.current = true
24 | updateFrame()
25 | return () => {
26 | isRunning.current = false
27 | }
28 | // eslint-disable-next-line react-hooks/exhaustive-deps
29 | }, deps)
30 |
31 | useEffect(() => {
32 | const continueEvaluations = fcn(time)
33 | if (!continueEvaluations) isRunning.current = false
34 | }, [fcn, time])
35 | }
36 |
--------------------------------------------------------------------------------
/apps/app/src/react/mobx/AnalogStore.ts:
--------------------------------------------------------------------------------
1 | import { makeAutoObservable, runInAction } from 'mobx'
2 | import { AnalogInput } from '../../models/project/AnalogInput'
3 | import { ActiveAnalog } from '../../models/rundown/Analog'
4 | import { IPCClient } from '../api/IPCClient'
5 | import { IPCServer } from '../api/IPCServer'
6 | import { ClientSideLogger } from '../api/logger'
7 | const { ipcRenderer } = window.require('electron')
8 |
9 | export class AnalogStore {
10 | private analogInputs = new Map()
11 |
12 | private activeAnalogListeners: ((activeAnalog: ActiveAnalog) => void)[] // Not an observable
13 | serverAPI: IPCServer
14 | logger: ClientSideLogger
15 | ipcClient: IPCClient
16 |
17 | constructor() {
18 | makeAutoObservable(this)
19 |
20 | this.activeAnalogListeners = []
21 |
22 | this.serverAPI = new IPCServer(ipcRenderer)
23 | this.logger = new ClientSideLogger(this.serverAPI)
24 | this.ipcClient = new IPCClient(this.logger, ipcRenderer, {
25 | updateAnalogInput: (fullIdentifier: string, analogInput: AnalogInput | null) =>
26 | this.updateAnalogInput(fullIdentifier, analogInput),
27 | })
28 | }
29 |
30 | updateAnalogInput(fullIdentifier: string, analogInput: AnalogInput | null): void {
31 | runInAction(() => {
32 | if (analogInput) {
33 | this.analogInputs.set(fullIdentifier, analogInput)
34 | } else {
35 | this.analogInputs.delete(fullIdentifier)
36 | }
37 | })
38 | }
39 | getAnalogInput(fullIdentifier: string): AnalogInput | undefined {
40 | return this.analogInputs.get(fullIdentifier)
41 | }
42 |
43 | updateActiveAnalog(activeAnalog: ActiveAnalog): void {
44 | for (const listener of this.activeAnalogListeners) {
45 | listener(activeAnalog)
46 | }
47 | }
48 | listenToActiveAnalog(listener: (activeAnalog: ActiveAnalog) => void): { stop: () => void } {
49 | this.activeAnalogListeners.push(listener)
50 | return {
51 | stop: () => {
52 | const index = this.activeAnalogListeners.indexOf(listener)
53 | if (index !== -1) this.activeAnalogListeners.splice(index, 1)
54 | },
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/apps/app/src/react/mobx/GDDValidatorStoreStore.ts:
--------------------------------------------------------------------------------
1 | import { makeAutoObservable, runInAction } from 'mobx'
2 | import { SchemaValidator, setupSchemaValidator, ValidatorCache } from 'graphics-data-definition'
3 | import { IPCServer } from '../api/IPCServer'
4 | const { ipcRenderer } = window.require('electron')
5 |
6 | export class GDDValidatorStore {
7 | private isInitialized = false
8 |
9 | // private gddCache: ValidatorCache | null | undefined = undefined
10 | public gddValidator: SchemaValidator | null = null
11 | private serverAPI: IPCServer | null = null
12 |
13 | constructor() {
14 | makeAutoObservable(this)
15 | }
16 |
17 | async initializeGDDSchemaValidator(): Promise {
18 | if (this.isInitialized) return
19 |
20 | this.isInitialized = true
21 | if (!this.serverAPI) this.serverAPI = new IPCServer(ipcRenderer)
22 |
23 | // First, retrieve a cache from server, if possible:
24 | let gddCache = await this.serverAPI.fetchGDDCache()
25 |
26 | try {
27 | const v = await setupSchemaValidator({
28 | fetch: async (url: string) => {
29 | const response = await fetch(url)
30 | return await response.json()
31 | },
32 | getCache: async (): Promise => {
33 | return gddCache ?? {}
34 | },
35 | })
36 | gddCache = v.cache
37 | runInAction(() => {
38 | this.gddValidator = v.validate
39 | })
40 | } catch (e) {
41 | ;(window as any).handleError(e)
42 | }
43 |
44 | // Store the cache:
45 | if (gddCache) {
46 | await this.serverAPI.storeGDDCache({ cache: gddCache })
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/apps/app/src/react/mobx/ProjectStore.ts:
--------------------------------------------------------------------------------
1 | import { makeAutoObservable } from 'mobx'
2 | import { Project } from '../../models/project/Project'
3 | import { PeripheralArea } from '../../models/project/Peripheral'
4 |
5 | /**
6 | * Information about currently opened project.
7 | * Updates regularly from electron backend.
8 | */
9 | export class ProjectStore {
10 | project: Project = {
11 | id: '',
12 | name: '',
13 | mappings: {},
14 | bridges: {},
15 | settings: {
16 | enableInternalBridge: false,
17 | },
18 | deviceNames: {},
19 | analogInputSettings: {},
20 | }
21 |
22 | public assignedAreas: {
23 | assignedToGroupId: string
24 | bridgeId: string
25 | deviceId: string
26 | areaId: string
27 | area: PeripheralArea
28 | }[] = []
29 |
30 | public availableAreas: {
31 | bridgeId: string
32 | deviceId: string
33 | areaId: string
34 | area: PeripheralArea
35 | }[] = []
36 |
37 | constructor() {
38 | makeAutoObservable(this)
39 | }
40 |
41 | update(project: Project): void {
42 | this.project = project
43 |
44 | this._updateAssignedAreas()
45 | }
46 |
47 | private _updateAssignedAreas() {
48 | this.assignedAreas = []
49 | this.availableAreas = []
50 |
51 | for (const [bridgeId, bridge] of Object.entries(this.project.bridges)) {
52 | for (const [deviceId, peripheralSettings] of Object.entries(bridge.clientSidePeripheralSettings)) {
53 | for (const [areaId, area] of Object.entries(peripheralSettings.areas)) {
54 | this.availableAreas.push({
55 | bridgeId,
56 | deviceId,
57 | areaId,
58 | area,
59 | })
60 |
61 | if (area.assignedToGroupId) {
62 | this.assignedAreas.push({
63 | assignedToGroupId: area.assignedToGroupId,
64 | bridgeId,
65 | deviceId,
66 | areaId,
67 | area,
68 | })
69 | }
70 | }
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/apps/app/src/react/mobx/TriggersStore.ts:
--------------------------------------------------------------------------------
1 | import { makeAutoObservable } from 'mobx'
2 | import { IPCServer } from '../api/IPCServer'
3 | import { IPCClient } from '../api/IPCClient'
4 | import { ClientSideLogger } from '../api/logger'
5 | const { ipcRenderer } = window.require('electron')
6 |
7 | export class TriggersStore {
8 | failedGlobalTriggers: Set = new Set()
9 |
10 | serverAPI: IPCServer
11 | logger: ClientSideLogger
12 | ipcClient: IPCClient
13 |
14 | constructor() {
15 | this.serverAPI = new IPCServer(ipcRenderer)
16 | this.logger = new ClientSideLogger(this.serverAPI)
17 | this.ipcClient = new IPCClient(this.logger, ipcRenderer, {
18 | updateFailedGlobalTriggers: (identifiers: string[]) => this.updateFailedGlobalTriggers(identifiers),
19 | })
20 |
21 | makeAutoObservable(this)
22 | }
23 |
24 | updateFailedGlobalTriggers(identifiers: string[]): void {
25 | this.failedGlobalTriggers = new Set(identifiers)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/apps/app/src/react/mobx/store.ts:
--------------------------------------------------------------------------------
1 | import { AppStore } from './AppStore'
2 | import { GuiStore } from './GuiStore'
3 | import { ResourcesStore } from './ResourcesStore'
4 | import { RundownsStore } from './RundownsStore'
5 | import { GroupPlayDataStore } from './GroupPlayDataStore'
6 | import { ProjectStore } from './ProjectStore'
7 | import { TriggersStore } from './TriggersStore'
8 | import { AnalogStore } from './AnalogStore'
9 | import { GDDValidatorStore } from './GDDValidatorStoreStore'
10 |
11 | export const store = {
12 | guiStore: new GuiStore(),
13 | appStore: new AppStore(),
14 | projectStore: new ProjectStore(),
15 | rundownsStore: new RundownsStore(),
16 | resourcesStore: new ResourcesStore(),
17 | groupPlayDataStore: new GroupPlayDataStore(),
18 | triggersStore: new TriggersStore(),
19 | analogStore: new AnalogStore(),
20 |
21 | gddValidatorStore: new GDDValidatorStore(),
22 | }
23 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/base/_reset.scss:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/btn.scss:
--------------------------------------------------------------------------------
1 | .btn-row-equal {
2 | display: flex;
3 | justify-content: space-between;
4 | margin-top: 1.5rem;
5 | }
6 |
7 | .btn-row-left {
8 | text-align: left;
9 | margin-top: 1.5rem;
10 | }
11 |
12 | .btn-row-right {
13 | text-align: right;
14 | margin-top: 1.5rem;
15 | }
16 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/connection-status.scss:
--------------------------------------------------------------------------------
1 | .connection-status {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | padding: 0 0.5rem;
6 | color: $deviceOfflineColor;
7 | font-size: 1.2rem;
8 | text-decoration: none;
9 |
10 | &__content {
11 | display: flex;
12 | align-items: center;
13 | border-radius: 50px;
14 | background-color: rgba(0, 0, 0, 0.75);
15 | padding: 0 0.5rem;
16 | }
17 |
18 | &__label {
19 | position: relative;
20 | top: -1px;
21 | }
22 |
23 | &__dot {
24 | margin-left: 0.35rem;
25 | border-radius: 50%;
26 | width: 0.8rem;
27 | height: 0.8rem;
28 | background-color: $deviceOfflineColor;
29 | flex-shrink: 0;
30 |
31 | &.ok {
32 | background-color: $deviceOnlineColor;
33 | }
34 | }
35 | &.clickable:hover,
36 | &.open {
37 | background-color: rgba(255, 255, 255, 0.05);
38 | }
39 |
40 | &.ok {
41 | color: $deviceOnlineColor;
42 |
43 | .connection-status__dot {
44 | background-color: $deviceOnlineColor;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/countDownHead.scss:
--------------------------------------------------------------------------------
1 | .countDownHead {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | z-index: 5;
6 | height: 100%;
7 | width: 100%;
8 | pointer-events: none;
9 |
10 | .line {
11 | position: absolute;
12 | bottom: 0;
13 | height: 100%;
14 | width: 0.2rem;
15 | background-color: $lightGreenColor;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/device-list.scss:
--------------------------------------------------------------------------------
1 | .device-list {
2 | border-left: 1px solid white;
3 | padding-left: 2rem;
4 | margin-top: 0.5rem;
5 | margin-bottom: 1.8rem;
6 | }
7 |
8 | .device-list-item {
9 | margin-bottom: 1rem;
10 | }
11 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/drop-zone.scss:
--------------------------------------------------------------------------------
1 | .drop-zone {
2 | position: relative;
3 |
4 | &__outline {
5 | display: none;
6 | box-shadow: inset 0px 0px 0px 3px #ffffff85;
7 | justify-content: center;
8 | align-items: center;
9 | text-align: center;
10 | font-size: 1.4rem;
11 | position: absolute;
12 | top: 0;
13 | left: 0;
14 | width: 100%;
15 | height: 100%;
16 | box-sizing: border-box;
17 | pointer-events: none;
18 | color: white;
19 | }
20 |
21 | &--isOver {
22 | .drop-zone__outline {
23 | display: flex;
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/foundation/_all.scss:
--------------------------------------------------------------------------------
1 | @import './variables';
2 | @import './colors';
3 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/foundation/_colors.scss:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * All colors used for the SuperConductor UI
4 | *
5 | */
6 |
7 | $accentDarkBg: #d45f05;
8 | $accentBg: #e96703;
9 | $accentLightBg: #fa7713;
10 | $accentText: white;
11 |
12 | $topheaderBg: #24203a;
13 | $sidebarBg: black;
14 | $mainBg: radial-gradient(50% 50% at 50% 50%, #2a2c3a 0%, #1a1b24 100%);
15 |
16 | $mediaLayerColor: #2d4a80;
17 | $templateLayerColor: #b26000;
18 | $movieLayerColor: #6817ff;
19 | $stillLayerColor: #ffe817;
20 | $audioLayerColor: #308579;
21 | $mixEffectLayerColor: #177400;
22 | $downstreamKeyerLayerColor: #177400;
23 | $auxiliaryLayerColor: #177400;
24 | $superSourceLayerColor: #177400;
25 | $superSourcePropsLayerColor: #177400;
26 | $macroPlayerLayerColor: #177400;
27 | $mediaPlayerLayerColor: #6817ff;
28 | $programLayerColor: #74002c;
29 | $currentSceneLayerColor: $programLayerColor;
30 | $currentTransitionLayerColor: #74002c;
31 | $muteLayerColor: #74002c;
32 | $recordingLayerColor: #74002c;
33 | $sceneItemRenderLayerColor: #74002c;
34 | $streamingLayerColor: #74002c;
35 | $sourceSettingsLayerColor: #74002c;
36 | $inputLayerColor: #74002c;
37 | $overlayLayerColor: $downstreamKeyerLayerColor;
38 | $fadeToBlackLayerColor: #000000;
39 | $videoFaderLayerColor: #177400;
40 | $previewLayerColor: $auxiliaryLayerColor;
41 | $oscLayerColor: #554054;
42 | $httpLayerColor: #554054;
43 | $transportLayerColor: #2d4a80;
44 | $invalidLayerColor: #c24242;
45 |
46 | $greenColor: #5fa852;
47 | $lightGreenColor: #76c469;
48 | $darkGreenColor: #5ab44a;
49 | $redColor: red;
50 | $redBg: #c24242;
51 | $redLightBg: #d85353;
52 | $lightRedColor: #d44e4e;
53 |
54 | $lightGreenColor: #5afc3e;
55 |
56 | $partOutlineColor: #5a5a5a;
57 | $partDragHandleColor: #2d3340;
58 | $partMetaColor: #2d3340;
59 |
60 | $emptyLayerColor: #1b1e26;
61 |
62 | $deviceOfflineColor: #ff5f5f;
63 | $deviceOnlineColor: #5bc780;
64 |
65 | $headerTabNormalBg: #505050;
66 | $headerTabHoverBg: #616161;
67 | $headerTabActiveBg: #111111;
68 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/foundation/_variables.scss:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * All variables used used across all SCSS files
4 | *
5 | */
6 |
7 | $mainFont: 'Barlow', sans-serif;
8 | $mainFontSemiCondensed: 'Barlow Semi Condensed', sans-serif;
9 | $mainFontCondensed: 'Barlow Condensed', sans-serif;
10 | $partTabWidth: 2.2rem;
11 | $partDragIndicatorSize: 8px;
12 | $layerHeight: 2.5rem;
13 | $borderRadius: 0.3rem;
14 | $sidebar-padding: 1rem;
15 | $default-transition: all 150ms ease;
16 | $default-transition-time: 150ms ease;
17 | $timelineObjHandleWidth: 8px;
18 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/global.scss:
--------------------------------------------------------------------------------
1 | @import 'base/reset';
2 | @import 'foundation/all';
3 |
4 | html {
5 | font-size: 62.5%;
6 | }
7 |
8 | body {
9 | font-size: 1.6rem;
10 | color: white;
11 | background-color: black;
12 | font-family: $mainFont;
13 | }
14 |
15 | input,
16 | button,
17 | textarea {
18 | font-family: $mainFont;
19 | }
20 |
21 | a {
22 | color: #b1b1ff;
23 | }
24 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/group-list.scss:
--------------------------------------------------------------------------------
1 | .group-list {
2 | // flex-grow: 1;
3 | width: 100%;
4 | padding: 0 1rem;
5 | box-sizing: border-box;
6 |
7 | flex-direction: column;
8 | height: 100%;
9 |
10 | .group-list__control-row {
11 | display: flex;
12 | flex-grow: 1;
13 | justify-content: space-between;
14 | padding-top: 1rem;
15 | padding-bottom: 1rem;
16 | margin-bottom: -1rem;
17 |
18 | > * {
19 | margin-right: 1rem;
20 | }
21 |
22 | &.last-in-rundown {
23 | height: calc(100vh - 20rem);
24 |
25 | margin-top: -2rem;
26 | }
27 | }
28 | .group-list__time-display {
29 | display: flex;
30 | // flex-direction: row;
31 | justify-content: flex-end;
32 |
33 | &__item {
34 | margin: 0 0.5em;
35 | &:last-child {
36 | margin: 0 0 0 0.5em;
37 | }
38 | }
39 | &__label {
40 | font-family: 'Barlow Condensed';
41 | font-weight: 300;
42 | }
43 | &__value {
44 | font-family: 'Barlow Semi Condensed';
45 | font-weight: 400;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/playHead.scss:
--------------------------------------------------------------------------------
1 | $line-width: 0.2rem;
2 |
3 | .playHead {
4 | position: absolute;
5 | top: 0;
6 | left: 0;
7 | z-index: 999;
8 | height: 100%;
9 | right: $line-width;
10 | pointer-events: none;
11 |
12 | .head {
13 | background-color: $redColor;
14 | position: absolute;
15 | transform: translateX(-50%) translateY(-100%);
16 | font-size: 1.2rem;
17 | padding: 0.2rem 0.3rem;
18 | margin-left: 0.1rem;
19 | }
20 |
21 | .line {
22 | height: 100%;
23 | width: $line-width;
24 | background-color: $redColor;
25 | position: absolute;
26 | }
27 |
28 | .shade {
29 | position: absolute;
30 | top: 0;
31 | width: 33%;
32 | height: 100%;
33 | background: linear-gradient(to left, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0) 100%);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/resourceLibrary.scss:
--------------------------------------------------------------------------------
1 | .resource {
2 | display: flex;
3 | box-shadow: inset 0px 0px 0px 0px #ffffff85;
4 | transition: $default-transition;
5 | padding: 0.2em;
6 | border-radius: 5px;
7 | color: #cdcdcd;
8 | font-size: 1.4rem;
9 | line-height: 1.4;
10 | cursor: pointer;
11 |
12 | &:hover {
13 | opacity: 0.8;
14 | }
15 |
16 | &:not(:last-child) {
17 | margin-bottom: 0.5rem;
18 | }
19 |
20 | @include useObjectTypeStyles();
21 |
22 | &.selected {
23 | box-shadow: inset 0px 0px 0px 3px #ffffff85;
24 | }
25 |
26 | &.dragged {
27 | opacity: 0.5;
28 | }
29 |
30 | &__details {
31 | display: flex;
32 | flex-direction: column;
33 | flex-grow: 1;
34 | flex-shrink: 1;
35 | min-width: 0;
36 | padding-right: 0.2rem;
37 | }
38 |
39 | &__name {
40 | color: white;
41 | word-wrap: break-word;
42 | flex-shrink: 1;
43 | min-width: 0;
44 | }
45 |
46 | &__attributes {
47 | display: grid;
48 | grid-template-columns: 3em repeat(2, 1fr);
49 | grid-gap: 0.5rem;
50 | align-self: stretch;
51 | }
52 | }
53 |
54 | .thumbnail {
55 | color: white;
56 | width: 71px;
57 | height: 40px;
58 | margin-left: 0.1rem;
59 | margin-right: 0.4rem;
60 | border-radius: 5px;
61 | flex-shrink: 0;
62 | }
63 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/settings.scss:
--------------------------------------------------------------------------------
1 | .MuiModal-root {
2 | .settings {
3 | display: flex;
4 | flex-direction: column;
5 | align-items: flex-start;
6 | padding: 2rem;
7 |
8 | > .MuiDivider-fullWidth {
9 | width: 100%;
10 | }
11 | }
12 | }
13 |
14 | .side-bar {
15 | .settings {
16 | .setting-separator {
17 | height: 1em;
18 | }
19 | .settings-group {
20 | padding: 0 0 0 0.5em;
21 | margin: 0.25em 0;
22 | border-left: 2px solid rgba(255, 255, 255, 0.25);
23 | }
24 | .header {
25 | font-size: 125%;
26 | }
27 | .label {
28 | button.btn {
29 | float: right;
30 | }
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/sidebar/resource-library.scss:
--------------------------------------------------------------------------------
1 | .sidebar.resource-library {
2 | > .sidebar__header {
3 | button.refresh {
4 | border: 0;
5 | background: transparent;
6 | padding: 0;
7 | margin: 0;
8 | cursor: pointer;
9 | }
10 |
11 | .refresh-resources {
12 | position: absolute;
13 | right: 0.6em;
14 | top: 0.6em;
15 | z-index: 1;
16 | background: $sidebar-background;
17 |
18 | button {
19 | border: 0;
20 | padding: 0.25em;
21 | margin: 0;
22 | text-transform: none;
23 | cursor: pointer;
24 | height: 32px;
25 | white-space: nowrap;
26 |
27 | &.on-hover {
28 | display: none;
29 | }
30 | &.selected {
31 | visibility: inherit;
32 | display: inherit;
33 | }
34 | }
35 | &:hover,
36 | &.hover {
37 | button.on-hover {
38 | display: inherit;
39 | &.selected {
40 | background-color: rgba(255, 255, 255, 0.1);
41 | }
42 | }
43 | }
44 | }
45 | }
46 |
47 | > .sidebar__content {
48 | > div {
49 | padding: 0 0.6em;
50 | }
51 | }
52 |
53 | .refresh.active {
54 | svg {
55 | animation: rotating 2s linear infinite;
56 |
57 | @keyframes rotating {
58 | from {
59 | transform: rotate(360deg);
60 | }
61 | to {
62 | transform: rotate(0deg);
63 | }
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/snackbar.scss:
--------------------------------------------------------------------------------
1 | div.SnackbarContainer-root {
2 | .SnackbarItem-message {
3 | padding-top: 1em;
4 | p {
5 | margin: 0.25em;
6 | }
7 | button.close-btn {
8 | position: absolute;
9 | top: 0;
10 | right: 0;
11 | cursor: pointer;
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/table.scss:
--------------------------------------------------------------------------------
1 | table.table {
2 | td {
3 | padding: 0.1em 0.25em;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/timeline-obj.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/src/react/styles/timeline-obj.scss
--------------------------------------------------------------------------------
/apps/app/src/react/styles/trigger.scss:
--------------------------------------------------------------------------------
1 | .trigger-pill {
2 | display: flex;
3 | align-items: center;
4 | font-family: $mainFontSemiCondensed;
5 | font-weight: 600;
6 | font-size: 12px;
7 | // line-height: 12px;
8 |
9 | margin-left: 0.5em;
10 |
11 | > .label-part {
12 | color: black;
13 | background: #49d3ff;
14 | box-shadow: inset 0px -2px 0px rgba(0, 0, 0, 0.25);
15 | border-radius: 2px;
16 | padding: 0 0.3rem;
17 |
18 | &.keyboard {
19 | background: #aaa99e;
20 | }
21 |
22 | > .label {
23 | display: block;
24 | transform: translateY(-1px);
25 | }
26 | }
27 |
28 | .connect-labels {
29 | margin: 0 0.2rem;
30 |
31 | &:last-child {
32 | display: none;
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/apps/app/src/react/styles/variables.scss:
--------------------------------------------------------------------------------
1 | $sidebar-background: #26272f;
2 |
--------------------------------------------------------------------------------
/apps/app/tools/notarize.js:
--------------------------------------------------------------------------------
1 | /* Based on https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/ */
2 |
3 | const { notarize } = require('electron-notarize')
4 |
5 | exports.default = async function notarizing(context) {
6 | const { electronPlatformName, appOutDir } = context
7 | if (electronPlatformName !== 'darwin') {
8 | return
9 | }
10 |
11 | if (!process.env.APPLEID || !process.env.APPLEIDPASS) {
12 | // eslint-disable-next-line no-console
13 | console.log('Skipping notarizing, due to missing APPLEID or APPLEIDPASS environment variables')
14 | return
15 | }
16 |
17 | const appName = context.packager.appInfo.productFilename
18 |
19 | return notarize({
20 | appBundleId: 'tv.superfly.superconductor',
21 | appPath: `${appOutDir}/${appName}.app`,
22 | appleId: process.env.APPLEID,
23 | appleIdPassword: process.env.APPLEIDPASS,
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/apps/app/tsconfig.electron.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": [
4 | "dist",
5 | "src/renderer.tsx",
6 | "src/react/**/*",
7 | "src/**/__tests__/**/*"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/apps/app/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist",
6 | "jsx": "react",
7 | "baseUrl": "./"
8 | },
9 | "exclude": [
10 | "src/**/__tests__/**/*"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/apps/app/webpack.config.js:
--------------------------------------------------------------------------------
1 | const reactConfigs = require('./webpack.react.js')
2 |
3 | module.exports = [reactConfigs]
4 |
--------------------------------------------------------------------------------
/apps/app/webpack.react.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin')
2 | const path = require('path')
3 |
4 | module.exports = {
5 | mode: 'development',
6 | entry: './src/renderer.tsx',
7 | target: 'web',
8 | devtool: 'source-map',
9 | devServer: {
10 | static: path.join(__dirname, 'dist/renderer.js'),
11 | compress: true,
12 | port: 9124,
13 | },
14 | resolve: {
15 | alias: {
16 | ['@']: path.resolve(__dirname, 'src'),
17 | },
18 | extensions: ['.tsx', '.ts', '.js'],
19 | fallback: { url: require.resolve('url/') },
20 | },
21 | module: {
22 | rules: [
23 | {
24 | test: /\.(png|jpe?g|gif)$/i,
25 | use: [{ loader: 'file-loader' }],
26 | },
27 | {
28 | test: /\.ts(x?)$/,
29 | include: /src/,
30 | use: [{ loader: 'ts-loader' }],
31 | },
32 | {
33 | test: /\.s[ac]ss$/i,
34 | use: ['style-loader', 'css-loader', 'sass-loader'],
35 | },
36 | {
37 | test: /\.css$/i,
38 | use: ['style-loader', 'css-loader'],
39 | },
40 | ],
41 | },
42 | output: {
43 | path: __dirname + '/dist',
44 | filename: 'renderer.js',
45 | },
46 | plugins: [
47 | new HtmlWebpackPlugin({
48 | template: './src/index.html',
49 | }),
50 | ],
51 | }
52 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | 'no-console': ['warn'],
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/README.md:
--------------------------------------------------------------------------------
1 | # SuperConductor TSR Bridge
2 |
3 | To run the project in development mode, run `yarn dev`.
4 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/assets/tray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/tsr-bridge/assets/tray.png
--------------------------------------------------------------------------------
/apps/tsr-bridge/assets/trayTemplate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/tsr-bridge/assets/trayTemplate.png
--------------------------------------------------------------------------------
/apps/tsr-bridge/assets/trayTemplate@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/tsr-bridge/assets/trayTemplate@2x.png
--------------------------------------------------------------------------------
/apps/tsr-bridge/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-unsigned-executable-memory
6 |
7 | com.apple.security.cs.disable-library-validation
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | SuperConductor
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["src/main.ts", "src/electron/*", "src/lib/*", "src/ipc/*", "src/models/*"],
3 | "exec": "tsc && electron ./dist/main.js",
4 | "ext": "ts"
5 | }
6 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/electron/IPCClient.ts:
--------------------------------------------------------------------------------
1 | import { BrowserWindow } from 'electron'
2 | import { LogEntry } from 'winston'
3 | import { IPCClientMethods } from '../ipc/IPCAPI'
4 | import { AppSettings, AppSystem } from '../models/AppData'
5 |
6 | /** This class is used server-side, to send messages to the client */
7 | export class IPCClient implements IPCClientMethods {
8 | constructor(private mainWindow: BrowserWindow) {}
9 |
10 | log(entry: LogEntry): void {
11 | this.mainWindow?.webContents.send('callMethod', 'log', entry)
12 | }
13 | settings(settings: AppSettings): void {
14 | this.mainWindow?.webContents.send('callMethod', 'settings', settings)
15 | }
16 | system(system: AppSystem): void {
17 | this.mainWindow?.webContents.send('callMethod', 'system', system)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/electron/IPCServer.ts:
--------------------------------------------------------------------------------
1 | import { IPCServerMethods } from '../ipc/IPCAPI'
2 |
3 | import { LoggerLike } from '@shared/api'
4 | import { AppSettings } from '../models/AppData'
5 | import { StorageHandler } from './storageHandler'
6 |
7 | /** This class is used server-side, to handle requests from the client */
8 | export class IPCServer implements IPCServerMethods {
9 | constructor(
10 | ipcMain: Electron.IpcMain,
11 | private _log: LoggerLike,
12 | private storage: StorageHandler,
13 | private callbacks: {
14 | initialized: () => void
15 | }
16 | ) {
17 | for (const methodName of Object.getOwnPropertyNames(IPCServer.prototype)) {
18 | if (methodName[0] !== '_') {
19 | const fcn = (this as any)[methodName].bind(this)
20 | if (fcn) {
21 | ipcMain.handle(methodName, async (event, ...args) => {
22 | try {
23 | return fcn(...args)
24 | } catch (error) {
25 | this._log.error(`Error when calling ${methodName}:`, error)
26 | throw error
27 | }
28 | })
29 | }
30 | }
31 | }
32 | }
33 | public updateSettings(newSettings: Partial): void {
34 | const appData = this.storage.getAppData()
35 |
36 | this.storage.updateAppData({
37 | ...appData,
38 | settings: {
39 | ...appData.settings,
40 | ...newSettings,
41 | },
42 | })
43 | }
44 | public initialized(): void {
45 | this.callbacks.initialized()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/electron/lib/baseFolder.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import os from 'os'
3 |
4 | export function baseFolder(): string {
5 | const homeDirPath = os.homedir()
6 | if (os.type() === 'Linux') {
7 | return path.join(homeDirPath, '.superconductor-tsr-bridge')
8 | }
9 | return path.join(homeDirPath, 'Documents', 'SuperConductor-TSR-Bridge')
10 | }
11 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/electron/lib/lib.ts:
--------------------------------------------------------------------------------
1 | import shortUUID from 'short-uuid'
2 |
3 | export function shortID(): string {
4 | return shortUUID.generate().slice(0, 8)
5 | }
6 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | TSR-Bridge
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/ipc/IPCAPI.ts:
--------------------------------------------------------------------------------
1 | import { LogEntry } from 'winston'
2 | import { AppSettings, AppSystem } from '../models/AppData'
3 |
4 | /** This class is used server-side, to send messages to the client */
5 | export interface IPCClientMethods {
6 | log: (entry: LogEntry) => void
7 | settings: (settings: AppSettings) => void
8 | system: (system: AppSystem) => void
9 | }
10 | /** Methods that can be called on the server, by the client */
11 | export interface IPCServerMethods {
12 | updateSettings: (settings: Partial) => void
13 | initialized: () => void
14 | }
15 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/logging/index.ts:
--------------------------------------------------------------------------------
1 | export * from './logging'
2 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/logging/ipc-transport.ts:
--------------------------------------------------------------------------------
1 | import Transport from 'winston-transport'
2 | import { LogEntry } from 'winston'
3 | import { IPCClient } from '../electron/IPCClient'
4 |
5 | export default class IPCTransport extends Transport {
6 | constructor(private ipcClient: IPCClient) {
7 | super()
8 | }
9 |
10 | log(entry: LogEntry, callback: (...args: unknown[]) => unknown): void {
11 | this.ipcClient.log(entry)
12 | callback()
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/logging/logging.ts:
--------------------------------------------------------------------------------
1 | import * as Winston from 'winston'
2 | // @ts-expect-error This is a hack to ensure that electron-builder includes this file.
3 | import consoleTransport from 'winston/dist/winston/transports/console'
4 | import { IPCClient } from '../electron/IPCClient'
5 | import IPCTransport from './ipc-transport'
6 | import { utilFormatter } from './util-formatter'
7 |
8 | /**
9 | * https://github.com/winstonjs/winston#logging-levels
10 | */
11 | export enum LogLevel {
12 | Error = 'error',
13 | Warn = 'warn',
14 | Info = 'info',
15 | HTTP = 'http',
16 | Verbose = 'verbose',
17 | Debug = 'debug',
18 | Silly = 'silly',
19 | }
20 |
21 | export const createLogger = (): Winston.Logger => {
22 | const log = Winston.createLogger({
23 | level: LogLevel.Silly,
24 | format: utilFormatter(),
25 | transports: [
26 | new consoleTransport({
27 | format: Winston.format.simple(),
28 | }),
29 | ],
30 | })
31 |
32 | return log
33 | }
34 | export function addLoggerTransport(log: Winston.Logger, ipcClient: IPCClient): void {
35 | log.add(new IPCTransport(ipcClient))
36 | }
37 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/logging/util-formatter.ts:
--------------------------------------------------------------------------------
1 | // Adapted from https://stackoverflow.com/a/56842780
2 | import util from 'util'
3 | import { SPLAT } from 'triple-beam'
4 |
5 | const transform = (entry: any): any => {
6 | const args = entry[SPLAT]
7 | if (args) {
8 | entry.message = util.format(entry.message, ...args)
9 | }
10 | return entry
11 | }
12 |
13 | export const utilFormatter = (): { transform: (entry: any) => any } => {
14 | return { transform }
15 | }
16 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/models/AppData.ts:
--------------------------------------------------------------------------------
1 | export interface AppData {
2 | windowPosition: WindowPosition
3 | settings: AppSettings
4 | }
5 | export interface AppSettings {
6 | guiSettingsOpen: boolean
7 |
8 | /**
9 | * True: is a server that SuperConductor connects to.
10 | * False: is a client that connects to SuperConductor.
11 | */
12 | acceptConnections: boolean
13 |
14 | listenPort: number
15 |
16 | superConductorHost: string
17 | bridgeId: string
18 | }
19 | export type WindowPosition =
20 | | {
21 | y: number
22 | x: number
23 | width: number
24 | height: number
25 | maximized: boolean
26 | }
27 | | {
28 | // Note: undefined will center the window
29 | y: undefined
30 | x: undefined
31 | width: number
32 | height: number
33 | maximized: boolean
34 | }
35 | export interface AppSystem {
36 | networkAddresses: string[]
37 | }
38 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/react/api/IPCClient.ts:
--------------------------------------------------------------------------------
1 | import { LogEntry } from 'winston'
2 | import { IPCClientMethods } from '../../ipc/IPCAPI'
3 | import { AppSettings, AppSystem } from '../../models/AppData'
4 |
5 | /** This class is used client-side, to handle messages from the server */
6 | export class IPCClient implements IPCClientMethods {
7 | constructor(
8 | private ipcRenderer: Electron.IpcRenderer,
9 | private callbacks: {
10 | log: (entry: LogEntry) => void
11 | settings: (settings: AppSettings) => void
12 | system: (system: AppSystem) => void
13 | }
14 | ) {
15 | this.ipcRenderer.on('callMethod', this.handleCallMethod)
16 | }
17 |
18 | private handleCallMethod = ((_event: Electron.IpcRendererEvent, methodname: string, ...args: any[]): void => {
19 | const fcn = (this as any)[methodname]
20 | if (!fcn) {
21 | // eslint-disable-next-line no-console
22 | console.error(`IPCClient: method ${methodname} not found`)
23 | } else {
24 | fcn.apply(this, args)
25 | }
26 | }).bind(this)
27 |
28 | log(entry: LogEntry): void {
29 | this.callbacks.log(entry)
30 | }
31 | settings(settings: AppSettings): void {
32 | this.callbacks.settings(settings)
33 | }
34 | system(system: AppSystem): void {
35 | this.callbacks.system(system)
36 | }
37 | destroy(): void {
38 | this.ipcRenderer.off('callMethod', this.handleCallMethod)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/react/api/IPCServer.ts:
--------------------------------------------------------------------------------
1 | import { IPCServerMethods } from '../../ipc/IPCAPI'
2 |
3 | type Promisify = {
4 | [K in keyof T]: T[K] extends (...arg: any[]) => any
5 | ? (...args: Parameters) => Promise>
6 | : T[K]
7 | }
8 |
9 | type ServerArgs = Parameters
10 | type ServerReturn = Promise>
11 |
12 | /** This class is used client-side, to send requests to the server */
13 | export class IPCServer implements Promisify {
14 | constructor(private ipcRenderer: Electron.IpcRenderer) {}
15 |
16 | private async invokeServerMethod(methodname: T, ...args: any[]): ServerReturn {
17 | // Stringifying and parsing data will convert Mobx observable objects into object literals.
18 | // Otherwise, Electron won't be able to clone it.
19 | return this.ipcRenderer.invoke(methodname, ...JSON.parse(JSON.stringify(args)))
20 | }
21 |
22 | async updateSettings(...args: ServerArgs<'updateSettings'>): ServerReturn<'updateSettings'> {
23 | return this.invokeServerMethod('updateSettings', ...args)
24 | }
25 | async initialized(...args: ServerArgs<'initialized'>): ServerReturn<'initialized'> {
26 | return this.invokeServerMethod('initialized', ...args)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/react/components/log/Log.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ScrollToBottom from 'react-scroll-to-bottom'
3 |
4 | export const Log: React.FC<{ children: React.ReactNode }> = ({ children }) => {
5 | return (
6 |
7 | {children}
8 |
9 | )
10 | }
11 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/react/components/log/LogEntry.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { LogEntry as LogEntryType } from 'winston'
3 | import classNames from 'classnames'
4 |
5 | export const LogEntry: React.FC<{ entry: LogEntryType }> = ({ entry }) => {
6 | return (
7 |
8 | [
9 |
14 | {entry.level}
15 |
16 | ]: {entry.message}
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/react/contexts/IPCServer.ts:
--------------------------------------------------------------------------------
1 | import { IPCServer } from '../api/IPCServer'
2 | import React from 'react'
3 | /** Used to communicate with the backend */
4 | export const IPCServerContext = React.createContext({} as IPCServer)
5 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/react/styles/app.scss:
--------------------------------------------------------------------------------
1 | @import './globals.scss';
2 |
3 | .app {
4 | position: fixed;
5 | top: 0;
6 | bottom: 0;
7 | left: 0;
8 | right: 0;
9 | box-sizing: border-box;
10 |
11 | display: flex;
12 | flex-direction: column;
13 | flex-wrap: nowrap;
14 | justify-content: flex-start;
15 | align-items: stretch;
16 | }
17 |
18 | @import './log.scss';
19 | @import './toggle.scss';
20 | @import './settings.scss';
21 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/react/styles/globals.scss:
--------------------------------------------------------------------------------
1 | $mainFont: monospace;
2 |
3 | html {
4 | font-size: 62.5%;
5 | }
6 |
7 | body {
8 | margin: 0;
9 | padding: 0;
10 | font-size: 1.6rem;
11 | color: #e5e5e5;
12 | background-color: #1e1e1e;
13 | font-family: $mainFont;
14 | }
15 |
16 | input,
17 | button,
18 | textarea {
19 | font-family: $mainFont;
20 | }
21 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/react/styles/log.scss:
--------------------------------------------------------------------------------
1 | .log {
2 | flex: 1;
3 | position: relative;
4 | z-index: -1;
5 | box-sizing: border-box;
6 |
7 | .content {
8 | position: absolute;
9 | top: 0;
10 | left: 0;
11 | right: 0;
12 | bottom: 0;
13 | padding: 0.5em 0.5em 1em 0.5em;
14 | }
15 | }
16 |
17 | .logEntry {
18 | overflow-wrap: break-word;
19 | white-space: pre-wrap;
20 |
21 | &__level {
22 | &.error {
23 | color: #f14c4c;
24 | }
25 |
26 | &.warn {
27 | color: #e9e940;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/react/styles/settings.scss:
--------------------------------------------------------------------------------
1 | .settings {
2 | display: flex;
3 | flex-direction: column;
4 |
5 | position: relative;
6 | max-height: 100vh;
7 |
8 | background-color: rgba(0, 0, 0, 0.8);
9 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
10 |
11 | border-bottom-left-radius: 0.5em;
12 | border-bottom-right-radius: 0.5em;
13 | border-bottom: 1px solid gray;
14 |
15 | padding-bottom: 2em;
16 |
17 | &.closed .content {
18 | display: none;
19 | }
20 |
21 | .content {
22 | padding: 1em;
23 | overflow-y: auto;
24 | }
25 |
26 | .open-close-handle {
27 | position: absolute;
28 | left: 0;
29 | right: 0;
30 | bottom: 0;
31 | height: 2em;
32 |
33 | border-top: 1px solid rgba(255, 255, 255, 0.25);
34 | cursor: pointer;
35 | z-index: 10;
36 |
37 | background-color: rgba(0, 0, 0, 0.8);
38 |
39 | &:hover {
40 | background-color: rgba(30, 30, 30, 0.8);
41 | }
42 |
43 | text-align: center;
44 | svg {
45 | font-size: 30px;
46 | }
47 | }
48 |
49 | .setting {
50 | display: flex;
51 | flex-direction: row;
52 | }
53 | .label {
54 | width: 20em;
55 | margin-right: 0.5em;
56 |
57 | margin-top: 20px;
58 |
59 | text-align: right;
60 |
61 | button.btn {
62 | float: right;
63 | }
64 | }
65 | .value {
66 | max-width: 15em;
67 |
68 | > .sc-switch {
69 | margin-top: 23px;
70 | }
71 | }
72 | .header {
73 | font-size: 125%;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/react/styles/toggle.scss:
--------------------------------------------------------------------------------
1 | .sc-switch {
2 | display: flex;
3 | align-items: center;
4 |
5 | .react-toggle-track {
6 | width: 40px;
7 | height: 20px;
8 | background-color: #bcbcc0;
9 | }
10 |
11 | // When hovered
12 | .react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
13 | background-color: #8c8c90;
14 | }
15 |
16 | // When checked
17 | .react-toggle--checked:not(.react-toggle--disabled) .react-toggle-track {
18 | //background-color: #47ae6b;
19 | background: #bcbcc0;
20 | }
21 |
22 | // When checked and disabled
23 | .react-toggle--checked.react-toggle--disabled .react-toggle-track {
24 | //background-color: #47ae6b;
25 | background: #bcbcc0;
26 | }
27 |
28 | // When checked and hovered
29 | .react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track {
30 | //background-color: #63d38a;
31 | background-color: #8c8c90;
32 | }
33 |
34 | .react-toggle-thumb {
35 | width: 15px;
36 | height: 15px;
37 | top: 2px;
38 | left: 3px;
39 | background: #1b1c26;
40 | box-shadow: none !important;
41 | border: none !important;
42 | }
43 |
44 | .react-toggle-track-check {
45 | left: 6px;
46 | top: 2px;
47 | svg {
48 | width: 11px;
49 | height: 9px;
50 | path {
51 | fill: #1b1c26;
52 | }
53 | }
54 | }
55 |
56 | .react-toggle-track-x {
57 | right: 5px;
58 | top: 2px;
59 | svg {
60 | width: 8px;
61 | height: 8px;
62 | path {
63 | fill: #1b1c26;
64 | }
65 | stroke-width: 0;
66 | }
67 | }
68 |
69 | .react-toggle--checked .react-toggle-thumb {
70 | left: 22px;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/src/types/react-scroll-to-bottom.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-scroll-to-bottom' {
2 | import * as React from 'react'
3 |
4 | export interface ReactScrollToBottomProps {
5 | checkInterval?: number
6 | className?: string
7 | debounce?: number
8 | followButtonClassName?: string
9 | mode?: string
10 | scrollViewClassName?: string
11 | children: React.ReactNode
12 | debug?: boolean
13 | }
14 |
15 | export interface ScrollOptions {
16 | behavior: ScrollBehavior
17 | }
18 |
19 | export interface FunctionContextProps {
20 | scrollTo: (scrollTo: number, options: ScrollOptions) => void
21 | scrollToBottom: (options: ScrollOptions) => void
22 | scrollToEnd: (options: ScrollOptions) => void
23 | scrollToStart: (options: ScrollOptions) => void
24 | scrollToTop: (options: ScrollOptions) => void
25 | }
26 |
27 | export const FunctionContext: React.Context
28 |
29 | export default class ReactScrollToBottom extends React.PureComponent {}
30 | }
31 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/tools/notarize.js:
--------------------------------------------------------------------------------
1 | /* Based on https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/ */
2 |
3 | const { notarize } = require('electron-notarize')
4 |
5 | exports.default = async function notarizing(context) {
6 | const { electronPlatformName, appOutDir } = context
7 | if (electronPlatformName !== 'darwin') {
8 | return
9 | }
10 |
11 | if (!process.env.APPLEID || !process.env.APPLEIDPASS) {
12 | // eslint-disable-next-line no-console
13 | console.log('Skipping notarizing, due to missing APPLEID or APPLEIDPASS environment variables')
14 | return
15 | }
16 |
17 | const appName = context.packager.appInfo.productFilename
18 |
19 | return notarize({
20 | appBundleId: 'tv.superfly.tsr-bridge',
21 | appPath: `${appOutDir}/${appName}.app`,
22 | appleId: process.env.APPLEID,
23 | appleIdPassword: process.env.APPLEIDPASS,
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist",
6 | "jsx": "react",
7 | "baseUrl": "./"
8 | },
9 | "exclude": [
10 | "src/**/__tests__/**/*"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/webpack.config.js:
--------------------------------------------------------------------------------
1 | const reactConfigs = require('./webpack.react.js')
2 |
3 | module.exports = [reactConfigs]
4 |
--------------------------------------------------------------------------------
/apps/tsr-bridge/webpack.react.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin')
2 | const path = require('path')
3 |
4 | module.exports = {
5 | mode: 'development',
6 | entry: './src/renderer.tsx',
7 | target: 'web',
8 | devtool: 'source-map',
9 | devServer: {
10 | static: path.join(__dirname, 'dist/renderer.js'),
11 | compress: true,
12 | port: 9125,
13 | },
14 | resolve: {
15 | extensions: ['.tsx', '.ts', '.js'],
16 | },
17 | module: {
18 | rules: [
19 | {
20 | test: /\.ts(x?)$/,
21 | include: /src/,
22 | use: [{ loader: 'ts-loader' }],
23 | },
24 | {
25 | test: /\.s[ac]ss$/i,
26 | use: ['style-loader', 'css-loader', 'sass-loader'],
27 | },
28 | {
29 | test: /\.css$/i,
30 | use: ['style-loader', 'css-loader'],
31 | },
32 | ],
33 | },
34 | output: {
35 | path: __dirname + '/dist',
36 | filename: 'renderer.js',
37 | },
38 | plugins: [
39 | new HtmlWebpackPlugin({
40 | template: './src/index.html',
41 | }),
42 | ],
43 | }
44 |
--------------------------------------------------------------------------------
/doc/img/copy-from-caspar-client.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/doc/img/copy-from-caspar-client.gif
--------------------------------------------------------------------------------
/doc/img/edit-timeline.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/doc/img/edit-timeline.gif
--------------------------------------------------------------------------------
/doc/img/gdd-input.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/doc/img/gdd-input.png
--------------------------------------------------------------------------------
/doc/img/intro0.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/doc/img/intro0.gif
--------------------------------------------------------------------------------
/doc/img/play-mode-multi.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/doc/img/play-mode-multi.gif
--------------------------------------------------------------------------------
/doc/img/play-mode-single.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/doc/img/play-mode-single.gif
--------------------------------------------------------------------------------
/doc/img/play.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/doc/img/play.gif
--------------------------------------------------------------------------------
/doc/img/resource-pane.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/doc/img/resource-pane.gif
--------------------------------------------------------------------------------
/doc/img/screenshot0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/doc/img/screenshot0.png
--------------------------------------------------------------------------------
/doc/img/select-drag-multiple-parts.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/doc/img/select-drag-multiple-parts.gif
--------------------------------------------------------------------------------
/doc/img/select-timeline-objects.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/doc/img/select-timeline-objects.gif
--------------------------------------------------------------------------------
/doc/img/streamdeck-GUI.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/doc/img/streamdeck-GUI.gif
--------------------------------------------------------------------------------
/doc/img/streamdeck.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/doc/img/streamdeck.gif
--------------------------------------------------------------------------------
/jest.config.base.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | roots: ['/src'],
3 | projects: [''],
4 | preset: 'ts-jest',
5 | moduleFileExtensions: ['js', 'ts'],
6 | transform: {
7 | '^.+\\.(ts|tsx)$': [
8 | 'ts-jest',
9 | {
10 | tsconfig: 'tsconfig.json',
11 | },
12 | ],
13 | },
14 | testMatch: ['**/__tests__/**/*.test.(ts|js)'],
15 | testEnvironment: 'node',
16 | coverageThreshold: {
17 | global: {
18 | branches: 100,
19 | functions: 100,
20 | lines: 100,
21 | statements: 100,
22 | },
23 | },
24 | coverageDirectory: '/coverage/',
25 | collectCoverage: false,
26 | // verbose: true,
27 | }
28 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": [
3 | "shared/**",
4 | "apps/**",
5 | "tests/**"
6 | ],
7 | "version": "0.11.3",
8 | "npmClient": "yarn",
9 | "useWorkspaces": true
10 | }
11 |
--------------------------------------------------------------------------------
/scripts/license-check.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const fs = require('fs')
3 | const path = require('path')
4 | const shell = require('shelljs')
5 |
6 | function getDirectories(source) {
7 | return fs
8 | .readdirSync(source, { withFileTypes: true })
9 | .filter((dirent) => dirent.isDirectory())
10 | .map((dirent) => dirent.name)
11 | }
12 |
13 | const sharedPackages = getDirectories(path.resolve('./shared/packages'))
14 | if (sharedPackages.length < 4) throw new Error('expected more shared/packages')
15 |
16 | const CURRENT_VERSION = require('@shared/api/package.json').version
17 |
18 | const allowPackages = [
19 | `buffers@0.1.1`,
20 | `caniuse-lite@1.0.30001465`,
21 | `cycle@1.0.3`,
22 | `truncate-utf8-bytes@1.0.2`,
23 | `utf8-byte-length@1.0.4`,
24 | ...sharedPackages.map((pkg) => `@shared/${pkg}@${CURRENT_VERSION}`),
25 | `superconductor@${CURRENT_VERSION}`,
26 | `tsr-bridge@${CURRENT_VERSION}`,
27 | ]
28 |
29 | const cmd = ['yarn', 'sofie-licensecheck', '--allowPackages', `"${allowPackages.join(';')}"`]
30 |
31 | const res = shell.exec(cmd.join(' '))
32 | process.exit(res.code)
33 |
--------------------------------------------------------------------------------
/scripts/version.js:
--------------------------------------------------------------------------------
1 | // This script is executed after the version is updated, before the new tag is set.
2 | const fs = require('fs').promises
3 | const { exec } = require('child_process')
4 |
5 | function cmd(command) {
6 | return new Promise((resolve, reject) => {
7 | exec(command, (error, stdout, stderr) => {
8 | if (error) {
9 | reject(error)
10 | } else {
11 | resolve({ stderr, stdout })
12 | }
13 | })
14 | })
15 | }
16 |
17 | ;(async () => {
18 | const lernaPackage = require('../lerna.json')
19 | const currentVersion = lernaPackage.version
20 |
21 | if (isPrerelease(currentVersion)) return // Don't update links for pre-release versions...
22 |
23 | console.log(`Updating links in README to ${currentVersion}...`)
24 |
25 | const readmeTextOrg = await fs.readFile('README.md', 'utf8')
26 | let readmeText = readmeTextOrg
27 |
28 | // Replace version "x.y.z":
29 | readmeText = readmeText.replace(/\d{1,2}\.\d{1,3}\.\d{1,3}/g, currentVersion)
30 |
31 | if (readmeTextOrg !== readmeText) {
32 | console.log('Saving...')
33 | fs.writeFile('README.md', readmeText)
34 | // console.log('and committing...')
35 | cmd('git add README.md')
36 | // cmd(`git commit -m "Update README to ${currentVersion}"`)
37 | } else {
38 | console.log('no change')
39 | }
40 | })().catch(console.error)
41 |
42 | function isPrerelease(version) {
43 | return !version.match(/^\d+\.\d+\.\d+$/)
44 | }
45 |
--------------------------------------------------------------------------------
/shared/packages/api/README.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | This package contains common API definitions and classes that are consumed by the other packages in the mono-repo.
4 |
--------------------------------------------------------------------------------
/shared/packages/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@shared/api",
3 | "version": "0.11.3",
4 | "description": "",
5 | "author": {
6 | "name": "SuperFlyTV AB",
7 | "email": "info@superfly.tv",
8 | "url": "https://superfly.tv"
9 | },
10 | "homepage": "https://github.com/SuperFlyTV/SuperConductor#readme",
11 | "license": "AGPL-3.0-or-later",
12 | "private": true,
13 | "engines": {
14 | "node": "^16.16.0 || 18"
15 | },
16 | "main": "dist/index",
17 | "types": "dist/index",
18 | "files": [
19 | "dist"
20 | ],
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/SuperFlyTV/SuperConductor.git"
24 | },
25 | "scripts": {
26 | "build": "rimraf dist && yarn build:main",
27 | "build:main": "tsc -p tsconfig.json",
28 | "precommit": "lint-staged",
29 | "lint:raw": "eslint --ext .ts --ext .js --ext .tsx --ext .jsx --ignore-pattern dist"
30 | },
31 | "bugs": {
32 | "url": "https://github.com/SuperFlyTV/SuperConductor/issues"
33 | },
34 | "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json",
35 | "lint-staged": {
36 | "*.{css,json,md,scss}": [
37 | "prettier --write"
38 | ],
39 | "*.{ts,tsx,js,jsx}": [
40 | "yarn lint:raw --fix"
41 | ]
42 | },
43 | "dependencies": {
44 | "@shared/models": "^0.11.3"
45 | },
46 | "devDependencies": {
47 | "@types/ws": "^8.2.2",
48 | "superfly-timeline": "^8.2.5",
49 | "xkeys": "^3.0.0"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/shared/packages/api/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './bridgeAPI'
2 | export * from './peripherals'
3 | export * from './logging'
4 |
--------------------------------------------------------------------------------
/shared/packages/api/src/logging.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * https://github.com/winstonjs/winston#logging-levels
3 | */
4 | export enum LogLevel {
5 | Error = 'error',
6 | Warn = 'warn',
7 | Info = 'info',
8 | HTTP = 'http',
9 | Verbose = 'verbose',
10 | Debug = 'debug',
11 | Silly = 'silly',
12 | }
13 |
14 | export type LogFn = (...args: any[]) => void
15 |
16 | export type LoggerLike = {
17 | [LogLevel.Error]: LogFn
18 | [LogLevel.Warn]: LogFn
19 | [LogLevel.Info]: LogFn
20 | [LogLevel.HTTP]: LogFn
21 | [LogLevel.Verbose]: LogFn
22 | [LogLevel.Debug]: LogFn
23 | [LogLevel.Silly]: LogFn
24 | }
25 |
--------------------------------------------------------------------------------
/shared/packages/api/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist",
6 | "lib": [
7 | "es6"
8 | ]
9 | },
10 | "exclude": [
11 | "src/**/__tests__/**/*"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/shared/packages/lib/README.md:
--------------------------------------------------------------------------------
1 | # Lib
2 |
3 | This package contains common library functions that are consumed by the other packages in the mono-repo.
4 |
--------------------------------------------------------------------------------
/shared/packages/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@shared/lib",
3 | "version": "0.11.3",
4 | "description": "",
5 | "author": {
6 | "name": "SuperFlyTV AB",
7 | "email": "info@superfly.tv",
8 | "url": "https://superfly.tv"
9 | },
10 | "homepage": "https://github.com/SuperFlyTV/SuperConductor#readme",
11 | "license": "AGPL-3.0-or-later",
12 | "private": true,
13 | "engines": {
14 | "node": "^16.16.0 || 18"
15 | },
16 | "main": "dist/index",
17 | "types": "dist/index",
18 | "files": [
19 | "dist"
20 | ],
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/SuperFlyTV/SuperConductor.git"
24 | },
25 | "scripts": {
26 | "build": "rimraf dist && yarn build:main",
27 | "build:main": "tsc -p tsconfig.json",
28 | "precommit": "lint-staged",
29 | "lint:raw": "eslint --ext .ts --ext .js --ext .tsx --ext .jsx --ignore-pattern dist"
30 | },
31 | "bugs": {
32 | "url": "https://github.com/SuperFlyTV/SuperConductor/issues"
33 | },
34 | "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json",
35 | "lint-staged": {
36 | "*.{css,json,md,scss}": [
37 | "prettier --write"
38 | ],
39 | "*.{ts,tsx,js,jsx}": [
40 | "yarn lint:raw --fix"
41 | ]
42 | },
43 | "dependencies": {
44 | "@shared/api": "^0.11.3",
45 | "@shared/models": "^0.11.3",
46 | "fast-copy": "^2.1.1",
47 | "lodash": "^4.17.21",
48 | "superfly-timeline": "^8.2.5"
49 | },
50 | "devDependencies": {
51 | "winston": "^3.7.2"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/shared/packages/lib/src/bytesToSize.ts:
--------------------------------------------------------------------------------
1 | export const bytesToSize = (bytes: number): string => {
2 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
3 | if (bytes == 0) return '0 Byte'
4 | const i = Math.floor(Math.log(bytes) / Math.log(1024))
5 | return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]
6 | }
7 |
--------------------------------------------------------------------------------
/shared/packages/lib/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './bytesToSize'
2 | export * from './color'
3 | export * from './lib'
4 | export * from './peripheral'
5 | export * from './Resources'
6 | export * from './TimelineTracker'
7 |
--------------------------------------------------------------------------------
/shared/packages/lib/src/peripheral.ts:
--------------------------------------------------------------------------------
1 | export function getPeripheralId(bridgeId: string, deviceId: string): string {
2 | return `${bridgeId}-${deviceId}`
3 | }
4 |
--------------------------------------------------------------------------------
/shared/packages/lib/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist",
6 | "lib": [
7 | "es6"
8 | ]
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/shared/packages/models/README.md:
--------------------------------------------------------------------------------
1 | # Models
2 |
3 | This package contains common data model definitions that are consumed by the other packages in the mono-repo.
4 |
--------------------------------------------------------------------------------
/shared/packages/models/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@shared/models",
3 | "version": "0.11.3",
4 | "description": "",
5 | "author": {
6 | "name": "SuperFlyTV AB",
7 | "email": "info@superfly.tv",
8 | "url": "https://superfly.tv"
9 | },
10 | "homepage": "https://github.com/SuperFlyTV/SuperConductor#readme",
11 | "license": "AGPL-3.0-or-later",
12 | "private": true,
13 | "engines": {
14 | "node": "^16.16.0 || 18"
15 | },
16 | "main": "dist/index",
17 | "types": "dist/index",
18 | "files": [
19 | "dist"
20 | ],
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/SuperFlyTV/SuperConductor.git"
24 | },
25 | "scripts": {
26 | "build": "rimraf dist && yarn build:main",
27 | "build:main": "tsc -p tsconfig.json",
28 | "precommit": "lint-staged",
29 | "lint:raw": "eslint --ext .ts --ext .js --ext .tsx --ext .jsx --ignore-pattern dist"
30 | },
31 | "bugs": {
32 | "url": "https://github.com/SuperFlyTV/SuperConductor/issues"
33 | },
34 | "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json",
35 | "lint-staged": {
36 | "*.{css,json,md,scss}": [
37 | "prettier --write"
38 | ],
39 | "*.{ts,tsx,js,jsx}": [
40 | "yarn lint:raw --fix"
41 | ]
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/shared/packages/models/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './resource'
2 |
--------------------------------------------------------------------------------
/shared/packages/models/src/resource/Atem.ts:
--------------------------------------------------------------------------------
1 | import { ResourceBase, ResourceType } from './resource'
2 |
3 | export type AtemAny =
4 | | AtemMe
5 | | AtemDsk
6 | | AtemAux
7 | | AtemSsrc
8 | | AtemSsrcProps
9 | | AtemMacroPlayer
10 | | AtemAudioChannel
11 | | AtemMediaPlayer
12 |
13 | export interface AtemMe extends ResourceBase {
14 | resourceType: ResourceType.ATEM_ME
15 |
16 | /** The 0-based index of the ME */
17 | index: number
18 | }
19 |
20 | export interface AtemDsk extends ResourceBase {
21 | resourceType: ResourceType.ATEM_DSK
22 |
23 | /** The 0-based index of the DSK */
24 | index: number
25 | }
26 |
27 | export interface AtemAux extends ResourceBase {
28 | resourceType: ResourceType.ATEM_AUX
29 |
30 | /** The 0-based index of the Aux output */
31 | index: number
32 | }
33 |
34 | export interface AtemSsrc extends ResourceBase {
35 | resourceType: ResourceType.ATEM_SSRC
36 |
37 | /** The 0-based index of the SSrc */
38 | index: number
39 | }
40 |
41 | export interface AtemSsrcProps extends ResourceBase {
42 | resourceType: ResourceType.ATEM_SSRC_PROPS
43 |
44 | /** The 0-based index of the SSrc */
45 | index: number
46 | }
47 |
48 | export interface AtemMacroPlayer extends ResourceBase {
49 | resourceType: ResourceType.ATEM_MACRO_PLAYER
50 | }
51 |
52 | export interface AtemAudioChannel extends ResourceBase {
53 | resourceType: ResourceType.ATEM_AUDIO_CHANNEL
54 |
55 | /** The 1-based index of the Audio Channel */
56 | index: number
57 | }
58 |
59 | export interface AtemMediaPlayer extends ResourceBase {
60 | resourceType: ResourceType.ATEM_MEDIA_PLAYER
61 |
62 | /** The 0-based index of the Media Player */
63 | index: number
64 | }
65 |
--------------------------------------------------------------------------------
/shared/packages/models/src/resource/CasparCG.ts:
--------------------------------------------------------------------------------
1 | import { ResourceBase, ResourceType } from './resource'
2 |
3 | export type CasparCGAny = CasparCGServer | CasparCGMedia | CasparCGTemplate
4 |
5 | export interface CasparCGServer extends ResourceBase {
6 | resourceType: ResourceType.CASPARCG_SERVER
7 |
8 | /** The number of channels the server has */
9 | channels: number
10 | }
11 | export interface CasparCGMedia extends ResourceBase {
12 | resourceType: ResourceType.CASPARCG_MEDIA
13 |
14 | type: 'image' | 'video' | 'audio'
15 | name: string
16 | size: number
17 | changed: number
18 | frames: number
19 | frameTime: string
20 | frameRate: number
21 | duration: number
22 | thumbnail?: string
23 |
24 | // prefilled data:
25 | channel?: number
26 | layer?: number
27 | }
28 |
29 | export interface CasparCGTemplate extends ResourceBase {
30 | resourceType: ResourceType.CASPARCG_TEMPLATE
31 |
32 | name: string
33 | size: number
34 | changed: number
35 |
36 | // prefilled data:
37 | useStopCommand?: boolean
38 | duration?: number | null
39 | data?: any
40 | sendDataAsXML?: boolean
41 | channel?: number
42 | layer?: number
43 |
44 | errorMessage?: string
45 | gdd?: any
46 | }
47 |
--------------------------------------------------------------------------------
/shared/packages/models/src/resource/HTTPSend.ts:
--------------------------------------------------------------------------------
1 | import { ResourceBase, ResourceType } from './resource'
2 |
3 | export type HTTPSendAny = HTTPRequest
4 |
5 | export interface HTTPRequest extends ResourceBase {
6 | resourceType: ResourceType.HTTP_REQUEST
7 | }
8 |
--------------------------------------------------------------------------------
/shared/packages/models/src/resource/Hyperdeck.ts:
--------------------------------------------------------------------------------
1 | import { ResourceBase, ResourceType } from './resource'
2 |
3 | export type HyperdeckAny = HyperdeckPlay | HyperdeckRecord | HyperdeckPreview | HyperdeckClip
4 |
5 | export interface HyperdeckPlay extends ResourceBase {
6 | resourceType: ResourceType.HYPERDECK_PLAY
7 | }
8 |
9 | export interface HyperdeckRecord extends ResourceBase {
10 | resourceType: ResourceType.HYPERDECK_RECORD
11 | }
12 |
13 | export interface HyperdeckPreview extends ResourceBase {
14 | resourceType: ResourceType.HYPERDECK_PREVIEW
15 | }
16 |
17 | export interface HyperdeckClip extends ResourceBase {
18 | resourceType: ResourceType.HYPERDECK_CLIP
19 | slotId: number
20 | clipId: number
21 | clipName: string
22 | }
23 |
--------------------------------------------------------------------------------
/shared/packages/models/src/resource/OBS.ts:
--------------------------------------------------------------------------------
1 | import { ResourceBase, ResourceType } from './resource'
2 |
3 | export type OBSAny = OBSScene | OBSTransition | OBSRecording | OBSStreaming | OBSSourceSettings | OBSMute | OBSRender
4 |
5 | export interface OBSScene extends ResourceBase {
6 | resourceType: ResourceType.OBS_SCENE
7 |
8 | name: string
9 | }
10 |
11 | export interface OBSTransition extends ResourceBase {
12 | resourceType: ResourceType.OBS_TRANSITION
13 |
14 | name: string
15 | }
16 |
17 | export interface OBSRecording extends ResourceBase {
18 | resourceType: ResourceType.OBS_RECORDING
19 | }
20 |
21 | export interface OBSStreaming extends ResourceBase {
22 | resourceType: ResourceType.OBS_STREAMING
23 | }
24 |
25 | export interface OBSSourceSettings extends ResourceBase {
26 | resourceType: ResourceType.OBS_SOURCE_SETTINGS
27 | }
28 |
29 | export interface OBSMute extends ResourceBase {
30 | resourceType: ResourceType.OBS_MUTE
31 | }
32 |
33 | export interface OBSRender extends ResourceBase {
34 | resourceType: ResourceType.OBS_RENDER
35 | }
36 |
--------------------------------------------------------------------------------
/shared/packages/models/src/resource/OSC.ts:
--------------------------------------------------------------------------------
1 | import { ResourceBase, ResourceType } from './resource'
2 |
3 | export type OSCAny = OSCMessage
4 |
5 | export interface OSCMessage extends ResourceBase {
6 | resourceType: ResourceType.OSC_MESSAGE
7 | }
8 |
--------------------------------------------------------------------------------
/shared/packages/models/src/resource/TCPSend.ts:
--------------------------------------------------------------------------------
1 | import { ResourceBase, ResourceType } from './resource'
2 |
3 | export type TCPSendAny = TCPRequest
4 |
5 | export interface TCPRequest extends ResourceBase {
6 | resourceType: ResourceType.TCP_REQUEST
7 | }
8 |
--------------------------------------------------------------------------------
/shared/packages/models/src/resource/VMix.ts:
--------------------------------------------------------------------------------
1 | import { ResourceBase, ResourceType } from './resource'
2 |
3 | export type VMixAny =
4 | | VMixInput
5 | | VMixPreview
6 | | VMixInputSettings
7 | | VMixAudioSettings
8 | | VMixOutputSettings
9 | | VMixOverlaySettings
10 | | VMixRecording
11 | | VMixStreaming
12 | | VMixExternal
13 | | VMixFadeToBlack
14 | | VMixFader
15 |
16 | export interface VMixInput extends ResourceBase {
17 | resourceType: ResourceType.VMIX_INPUT
18 |
19 | number: number
20 | type: string
21 | }
22 |
23 | export interface VMixPreview extends ResourceBase {
24 | resourceType: ResourceType.VMIX_PREVIEW
25 | }
26 |
27 | export interface VMixInputSettings extends ResourceBase {
28 | resourceType: ResourceType.VMIX_INPUT_SETTINGS
29 | }
30 |
31 | export interface VMixAudioSettings extends ResourceBase {
32 | resourceType: ResourceType.VMIX_AUDIO_SETTINGS
33 | }
34 |
35 | export interface VMixOutputSettings extends ResourceBase {
36 | resourceType: ResourceType.VMIX_OUTPUT_SETTINGS
37 | }
38 |
39 | export interface VMixOverlaySettings extends ResourceBase {
40 | resourceType: ResourceType.VMIX_OVERLAY_SETTINGS
41 | }
42 |
43 | export interface VMixRecording extends ResourceBase {
44 | resourceType: ResourceType.VMIX_RECORDING
45 | }
46 |
47 | export interface VMixStreaming extends ResourceBase {
48 | resourceType: ResourceType.VMIX_STREAMING
49 | }
50 |
51 | export interface VMixExternal extends ResourceBase {
52 | resourceType: ResourceType.VMIX_EXTERNAL
53 | }
54 |
55 | export interface VMixFadeToBlack extends ResourceBase {
56 | resourceType: ResourceType.VMIX_FADE_TO_BLACK
57 | }
58 |
59 | export interface VMixFader extends ResourceBase {
60 | resourceType: ResourceType.VMIX_FADER
61 | }
62 |
--------------------------------------------------------------------------------
/shared/packages/models/src/resource/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Atem'
2 | export * from './CasparCG'
3 | export * from './OBS'
4 | export * from './VMix'
5 | export * from './OSC'
6 | export * from './HTTPSend'
7 | export * from './Hyperdeck'
8 | export * from './resource'
9 | export * from './TCPSend'
10 |
--------------------------------------------------------------------------------
/shared/packages/models/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist",
6 | "lib": [
7 | "es6"
8 | ]
9 | },
10 | "exclude": [
11 | "src/**/__tests__/**/*"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/shared/packages/peripherals/README.md:
--------------------------------------------------------------------------------
1 | # Models
2 |
3 | This package contains common data model definitions that are consumed by the other packages in the mono-repo.
4 |
--------------------------------------------------------------------------------
/shared/packages/peripherals/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@shared/peripherals",
3 | "version": "0.11.3",
4 | "description": "",
5 | "author": {
6 | "name": "SuperFlyTV AB",
7 | "email": "info@superfly.tv",
8 | "url": "https://superfly.tv"
9 | },
10 | "homepage": "https://github.com/SuperFlyTV/SuperConductor#readme",
11 | "license": "AGPL-3.0-or-later",
12 | "private": true,
13 | "engines": {
14 | "node": "^16.16.0 || 18"
15 | },
16 | "main": "dist/index",
17 | "types": "dist/index",
18 | "files": [
19 | "dist"
20 | ],
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/SuperFlyTV/SuperConductor.git"
24 | },
25 | "scripts": {
26 | "build": "rimraf dist && yarn build:main",
27 | "build:main": "tsc -p tsconfig.json",
28 | "precommit": "lint-staged",
29 | "lint:raw": "eslint --ext .ts --ext .js --ext .tsx --ext .jsx --ignore-pattern dist"
30 | },
31 | "bugs": {
32 | "url": "https://github.com/SuperFlyTV/SuperConductor/issues"
33 | },
34 | "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json",
35 | "lint-staged": {
36 | "*.{css,json,md,scss}": [
37 | "prettier --write"
38 | ],
39 | "*.{ts,tsx,js,jsx}": [
40 | "yarn lint:raw --fix"
41 | ]
42 | },
43 | "devDependencies": {
44 | "@types/sharp": "^0.31.0",
45 | "winston": "^3.7.2"
46 | },
47 | "dependencies": {
48 | "@elgato-stream-deck/node": "^5.7.2",
49 | "@julusian/jpeg-turbo": "^2.1.0",
50 | "@julusian/midi": "^3.0.0",
51 | "@shared/api": "^0.11.3",
52 | "@shared/lib": "^0.11.3",
53 | "lodash": "^4.17.21",
54 | "p-queue": "^6.6.2",
55 | "sharp": "^0.31.2",
56 | "superfly-timeline": "^8.2.5",
57 | "xkeys": "^3.0.0"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/shared/packages/peripherals/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './peripheralsHandler'
2 | export * from './peripherals/peripheral'
3 | export * from './peripherals/streamdeck'
4 |
--------------------------------------------------------------------------------
/shared/packages/peripherals/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist",
6 | "lib": [
7 | "es6"
8 | ]
9 | },
10 | "include": [
11 | "src/**.ts"
12 | ],
13 | "exclude": [
14 | "src/**/__tests__/**/*"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/shared/packages/server-lib/README.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | This package contains common API definitions and classes that are consumed by the other packages in the mono-repo.
4 |
--------------------------------------------------------------------------------
/shared/packages/server-lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@shared/server-lib",
3 | "version": "0.11.3",
4 | "description": "",
5 | "author": {
6 | "name": "SuperFlyTV AB",
7 | "email": "info@superfly.tv",
8 | "url": "https://superfly.tv"
9 | },
10 | "homepage": "https://github.com/SuperFlyTV/SuperConductor#readme",
11 | "license": "AGPL-3.0-or-later",
12 | "private": true,
13 | "engines": {
14 | "node": "^16.16.0 || 18"
15 | },
16 | "main": "dist/index",
17 | "types": "dist/index",
18 | "files": [
19 | "dist"
20 | ],
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/SuperFlyTV/SuperConductor.git"
24 | },
25 | "scripts": {
26 | "build": "rimraf dist && yarn build:main",
27 | "build:main": "tsc -p tsconfig.json",
28 | "precommit": "lint-staged",
29 | "lint:raw": "eslint --ext .ts --ext .js --ext .tsx --ext .jsx --ignore-pattern dist"
30 | },
31 | "bugs": {
32 | "url": "https://github.com/SuperFlyTV/SuperConductor/issues"
33 | },
34 | "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json",
35 | "lint-staged": {
36 | "*.{css,json,md,scss}": [
37 | "prettier --write"
38 | ],
39 | "*.{ts,tsx,js,jsx}": [
40 | "yarn lint:raw --fix"
41 | ]
42 | },
43 | "dependencies": {
44 | "@shared/api": "^0.11.3",
45 | "@shared/models": "^0.11.3",
46 | "ws": "^8.4.2"
47 | },
48 | "devDependencies": {
49 | "@types/ws": "^8.2.2",
50 | "superfly-timeline": "^8.2.5"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/shared/packages/server-lib/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './WebsocketServer'
2 |
3 | // Note: This libary has dependencies that is server-side only.
4 | // Don't include it in the client-side bundle.
5 |
--------------------------------------------------------------------------------
/shared/packages/server-lib/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist",
6 | "lib": [
7 | "es6"
8 | ]
9 | },
10 | "exclude": [
11 | "src/**/__tests__/**/*"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/shared/packages/tsr-bridge/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@shared/tsr-bridge",
3 | "version": "0.11.3",
4 | "description": "",
5 | "author": {
6 | "name": "SuperFlyTV AB",
7 | "email": "info@superfly.tv",
8 | "url": "https://superfly.tv"
9 | },
10 | "homepage": "https://github.com/SuperFlyTV/SuperConductor#readme",
11 | "license": "AGPL-3.0-or-later",
12 | "private": true,
13 | "engines": {
14 | "node": "^16.16.0 || 18"
15 | },
16 | "main": "dist/index",
17 | "types": "dist/index",
18 | "files": [
19 | "dist"
20 | ],
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/SuperFlyTV/SuperConductor.git"
24 | },
25 | "scripts": {
26 | "build": "rimraf dist && yarn build:main",
27 | "build:main": "tsc -p tsconfig.json",
28 | "precommit": "lint-staged",
29 | "lint:raw": "eslint --ext .ts --ext .js --ext .tsx --ext .jsx --ignore-pattern dist"
30 | },
31 | "bugs": {
32 | "url": "https://github.com/SuperFlyTV/SuperConductor/issues"
33 | },
34 | "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json",
35 | "lint-staged": {
36 | "*.{css,json,md,scss}": [
37 | "prettier --write"
38 | ],
39 | "*.{ts,tsx,js,jsx}": [
40 | "yarn lint:raw --fix"
41 | ]
42 | },
43 | "dependencies": {
44 | "@shared/api": "^0.11.3",
45 | "@shared/lib": "^0.11.3",
46 | "@shared/models": "^0.11.3",
47 | "@shared/peripherals": "^0.11.3",
48 | "cheerio": "^1.0.0-rc.12",
49 | "got": "^11.8.5",
50 | "lodash": "^4.17.21",
51 | "recursive-readdir": "^2.2.3",
52 | "timeline-state-resolver": "7.5.0-nightly-release47-20221116-134940-9a43f95c5.0",
53 | "winston": "^3.7.2"
54 | },
55 | "devDependencies": {
56 | "@types/lodash": "^4.14.178",
57 | "@types/recursive-readdir": "^2.2.1"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/shared/packages/tsr-bridge/src/sideload/HTTPSend.ts:
--------------------------------------------------------------------------------
1 | import { DeviceOptionsHTTPSend } from 'timeline-state-resolver'
2 | import { ResourceAny, ResourceType, HTTPRequest } from '@shared/models'
3 | import { SideLoadDevice } from './sideload'
4 | import { LoggerLike } from '@shared/api'
5 |
6 | export class HTTPSendSideload implements SideLoadDevice {
7 | constructor(private deviceId: string, _deviceOptions: DeviceOptionsHTTPSend, _log: LoggerLike) {}
8 | public async refreshResources(): Promise {
9 | return this._refreshResources()
10 | }
11 | async close(): Promise {
12 | // Nothing to cleanup.
13 | }
14 | private async _refreshResources() {
15 | const resources: { [id: string]: ResourceAny } = {}
16 |
17 | // HTTP Request
18 | {
19 | const resource: HTTPRequest = {
20 | resourceType: ResourceType.HTTP_REQUEST,
21 | deviceId: this.deviceId,
22 | id: `${this.deviceId}_http_request`,
23 | displayName: 'HTTP Request',
24 | }
25 | resources[resource.id] = resource
26 | }
27 |
28 | return Object.values(resources)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/shared/packages/tsr-bridge/src/sideload/OSC.ts:
--------------------------------------------------------------------------------
1 | import { DeviceOptionsOSC } from 'timeline-state-resolver'
2 | import { ResourceAny, ResourceType, OSCMessage } from '@shared/models'
3 | import { SideLoadDevice } from './sideload'
4 | import { LoggerLike } from '@shared/api'
5 |
6 | export class OSCSideload implements SideLoadDevice {
7 | constructor(private deviceId: string, _deviceOptions: DeviceOptionsOSC, _log: LoggerLike) {}
8 | public async refreshResources(): Promise {
9 | return this._refreshResources()
10 | }
11 | async close(): Promise {
12 | // Nothing to cleanup.
13 | }
14 | private async _refreshResources() {
15 | const resources: { [id: string]: ResourceAny } = {}
16 |
17 | // Message
18 | {
19 | const resource: OSCMessage = {
20 | resourceType: ResourceType.OSC_MESSAGE,
21 | deviceId: this.deviceId,
22 | id: `${this.deviceId}_osc_message`,
23 | displayName: 'OSC Message',
24 | }
25 | resources[resource.id] = resource
26 | }
27 |
28 | return Object.values(resources)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/shared/packages/tsr-bridge/src/sideload/TCPSend.ts:
--------------------------------------------------------------------------------
1 | import { DeviceOptionsTCPSend } from 'timeline-state-resolver'
2 | import { ResourceAny, ResourceType, TCPRequest } from '@shared/models'
3 | import { SideLoadDevice } from './sideload'
4 | import { LoggerLike } from '@shared/api'
5 |
6 | export class TCPSendSideload implements SideLoadDevice {
7 | constructor(private deviceId: string, _deviceOptions: DeviceOptionsTCPSend, _log: LoggerLike) {}
8 | async refreshResources(): Promise {
9 | return this._refreshResources()
10 | }
11 | async close(): Promise {
12 | // Nothing to cleanup.
13 | }
14 | private async _refreshResources() {
15 | const resources: { [id: string]: ResourceAny } = {}
16 |
17 | // HTTP Request
18 | {
19 | const resource: TCPRequest = {
20 | resourceType: ResourceType.TCP_REQUEST,
21 | deviceId: this.deviceId,
22 | id: `${this.deviceId}_tcp_request`,
23 | displayName: 'TCP Request',
24 | }
25 | resources[resource.id] = resource
26 | }
27 |
28 | return Object.values(resources)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/shared/packages/tsr-bridge/src/sideload/sideload.ts:
--------------------------------------------------------------------------------
1 | import { ResourceAny } from '@shared/models'
2 |
3 | export interface SideLoadDevice {
4 | refreshResources: () => Promise
5 | close: () => Promise
6 | }
7 |
--------------------------------------------------------------------------------
/shared/packages/tsr-bridge/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "src",
5 | "outDir": "dist",
6 | "lib": [
7 | "es6"
8 | ]
9 | },
10 | "include": [
11 | "src/**.ts"
12 | ],
13 | "exclude": [
14 | "src/**/__tests__/**/*"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.build.json"
3 | }
4 |
--------------------------------------------------------------------------------