-
Notifications
You must be signed in to change notification settings - Fork 682
Port SceneTransitions to React #5750
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 13 commits
ba2fa5d
6c8a597
e14c7e5
80622a7
6f1ce76
6b7e453
e87c64d
88a5281
e02930e
f9503ee
c11e88e
2923868
51d2059
89c548c
12cc6e9
0b81794
735f5ee
37672be
ffc0500
32528de
5c57b78
99c56ac
bab6c09
b0c574b
6854e46
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import React from 'react'; | ||
| import { metadata } from 'components-react/shared/inputs/metadata'; | ||
| import FormFactory from 'components-react/shared/inputs/FormFactory'; | ||
| import { useVuex } from 'components-react/hooks'; | ||
| import { Services } from 'components-react/service-provider'; | ||
| import { $t } from 'services/i18n'; | ||
|
|
||
| export default function ConnectionSettings(p: { connectionId: string }) { | ||
| const { ScenesService, TransitionsService, EditorCommandsService } = Services; | ||
|
|
||
| const { sceneOptions, transitionOptions, connection } = useVuex(() => ({ | ||
| sceneOptions: [ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just for my own edification, is there a difference in reactivity/rerenders between doing the logic in the hook vs outside the hook?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if you do it outside of the hook i suppose it risks recalculating every render (unless using like useMemo or something), which i don't think happens if you put the logic in useVuex but tbh i haven't looked super closely at the useVuex implementation |
||
| { label: $t('All'), value: 'ALL' }, | ||
| ...ScenesService.views.scenes.map(scene => ({ | ||
| label: scene.name, | ||
| value: scene.id, | ||
| })), | ||
| ], | ||
| transitionOptions: TransitionsService.state.transitions.map(transition => ({ | ||
|
gettinToasty marked this conversation as resolved.
Outdated
|
||
| label: transition.name, | ||
| value: transition.id, | ||
| })), | ||
| connection: TransitionsService.state.connections.find( | ||
| conn => conn.id === p.connectionId, | ||
| ) | ||
| })); | ||
|
|
||
| const meta = { | ||
| from: metadata.list({ label: $t('Beginning Scene'), options: sceneOptions }), | ||
| transition: metadata.list({ label: $t('Scene Transition'), options: transitionOptions }), | ||
| to: metadata.list({ label: $t('Ending Scene'), options: sceneOptions }), | ||
| }; | ||
|
|
||
| const values = { | ||
| from: connection?.fromSceneId || '', | ||
| transition: connection?.transitionId || '', | ||
| to: connection?.toSceneId || '', | ||
| } | ||
|
|
||
| function handleChange(key: string) { | ||
| return (val: string) => EditorCommandsService.executeCommand('EditConnectionCommand', p.connectionId, { | ||
|
gettinToasty marked this conversation as resolved.
Outdated
|
||
| [key]: val, | ||
| }); | ||
| } | ||
|
|
||
| return <FormFactory formOptions={{ layout: 'horizontal' }} metadata={meta} values={values} onChange={handleChange} /> | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import React from 'react'; | ||
| import { Button, Table } from 'antd'; | ||
| import cx from 'classnames'; | ||
| import { Services } from 'components-react/service-provider'; | ||
| import { useVuex } from 'components-react/hooks'; | ||
| import { $t } from 'services/i18n'; | ||
| import { ITransitionConnection } from 'services/transitions'; | ||
| import { ColumnsType } from 'antd/lib/table'; | ||
| import styles from './SceneTransitions.m.less'; | ||
| import Tooltip from 'components-react/shared/Tooltip'; | ||
|
|
||
| export default function ConnectionsTable(p: { setInspectedConnection: (id: string) => void; setShowConnectionModal: (val: boolean) => void; }) { | ||
| const { TransitionsService, EditorCommandsService, ScenesService } = Services; | ||
|
|
||
| const { transitions, connections } = useVuex(() => ({ | ||
| transitions: TransitionsService.state.transitions, | ||
|
gettinToasty marked this conversation as resolved.
Outdated
|
||
| connections: TransitionsService.state.connections, | ||
| })); | ||
|
|
||
| async function addConnection() { | ||
| const connection = EditorCommandsService.actions.return.executeCommand( | ||
| 'CreateConnectionCommand', | ||
| ScenesService.views.scenes[0].id, | ||
| ScenesService.views.scenes[1].id, | ||
| transitions[0].id, | ||
| ) as ITransitionConnection; | ||
|
|
||
| if (!connection) return; | ||
| editConnection(connection.id); | ||
| } | ||
|
|
||
| function editConnection(id: string) { | ||
| p.setInspectedConnection(id); | ||
| p.setShowConnectionModal(true); | ||
| } | ||
|
|
||
| function deleteConnection(id: string) { | ||
| EditorCommandsService.actions.executeCommand('RemoveConnectionCommand', id); | ||
| } | ||
|
|
||
| function getTransitionName(id: string) { | ||
| const transition = TransitionsService.views.getTransition(id); | ||
|
|
||
| if (transition) return transition.name; | ||
| return `<${$t('Deleted')}>`; | ||
| } | ||
|
|
||
| function getSceneName(id: string | 'ALL') { | ||
| if (id === 'ALL') return $t('All'); | ||
|
|
||
| const scene = ScenesService.views.getScene(id); | ||
|
|
||
| if (scene) return scene.name; | ||
| return `<${$t('Deleted')}>`; | ||
| } | ||
|
|
||
| function isConnectionRedundant(id: string) { | ||
| return TransitionsService.views.isConnectionRedundant(id); | ||
| } | ||
|
|
||
| const columns: ColumnsType<ITransitionConnection> = [ | ||
| { | ||
| title: $t('Beginning Scene'), | ||
| dataIndex: 'fromScene', | ||
| render: (_, { fromSceneId }) => getSceneName(fromSceneId), | ||
| }, | ||
| { | ||
| title: $t('Transition Name'), | ||
| dataIndex: 'transitionName', | ||
| render: (_, { transitionId }) => getTransitionName(transitionId), | ||
| }, | ||
| { | ||
| title: $t('Ending Scene'), | ||
| dataIndex: 'toScene', | ||
| render: (_, { toSceneId }) => getSceneName(toSceneId), | ||
| }, | ||
| { | ||
| dataIndex: 'controls', | ||
| render: (_, { id }) => ( | ||
| <span className={styles.tableControls}> | ||
| {isConnectionRedundant(id) && <Tooltip title={$t( | ||
| 'This connection is redundant because another connection already connects these scenes.', | ||
| )} placement='left'> | ||
| <i className={cx('icon-information', styles.transitionRedundant)} /> | ||
| </Tooltip>} | ||
| <i onClick={() => editConnection(id)} className={cx('icon-edit', styles.transitionControl)} /> | ||
| <i onClick={() => deleteConnection(id)} className={cx('icon-trash', styles.transitionControl)} /> | ||
| </span> | ||
| ), | ||
| } | ||
| ]; | ||
|
|
||
| return ( | ||
| <> | ||
| <Button className="button button--action" onClick={addConnection}>{$t('Add Connection')}</Button> | ||
| <Table columns={columns} dataSource={connections} rowKey={(record) => record.id} /> | ||
| </> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| @import '../../../styles/index'; | ||
|
|
||
|
|
||
| .transition-blank { | ||
| text-align: center; | ||
| padding: 50px; | ||
| } | ||
|
|
||
| .transition-default { | ||
| color: var(--teal); | ||
| } | ||
|
|
||
| .transition-default-selector { | ||
| cursor: pointer; | ||
| width: 90px; | ||
|
|
||
| &:hover { | ||
| :not(.transition-default) { | ||
| color: var(--white); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| .transition-control { | ||
| margin-left: 10px; | ||
| } | ||
|
|
||
| .transition-control:not(.disabled) { | ||
| cursor: pointer; | ||
| .icon-hover(); | ||
| } | ||
|
|
||
| .transition-redundant { | ||
| color: var(--info); | ||
|
|
||
| &:hover { | ||
| color: var(--info); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import React, { useMemo, useState, useEffect } from 'react'; | ||
| import { Services } from 'components-react/service-provider'; | ||
| import { metadata } from 'components-react/shared/inputs/metadata'; | ||
| import { $t } from 'services/i18n'; | ||
| import { useVuex } from 'components-react/hooks'; | ||
| import FormFactory from 'components-react/shared/inputs/FormFactory'; | ||
| import { ObsForm } from 'components-react/obs/ObsForm'; | ||
| import isEqual from 'lodash/isEqual'; | ||
| import { IObsInput, TObsValue } from 'components/obs/inputs/ObsInput'; | ||
|
|
||
| export default function TransitionSettings(p: { transitionId: string }) { | ||
| const { TransitionsService, EditorCommandsService } = Services; | ||
|
|
||
| const { values, typeOptions } = useVuex(() => ({ | ||
| values: TransitionsService.views.getPropertiesForTransition(p.transitionId), | ||
| typeOptions: TransitionsService.views.getTypes(), | ||
| })); | ||
|
|
||
| const meta = { | ||
| name: metadata.text({ label: $t('Name') }), | ||
| type: metadata.list({ | ||
| label: $t('Type'), options: typeOptions, children: { | ||
| duration: metadata.number({ label: $t('Duration'), displayed: values.type !== 'obs_stinger_transition' && values.type !== 'cut_transition' }), | ||
| } | ||
| }), | ||
| }; | ||
|
|
||
| function handleChange(key: string) { | ||
| return (val: string | number) => { | ||
| EditorCommandsService.actions.executeCommand('EditTransitionCommand', p.transitionId, { | ||
| [key]: val, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| return <FormFactory metadata={meta} values={values} onChange={handleChange} />; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import React, { useMemo } from 'react'; | ||
| import remote from '@electron/remote'; | ||
| import { Button, Table } from 'antd'; | ||
| import cx from 'classnames'; | ||
| import { Services } from 'components-react/service-provider'; | ||
| import { useVuex } from 'components-react/hooks'; | ||
| import { $t } from 'services/i18n'; | ||
| import { ETransitionType, ITransition } from 'services/transitions'; | ||
| import { ColumnsType } from 'antd/lib/table'; | ||
| import styles from './SceneTransitions.m.less'; | ||
| import Tooltip from 'components-react/shared/Tooltip'; | ||
|
|
||
| export default function TransitionsTable(p: | ||
| { setInspectedTransition: (id: string) => void, setShowTransitionModal: (val: boolean) => void; } | ||
| ) { | ||
| const { TransitionsService, EditorCommandsService } = Services; | ||
|
|
||
| const { transitions, defaultId } = useVuex(() => ({ | ||
| transitions: TransitionsService.state.transitions, | ||
| defaultId: TransitionsService.state.defaultTransitionId, | ||
|
gettinToasty marked this conversation as resolved.
Outdated
|
||
| })); | ||
|
|
||
| // * Scene transitions created from apps should not be editable | ||
| // * if the app developer specified `shouldLock` as part of their | ||
| // * scene transition creation options. | ||
| const lockStates = useMemo(() => TransitionsService.getLockedStates(), []); | ||
| function canEdit(id: string) { | ||
| return !lockStates[id]; | ||
| }; | ||
|
|
||
| function getEditableMessage(id: string) { | ||
| if (canEdit(id)) return; | ||
| return $t('This scene transition is managed by an App and cannot be edited.'); | ||
| } | ||
|
|
||
| async function addTransition() { | ||
| const transition = await EditorCommandsService.actions.return.executeCommand( | ||
| 'CreateTransitionCommand', | ||
| ETransitionType.Cut, | ||
| 'New Transition', | ||
| ) as ITransition; | ||
|
|
||
| if (!transition) return; | ||
| editTransition(transition.id); | ||
| } | ||
|
|
||
| function editTransition(id: string) { | ||
| if (!canEdit(id)) return; | ||
| p.setInspectedTransition(id); | ||
| p.setShowTransitionModal(true); | ||
| } | ||
|
|
||
| function deleteTransition(id: string) { | ||
| if (transitions.length === 1) { | ||
| remote.dialog.showMessageBox({ | ||
| title: 'Streamlabs Desktop', | ||
| message: $t('You need at least 1 transition.'), | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| EditorCommandsService.actions.executeCommand('RemoveTransitionCommand', id); | ||
| } | ||
|
|
||
| function makeDefault(id: string) { | ||
| EditorCommandsService.actions.executeCommand('SetDefaultTransitionCommand', id); | ||
| } | ||
|
|
||
| function nameForType(type: ETransitionType) { | ||
| return TransitionsService.views.getTypes().find(t => t.value === type)?.label; | ||
| } | ||
|
|
||
| const columns: ColumnsType<ITransition> = [ | ||
| { | ||
| title: $t('Default'), | ||
| dataIndex: 'default', | ||
| render: (_, { id }) => ( | ||
| <div className={styles.transitionDefaultSelector}> | ||
| <i | ||
| onClick={() => makeDefault(id)} | ||
| className={cx(defaultId === id ? 'fas' : 'far', 'fa-circle', defaultId === id && styles.transitionDefault)} | ||
| /> | ||
| </div> | ||
| ), | ||
| }, | ||
| { | ||
| title: $t('Name'), | ||
| dataIndex: 'name', | ||
| key: 'name', | ||
| }, | ||
| { | ||
| title: $t('Transition Type'), | ||
| dataIndex: 'type', | ||
| render: (_, { type }) => nameForType(type), | ||
| }, | ||
| { | ||
| dataIndex: 'controls', | ||
| render: (_, { id }) => ( | ||
| <span className={styles.tableControls}> | ||
| <Tooltip title={getEditableMessage(id)} placement='left'> | ||
| <i onClick={() => editTransition(id)} className={cx(styles.transitionControl, canEdit(id) ? 'icon-edit' : 'disabled icon-lock')} /> | ||
| </Tooltip> | ||
| <i onClick={() => deleteTransition(id)} className={cx('icon-trash', styles.transitionControl)} /> | ||
| </span> | ||
| ), | ||
| } | ||
| ]; | ||
|
|
||
| return ( | ||
| <> | ||
| <Button className="button button--action" onClick={addTransition}>{$t('Add Transition')}</Button> | ||
| <Table columns={columns} dataSource={transitions} rowKey={(record) => record.id} /> | ||
| </> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like
idwas removed from ModalLayout, unless it's already a prop in antd'sModalProps?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this PR adds it back in