Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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,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: [
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.state.transitions.map(transition => ({
Comment thread
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, {
Comment thread
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,
Comment thread
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} />;
}
115 changes: 115 additions & 0 deletions app/components-react/windows/scene-transitions/TransitionsTable.tsx
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,
Comment thread
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} />
</>
);
}
Loading
Loading