Skip to content
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

Output settings form to react #5299

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
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
446 changes: 416 additions & 30 deletions app/components-react/obs/ObsForm.tsx

Large diffs are not rendered by default.

17 changes: 12 additions & 5 deletions app/components-react/shared/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ import React, { CSSProperties } from 'react';
import { Tabs as AntdTabs } from 'antd';
import { $t } from 'services/i18n';

interface TabData {
interface ITab {
label: string | JSX.Element;
key: string;
}

interface ITabs {
data?: TabData[];
tabs?: string[];
onChange?: (param?: any) => void;
style?: CSSProperties;
tabStyle?: CSSProperties;
}

export default function Tabs(p: ITabs) {
const dualOutputData = [
const dualOutputData: ITab[] = [
{
label: (
<span>
Expand All @@ -36,11 +36,18 @@ export default function Tabs(p: ITabs) {
},
];

const data = p?.data ?? dualOutputData;
const data = p?.tabs ? formatTabs(p.tabs) : dualOutputData;

function formatTabs(tabs: string[]): ITab[] {
return tabs.map((tab: string) => ({
label: $t(tab),
key: tab,
}));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a reason to restructure the props and format them here instead of just doing this formatting before passing in the tabs under the data prop?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is how it was done in the vue component.


return (
<AntdTabs defaultActiveKey={data[0].key} onChange={p?.onChange} style={p?.style}>
{data.map(tab => (
{data.map((tab: ITab) => (
<AntdTabs.TabPane tab={tab.label} key={tab.key} style={p?.tabStyle} />
))}
</AntdTabs>
Expand Down
71 changes: 38 additions & 33 deletions app/components-react/shared/inputs/FileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,45 @@ type TFileInputProps = TSlobsInputProps<
InputProps
>;

export async function showFileDialog(p: TFileInputProps) {
if (p.save) {
const options: Electron.SaveDialogOptions = {
defaultPath: p.value,
filters: p.filters,
properties: [],
};

const { filePath } = await remote.dialog.showSaveDialog(options);

if (filePath && p.onChange) {
p.onChange(filePath);
}
} else {
const options: Electron.OpenDialogOptions = {
defaultPath: p.value,
filters: p.filters,
properties: [],
};

if (p.directory && options.properties) {
options.properties.push('openDirectory');
} else if (options.properties) {
options.properties.push('openFile');
}

const { filePaths } = await remote.dialog.showOpenDialog(options);

if (filePaths[0] && p.onChange) {
p.onChange(filePaths[0]);
}
}
}

export const FileInput = InputComponent((p: TFileInputProps) => {
const { wrapperAttrs, inputAttrs } = useInput('file', p);
async function showFileDialog() {
if (p.save) {
const options: Electron.SaveDialogOptions = {
defaultPath: p.value,
filters: p.filters,
properties: [],
};

const { filePath } = await remote.dialog.showSaveDialog(options);

if (filePath && p.onChange) {
p.onChange(filePath);
}
} else {
const options: Electron.OpenDialogOptions = {
defaultPath: p.value,
filters: p.filters,
properties: [],
};

if (p.directory && options.properties) {
options.properties.push('openDirectory');
} else if (options.properties) {
options.properties.push('openFile');
}

const { filePaths } = await remote.dialog.showOpenDialog(options);

if (filePaths[0] && p.onChange) {
p.onChange(filePaths[0]);
}
}

function handleShowFileDialog() {
showFileDialog(p);
}

return (
Expand All @@ -55,7 +60,7 @@ export const FileInput = InputComponent((p: TFileInputProps) => {
onChange={val => inputAttrs?.onChange(val.target.value)}
disabled
value={p.value}
addonAfter={<Button onClick={showFileDialog}>{$t('Browse')}</Button>}
addonAfter={<Button onClick={handleShowFileDialog}>{$t('Browse')}</Button>}
/>
</InputWrapper>
);
Expand Down
7 changes: 6 additions & 1 deletion app/components-react/shared/inputs/ListInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface ICustomListProps<TValue> {
onBeforeSearch?: (searchStr: string) => unknown;
options?: IListOption<TValue>[];
description?: string;
nolabel?: boolean;
}

// define a type for the component's props
Expand Down Expand Up @@ -87,7 +88,11 @@ export const ListInput = InputComponent(<T extends any>(p: TListInputProps<T>) =
const selectedOption = options?.find(opt => opt.value === p.value);

return (
<InputWrapper {...wrapperAttrs} extra={p?.description ?? selectedOption?.description}>
<InputWrapper
{...wrapperAttrs}
extra={p?.description ?? selectedOption?.description}
nolabel={p?.nolabel}
>
<Select
ref={$inputRef}
{...omit(inputAttrs, 'onChange')}
Expand Down
11 changes: 8 additions & 3 deletions app/components-react/shared/inputs/NumberInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import InputWrapper from './InputWrapper';
import { InputNumberProps } from 'antd/lib/input-number';

// select which features from the antd lib we are going to use
const ANT_NUMBER_FEATURES = ['min', 'max', 'step'] as const;
export const ANT_NUMBER_FEATURES = ['min', 'max', 'step'] as const;

type TProps = TSlobsInputProps<{}, number, InputNumberProps, ValuesOf<typeof ANT_NUMBER_FEATURES>>;
export type TNumberInputProps = TSlobsInputProps<
{},
number,
InputNumberProps,
ValuesOf<typeof ANT_NUMBER_FEATURES>
>;

export const NumberInput = React.memo((p: TProps) => {
export const NumberInput = React.memo((p: TNumberInputProps) => {
const { inputAttrs, wrapperAttrs, originalOnChange } = useTextInput<typeof p, number>(
'number',
p,
Expand Down
3 changes: 2 additions & 1 deletion app/components-react/shared/inputs/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import InputWrapper from './InputWrapper';
import { InputProps } from 'antd/lib/input';

// select which features from the antd lib we are going to use
const ANT_INPUT_FEATURES = ['addonBefore', 'addonAfter', 'autoFocus', 'prefix'] as const;
// note: to add a submit button for the text input, pass in a button to the `addonAfter` or `addonBefore` prop
export const ANT_INPUT_FEATURES = ['addonBefore', 'addonAfter', 'autoFocus', 'prefix'] as const;

export type TTextInputProps = TSlobsInputProps<
{
Expand Down
22 changes: 17 additions & 5 deletions app/components-react/windows/settings/ObsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@ import { useObsSettings } from './useObsSettings';
import { ObsFormGroup } from '../../obs/ObsForm';
import Form from '../../shared/inputs/Form';
import css from './ObsSettings.m.less';
import Tabs from 'components-react/shared/Tabs';

export type IObsFormType = 'default' | 'tabs' | 'collapsible';

/**
* Renders a settings page
*/
export function ObsSettings(p: { page: string }) {
const { setPage } = useObsSettings();
const { setPage, setDisplay } = useObsSettings();
setPage(p.page);
const PageComponent = getPageComponent(p.page);

// TODO: Comment in when switched to new API
// const showTabs = ['Output', 'Audio', 'Advanced'].includes(p.page);
const showTabs = false;
return (
<div className={css.obsSettingsWindow}>
{showTabs && <Tabs onChange={setDisplay} />}
<PageComponent />
</div>
);
Expand All @@ -22,10 +30,14 @@ export function ObsSettings(p: { page: string }) {
/**
* Renders generic inputs from OBS
*/
export function ObsGenericSettingsForm() {
export function ObsGenericSettingsForm(p: { type?: IObsFormType }) {
const { settingsFormData, saveSettings } = useObsSettings();
return (
<ObsFormGroup value={settingsFormData} onChange={newSettings => saveSettings(newSettings)} />
<ObsFormGroup
value={settingsFormData}
onChange={newSettings => saveSettings(newSettings)}
type={p?.type}
/>
);
}

Expand All @@ -50,7 +62,7 @@ export function ObsSettingsSection(
*/
function getPageComponent(page: string) {
const componentName = Object.keys(pageComponents).find(componentName => {
return pageComponents[componentName].page === page;
return (pageComponents as Record<string, any>)[componentName].page === page;
});
return componentName ? pageComponents[componentName] : null;
return componentName ? (pageComponents as Record<string, any>)[componentName] : null;
}
96 changes: 96 additions & 0 deletions app/components-react/windows/settings/Output.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useMemo, useState } from 'react';
import { useObsSettings } from './useObsSettings';
import {
IObsSectionedFormGroupProps,
ObsCollapsibleFormGroup,
ObsForm,
} from 'components-react/obs/ObsForm';
import Tabs from 'components-react/shared/Tabs';
import { $t } from 'services/i18n';
import cloneDeep from 'lodash/cloneDeep';
import { TObsFormData } from 'components/obs/inputs/ObsInput';

export function OutputSettings() {
const { settingsFormData, saveSettings } = useObsSettings();

const type = settingsFormData[0].parameters[0].currentValue === 'Simple' ? 'collapsible' : 'tabs';

function onChange(formData: TObsFormData, ind: number) {
const newVal = cloneDeep(settingsFormData);
newVal[ind].parameters = formData;
saveSettings(newVal);
}
const sections = settingsFormData.filter(
section => section.parameters.filter(p => p.visible).length,
);

return (
<div className="form-groups" style={{ paddingBottom: '12px' }}>
{type === 'tabs' && <ObsTabbedOutputFormGroup sections={sections} onChange={onChange} />}

{type === 'collapsible' && (
<ObsCollapsibleFormGroup sections={sections} onChange={onChange} />
)}
</div>
);
}

export function ObsTabbedOutputFormGroup(p: IObsSectionedFormGroupProps) {
const tabs = useMemo(() => {
// combine all audio tracks into one tab
const filtered = p.sections
.filter(sectionProps => sectionProps.nameSubCategory !== 'Untitled')
.filter(sectionProps => !sectionProps.nameSubCategory.startsWith('Audio - Track'))
.map(sectionProps => sectionProps.nameSubCategory);

filtered.splice(2, 0, 'Audio');
return filtered;
}, [p.sections]);

const [currentTab, setCurrentTab] = useState(p.sections[1].nameSubCategory);

return (
<div className="section" key="tabbed-section" style={{ marginBottom: '24px' }}>
{p.sections.map((sectionProps, ind) => (
<div className="section-content" key={`${sectionProps.nameSubCategory}${ind}`}>
{sectionProps.nameSubCategory === 'Untitled' && (
<>
<ObsForm
value={sectionProps.parameters}
onChange={formData => p.onChange(formData, ind)}
/>
<Tabs tabs={tabs} onChange={setCurrentTab} style={{ marginBottom: '24px' }} />
</>
)}

{sectionProps.nameSubCategory === currentTab && (
<ObsForm
name={sectionProps.nameSubCategory}
value={sectionProps.parameters}
onChange={formData => p.onChange(formData, ind)}
/>
)}

{currentTab === 'Audio' && sectionProps.nameSubCategory.startsWith('Audio - Track') && (
<div
style={{
backgroundColor: 'var(--section-wrapper)',
padding: '15px',
marginBottom: '30px',
borderRadius: '5px',
}}
>
<h2 className="section-title">{$t(sectionProps.nameSubCategory)}</h2>
<ObsForm
value={sectionProps.parameters}
onChange={formData => p.onChange(formData, ind)}
/>
</div>
)}
</div>
))}
</div>
);
}

OutputSettings.page = 'Output';
2 changes: 1 addition & 1 deletion app/components-react/windows/settings/pages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export * from './General';
export * from './Multistreaming';
export * from './Stream';
// 'Output',
export * from './Output';
export * from './Audio';
export * from './Video';
// 'Hotkeys',
Expand Down
6 changes: 6 additions & 0 deletions app/components-react/windows/settings/useObsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import React from 'react';
import { useModule, injectState } from 'slap';
import { Services } from '../../service-provider';
import { ISettingsSubCategory } from '../../../services/settings';
import { TDisplayType } from 'services/settings-v2';

/**
* A module for components in the SettingsWindow
*/
class ObsSettingsModule {
state = injectState({
page: '',
display: 'horizontal',
});

init() {
Expand All @@ -29,6 +31,10 @@ class ObsSettingsModule {
this.settingsService.setSettings(this.state.page, newSettings);
}

setDisplay(display: TDisplayType) {
this.state.setDisplay(display);
}

get settingsFormData() {
return this.settingsService.state[this.state.page]?.formData ?? [];
}
Expand Down
6 changes: 4 additions & 2 deletions app/components/obs/inputs/ObsInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export declare type TObsType =
| 'OBS_PROPERTY_EDITABLE_LIST'
| 'OBS_PROPERTY_BUTTON'
| 'OBS_PROPERTY_BITMASK'
| 'OBS_INPUT_RESOLUTION_LIST';
| 'OBS_INPUT_RESOLUTION_LIST'
| 'OBS_PROPERTY_UNIT';

/**
* OBS values that frontend application can change
Expand All @@ -51,6 +52,7 @@ export interface IObsInput<TValueType> {
visible?: boolean;
masked?: boolean;
type: TObsType;
subType?: TObsType | string;
}

export declare type TObsFormData = (IObsInput<TObsValue> | IObsListInput<TObsValue>)[];
Expand Down Expand Up @@ -449,7 +451,7 @@ export function setPropertiesFormData(

if (property.type === 'OBS_PROPERTY_FONT') {
settings['custom_font'] = (property.value as IObsFont).path;
delete settings[property.name]['path'];
delete (settings[property.name] as IObsFont).path;
}
});

Expand Down
Loading
Loading