├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md └── workflows │ ├── create-release.yaml │ └── node.yaml ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── COPYING ├── LICENSE ├── README.md ├── apps ├── app │ ├── README.md │ ├── build │ │ ├── icon.icns │ │ ├── icon.ico │ │ ├── icon.png │ │ └── icons │ │ │ ├── 1024x1024.png │ │ │ ├── 128x128.png │ │ │ ├── 16x16.png │ │ │ ├── 24x24.png │ │ │ ├── 256x256.png │ │ │ ├── 32x32.png │ │ │ ├── 48x48.png │ │ │ ├── 512x512.png │ │ │ ├── 64x64.png │ │ │ └── 96x96.png │ ├── entitlements.mac.plist │ ├── jest.config.js │ ├── nodemon.json │ ├── package.json │ ├── src │ │ ├── electron │ │ │ ├── HTTPAPI.ts │ │ │ ├── IPCClient.ts │ │ │ ├── IPCServer.ts │ │ │ ├── SuperConductor.ts │ │ │ ├── analogHandler.ts │ │ │ ├── bridgeHandler.ts │ │ │ ├── makeDevData.ts │ │ │ ├── menu.ts │ │ │ ├── rundown.ts │ │ │ ├── rundownActions.ts │ │ │ ├── sessionHandler.ts │ │ │ ├── storageHandler.ts │ │ │ ├── telemetry.ts │ │ │ ├── timeline.ts │ │ │ └── triggersHandler.ts │ │ ├── img │ │ │ ├── atem.png │ │ │ ├── casparcg.png │ │ │ ├── fix.d.ts │ │ │ ├── hyperdeck.png │ │ │ ├── midi.png │ │ │ ├── obs.png │ │ │ ├── streamdeck.png │ │ │ ├── vmix.png │ │ │ └── xkeys.png │ │ ├── index.html │ │ ├── ipc │ │ │ └── IPCAPI.ts │ │ ├── lib │ │ │ ├── GUI.ts │ │ │ ├── TSR.ts │ │ │ ├── TSRMappings.ts │ │ │ ├── TimelineObj.ts │ │ │ ├── __tests__ │ │ │ │ └── timeLib.test.ts │ │ │ ├── autoFill.ts │ │ │ ├── baseFolder.ts │ │ │ ├── defaults.ts │ │ │ ├── getDefaults.ts │ │ │ ├── logging │ │ │ │ ├── index.ts │ │ │ │ ├── logging.ts │ │ │ │ └── util-formatter.ts │ │ │ ├── moveTimelineObj.ts │ │ │ ├── partTimeline.ts │ │ │ ├── playout │ │ │ │ ├── __tests__ │ │ │ │ │ ├── __snapshots__ │ │ │ │ │ │ └── preparedGroupPlayData.test.ts.snap │ │ │ │ │ └── preparedGroupPlayData.test.ts │ │ │ │ ├── groupPlayData.ts │ │ │ │ └── preparedGroupPlayData.ts │ │ │ ├── resources.ts │ │ │ ├── timeLib.ts │ │ │ ├── timeline.ts │ │ │ ├── triggers │ │ │ │ ├── action.ts │ │ │ │ ├── identifiers.ts │ │ │ │ └── keyDisplay │ │ │ │ │ ├── applicationAction.ts │ │ │ │ │ ├── keyDisplay.ts │ │ │ │ │ ├── lib.ts │ │ │ │ │ └── rundownAction.ts │ │ │ ├── useMovable.tsx │ │ │ ├── userAgreement.ts │ │ │ └── util.ts │ │ ├── main.ts │ │ ├── models │ │ │ ├── App │ │ │ │ └── AppData.ts │ │ │ ├── GUI │ │ │ │ └── PreparedPlayhead.ts │ │ │ ├── project │ │ │ │ ├── AnalogInput.ts │ │ │ │ ├── Bridge.ts │ │ │ │ ├── Peripheral.ts │ │ │ │ └── Project.ts │ │ │ └── rundown │ │ │ │ ├── Analog.ts │ │ │ │ ├── Group.ts │ │ │ │ ├── Part.ts │ │ │ │ ├── Rundown.ts │ │ │ │ ├── TimelineObj.ts │ │ │ │ └── Trigger.ts │ │ ├── react │ │ │ ├── App.tsx │ │ │ ├── api │ │ │ │ ├── DragItemTypes.ts │ │ │ │ ├── IPCClient.ts │ │ │ │ ├── IPCServer.ts │ │ │ │ ├── clipboard │ │ │ │ │ ├── __tests__ │ │ │ │ │ │ └── transformURL.test.ts │ │ │ │ │ ├── casparCGClient.ts │ │ │ │ │ ├── clipboard.ts │ │ │ │ │ ├── convenience.ts │ │ │ │ │ ├── internal.ts │ │ │ │ │ ├── lib.ts │ │ │ │ │ └── transformURL.ts │ │ │ │ └── logger.ts │ │ │ ├── components │ │ │ │ ├── SplashScreen.tsx │ │ │ │ ├── UserAgreementScreen.tsx │ │ │ │ ├── headerBar │ │ │ │ │ ├── HeaderBar.tsx │ │ │ │ │ ├── deviceStatuses │ │ │ │ │ │ ├── ConnectionStatus.tsx │ │ │ │ │ │ ├── DeviceStatuses.tsx │ │ │ │ │ │ ├── DisabledPeripherals │ │ │ │ │ │ │ ├── DisabledPeripheralsSettings.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ └── PeripheralSettings │ │ │ │ │ │ │ ├── PeripheralSettings.tsx │ │ │ │ │ │ │ ├── TimelineDisplay.tsx │ │ │ │ │ │ │ ├── midi.tsx │ │ │ │ │ │ │ ├── streamdeck.tsx │ │ │ │ │ │ │ └── xkeys.tsx │ │ │ │ │ ├── style.scss │ │ │ │ │ └── tabs │ │ │ │ │ │ ├── Tabs.tsx │ │ │ │ │ │ ├── newTabBtn │ │ │ │ │ │ ├── NewTabBtn.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── style.scss │ │ │ │ │ │ └── tab │ │ │ │ │ │ ├── Tab.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ ├── inputs │ │ │ │ │ ├── AddBtn.tsx │ │ │ │ │ ├── AnalogInputPicker │ │ │ │ │ │ ├── AnalogInputPicker.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ │ ├── BooleanInput.tsx │ │ │ │ │ ├── Btn │ │ │ │ │ │ ├── Btn.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ │ ├── DateTimeInput.tsx │ │ │ │ │ ├── DuplicateBtn.tsx │ │ │ │ │ ├── DurationInput.tsx │ │ │ │ │ ├── EditTrigger.tsx │ │ │ │ │ ├── FloatInput.tsx │ │ │ │ │ ├── HelpButton │ │ │ │ │ │ ├── HelpButton.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ │ ├── IntInput.tsx │ │ │ │ │ ├── PauseBtn │ │ │ │ │ │ ├── PauseBtn.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ │ ├── PlayBtn │ │ │ │ │ │ ├── PlayBtn.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ │ ├── SelectEnum.tsx │ │ │ │ │ ├── SelectMultiple.tsx │ │ │ │ │ ├── StopBtn │ │ │ │ │ │ └── StopBtn.tsx │ │ │ │ │ ├── TextInput.tsx │ │ │ │ │ ├── ToggleBtn │ │ │ │ │ │ ├── ToggleBtn.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ │ ├── ToggleInput.tsx │ │ │ │ │ ├── TrashBtn.tsx │ │ │ │ │ ├── TriggerBtn │ │ │ │ │ │ ├── TriggerBtn.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ │ ├── parsedValueInput.tsx │ │ │ │ │ └── textBtn │ │ │ │ │ │ ├── TextBtn.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ ├── pages │ │ │ │ │ ├── homePage │ │ │ │ │ │ ├── AnalogInputsPage │ │ │ │ │ │ │ ├── AnalogInputsPage.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── ApplicationActionsPage │ │ │ │ │ │ │ ├── ApplicationActionsPage.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── HomePage.tsx │ │ │ │ │ │ ├── applicationPage │ │ │ │ │ │ │ ├── ApplicationPage.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── bridgeItem │ │ │ │ │ │ │ ├── BridgeItemContent.tsx │ │ │ │ │ │ │ ├── BridgeItemHeader.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── bridgesPage │ │ │ │ │ │ │ ├── BridgesPage.tsx │ │ │ │ │ │ │ ├── NewBridgeDialog.tsx │ │ │ │ │ │ │ └── NewDeviceDialog.tsx │ │ │ │ │ │ ├── deviceIcon │ │ │ │ │ │ │ ├── DeviceIcon.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── deviceItem │ │ │ │ │ │ │ ├── DeviceItemContent.tsx │ │ │ │ │ │ │ ├── DeviceItemHeader.tsx │ │ │ │ │ │ │ ├── DevicesList.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── deviceShorcut │ │ │ │ │ │ │ ├── DeviceShortcut.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── layerItem │ │ │ │ │ │ │ ├── LayerItemContent.tsx │ │ │ │ │ │ │ ├── LayerItemHeader.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── layersPage │ │ │ │ │ │ │ ├── DeviceSpecificSettings.tsx │ │ │ │ │ │ │ ├── LayersPage.tsx │ │ │ │ │ │ │ └── device-specific-settings │ │ │ │ │ │ │ │ ├── AtemMappingSettings.tsx │ │ │ │ │ │ │ │ ├── CasparCGMappingSettings.tsx │ │ │ │ │ │ │ │ ├── OBSMappingSettings.tsx │ │ │ │ │ │ │ │ └── VMixMappingSettings.tsx │ │ │ │ │ │ ├── message │ │ │ │ │ │ │ ├── Message.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── peripheralItem │ │ │ │ │ │ │ └── PeripheralItemHeader.tsx │ │ │ │ │ │ ├── peripheralsList │ │ │ │ │ │ │ └── PeripheralsList.tsx │ │ │ │ │ │ ├── projectPage │ │ │ │ │ │ │ ├── ProjectPage.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── projectPageLayout │ │ │ │ │ │ │ ├── ProjectPageLayout.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── projectPageMenubar │ │ │ │ │ │ │ ├── ProjectPageMenubar.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── roundedSection │ │ │ │ │ │ │ ├── RoundedSection.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ └── scList │ │ │ │ │ │ │ ├── ScList.tsx │ │ │ │ │ │ │ ├── ScListItemLabel.tsx │ │ │ │ │ │ │ ├── StatusCircle.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ └── newRundownPage │ │ │ │ │ │ ├── ImportRundownIcon.tsx │ │ │ │ │ │ ├── NewGropIcon.tsx │ │ │ │ │ │ ├── NewRundownOption.tsx │ │ │ │ │ │ ├── NewRundownPage.tsx │ │ │ │ │ │ ├── newRundownOption.scss │ │ │ │ │ │ └── newRundownPage.scss │ │ │ │ ├── rundown │ │ │ │ │ ├── GroupPropertiesDialog.tsx │ │ │ │ │ ├── GroupView │ │ │ │ │ │ ├── EmptyLayer.tsx │ │ │ │ │ │ ├── GroupAutoFillPopover.tsx │ │ │ │ │ │ ├── GroupButtonAreaPopover.tsx │ │ │ │ │ │ ├── GroupView.tsx │ │ │ │ │ │ ├── Layer.tsx │ │ │ │ │ │ ├── PartSubmenu.tsx │ │ │ │ │ │ ├── PartView.tsx │ │ │ │ │ │ ├── PlayHead.tsx │ │ │ │ │ │ ├── TimelineObject.tsx │ │ │ │ │ │ └── part │ │ │ │ │ │ │ ├── CountdownHeads │ │ │ │ │ │ │ ├── CountdownHead.tsx │ │ │ │ │ │ │ └── CountdownHeads.tsx │ │ │ │ │ │ │ ├── CurrentTime │ │ │ │ │ │ │ └── CurrentTime.tsx │ │ │ │ │ │ │ ├── LayerName │ │ │ │ │ │ │ ├── LayerName.tsx │ │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ │ ├── RemainingTime │ │ │ │ │ │ │ └── RemainingTime.tsx │ │ │ │ │ │ │ └── TriggersSubmenu │ │ │ │ │ │ │ └── TriggersSubmenu.tsx │ │ │ │ │ ├── PartPropertiesDialog.tsx │ │ │ │ │ ├── RundownView.tsx │ │ │ │ │ └── ScrollWatcher │ │ │ │ │ │ ├── ScrollWatcher.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ ├── sidebar │ │ │ │ │ ├── DataRow │ │ │ │ │ │ ├── DataRow.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ │ ├── SideBarEditTimelineObject.tsx │ │ │ │ │ ├── Sidebar.tsx │ │ │ │ │ ├── SidebarContent.tsx │ │ │ │ │ ├── SidebarResourceLibrary.tsx │ │ │ │ │ ├── editGroup │ │ │ │ │ │ └── SideBarEditGroup.tsx │ │ │ │ │ ├── editPart │ │ │ │ │ │ └── SideBarEditPart.tsx │ │ │ │ │ ├── resource │ │ │ │ │ │ ├── ResourceData.tsx │ │ │ │ │ │ ├── ResourceLibraryItem.tsx │ │ │ │ │ │ └── ResourceLibraryItemThumbnail.tsx │ │ │ │ │ └── timelineObj │ │ │ │ │ │ ├── GDD │ │ │ │ │ │ ├── GDDTypes │ │ │ │ │ │ │ ├── select.tsx │ │ │ │ │ │ │ ├── string-color-rrggbb.tsx │ │ │ │ │ │ │ ├── string-multi-line.tsx │ │ │ │ │ │ │ └── string-single-line.tsx │ │ │ │ │ │ ├── basicProperties.tsx │ │ │ │ │ │ ├── componentAny.tsx │ │ │ │ │ │ ├── gddEdit.tsx │ │ │ │ │ │ ├── lib.tsx │ │ │ │ │ │ └── style.scss │ │ │ │ │ │ ├── editTimelineObj.tsx │ │ │ │ │ │ └── timelineObjs │ │ │ │ │ │ ├── abstract.tsx │ │ │ │ │ │ ├── atem.tsx │ │ │ │ │ │ ├── casparcg.scss │ │ │ │ │ │ ├── casparcg.tsx │ │ │ │ │ │ ├── empty.tsx │ │ │ │ │ │ ├── httpSend.tsx │ │ │ │ │ │ ├── hyperdeck.tsx │ │ │ │ │ │ ├── lawo.tsx │ │ │ │ │ │ ├── lib.tsx │ │ │ │ │ │ ├── obs.tsx │ │ │ │ │ │ ├── osc.tsx │ │ │ │ │ │ ├── panasonic.tsx │ │ │ │ │ │ ├── pharos.tsx │ │ │ │ │ │ ├── quantel.tsx │ │ │ │ │ │ ├── shotoku.tsx │ │ │ │ │ │ ├── singularLive.tsx │ │ │ │ │ │ ├── sisyfos.tsx │ │ │ │ │ │ ├── sofieChef.tsx │ │ │ │ │ │ ├── tcpSend.tsx │ │ │ │ │ │ ├── telemetrics.tsx │ │ │ │ │ │ ├── unknown.tsx │ │ │ │ │ │ ├── vMix.tsx │ │ │ │ │ │ └── vizMSE.tsx │ │ │ │ └── util │ │ │ │ │ ├── AntiWiggle │ │ │ │ │ ├── AntiWiggle.tsx │ │ │ │ │ └── style.scss │ │ │ │ │ ├── ConfirmationDialog.tsx │ │ │ │ │ ├── Debug.tsx │ │ │ │ │ ├── DropZone.tsx │ │ │ │ │ ├── ErrorBoundary.tsx │ │ │ │ │ ├── SmallCheckbox.tsx │ │ │ │ │ └── Spinner.tsx │ │ │ ├── constants.ts │ │ │ ├── contexts │ │ │ │ ├── ErrorHandler.ts │ │ │ │ ├── Hotkey.ts │ │ │ │ ├── IPCServer.ts │ │ │ │ ├── Logger.ts │ │ │ │ └── Project.ts │ │ │ ├── lib │ │ │ │ ├── clientUtil.ts │ │ │ │ ├── errorHandling.ts │ │ │ │ ├── multipleEdit.ts │ │ │ │ ├── useDebounce.tsx │ │ │ │ └── useFrame.tsx │ │ │ ├── mobx │ │ │ │ ├── AnalogStore.ts │ │ │ │ ├── AppStore.ts │ │ │ │ ├── GDDValidatorStoreStore.ts │ │ │ │ ├── GroupPlayDataStore.ts │ │ │ │ ├── GuiStore.ts │ │ │ │ ├── ProjectStore.ts │ │ │ │ ├── ResourcesStore.ts │ │ │ │ ├── RundownsStore.ts │ │ │ │ ├── TriggersStore.ts │ │ │ │ ├── lib.ts │ │ │ │ └── store.ts │ │ │ └── styles │ │ │ │ ├── app.scss │ │ │ │ ├── base │ │ │ │ └── _reset.scss │ │ │ │ ├── btn.scss │ │ │ │ ├── connection-status.scss │ │ │ │ ├── countDownHead.scss │ │ │ │ ├── device-list.scss │ │ │ │ ├── drop-zone.scss │ │ │ │ ├── foundation │ │ │ │ ├── _all.scss │ │ │ │ ├── _colors.scss │ │ │ │ └── _variables.scss │ │ │ │ ├── global.scss │ │ │ │ ├── group-list.scss │ │ │ │ ├── group.scss │ │ │ │ ├── layer.scss │ │ │ │ ├── objectTypeStyling.scss │ │ │ │ ├── part.scss │ │ │ │ ├── peripheral-settings.scss │ │ │ │ ├── playHead.scss │ │ │ │ ├── resourceLibrary.scss │ │ │ │ ├── settings.scss │ │ │ │ ├── sidebar │ │ │ │ ├── resource-library.scss │ │ │ │ └── sidebar.scss │ │ │ │ ├── snackbar.scss │ │ │ │ ├── table.scss │ │ │ │ ├── timeline-obj.scss │ │ │ │ ├── trigger.scss │ │ │ │ └── variables.scss │ │ └── renderer.tsx │ ├── tools │ │ └── notarize.js │ ├── tsconfig.electron.json │ ├── tsconfig.json │ ├── webpack.config.js │ └── webpack.react.js └── tsr-bridge │ ├── .eslintrc.js │ ├── README.md │ ├── assets │ ├── tray.png │ ├── trayTemplate.png │ └── trayTemplate@2x.png │ ├── entitlements.mac.plist │ ├── index.html │ ├── nodemon.json │ ├── package.json │ ├── src │ ├── electron │ │ ├── IPCClient.ts │ │ ├── IPCServer.ts │ │ ├── lib │ │ │ ├── baseFolder.ts │ │ │ └── lib.ts │ │ ├── server.ts │ │ └── storageHandler.ts │ ├── index.html │ ├── ipc │ │ └── IPCAPI.ts │ ├── logging │ │ ├── index.ts │ │ ├── ipc-transport.ts │ │ ├── logging.ts │ │ └── util-formatter.ts │ ├── main.ts │ ├── models │ │ └── AppData.ts │ ├── react │ │ ├── App.tsx │ │ ├── api │ │ │ ├── IPCClient.ts │ │ │ └── IPCServer.ts │ │ ├── components │ │ │ ├── Settings.tsx │ │ │ └── log │ │ │ │ ├── Log.tsx │ │ │ │ └── LogEntry.tsx │ │ ├── contexts │ │ │ └── IPCServer.ts │ │ └── styles │ │ │ ├── app.scss │ │ │ ├── globals.scss │ │ │ ├── log.scss │ │ │ ├── settings.scss │ │ │ └── toggle.scss │ ├── renderer.tsx │ └── types │ │ └── react-scroll-to-bottom.d.ts │ ├── tools │ └── notarize.js │ ├── tsconfig.json │ ├── webpack.config.js │ └── webpack.react.js ├── doc ├── FOR_DEVELOPERS.md └── img │ ├── copy-from-caspar-client.gif │ ├── edit-timeline.gif │ ├── gdd-input.png │ ├── intro0.gif │ ├── play-mode-multi.gif │ ├── play-mode-single.gif │ ├── play.gif │ ├── resource-pane.gif │ ├── screenshot0.png │ ├── select-drag-multiple-parts.gif │ ├── select-timeline-objects.gif │ ├── streamdeck-GUI.gif │ └── streamdeck.gif ├── jest.config.base.js ├── lerna.json ├── package.json ├── scripts ├── license-check.js └── version.js ├── shared └── packages │ ├── api │ ├── README.md │ ├── package.json │ ├── src │ │ ├── bridgeAPI.ts │ │ ├── index.ts │ │ ├── logging.ts │ │ └── peripherals.ts │ └── tsconfig.json │ ├── lib │ ├── README.md │ ├── package.json │ ├── src │ │ ├── Resources.ts │ │ ├── TimelineTracker.ts │ │ ├── bytesToSize.ts │ │ ├── color.ts │ │ ├── index.ts │ │ ├── lib.ts │ │ └── peripheral.ts │ └── tsconfig.json │ ├── models │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── resource │ │ │ ├── Atem.ts │ │ │ ├── CasparCG.ts │ │ │ ├── HTTPSend.ts │ │ │ ├── Hyperdeck.ts │ │ │ ├── OBS.ts │ │ │ ├── OSC.ts │ │ │ ├── TCPSend.ts │ │ │ ├── VMix.ts │ │ │ ├── index.ts │ │ │ └── resource.ts │ └── tsconfig.json │ ├── peripherals │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── peripheralWatcher.ts │ │ ├── peripherals │ │ │ ├── lib │ │ │ │ └── estimateTextSize.ts │ │ │ ├── midi.ts │ │ │ ├── peripheral.ts │ │ │ ├── streamdeck.ts │ │ │ └── xkeys.ts │ │ └── peripheralsHandler.ts │ └── tsconfig.json │ ├── server-lib │ ├── README.md │ ├── package.json │ ├── src │ │ ├── WebsocketServer.ts │ │ └── index.ts │ └── tsconfig.json │ └── tsr-bridge │ ├── package.json │ ├── src │ ├── TSR.ts │ ├── index.ts │ └── sideload │ │ ├── Atem.ts │ │ ├── CasparCG.ts │ │ ├── CasparCGTemplates.ts │ │ ├── HTTPSend.ts │ │ ├── Hyperdeck.ts │ │ ├── OBS.ts │ │ ├── OSC.ts │ │ ├── TCPSend.ts │ │ ├── VMix.ts │ │ └── sideload.ts │ └── tsconfig.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = tab 3 | end_of_line = lf 4 | insert_final_newline = true 5 | 6 | [*.{css,scss,js,jsx,ts,tsx,json}] 7 | indent_size = 4 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/.github/ISSUE_TEMPLATE.md -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: ['bug'] 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | Tip: Just click here and Paste your screenshot! 26 | 27 | **Desktop (please complete the following information):** 28 | - OS: [e.g. iOS] 29 | - Version [e.g. 1.2.0] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'Feature request' 5 | labels: ['enhancement'] 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about this project 4 | title: 'Question:' 5 | labels: ['question'] 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe your question** 11 | 12 | 13 | 14 | **Additional context** 15 | Please provide any context relevant to your question. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | 4 | # env vars 5 | /.env 6 | 7 | # misc 8 | .DS_Store 9 | .env.local 10 | .env.development.local 11 | .env.test.local 12 | .env.production.local 13 | 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | lerna-debug.log* 18 | .vscode/ 19 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lerna run --concurrency 1 --stream precommit --since HEAD --exclude-dependents 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | SuperConductor, a playout client for Windows, Linux or MacOS to control CasparCG, Atem, OBS and more! 2 | Copyright (C) 2022 SuperFlyTV AB 3 | 4 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 5 | 6 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 7 | 8 | You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -------------------------------------------------------------------------------- /apps/app/README.md: -------------------------------------------------------------------------------- 1 | # SuperConductor App 2 | 3 | To run the project in development mode (both React and Electron), run `yarn dev`. 4 | -------------------------------------------------------------------------------- /apps/app/build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/build/icon.icns -------------------------------------------------------------------------------- /apps/app/build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/build/icon.ico -------------------------------------------------------------------------------- /apps/app/build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/build/icon.png -------------------------------------------------------------------------------- /apps/app/build/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/build/icons/1024x1024.png -------------------------------------------------------------------------------- /apps/app/build/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/build/icons/128x128.png -------------------------------------------------------------------------------- /apps/app/build/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/build/icons/16x16.png -------------------------------------------------------------------------------- /apps/app/build/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/build/icons/24x24.png -------------------------------------------------------------------------------- /apps/app/build/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/build/icons/256x256.png -------------------------------------------------------------------------------- /apps/app/build/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/build/icons/32x32.png -------------------------------------------------------------------------------- /apps/app/build/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/build/icons/48x48.png -------------------------------------------------------------------------------- /apps/app/build/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/build/icons/512x512.png -------------------------------------------------------------------------------- /apps/app/build/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/build/icons/64x64.png -------------------------------------------------------------------------------- /apps/app/build/icons/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/build/icons/96x96.png -------------------------------------------------------------------------------- /apps/app/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/app/jest.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line node/no-unpublished-require 2 | const base = require('../../jest.config.base') 3 | const packageJson = require('./package') 4 | 5 | module.exports = { 6 | ...base, 7 | displayName: packageJson.name, 8 | } 9 | -------------------------------------------------------------------------------- /apps/app/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src/**/*"], 3 | "ignore": ["*.test.ts", "src/react/*", "README"], 4 | "exec": "tsc -p tsconfig.electron.json && electron ./dist/main.js", 5 | "ext": "ts" 6 | } 7 | -------------------------------------------------------------------------------- /apps/app/src/electron/timeline.ts: -------------------------------------------------------------------------------- 1 | import { prepareGroupPlayData } from '../lib/playout/preparedGroupPlayData' 2 | import { Group } from '../models/rundown/Group' 3 | import { GroupPreparedPlayData } from '../models/GUI/PreparedPlayhead' 4 | import { TSRTimeline } from 'timeline-state-resolver-types' 5 | import { StorageHandler } from './storageHandler' 6 | import { BridgeHandler } from './bridgeHandler' 7 | import { getTimelineForGroup } from '../lib/timeline' 8 | 9 | const queuedUpdateTimelines = new Map() 10 | 11 | export function updateTimeline( 12 | storage: StorageHandler, 13 | bridgeHandler: BridgeHandler, 14 | group: Group 15 | ): GroupPreparedPlayData | null { 16 | const prepared = prepareGroupPlayData(group) 17 | 18 | // Defer update, to allow for multiple updates to be batched together: 19 | const existingTimeout = queuedUpdateTimelines.get(group.id) 20 | if (existingTimeout) clearTimeout(existingTimeout) 21 | 22 | queuedUpdateTimelines.set( 23 | group.id, 24 | setTimeout(() => { 25 | const queued = queuedUpdateTimelines.get(group.id) 26 | if (!queued) return 27 | queuedUpdateTimelines.delete(group.id) 28 | 29 | const timeline = getTimelineForGroup(group, prepared, undefined) as TSRTimeline 30 | bridgeHandler.updateTimeline(group.id, timeline) 31 | 32 | const project = storage.getProject() 33 | bridgeHandler.updateMappings(project.mappings) 34 | }, 1) 35 | ) 36 | 37 | return prepared || null 38 | } 39 | -------------------------------------------------------------------------------- /apps/app/src/img/atem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/src/img/atem.png -------------------------------------------------------------------------------- /apps/app/src/img/casparcg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/src/img/casparcg.png -------------------------------------------------------------------------------- /apps/app/src/img/fix.d.ts: -------------------------------------------------------------------------------- 1 | // This is here just to fix png importing with Webpack 2 | 3 | declare module '*.png' { 4 | const value: any 5 | export default value 6 | } 7 | -------------------------------------------------------------------------------- /apps/app/src/img/hyperdeck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/src/img/hyperdeck.png -------------------------------------------------------------------------------- /apps/app/src/img/midi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/src/img/midi.png -------------------------------------------------------------------------------- /apps/app/src/img/obs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/src/img/obs.png -------------------------------------------------------------------------------- /apps/app/src/img/streamdeck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/src/img/streamdeck.png -------------------------------------------------------------------------------- /apps/app/src/img/vmix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/src/img/vmix.png -------------------------------------------------------------------------------- /apps/app/src/img/xkeys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/src/img/xkeys.png -------------------------------------------------------------------------------- /apps/app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SuperConductor 8 | 9 | 10 | 11 |
12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/app/src/lib/GUI.ts: -------------------------------------------------------------------------------- 1 | export type CurrentSelectionAny = CurrentSelectionGroup | CurrentSelectionPart | CurrentSelectionTimelineObj 2 | export interface CurrentSelectionBase { 3 | type: 'group' | 'part' | 'timelineObj' 4 | } 5 | export interface CurrentSelectionGroup extends CurrentSelectionBase { 6 | type: 'group' 7 | groupId: string 8 | } 9 | export interface CurrentSelectionPart extends CurrentSelectionBase { 10 | type: 'part' 11 | groupId: string 12 | partId: string 13 | } 14 | export interface CurrentSelectionTimelineObj extends CurrentSelectionBase { 15 | type: 'timelineObj' 16 | groupId: string 17 | partId: string 18 | timelineObjId: string 19 | } 20 | -------------------------------------------------------------------------------- /apps/app/src/lib/TSR.ts: -------------------------------------------------------------------------------- 1 | export const ATEM_DEFAULT_TRANSITION_RATE = 25 2 | 3 | export function getAtemFrameRate(): number { 4 | // This is just a placeholder for now, assuming a frame rate. 5 | // Ideally, we'd be quering the atem device for the frame rate and cache it. 6 | 7 | return 25 8 | } 9 | -------------------------------------------------------------------------------- /apps/app/src/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') 8 | } 9 | return path.join(homeDirPath, 'Documents', 'SuperConductor') 10 | } 11 | -------------------------------------------------------------------------------- /apps/app/src/lib/getDefaults.ts: -------------------------------------------------------------------------------- 1 | import { Mappings } from 'timeline-state-resolver-types' 2 | 3 | export function getDefaultMappingLayer(mappings?: Mappings): string | undefined { 4 | if (mappings) { 5 | // Check length 6 | const keys = Object.keys(mappings) 7 | if (keys.length <= 0) { 8 | return undefined 9 | } else { 10 | return keys[0] 11 | } 12 | } else { 13 | return undefined 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/app/src/lib/logging/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logging' 2 | -------------------------------------------------------------------------------- /apps/app/src/lib/logging/logging.ts: -------------------------------------------------------------------------------- 1 | import { format, createLogger, transports, Logger } from 'winston' 2 | import { utilFormatter } from './util-formatter' 3 | import DailyRotateFile from 'winston-daily-rotate-file' 4 | import { LogLevel } from '@shared/api' 5 | 6 | const myFormat = format.printf(({ level, message, label, timestamp }) => { 7 | return `${timestamp} [${label}] ${level}: ${message}` 8 | }) 9 | 10 | export const createLoggers = (dirname: string): { electronLogger: Logger; rendererLogger: Logger } => { 11 | const myTransports = [ 12 | new transports.Console(), 13 | new DailyRotateFile({ 14 | dirname, 15 | filename: 'SuperConductor-%DATE%.log', 16 | maxSize: '20m', 17 | maxFiles: '30d', 18 | createSymlink: true, 19 | }), 20 | ] 21 | 22 | const electronLogger = createLogger({ 23 | level: LogLevel.Silly, 24 | format: format.combine( 25 | format.label({ label: 'electron' }), 26 | format.timestamp(), 27 | utilFormatter(), 28 | format.simple(), 29 | myFormat 30 | ), 31 | transports: myTransports, 32 | }) 33 | 34 | const rendererLogger = createLogger({ 35 | level: LogLevel.Silly, 36 | format: format.combine( 37 | format.label({ label: 'renderer' }), 38 | format.timestamp(), 39 | utilFormatter(), 40 | format.simple(), 41 | myFormat 42 | ), 43 | transports: myTransports, 44 | }) 45 | 46 | return { electronLogger, rendererLogger } 47 | } 48 | -------------------------------------------------------------------------------- /apps/app/src/lib/logging/util-formatter.ts: -------------------------------------------------------------------------------- 1 | // Adapted from https://stackoverflow.com/a/56842780 2 | import util from 'util' 3 | import { SPLAT } from 'triple-beam' 4 | import { deepClone, stringifyError } from '@shared/lib' 5 | 6 | export function utilFormatter(): { transform(info: any): any } { 7 | return { 8 | transform: (info: any) => { 9 | const args = info[SPLAT] 10 | if (args) { 11 | info.message = util.format(info.message, ...args) 12 | 13 | return info 14 | } else { 15 | // Handle special case, when the single argument is an error: 16 | if (info instanceof Error) { 17 | const formattedInfo: any = deepClone(info) 18 | formattedInfo.message = (formattedInfo.message ?? '') + stringifyError(info) 19 | 20 | return formattedInfo 21 | } else { 22 | return info 23 | } 24 | } 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/app/src/lib/userAgreement.ts: -------------------------------------------------------------------------------- 1 | export const USER_AGREEMENT_VERSION = '1' 2 | -------------------------------------------------------------------------------- /apps/app/src/models/App/AppData.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationTrigger } from '../rundown/Trigger' 2 | 3 | export interface AppData { 4 | windowPosition: WindowPosition 5 | version: { 6 | /** The version of the SuperConductor that the user has seen */ 7 | seenVersion: string | null 8 | /** The version of the SuperConductor who saved the data*/ 9 | currentVersion: string 10 | /** Wether the current version is a pre-release or not */ 11 | currentVersionIsPrerelease: boolean 12 | } 13 | /** Which version of the user agreement the user has agreed to */ 14 | userAgreement?: string 15 | 16 | /** 17 | * If the application should update to the latest pre-release. 18 | * When undefined, it's treated as true if the current version is a pre-release, false otherwise 19 | */ 20 | preReleaseAutoUpdate?: boolean 21 | 22 | /** How many decimals to use in the GUI, defaults to 0 */ 23 | guiDecimalCount?: number 24 | 25 | project: { 26 | id: string 27 | } 28 | rundowns: { 29 | [fileName: string]: { 30 | name: string 31 | open: boolean 32 | } 33 | } 34 | triggers: { 35 | [Key in ApplicationTrigger['action']]?: ApplicationTrigger[] 36 | } 37 | } 38 | export type WindowPosition = 39 | | { 40 | y: number 41 | x: number 42 | width: number 43 | height: number 44 | maximized: boolean 45 | } 46 | | { 47 | // Note: undefined will center the window 48 | y: undefined 49 | x: undefined 50 | width: number 51 | height: number 52 | maximized: boolean 53 | } 54 | -------------------------------------------------------------------------------- /apps/app/src/models/project/AnalogInput.ts: -------------------------------------------------------------------------------- 1 | import { ActiveAnalog } from '../rundown/Analog' 2 | 3 | export interface AnalogInputs { 4 | analogs: { 5 | [fullIdentifier: string]: AnalogInput 6 | } 7 | } 8 | 9 | export interface AnalogInput { 10 | /** A reference to Project.analogInputSettings */ 11 | datastoreKey: string 12 | 13 | /** Calculated value */ 14 | value: number 15 | /** Timestamp when was last modified */ 16 | modified: number 17 | 18 | activeAnalog: ActiveAnalog 19 | } 20 | -------------------------------------------------------------------------------- /apps/app/src/models/project/Bridge.ts: -------------------------------------------------------------------------------- 1 | import { KnownPeripheral, PeripheralSettingsAny } from '@shared/api' 2 | import { DeviceOptionsAny } from 'timeline-state-resolver-types' 3 | import { PeripheralArea } from './Peripheral' 4 | 5 | export interface Bridge { 6 | id: string 7 | name: string 8 | 9 | outgoing: boolean 10 | 11 | url: string 12 | 13 | settings: { 14 | devices: { 15 | [deviceId: string]: DeviceOptionsAny 16 | } 17 | peripherals: { 18 | [peripheralId: string]: PeripheralSettingsAny 19 | } 20 | autoConnectToAllPeripherals: boolean 21 | } 22 | 23 | clientSidePeripheralSettings: { 24 | [peripheralId: string]: BridgePeripheralSettings 25 | } 26 | } 27 | 28 | export interface BridgeStatus { 29 | connected: boolean 30 | 31 | devices: { 32 | [deviceId: string]: BridgeDevice 33 | } 34 | 35 | peripherals: { 36 | [peripheralId: string]: BridgePeripheral 37 | } 38 | } 39 | 40 | export interface BridgeDevice { 41 | connectionId: number 42 | ok: boolean 43 | message: string 44 | } 45 | 46 | export type BridgePeripheral = KnownPeripheral 47 | 48 | export interface BridgePeripheralSettings { 49 | // overrideName?: string 50 | 51 | areas: { 52 | [areaId: string]: PeripheralArea 53 | } 54 | } 55 | 56 | export const INTERNAL_BRIDGE_ID = '__INTERNAL__' 57 | -------------------------------------------------------------------------------- /apps/app/src/models/project/Peripheral.ts: -------------------------------------------------------------------------------- 1 | import { PeripheralInfo } from '@shared/api' 2 | import { RundownTrigger } from '../rundown/Trigger' 3 | 4 | export interface PeripheralStatus { 5 | id: string 6 | bridgeId: string 7 | 8 | info: PeripheralInfo 9 | 10 | status: { 11 | connected: boolean 12 | /** Timestamp */ 13 | lastConnected: number 14 | } 15 | } 16 | 17 | export interface PeripheralArea { 18 | name: string 19 | identifiers: string[] 20 | assignedToGroupId: string | undefined 21 | action: RundownTrigger['action'] 22 | } 23 | -------------------------------------------------------------------------------- /apps/app/src/models/project/Project.ts: -------------------------------------------------------------------------------- 1 | import { Mappings } from 'timeline-state-resolver-types' 2 | import { Bridge } from './Bridge' 3 | 4 | export interface Project { 5 | id: string 6 | name: string 7 | 8 | mappings: Mappings 9 | bridges: { 10 | [bridgeId: string]: Bridge 11 | } 12 | analogInputSettings: AnalogInputSettings 13 | 14 | deviceNames: { [deviceId: string]: string } 15 | 16 | settings: Settings 17 | 18 | autoRefreshInterval?: number 19 | } 20 | 21 | export interface Settings { 22 | enableInternalBridge: boolean 23 | } 24 | 25 | export interface AnalogInputSettings { 26 | [datastoreKey: string]: AnalogInputSetting 27 | } 28 | export interface AnalogInputSetting { 29 | label: string 30 | 31 | /** Reference to an entry in the AnalogStore */ 32 | fullIdentifier: string | null 33 | 34 | /** Whether to update the analog value using the absolute or the relative analog value. */ 35 | updateUsingAbsolute?: boolean 36 | scaleFactor?: number 37 | relativeMinCap?: number 38 | relativeMaxCap?: number 39 | absoluteOffset?: number 40 | } 41 | -------------------------------------------------------------------------------- /apps/app/src/models/rundown/Analog.ts: -------------------------------------------------------------------------------- 1 | import { AnalogValue } from '@shared/api' 2 | /* 3 | An Analog defines an external analog value 4 | So it could be a jog-wheel, a T-bar etc 5 | */ 6 | 7 | export interface ActiveAnalog { 8 | fullIdentifier: string 9 | bridgeId: string 10 | deviceId: string 11 | deviceName: string 12 | identifier: string 13 | value: AnalogValue 14 | } 15 | -------------------------------------------------------------------------------- /apps/app/src/models/rundown/Part.ts: -------------------------------------------------------------------------------- 1 | import { TimelineObj } from './TimelineObj' 2 | import { RundownTrigger } from './Trigger' 3 | 4 | export interface PartBase { 5 | id: string 6 | name: string 7 | 8 | /** Disables the ability to play out the Part. */ 9 | disabled?: boolean 10 | loop?: boolean 11 | /** Disables the ability to edit the Part in GUI. Does not affect ability to play out. */ 12 | locked?: boolean 13 | 14 | triggers: RundownTrigger[] 15 | 16 | duration?: number 17 | 18 | resolved: { 19 | /** Duration of the part, derived by resolving the timeline in the Part */ 20 | duration: number | null // null means infinite 21 | /** Label of the part (derived from the name/timeline of the Part) */ 22 | label: string 23 | } 24 | /** If this part was created from the AutoFill */ 25 | autoFilled?: boolean 26 | } 27 | export interface Part extends PartBase { 28 | timeline: TimelineObj[] 29 | } 30 | export interface PartGUI extends PartBase { 31 | timelineIds: string[] 32 | } 33 | export function isPart(part: PartBase): part is Part { 34 | return !!(part as any as Part).timeline 35 | } 36 | export function isPartGUI(part: PartBase): part is PartGUI { 37 | return !!(part as any as PartGUI).timelineIds 38 | } 39 | -------------------------------------------------------------------------------- /apps/app/src/models/rundown/Rundown.ts: -------------------------------------------------------------------------------- 1 | import { Group } from './Group' 2 | 3 | export interface RundownBase { 4 | id: string 5 | name: string 6 | } 7 | export interface Rundown extends RundownBase { 8 | groups: Group[] 9 | } 10 | export interface RundownGUI extends RundownBase { 11 | groupIds: string[] 12 | } 13 | export function isRundown(rundown: RundownBase): rundown is Rundown { 14 | return !!(rundown as any as Rundown).groups 15 | } 16 | export function isRundownGUI(rundown: RundownBase): rundown is RundownGUI { 17 | return !!(rundown as any as RundownGUI).groupIds 18 | } 19 | -------------------------------------------------------------------------------- /apps/app/src/models/rundown/TimelineObj.ts: -------------------------------------------------------------------------------- 1 | import { TSRTimelineObj } from 'timeline-state-resolver-types' 2 | 3 | export interface TimelineObj { 4 | resourceId?: string 5 | 6 | obj: TSRTimelineObj 7 | 8 | resolved: { 9 | instances: TimelineObjResolvedInstance[] 10 | } 11 | } 12 | 13 | export interface TimelineObjResolvedInstance { 14 | /** The resolved startTime of the object. 0 is the beginning of the Part */ 15 | start: number 16 | /** The resolved sendTime of the object. 0 is the beginning of the Part, null = Infinity */ 17 | end: number | null 18 | } 19 | 20 | /** 21 | * Default duration of timeline-objects. If the duration is infinite, 22 | * this duration is used instead in GUI when starting a drag operation. 23 | */ 24 | export const DEFAULT_DURATION = 10 * 1000 25 | -------------------------------------------------------------------------------- /apps/app/src/react/api/DragItemTypes.ts: -------------------------------------------------------------------------------- 1 | import { ResourceAny } from '@shared/models' 2 | import { MoveTarget } from '../../lib/util' 3 | import { GroupGUI } from '../../models/rundown/Group' 4 | 5 | export enum DragItemTypes { 6 | RESOURCE_ITEM = 'resource_item', 7 | PART_ITEM = 'part_item', 8 | GROUP_ITEM = 'group_item', 9 | } 10 | 11 | export interface ResourceDragItem { 12 | type: DragItemTypes.RESOURCE_ITEM 13 | resources: ResourceAny[] 14 | } 15 | 16 | export interface PartDragItem { 17 | type: DragItemTypes.PART_ITEM 18 | 19 | parts: { 20 | partId: string 21 | fromGroup: GroupGUI 22 | }[] 23 | 24 | /** null = make a new transparent group */ 25 | toGroupId: string | null 26 | target: MoveTarget | null 27 | } 28 | 29 | export interface GroupDragItem { 30 | type: DragItemTypes.GROUP_ITEM 31 | 32 | groupIds: string[] 33 | /** The position in the Rundown at which to place the dragged Group */ 34 | target: MoveTarget | null 35 | } 36 | 37 | export type AnyDragItem = ResourceDragItem | PartDragItem | GroupDragItem 38 | 39 | export function isDragItem(item: unknown): item is AnyDragItem { 40 | return typeof item === 'object' && item !== null && 'type' in item 41 | } 42 | 43 | export function isResourceDragItem(item: unknown): item is ResourceDragItem { 44 | return isDragItem(item) && item.type === DragItemTypes.RESOURCE_ITEM 45 | } 46 | 47 | export function isPartDragItem(item: unknown): item is PartDragItem { 48 | return isDragItem(item) && item.type === DragItemTypes.PART_ITEM 49 | } 50 | 51 | export function isGroupDragItem(item: unknown): item is GroupDragItem { 52 | return isDragItem(item) && item.type === DragItemTypes.GROUP_ITEM 53 | } 54 | -------------------------------------------------------------------------------- /apps/app/src/react/api/clipboard/__tests__/transformURL.test.ts: -------------------------------------------------------------------------------- 1 | import { transformURL } from '../transformURL' 2 | 3 | describe('transformURL', () => { 4 | test('Youtube video', () => { 5 | { 6 | const u = transformURL(new URL('https://www.youtube.com/watch?v=abcdefg')) 7 | expect(u.href).toBe( 8 | 'https://www.youtube.com/embed/abcdefg?autoplay=true&loop=1&showinfo=0&controls=0&modestbranding=1' 9 | ) 10 | } 11 | { 12 | const u = transformURL(new URL('https://www.youtube.com/watch?v=asdf&t=10m14s')) 13 | expect(u.href).toBe( 14 | 'https://www.youtube.com/embed/asdf?start=614&autoplay=true&loop=1&showinfo=0&controls=0&modestbranding=1' 15 | ) 16 | } 17 | }) 18 | test('Any other url', () => { 19 | // Any other url should not be transformed: 20 | const otherURL = 'https://superfly.tv/qwerty/asdf?a=1&b=2&c=3' 21 | const u = transformURL(new URL(otherURL)) 22 | expect(u.href).toBe(otherURL) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /apps/app/src/react/api/clipboard/transformURL.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Do various convenient URL transforms 3 | */ 4 | export function transformURL(url: URL): URL { 5 | // Youtube video 6 | // "https://www.youtube.com/watch?v=VIDEO_ID" 7 | if (url.host.match(/youtube/) && url.pathname.match(/watch/) && url.searchParams.has('v')) { 8 | // Youtube videos are played the best by using the embed url: 9 | // https://www.youtube.com/embed/VIDEO_ID?autoplay=true 10 | 11 | const newURL = new URL(url) 12 | newURL.pathname = `/embed/${newURL.searchParams.get('v')}` 13 | newURL.searchParams.delete('v') 14 | 15 | // Handle time offset: 16 | if (newURL.searchParams.has('t')) { 17 | // on the form "XXmYYs" or "YYs" 18 | let seconds = 0 19 | 20 | if (!seconds) { 21 | const m = newURL.searchParams.get('t')?.match(/((\d+)m)?((\d+)s)/) 22 | if (m) { 23 | if (m[2]) seconds += parseInt(m[2]) * 60 24 | seconds += parseInt(m[4]) 25 | } 26 | } 27 | if (!seconds) { 28 | seconds = parseInt(newURL.searchParams.get('t') ?? '0') 29 | } 30 | 31 | if (seconds) { 32 | newURL.searchParams.set('start', seconds.toString()) 33 | } 34 | newURL.searchParams.delete('t') 35 | } 36 | 37 | // Add some good-to-have parameters: 38 | // ref: https://developers.google.com/youtube/player_parameters 39 | newURL.searchParams.set('autoplay', 'true') 40 | newURL.searchParams.set('loop', '1') 41 | newURL.searchParams.set('showinfo', '0') 42 | newURL.searchParams.set('controls', '0') 43 | newURL.searchParams.set('modestbranding', '1') 44 | 45 | return newURL 46 | } 47 | 48 | return url 49 | } 50 | -------------------------------------------------------------------------------- /apps/app/src/react/api/logger.ts: -------------------------------------------------------------------------------- 1 | import { LogLevel } from '@shared/api' 2 | import { stringifyError } from '@shared/lib' 3 | import { IPCServer } from './IPCServer' 4 | 5 | export class ClientSideLogger { 6 | constructor(private serverAPI: IPCServer) { 7 | this.error = this.error.bind(this) 8 | this.warn = this.warn.bind(this) 9 | this.info = this.info.bind(this) 10 | this.http = this.http.bind(this) 11 | this.verbose = this.verbose.bind(this) 12 | this.debug = this.debug.bind(this) 13 | this.silly = this.silly.bind(this) 14 | } 15 | 16 | private fixArgs(...args: any[]) { 17 | return args.map((arg) => stringifyError(arg)) 18 | } 19 | 20 | /* eslint-disable no-console */ 21 | error(...args: any[]): void { 22 | console.error(...args) 23 | this.serverAPI.log({ level: LogLevel.Error, params: this.fixArgs(...args) }).catch(console.error) 24 | } 25 | warn(...args: any[]): void { 26 | console.warn(...args) 27 | this.serverAPI.log({ level: LogLevel.Warn, params: this.fixArgs(...args) }).catch(console.error) 28 | } 29 | info(...args: any[]): void { 30 | console.info(...args) 31 | this.serverAPI.log({ level: LogLevel.Info, params: this.fixArgs(...args) }).catch(console.error) 32 | } 33 | http(...args: any[]): void { 34 | console.debug(...args) 35 | this.serverAPI.log({ level: LogLevel.HTTP, params: this.fixArgs(...args) }).catch(console.error) 36 | } 37 | verbose(...args: any[]): void { 38 | console.debug(...args) 39 | this.serverAPI.log({ level: LogLevel.Verbose, params: this.fixArgs(...args) }).catch(console.error) 40 | } 41 | debug(...args: any[]): void { 42 | console.debug(...args) 43 | this.serverAPI.log({ level: LogLevel.Debug, params: this.fixArgs(...args) }).catch(console.error) 44 | } 45 | silly(...args: any[]): void { 46 | console.debug(...args) 47 | this.serverAPI.log({ level: LogLevel.Silly, params: this.fixArgs(...args) }).catch(console.error) 48 | } 49 | /* eslint-enable no-console */ 50 | } 51 | -------------------------------------------------------------------------------- /apps/app/src/react/components/headerBar/deviceStatuses/ConnectionStatus.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler } from 'react' 2 | import classNames from 'classnames' 3 | 4 | export const ConnectionStatus: React.FC<{ 5 | ok?: boolean 6 | tooltip?: string 7 | label?: string 8 | children?: React.ReactNode 9 | onClick?: MouseEventHandler 10 | open?: boolean 11 | }> = (props) => { 12 | return ( 13 | 23 |
24 |
{props.label}
25 | 26 |
27 |
28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /apps/app/src/react/components/headerBar/deviceStatuses/DisabledPeripherals/DisabledPeripheralsSettings.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, Typography } from '@mui/material' 2 | import { KnownPeripheral } from '@shared/api' 3 | import React, { useCallback, useContext } from 'react' 4 | import { IPCServerContext } from '../../../../contexts/IPCServer' 5 | import { ProjectContext } from '../../../../contexts/Project' 6 | import { ErrorHandlerContext } from '../../../../contexts/ErrorHandler' 7 | 8 | import './style.scss' 9 | import { DeviceIcon } from '../../../pages/homePage/deviceIcon/DeviceIcon' 10 | 11 | export interface DisabledPeripheralInfo { 12 | bridgeId: string 13 | deviceId: string 14 | info: KnownPeripheral 15 | } 16 | 17 | export const DisabledPeripheralsSettings: React.FC<{ 18 | peripherals: DisabledPeripheralInfo[] 19 | onPeripheralClicked?: () => void 20 | }> = function DeviceStatuses({ peripherals, onPeripheralClicked }) { 21 | const ipcServer = useContext(IPCServerContext) 22 | const project = useContext(ProjectContext) 23 | const { handleError } = useContext(ErrorHandlerContext) 24 | 25 | const toggleManualConnect = useCallback( 26 | (peripheral?: DisabledPeripheralInfo) => { 27 | if (!peripheral) return 28 | const peripheralSettings = project.bridges[peripheral.bridgeId].settings.peripherals[peripheral.deviceId] 29 | peripheralSettings.manualConnect = !peripheralSettings.manualConnect 30 | ipcServer.updateProject({ id: project.id, project }).catch(handleError) 31 | }, 32 | [handleError, ipcServer, project] 33 | ) 34 | 35 | return ( 36 | 37 | Select a panel to connect to: 38 | {peripherals.map((peripheral) => ( 39 | { 42 | toggleManualConnect(peripheral) 43 | onPeripheralClicked && onPeripheralClicked() 44 | }} 45 | > 46 | 47 | {peripheral.info.name} 48 | 49 | ))} 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /apps/app/src/react/components/headerBar/deviceStatuses/DisabledPeripherals/style.scss: -------------------------------------------------------------------------------- 1 | .disabled-peripherals { 2 | min-width: 20rem; 3 | 4 | > * { 5 | padding: 1rem; 6 | } 7 | 8 | a { 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | color: white; 13 | cursor: pointer; 14 | 15 | &:hover { 16 | background: rgba(255, 255, 255, 0.05); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/app/src/react/components/headerBar/deviceStatuses/PeripheralSettings/TimelineDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { KeyDisplay, KeyDisplayTimeline } from '@shared/api' 2 | import { TimelineTracker } from '@shared/lib' 3 | import { useContext, useEffect, useRef, useState } from 'react' 4 | import { LoggerContext } from '../../../../contexts/Logger' 5 | 6 | export const TimelineDisplay: React.FC<{ 7 | keyDisplayTimeline: KeyDisplay | KeyDisplayTimeline 8 | render: (keyDisplay: KeyDisplay) => JSX.Element 9 | }> = ({ keyDisplayTimeline, render }) => { 10 | const [keyDisplay, setKeyDisplay] = useState(null) 11 | const tracker = useRef(undefined) 12 | const log = useContext(LoggerContext) 13 | 14 | useEffect(() => { 15 | if (Array.isArray(keyDisplayTimeline)) { 16 | // It is a timeline, which means that we should resolve it and track it. 17 | tracker.current = new TimelineTracker(log, keyDisplayTimeline, (keyDisplay: KeyDisplay) => { 18 | setKeyDisplay(keyDisplay) 19 | }) 20 | } else { 21 | setKeyDisplay(keyDisplayTimeline) 22 | } 23 | 24 | return () => { 25 | if (tracker.current) { 26 | tracker.current.stop() 27 | tracker.current = undefined 28 | } 29 | } 30 | }, [keyDisplayTimeline, log]) 31 | 32 | return keyDisplay ? render(keyDisplay) : null 33 | } 34 | -------------------------------------------------------------------------------- /apps/app/src/react/components/headerBar/deviceStatuses/PeripheralSettings/midi.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { observer } from 'mobx-react-lite' 3 | import { PeripheralStatus } from '../../../../../models/project/Peripheral' 4 | import { DefiningArea } from '../../../../../lib/triggers/keyDisplay/keyDisplay' 5 | import { PeripheralType } from '@shared/api' 6 | 7 | export const MIDISettings: React.FC<{ 8 | bridgeId: string 9 | deviceId: string 10 | peripheral: PeripheralStatus 11 | definingArea: DefiningArea | null 12 | }> = observer(function MIDISettings({ peripheral }) { 13 | if (peripheral.info.gui.type !== PeripheralType.MIDI) throw new Error('Wrong type, expected "midi"') 14 | 15 | return null 16 | }) 17 | -------------------------------------------------------------------------------- /apps/app/src/react/components/headerBar/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/foundation/all'; 2 | 3 | .header-bar { 4 | display: flex; 5 | align-items: stretch; 6 | height: 5rem; 7 | background: linear-gradient(180deg, #545972 0%, #414556 100%); 8 | 9 | .device-statuses { 10 | margin-left: auto; 11 | display: flex; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/app/src/react/components/headerBar/tabs/newTabBtn/NewTabBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { MdAdd } from 'react-icons/md' 3 | 4 | import './style.scss' 5 | 6 | export const NewTabBtn: React.FC<{ onClick: () => void }> = (props) => { 7 | return ( 8 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /apps/app/src/react/components/headerBar/tabs/newTabBtn/style.scss: -------------------------------------------------------------------------------- 1 | .new-tab-button { 2 | border-radius: 50%; 3 | width: 2.4rem; 4 | height: 2.4rem; 5 | background: transparent; 6 | position: relative; 7 | border: 0; 8 | cursor: pointer; 9 | margin-left: 0.4rem; 10 | 11 | svg { 12 | position: absolute; 13 | fill: white; 14 | top: 50%; 15 | left: 50%; 16 | transform: translate(-50%, -50%); 17 | width: 1.7rem; 18 | height: 1.7rem; 19 | } 20 | 21 | &:hover { 22 | background: rgba(255, 255, 255, 0.2); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/app/src/react/components/headerBar/tabs/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../styles/foundation/all'; 2 | 3 | .tabs { 4 | display: flex; 5 | align-items: flex-end; 6 | padding-top: 2rem; 7 | } 8 | -------------------------------------------------------------------------------- /apps/app/src/react/components/headerBar/tabs/tab/Tab.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | import React from 'react' 3 | import { MdClose } from 'react-icons/md' 4 | 5 | import './style.scss' 6 | 7 | export const Tab: React.FC<{ 8 | id: string 9 | name: string 10 | active?: boolean 11 | onMouseDown: () => void 12 | onDoubleClick?: () => void 13 | onClose?: (id: string) => void 14 | disableClose?: boolean 15 | icon?: React.ReactNode 16 | showSeparator?: boolean 17 | }> = (props) => { 18 | return ( 19 |
27 | {props.icon &&
{props.icon}
} 28 |
{props.name}
29 | {!props.disableClose && ( 30 |
31 | 42 |
43 | )} 44 | {props.showSeparator &&
} 45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /apps/app/src/react/components/headerBar/tabs/tab/style.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/foundation/all'; 2 | 3 | .tab { 4 | position: relative; 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | height: 2.5rem; 9 | border-radius: 0.5rem 0.5rem 0 0; 10 | padding: 0 2.4rem 0 1.6rem; 11 | min-width: 8rem; 12 | 13 | .icon { 14 | margin-right: 0.5rem; 15 | display: flex; 16 | align-items: center; 17 | 18 | svg { 19 | height: 1.4rem; 20 | } 21 | } 22 | 23 | .separator { 24 | width: 0.1rem; 25 | height: 1rem; 26 | background: #b7bdd4; 27 | position: absolute; 28 | right: 0; 29 | } 30 | 31 | .label { 32 | font-size: 1.2rem; 33 | } 34 | 35 | .close { 36 | position: absolute; 37 | right: 0.6rem; 38 | top: 0.1rem; 39 | display: none; 40 | opacity: 0.5; 41 | 42 | &:hover { 43 | opacity: 1; 44 | } 45 | 46 | button { 47 | padding: 0; 48 | background: transparent; 49 | border: 0; 50 | cursor: pointer; 51 | } 52 | 53 | svg { 54 | fill: white; 55 | } 56 | } 57 | 58 | &:hover .close { 59 | display: block; 60 | } 61 | 62 | &.active { 63 | background: #191b23; 64 | 65 | .separator { 66 | display: none; 67 | } 68 | 69 | .close { 70 | display: block; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/AddBtn.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@mui/material' 2 | import React from 'react' 3 | import { BsPlusLg } from 'react-icons/bs' 4 | 5 | export const AddBtn = (props: { 6 | className?: string 7 | disabled?: boolean 8 | title: string 9 | onClick: () => void 10 | }): JSX.Element => { 11 | return ( 12 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/AnalogInputPicker/style.scss: -------------------------------------------------------------------------------- 1 | .analog-input-picker-popover li.MuiMenuItem-root > svg { 2 | margin-right: 0.5em; 3 | margin-top: 0.1em; 4 | } 5 | .analog-input-picker { 6 | opacity: 0.6; 7 | 8 | > svg { 9 | /* Just outline the icon: */ 10 | fill: none; 11 | stroke: white; 12 | stroke-width: 0.5px; 13 | } 14 | 15 | &.linked { 16 | opacity: 1; 17 | > svg { 18 | fill: white; 19 | } 20 | } 21 | 22 | &__value { 23 | font-size: 80%; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/BooleanInput.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox, FormControlLabel, FormGroup, Tooltip } from '@mui/material' 2 | import React from 'react' 3 | 4 | export const BooleanInput: React.FC<{ 5 | currentValue: boolean | undefined 6 | onChange: (newValue: boolean) => void 7 | label: string | number | React.ReactElement> 8 | disabled?: boolean 9 | indeterminate?: boolean 10 | endAdornment?: React.ReactNode 11 | tooltip?: string 12 | }> = ({ currentValue, onChange, label, disabled, indeterminate, endAdornment, tooltip }) => { 13 | let elInput = ( 14 | { 16 | onChange(e.target.checked) 17 | }} 18 | checked={!!currentValue} 19 | disabled={disabled} 20 | indeterminate={indeterminate} 21 | /> 22 | ) 23 | 24 | if (tooltip) { 25 | const displayTooltip = tooltip ?? '' 26 | 27 | elInput = ( 28 | 29 | {elInput} 30 | 31 | ) 32 | } 33 | 34 | return ( 35 | 36 | 37 | {endAdornment} 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/Btn/Btn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | import './style.scss' 4 | 5 | // This is an element intended to be used as the Button exposed by mui/Button 6 | // The reason for this existing is that the Button renders quite slowly... 7 | export const Btn: React.FC<{ 8 | className?: string 9 | onClick?: (event: React.MouseEvent) => void 10 | title?: string 11 | selected?: boolean 12 | disabled?: boolean 13 | size?: string 14 | variant?: 'contained' 15 | children: React.ReactNode 16 | }> = (props) => { 17 | return ( 18 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/Btn/style.scss: -------------------------------------------------------------------------------- 1 | @import './../../../styles/foundation/variables'; 2 | 3 | .btn { 4 | cursor: pointer; 5 | display: inline-flex; 6 | align-items: center; 7 | -webkit-box-pack: center; 8 | justify-content: center; 9 | position: relative; 10 | box-sizing: border-box; 11 | outline: 0; 12 | margin: 0; 13 | vertical-align: middle; 14 | 15 | font-family: $mainFont; 16 | font-weight: 500; 17 | font-size: 1.3rem; 18 | line-height: 1.75; 19 | text-transform: uppercase; 20 | border-radius: 4px; 21 | 22 | color: #fff; 23 | background: #545a78; 24 | padding: 6px 16px; 25 | min-width: 22px; 26 | min-height: 22px; 27 | border: none; 28 | // background: none; 29 | // opacity: 0.3; 30 | 31 | // &.selected { 32 | // opacity: 1; 33 | // } 34 | transition: background-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, 35 | box-shadow 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, border-color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms, 36 | color 250ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; 37 | 38 | &:hover { 39 | background-color: #3a3e54; 40 | } 41 | 42 | &.size-small { 43 | padding: 0; 44 | :hover { 45 | text-decoration: none; 46 | background-color: rgba(255, 255, 255, 0.08); 47 | } 48 | } 49 | 50 | &.disabled { 51 | opacity: 0.5; 52 | pointer-events: none; 53 | cursor: inherit; 54 | } 55 | 56 | &.btn-active { 57 | color: #fff; 58 | background: #e63232; 59 | 60 | &:hover { 61 | background: #541313; 62 | } 63 | } 64 | &.btn-inactive { 65 | color: #ddd; 66 | background: #8e8e8e; 67 | 68 | &:hover { 69 | background: #333; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/DuplicateBtn.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@mui/material' 2 | import React from 'react' 3 | import { IoDuplicateOutline } from 'react-icons/io5' 4 | 5 | type PropsType = { 6 | className?: string 7 | disabled?: boolean 8 | title: string 9 | onClick: () => void 10 | } 11 | 12 | export const DuplicateBtn = function DuplicateBtn(props: PropsType): JSX.Element { 13 | return ( 14 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/HelpButton/HelpButton.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | import React from 'react' 3 | import { FiHelpCircle } from 'react-icons/fi' 4 | 5 | import './style.scss' 6 | 7 | export const HelpButton: React.FC<{ showHelp: boolean; onClick: () => void }> = (props) => { 8 | return ( 9 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/HelpButton/style.scss: -------------------------------------------------------------------------------- 1 | .help-button { 2 | border: 0; 3 | padding: 0; 4 | background: transparent; 5 | cursor: pointer; 6 | opacity: 0.7; 7 | margin-bottom: 0.4rem; 8 | margin-left: 0.5rem; 9 | margin-right: 0.5rem; 10 | 11 | &.open { 12 | opacity: 1; 13 | } 14 | 15 | &:hover { 16 | opacity: 1; 17 | } 18 | 19 | svg { 20 | fill: #8f929f; 21 | width: 2rem; 22 | height: 2rem; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/PauseBtn/style.scss: -------------------------------------------------------------------------------- 1 | .playcount { 2 | position: absolute; 3 | bottom: -12px; 4 | right: 0; 5 | color: white; 6 | } 7 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/PlayBtn/PlayBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { MdPlayArrow, MdReplay } from 'react-icons/md' 3 | import { Btn } from '../Btn/Btn' 4 | import { PlayButtonData } from '../StopBtn/StopBtn' 5 | 6 | import './style.scss' 7 | export const PlayBtn: React.FC<{ 8 | groupId: string 9 | partId?: string 10 | disabled?: boolean 11 | className?: string 12 | onClick?: () => void 13 | data: PlayButtonData 14 | }> = function PlayBtn(props) { 15 | const groupOrPartDisabled = props.data.groupDisabled || props.disabled 16 | let willDo: 'play' | 'restart' 17 | let title: string 18 | if (props.partId) { 19 | // This is a play button for a Part. 20 | if (props.data.partIsPlaying) { 21 | willDo = 'restart' 22 | title = 'Restart Part' 23 | } else { 24 | willDo = 'play' 25 | title = 'Play Part' 26 | } 27 | } else { 28 | // This is a play button for a Group. 29 | if (props.data.groupOneAtATime) { 30 | if (props.data.groupIsPlaying) { 31 | willDo = 'restart' 32 | title = 'Restart and play first Part' 33 | } else { 34 | willDo = 'play' 35 | title = 'Play first Part' 36 | } 37 | } else { 38 | if (props.data.anyPartIsPlaying) { 39 | willDo = 'restart' 40 | title = 'Restart and play all Parts in Group' 41 | } else { 42 | willDo = 'play' 43 | title = 'Play all Parts in Group' 44 | } 45 | } 46 | } 47 | 48 | return ( 49 | 57 | {willDo === 'play' && } 58 | {willDo === 'restart' && } 59 | {!props.partId && ( 60 |
{props.data.groupOneAtATime ? 1 : props.data.countPlayablePartsInGroup}
61 | )} 62 |
63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/PlayBtn/style.scss: -------------------------------------------------------------------------------- 1 | .playcount { 2 | position: absolute; 3 | bottom: -12px; 4 | right: 0; 5 | color: white; 6 | } 7 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/StopBtn/StopBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { MdStop } from 'react-icons/md' 3 | import { Btn } from '../Btn/Btn' 4 | 5 | export interface PlayButtonData { 6 | groupDisabled: boolean 7 | groupOneAtATime: boolean 8 | countPlayablePartsInGroup: number 9 | 10 | groupIsPlaying: boolean 11 | anyPartIsPlaying: boolean 12 | allPartsArePaused: boolean 13 | playheadCount: number 14 | partIsPlaying: boolean 15 | partIsPaused: boolean 16 | } 17 | 18 | export const StopBtn: React.FC<{ 19 | groupId: string 20 | partId?: string 21 | disabled?: boolean 22 | className?: string 23 | onClick?: () => void 24 | 25 | data: PlayButtonData 26 | }> = function StopBtn(props) { 27 | const groupOrPartDisabled = props.data.groupDisabled || props.disabled 28 | let canStop = false 29 | let title = '' 30 | if (props.partId) { 31 | // This is a play button for a Part. 32 | canStop = props.data.groupOneAtATime ? props.data.groupIsPlaying : props.data.partIsPlaying 33 | title = 'Stop playout of Part' 34 | } else { 35 | // This is a play button for a Group. 36 | canStop = props.data.anyPartIsPlaying 37 | title = props.data.groupOneAtATime ? 'Stop' : 'Stop playout of all Parts in Group' 38 | } 39 | 40 | return ( 41 | 49 | 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/ToggleBtn/ToggleBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import classNames from 'classnames' 3 | import './style.scss' 4 | // This is an element intended to be used as the ToggleButton exposed by mui/Button 5 | // The reason for this existing is that the Button renders quite slowly... 6 | export const ToggleBtn: React.FC<{ 7 | className?: string 8 | onChange: (event: React.MouseEvent) => void 9 | title?: string 10 | selected?: boolean 11 | disabled?: boolean 12 | size?: string 13 | children: React.ReactNode 14 | }> = (props) => { 15 | return ( 16 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/ToggleBtn/style.scss: -------------------------------------------------------------------------------- 1 | @import './../../../styles/foundation/variables'; 2 | 3 | .toggle-btn { 4 | cursor: pointer; 5 | display: inline-flex; 6 | align-items: center; 7 | -webkit-box-pack: center; 8 | justify-content: center; 9 | position: relative; 10 | box-sizing: border-box; 11 | outline: 0; 12 | margin: 0; 13 | 14 | font-family: $mainFont; 15 | font-weight: 500; 16 | font-size: 1.3rem; 17 | line-height: 1.75; 18 | text-transform: uppercase; 19 | border-radius: 4px; 20 | 21 | color: #fff; 22 | padding: 0; 23 | width: 22px; 24 | height: 22px; 25 | border: none; 26 | background: none; 27 | opacity: 0.3; 28 | 29 | &.selected { 30 | opacity: 1; 31 | } 32 | 33 | :hover { 34 | text-decoration: none; 35 | background-color: rgba(255, 255, 255, 0.08); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/ToggleInput.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | import React from 'react' 3 | import Toggle from 'react-toggle' 4 | 5 | export const ToggleInput: React.FC<{ 6 | currentValue: boolean | undefined 7 | indeterminate?: boolean 8 | onChange: (newValue: boolean) => void 9 | disabled?: boolean 10 | label?: string 11 | id?: string 12 | }> = ({ currentValue, indeterminate, onChange, disabled, label, id }) => { 13 | return ( 14 | { 17 | if (indeterminate) onChange(true) 18 | else onChange(e.target.checked) 19 | }} 20 | checked={indeterminate ? false : !!currentValue} 21 | disabled={disabled} 22 | title={label} 23 | className={classNames({ 24 | indeterminate, 25 | })} 26 | /> 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/TrashBtn.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@mui/material' 2 | import React from 'react' 3 | import { BsTrash } from 'react-icons/bs' 4 | 5 | type PropsType = { 6 | className?: string 7 | disabled?: boolean 8 | title: string 9 | onClick: () => void 10 | } 11 | 12 | export const TrashBtn = (props: PropsType): JSX.Element => { 13 | return ( 14 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/TriggerBtn/TriggerBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BsKeyboardFill, BsKeyboard } from 'react-icons/bs' 3 | import { ToggleBtn } from '../ToggleBtn/ToggleBtn' 4 | import './style.scss' 5 | 6 | export const TriggerBtn: React.FC<{ 7 | className?: string 8 | onTrigger: (event: React.MouseEvent) => void 9 | disabled?: boolean 10 | triggerCount: number 11 | locked: boolean 12 | anyGlobalTriggerFailed: boolean 13 | }> = (props) => { 14 | const handleOnClick = (e: React.MouseEvent) => { 15 | props.onTrigger(e) 16 | } 17 | 18 | const color = props.anyGlobalTriggerFailed ? 'red' : 'white' 19 | 20 | if (props.locked && props.triggerCount === 0) return null 21 | 22 | return ( 23 | 0} 32 | size="small" 33 | onChange={handleOnClick} 34 | > 35 | {props.triggerCount > 0 ? ( 36 | <> 37 | 38 |
{props.triggerCount}
39 | 40 | ) : ( 41 | 42 | )} 43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/TriggerBtn/style.scss: -------------------------------------------------------------------------------- 1 | .triggercount { 2 | position: absolute; 3 | bottom: -12px; 4 | right: 0; 5 | color: white; 6 | } 7 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/textBtn/TextBtn.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import './style.scss' 4 | 5 | export const TextBtn: React.FC<{ 6 | label: string | JSX.Element 7 | style?: 'normal' | 'warning' | 'danger' 8 | onClick?: React.MouseEventHandler 9 | }> = (props) => { 10 | return ( 11 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /apps/app/src/react/components/inputs/textBtn/style.scss: -------------------------------------------------------------------------------- 1 | button.sc-btn { 2 | border-radius: 0.5rem; 3 | color: white; 4 | border: 0; 5 | font-size: 1.1rem; 6 | padding: 0.4rem 0.8rem; 7 | box-shadow: inset 0px -2px 0px rgba(0, 0, 0, 0.25); 8 | 9 | cursor: pointer; 10 | 11 | &.normal { 12 | background: #545a78; 13 | 14 | &:hover { 15 | background: #666d8d; 16 | } 17 | } 18 | 19 | &.danger { 20 | background: #963939; 21 | 22 | &:hover { 23 | background: #b14949; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/app/src/react/components/pages/homePage/AnalogInputsPage/style.scss: -------------------------------------------------------------------------------- 1 | .analog-input-header-item { 2 | display: flex; 3 | align-items: center; 4 | width: 100%; 5 | 6 | .value { 7 | display: flex; 8 | .content { 9 | background-color: #17522c; 10 | color: #fff; 11 | padding: 0 0.5em; 12 | font-weight: bold; 13 | margin: 0 0.25em; 14 | } 15 | margin: 0 0.5em; 16 | } 17 | 18 | .identifier { 19 | margin: 0 0.5em; 20 | } 21 | .trash { 22 | flex-grow: 1; 23 | text-align: right; 24 | } 25 | } 26 | .analog-input-settings { 27 | display: flex; 28 | flex-direction: column; 29 | .main-settings { 30 | display: flex; 31 | flex-direction: column; 32 | 33 | .trigger-button { 34 | margin: 0 1em; 35 | padding-top: 1em; 36 | } 37 | } 38 | .analog-input { 39 | display: flex; 40 | flex-direction: row; 41 | 42 | .raw-input { 43 | border: 1px solid #999; 44 | border-radius: 2px; 45 | padding: 0.25em; 46 | margin: 0.25em; 47 | 48 | label { 49 | font-weight: bold; 50 | text-align: center; 51 | width: 100%; 52 | } 53 | } 54 | } 55 | .analog-settings { 56 | display: flex; 57 | flex-direction: row; 58 | flex-wrap: wrap; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /apps/app/src/react/components/pages/homePage/ApplicationActionsPage/style.scss: -------------------------------------------------------------------------------- 1 | .application-actions { 2 | .triggers { 3 | display: flex; 4 | flex-direction: row; 5 | 6 | margin-right: 0.5em; 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/app/src/react/components/pages/homePage/applicationPage/style.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperFlyTV/SuperConductor/ecaa009cd0c638e0541493a4fa2352d6959920b2/apps/app/src/react/components/pages/homePage/applicationPage/style.scss -------------------------------------------------------------------------------- /apps/app/src/react/components/pages/homePage/bridgeItem/BridgeItemHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Bridge, BridgeStatus } from '../../../../../models/project/Bridge' 3 | import { DeviceShortcut } from '../deviceShorcut/DeviceShortcut' 4 | import { ScListItemLabel } from '../scList/ScListItemLabel' 5 | import { StatusCircle } from '../scList/StatusCircle' 6 | 7 | import './style.scss' 8 | 9 | export const BridgeItemHeader: React.FC<{ 10 | id: string 11 | /** 12 | * Information about the bridge and devices 13 | */ 14 | bridge: Bridge 15 | /** 16 | * Status of the bridge and all devices 17 | */ 18 | bridgeStatus: BridgeStatus | undefined 19 | }> = (props) => { 20 | const bridgeStatus: BridgeStatus = props.bridgeStatus || { 21 | connected: false, 22 | devices: {}, 23 | peripherals: {}, 24 | } 25 | return ( 26 |
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 |
31 | 32 | 33 |
34 | {deviceSettings.disable 35 | ? 'Disabled' 36 | : props.device.ok 37 | ? 'Connected' 38 | : props.device.message 39 | ? props.device.message 40 | : 'Not Connected'} 41 |
42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /apps/app/src/react/components/pages/homePage/deviceItem/style.scss: -------------------------------------------------------------------------------- 1 | .device-item-header { 2 | display: flex; 3 | align-items: center; 4 | 5 | > .header-label { 6 | border-right: 0.2rem solid rgba(255, 255, 255, 0.3); 7 | padding-right: 2rem; 8 | margin-right: 2rem; 9 | } 10 | 11 | .status { 12 | font-size: 1.2rem; 13 | color: rgba(255, 255, 255, 0.7); 14 | } 15 | } 16 | 17 | .device-item-content { 18 | display: flex; 19 | align-items: flex-start; 20 | 21 | .fields { 22 | flex-grow: 1; 23 | display: flex; 24 | align-items: baseline; 25 | flex-wrap: wrap; 26 | } 27 | 28 | .actions { 29 | margin-left: 2rem; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/app/src/react/components/pages/homePage/deviceShorcut/DeviceShortcut.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BridgeDevice } from 'src/models/project/Bridge' 3 | import { DeviceType } from 'timeline-state-resolver-types' 4 | import { DeviceIcon } from '../deviceIcon/DeviceIcon' 5 | import { StatusCircle } from '../scList/StatusCircle' 6 | 7 | import './style.scss' 8 | 9 | export const DeviceShortcut: React.FC<{ device: BridgeDevice; type: DeviceType }> = (props) => { 10 | const statusMessage = props.device.ok ? 'Connected' : props.device.message ? props.device.message : 'Not Connected' 11 | 12 | return ( 13 |
14 | 15 | 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /apps/app/src/react/components/pages/homePage/deviceShorcut/style.scss: -------------------------------------------------------------------------------- 1 | .device-shortcut { 2 | display: flex; 3 | align-items: center; 4 | margin-right: 1.7rem; 5 | min-width: 4.5rem; 6 | 7 | .status-circle { 8 | margin-right: 0.8rem; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/app/src/react/components/pages/homePage/layerItem/LayerItemHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { Mapping } from 'timeline-state-resolver-types' 3 | import { ScListItemLabel } from '../scList/ScListItemLabel' 4 | import { describeMappingConfiguration } from '../../../../../lib/TSRMappings' 5 | import { DeviceIcon } from '../deviceIcon/DeviceIcon' 6 | import './style.scss' 7 | import { ProjectContext } from '../../../../contexts/Project' 8 | import { getDeviceName, getMappingName } from '../../../../../lib/util' 9 | 10 | export const LayerItemHeader: React.FC<{ 11 | id: string 12 | mapping: Mapping 13 | deviceId: string 14 | }> = (props) => { 15 | const project = useContext(ProjectContext) 16 | const mappingDescription = describeMappingConfiguration(props.mapping) 17 | return ( 18 |
19 | 20 | 21 | 22 | {mappingDescription &&
{mappingDescription}
} 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /apps/app/src/react/components/pages/homePage/layerItem/style.scss: -------------------------------------------------------------------------------- 1 | .layer-item-header { 2 | display: flex; 3 | align-items: center; 4 | 5 | .device-icon { 6 | margin-right: 1rem; 7 | } 8 | 9 | .config-description { 10 | font-size: 1.2rem; 11 | } 12 | } 13 | 14 | .layer-item-content { 15 | display: flex; 16 | align-items: flex-start; 17 | 18 | .fields { 19 | flex-grow: 1; 20 | display: flex; 21 | align-items: baseline; 22 | flex-wrap: wrap; 23 | } 24 | 25 | .actions { 26 | margin-left: 2rem; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/app/src/react/components/pages/homePage/layersPage/DeviceSpecificSettings.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | DeviceType, 4 | Mapping, 5 | MappingAtem, 6 | MappingCasparCG, 7 | MappingOBS, 8 | MappingVMixAny, 9 | } from 'timeline-state-resolver-types' 10 | import { CasparCGMappingSettings } from './device-specific-settings/CasparCGMappingSettings' 11 | import { AtemMappingSettings } from './device-specific-settings/AtemMappingSettings' 12 | import { OBSMappingSettings } from './device-specific-settings/OBSMappingSettings' 13 | import { VMixMappingSettings } from './device-specific-settings/VMixMappingSettings' 14 | 15 | export const DeviceSpecificSettings: React.FC<{ 16 | mapping?: Mapping 17 | device: DeviceType 18 | onUpdate: (mappingUpdate: Mapping) => void 19 | }> = (props) => { 20 | switch (props.device) { 21 | case DeviceType.CASPARCG: 22 | return 23 | case DeviceType.ATEM: 24 | return 25 | case DeviceType.OBS: 26 | return 27 | case DeviceType.VMIX: 28 | return 29 | default: 30 | // @TODO: More device types 31 | // assertNever(mapping.device) 32 | return null 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/app/src/react/components/pages/homePage/layersPage/device-specific-settings/AtemMappingSettings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useContext } from 'react' 2 | import { MappingAtem, MappingAtemType } from 'timeline-state-resolver-types' 3 | import { ErrorHandlerContext } from '../../../../../contexts/ErrorHandler' 4 | import { IPCServerContext } from '../../../../../contexts/IPCServer' 5 | import { ProjectContext } from '../../../../../contexts/Project' 6 | import { IntInput } from '../../../../inputs/IntInput' 7 | import { SelectEnum } from '../../../../inputs/SelectEnum' 8 | 9 | interface IAtemMappingSettingsProps { 10 | mapping: MappingAtem 11 | } 12 | 13 | export const AtemMappingSettings: React.FC = ({ 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 | 5 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /apps/app/src/react/components/pages/newRundownPage/NewGropIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const NewGroupIcon = (): JSX.Element => ( 4 | 5 | 12 | 13 | 14 | 15 | 16 | 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 |
    29 |
    30 |
    31 |
    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 {resource.name} 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 |