Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/components-react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import ReactiveDataEditorWindow from './windows/reactive-data-editor/ReactiveDat
import { PlatformMerge, PlatformAppStore, PlatformAppMainPage } from './pages';
import Settings from './windows/Settings';
import Troubleshooter from './windows/Troubleshooter';
import SceneTransitions from './windows/scene-transitions';

// list of React components to be used inside Vue components
export const components = {
Expand Down Expand Up @@ -102,4 +103,5 @@ export const components = {
ReactiveDataEditorWindow,
Settings: createRoot(Settings),
Troubleshooter,
SceneTransitions,
};
2 changes: 2 additions & 0 deletions app/components-react/shared/ModalLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type TProps = {
wrapperStyle?: React.CSSProperties;
className?: string;
bodyClassName?: string;
id?: string;
} & Pick<ModalProps, 'footer' | 'onOk' | 'okText' | 'bodyStyle' | 'confirmLoading' | 'onCancel'>;

// calculate OS dependent styles
Expand Down Expand Up @@ -80,6 +81,7 @@ export function ModalLayout(p: TProps) {
<div
className={cx('ant-modal-content', currentTheme, p.className)}
style={{ ...wrapperStyles, ...p.wrapperStyle }}
id={p.id}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like id was removed from ModalLayout, unless it's already a prop in antd's ModalProps?

Copy link
Copy Markdown
Contributor Author

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

>
{p.fixedChild && <div style={fixedStyles}>{p.fixedChild}</div>}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, { useMemo } from 'react';
import { metadata } from 'components-react/shared/inputs/metadata';
import FormFactory, { TInputValue } 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: [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.views.transitions.map(transition => ({
label: transition.name,
value: transition.id,
})),
connection: TransitionsService.views.connections.find(conn => conn.id === p.connectionId),
}));

const meta = {
fromSceneId: metadata.list({ label: $t('Beginning Scene'), options: sceneOptions }),
transitionId: metadata.list({ label: $t('Scene Transition'), options: transitionOptions }),
toSceneId: metadata.list({ label: $t('Ending Scene'), options: sceneOptions }),
};

const values = useMemo(
() => ({
fromSceneId: connection?.fromSceneId || '',
transitionId: connection?.transitionId || '',
toSceneId: connection?.toSceneId || '',
}),
[connection?.fromSceneId, connection?.transitionId, connection?.toSceneId],
);

function handleChange(key: string) {
return (val: TInputValue) =>
EditorCommandsService.actions.executeCommand('EditConnectionCommand', p.connectionId, {
[key]: val,
});
}

return (
<FormFactory
formOptions={{ layout: 'horizontal' }}
metadata={meta}
values={values}
onChange={handleChange}
/>
);
}
121 changes: 121 additions & 0 deletions app/components-react/windows/scene-transitions/ConnectionsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
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.views.transitions,
connections: TransitionsService.views.connections,
}));

async function addConnection() {
const connection = (await 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);
p.setInspectedConnection('');
}

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" style={{ margin: 16 }} onClick={addConnection}>
{$t('Add Connection')}
</button>
<Table
columns={columns}
dataSource={connections}
rowKey={record => record.id}
pagination={false}
/>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@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);
}
}
}

.table-container {
:global(.ant-table-thead) > tr > th {
background: none;
border-bottom: none;
}
}

.table-controls {
display: flex;
}

.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,55 @@
import React, { useMemo } 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, { TInputValue } from 'components-react/shared/inputs/FormFactory';
import { ObsForm } from 'components-react/obs/ObsForm';
import { TObsFormData } from 'components/obs/inputs/ObsInput';
import Scrollable from 'components-react/shared/Scrollable';

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',
}),
},
}),
};

const formData = useMemo(() => {
return TransitionsService.getPropertiesFormData(p.transitionId);
}, [p.transitionId, values.type]);

function handleObsChange(patch: TObsFormData) {
TransitionsService.actions.setPropertiesFormData(p.transitionId, patch);
}

function handleChange(key: string) {
return (val: TInputValue) => {
EditorCommandsService.actions.executeCommand('EditTransitionCommand', p.transitionId, {
[key]: val,
});
};
}

return (
<Scrollable style={{ height: '100%' }} snapToWindowEdge>
<FormFactory metadata={meta} values={values} onChange={handleChange} />
<ObsForm value={formData} onChange={handleObsChange} layout="horizontal" />
</Scrollable>
);
}
Loading
Loading