diff --git a/app/components-react/highlighter/Export/ExportModal.tsx b/app/components-react/highlighter/Export/ExportModal.tsx index 8a9720adb73c..94a454362e4c 100644 --- a/app/components-react/highlighter/Export/ExportModal.tsx +++ b/app/components-react/highlighter/Export/ExportModal.tsx @@ -450,7 +450,7 @@ function ExportFlow({ { value: '1080', label: '1080p' }, ]} onChange={setResolution} - buttons={true} + optionType="button" /> @@ -464,7 +464,7 @@ function ExportFlow({ { value: '60', label: '60 FPS' }, ]} onChange={setFps} - buttons={true} + optionType="button" /> @@ -479,7 +479,7 @@ function ExportFlow({ { value: 'slow', label: $t('Smaller File') }, ]} onChange={setPreset} - buttons={true} + optionType="button" /> diff --git a/app/components-react/shared/AddDestinationButton.m.less b/app/components-react/shared/AddDestinationButton.m.less index cbf7f0587b4c..3a4485d937df 100644 --- a/app/components-react/shared/AddDestinationButton.m.less +++ b/app/components-react/shared/AddDestinationButton.m.less @@ -28,7 +28,6 @@ button.add-destination-btn { position: relative; margin: auto; margin-bottom: 15px; - background-color: var(--background); &:hover { transition: 0.3s !important; @@ -59,7 +58,7 @@ button.add-destination-btn { button.info-banner { width: unset !important; padding: 5px !important; - margin: 0px 15px 12px; + margin: 0px 15px; height: fit-content; align-items: flex-start; border-radius: 6px; @@ -170,7 +169,7 @@ button.add-destination-header { width: 12px; height: 14px; float: inline-end; - margin-right: 50px; + margin-right: 10px; margin-top: 1px; } } @@ -192,6 +191,7 @@ button.add-destination-header { .ultra-btn:extend(button.add-destination-btn) { margin: 0px 15px !important; width: calc(100% - 30px) !important; + background-color: var(--background); font-weight: 500; .ultra-icon { @@ -201,29 +201,19 @@ button.add-destination-header { } .small-btn-group { - padding: 0px !important; + margin: 0px !important; } -.small-btn:extend(button.add-destination-btn) { - height: 40px !important; - font-weight: 500; +button.small-btn { color: var(--title) !important; - text-align: left !important; - font-size: 14px; - border: 0px !important; - background-color: var(--section-alt) !important; - margin: 0px 15px !important; - width: calc(100% - 30px) !important; - :global(.anticon.anticon-plus) { - font-size: 14px !important; + span.anticon.anticon-plus { color: var(--title) !important; - padding-left: 65px !important; } &:hover { transition: 0.3s; - background-color: var(--button); + background-color: var(--button) !important; } } diff --git a/app/components-react/shared/AddDestinationButton.tsx b/app/components-react/shared/AddDestinationButton.tsx index c56abdbc1e76..c15f8cb6bdd7 100644 --- a/app/components-react/shared/AddDestinationButton.tsx +++ b/app/components-react/shared/AddDestinationButton.tsx @@ -115,12 +115,12 @@ function SmallAddDestinationButton(p: { className?: string; onClick: () => void return ( ); } diff --git a/app/components-react/shared/AnimatedWrapper.m.less b/app/components-react/shared/AnimatedWrapper.m.less new file mode 100644 index 000000000000..6fd408519dc6 --- /dev/null +++ b/app/components-react/shared/AnimatedWrapper.m.less @@ -0,0 +1,12 @@ +.visible { + opacity: 1; + visibility: visible; + transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out, height 0.3s ease-in-out; +} + +.hidden { + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out, height 0.3s ease-in-out; + height: 0px !important; +} diff --git a/app/components-react/shared/AnimatedWrapper.tsx b/app/components-react/shared/AnimatedWrapper.tsx new file mode 100644 index 000000000000..8ff935b7d092 --- /dev/null +++ b/app/components-react/shared/AnimatedWrapper.tsx @@ -0,0 +1,24 @@ +import React, { CSSProperties } from 'react'; +import styles from './AnimatedWrapper.m.less'; +import cx from 'classnames'; + +interface IAnimatedWrapperProps { + className?: string; + visible: boolean; + style?: CSSProperties; + children?: React.ReactNode; + height: string; + onClick?: (props?: any) => void; +} + +export default function AnimatedWrapper(p: IAnimatedWrapperProps) { + return ( +
+ {p.children} +
+ ); +} diff --git a/app/components-react/shared/DisplaySelector.tsx b/app/components-react/shared/DisplaySelector.tsx index d380c8abf2b4..44276091391b 100644 --- a/app/components-react/shared/DisplaySelector.tsx +++ b/app/components-react/shared/DisplaySelector.tsx @@ -15,6 +15,8 @@ interface IDisplaySelectorProps { className?: string; style?: CSSProperties; nolabel?: boolean; + alignIcons?: 'left' | 'center' | 'right'; + visible?: boolean; } export default function DisplaySelector(p: IDisplaySelectorProps) { @@ -107,12 +109,14 @@ export default function DisplaySelector(p: IDisplaySelectorProps) { value={value} defaultValue="horizontal" options={displays} + alignIcons={p?.alignIcons} onChange={onChange} icons={true} className={p?.className} style={p?.style} direction="horizontal" gapsize={0} + optionType="button" /> ); } diff --git a/app/components-react/shared/InfoBadge.m.less b/app/components-react/shared/InfoBadge.m.less index 95b4eb16e167..f85e6e0c91cc 100644 --- a/app/components-react/shared/InfoBadge.m.less +++ b/app/components-react/shared/InfoBadge.m.less @@ -7,8 +7,19 @@ background-color: var(--info-badge); color: var(--info-badge-text); font-size: 10px; + display: inline-flex; + align-items: center; &.margin { margin: 15px; } + + &.sm { + font-size: 14px; + padding: 2px 6px; + + i { + margin-right: 5px; + } + } } diff --git a/app/components-react/shared/InfoBadge.tsx b/app/components-react/shared/InfoBadge.tsx index c8b4b24cd7d6..d289c23fc14e 100644 --- a/app/components-react/shared/InfoBadge.tsx +++ b/app/components-react/shared/InfoBadge.tsx @@ -1,6 +1,5 @@ import React, { CSSProperties } from 'react'; import styles from './InfoBadge.m.less'; -import { $t } from 'services/i18n'; import cx from 'classnames'; interface IInfoBadge { @@ -8,15 +7,25 @@ interface IInfoBadge { className?: string; style?: CSSProperties; hasMargin?: boolean; + size?: 'sm' | 'md' | 'lg'; + color?: string; + bgColor?: string; } export default function InfoBadge(p: IInfoBadge) { return (
- {typeof p.content === 'string' ? $t(p.content) : p.content} + {p.content}
); } diff --git a/app/components-react/shared/StreamShiftToggle.m.less b/app/components-react/shared/StreamShiftToggle.m.less index 1d96c540327f..853bb62e77c3 100644 --- a/app/components-react/shared/StreamShiftToggle.m.less +++ b/app/components-react/shared/StreamShiftToggle.m.less @@ -1,25 +1,29 @@ .stream-shift-wrapper { display: inline-flex; align-items: center; + width: 100%; } .stream-shift-toggle { display: flex; - flex-direction: row; + color: var(--title) !important; +} + +.label-ultra-badge { + display: flex; align-items: center; - font-size: 14px; - color: var(--title); +} - :global(.ant-checkbox-wrapper) { - align-items: flex-start; - } +.label-checkbox { + width: 100%; + text-wrap: nowrap; } -.label-ultra-badge { +.tooltip { display: flex; align-items: center; } .beta-badge { - margin: 1px 0 0 8px !important; + margin-left: 10px; } diff --git a/app/components-react/shared/StreamShiftToggle.tsx b/app/components-react/shared/StreamShiftToggle.tsx index d392d22f3f84..f2d9e9f22ff6 100644 --- a/app/components-react/shared/StreamShiftToggle.tsx +++ b/app/components-react/shared/StreamShiftToggle.tsx @@ -44,17 +44,16 @@ export default function StreamShiftToggle(p: IStreamShiftToggle) {
{ - Services.MagicLinkService.actions.linkToPrime( - 'slobs-streamswitcher', - { event: 'StreamShift' }, - ); + Services.MagicLinkService.actions.linkToPrime('slobs-streamswitcher', { + event: 'StreamShift', + }); }} > - {label} +
{label}
) : ( - <>{label} +
{label}
) } name="streamShift" @@ -80,8 +79,9 @@ export default function StreamShiftToggle(p: IStreamShiftToggle) { placement="top" lightShadow={true} disabled={p?.disabled} + className={styles.tooltip} > - + diff --git a/app/components-react/shared/Tabs.tsx b/app/components-react/shared/Tabs.tsx index 2d4e7afaa838..b5457d427d03 100644 --- a/app/components-react/shared/Tabs.tsx +++ b/app/components-react/shared/Tabs.tsx @@ -1,54 +1,72 @@ -import React, { CSSProperties } from 'react'; -import { Tabs as AntdTabs } from 'antd'; +import React, { CSSProperties, useMemo } from 'react'; +import { Tabs as AntdTabs, TabsProps } from 'antd'; import { $t } from 'services/i18n'; +import { TSlobsInputProps, ValuesOf } from './inputs'; +import omit from 'lodash/omit'; + +const ANT_TAB_FEATURES = ['type', 'moreIcon', 'tabBarGutter', 'tabPosition'] as const; interface ITab { label: string | JSX.Element; - key: string; + id: string; + content?: JSX.Element; } +type TTabInputProps = TSlobsInputProps< + ICustomTabs, + string, + TabsProps, + ValuesOf +>; -interface ITabs { - tabs?: string[]; +interface ICustomTabs { + tabs?: string[] | ITab[]; onChange?: (param?: any) => void; style?: CSSProperties; tabStyle?: CSSProperties; } -export default function Tabs(p: ITabs) { - const dualOutputData: ITab[] = [ - { - label: ( - - - {$t('Horizontal')} - - ), - key: 'horizontal', - }, - { - label: ( - - - {$t('Vertical')} - - ), - key: 'vertical', - }, - ]; - - const data = p?.tabs ? formatTabs(p.tabs) : dualOutputData; - - function formatTabs(tabs: string[]): ITab[] { - return tabs.map((tab: string) => ({ - label: $t(tab), - key: tab, - })); - } +export default function Tabs(p: TTabInputProps) { + const tabProps = omit(p, 'tabStyle', 'tabs', 'onInput'); + // return dual output tab data by default + const tabs = useMemo(() => { + return p.tabs + ? p.tabs.map((tab: string | ITab) => { + if (typeof tab === 'string') { + return { + label: $t(tab), + id: tab, + }; + } + return tab; + }) + : [ + { + label: ( + + + {$t('Horizontal')} + + ), + id: 'horizontal', + }, + { + label: ( + + + {$t('Vertical')} + + ), + id: 'vertical', + }, + ]; + }, [p.tabs]); return ( - - {data.map((tab: ITab) => ( - + + {tabs.map((tab: ITab) => ( + + {tab.content} + ))} ); diff --git a/app/components-react/shared/inputs/CheckboxInput.tsx b/app/components-react/shared/inputs/CheckboxInput.tsx index 9e36b28cd4b1..071723bd2102 100644 --- a/app/components-react/shared/inputs/CheckboxInput.tsx +++ b/app/components-react/shared/inputs/CheckboxInput.tsx @@ -5,7 +5,7 @@ import { CheckboxProps } from 'antd/lib/checkbox'; import { QuestionCircleOutlined } from '@ant-design/icons'; export type TCheckboxInputProps = TSlobsInputProps< - { nolabel?: boolean; className?: string; style?: CSSProperties }, + { nolabel?: boolean; className?: string; style?: CSSProperties; tooltipIcon?: React.ReactNode }, boolean, CheckboxProps >; @@ -22,7 +22,7 @@ export const CheckboxInput = InputComponent((p: TCheckboxInputProps) => { {p.label} {p.tooltip && ( - + {p.tooltipIcon ?? } )} diff --git a/app/components-react/shared/inputs/ListInput.tsx b/app/components-react/shared/inputs/ListInput.tsx index 832f7b9cc885..fff6d32aa57b 100644 --- a/app/components-react/shared/inputs/ListInput.tsx +++ b/app/components-react/shared/inputs/ListInput.tsx @@ -24,6 +24,7 @@ const ANT_SELECT_FEATURES = [ 'suffixIcon', 'size', 'dropdownMatchSelectWidth', + 'bordered', ] as const; export interface IListGroup { @@ -39,7 +40,7 @@ export interface ICustomListProps { labelRender?: (opt: IListOption) => ReactNode; onBeforeSearch?: (searchStr: string) => unknown; options?: IListOption[] | IListGroup[]; - description?: string; + description?: string | ReactNode; nolabel?: boolean; filter?: string; } diff --git a/app/components-react/shared/inputs/RadioInput.m.less b/app/components-react/shared/inputs/RadioInput.m.less index 3ebebe640835..e05fbc5f235e 100644 --- a/app/components-react/shared/inputs/RadioInput.m.less +++ b/app/components-react/shared/inputs/RadioInput.m.less @@ -1,20 +1,17 @@ @import '../../../styles/index.less'; .icon-radio { - flex: 1; - display: flex; - flex-direction: row; - align-items: center; + i.icon-toggle { + padding: 0px 1px; + } :global(.ant-row.ant-form-item) { margin-right: 0px !important; } - :global(.ant-radio-wrapper::after) { - width: inherit; - } - :global(.ant-radio-wrapper) { + margin: 0px !important; + i { color: var(--icon-toggle); transition: color 0.3s ease-in-out; @@ -31,32 +28,59 @@ } :global(.ant-radio) { - position: absolute; - left: -9999px; - overflow: hidden; - } - - :global(.ant-radio) + * { - padding: 0px; - overflow: hidden; + display: none; } } -.icon-radio { +.icon-default:extend(.icon-radio) { flex: 1; display: flex; flex-direction: row; align-items: center; +} - :global(.ant-row.ant-form-item) { - margin-right: 0px !important; - } +.icon-button:extend(.icon-radio) { + background-color: var(--dark-background); + border-radius: 4px; + padding: 4px; :global(.ant-radio-wrapper) { margin: 0px !important; + padding: 4px 0px; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; } - i.icon-toggle { - padding: 0px 1px; + :global(.ant-radio-wrapper):last-child { + border-top-left-radius: unset; + border-bottom-left-radius: unset; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + } + + :global(.ant-radio-wrapper-checked) { + background-color: var(--button); + + i { + transition: unset; + } + } +} + +.icons-left { + text-align: left; +} + +.icons-center { + text-align: center; +} + +.icons-right { + text-align: right; +} + +.nomargin { + &:global(.ant-form-item) { + margin: 0 !important; } } diff --git a/app/components-react/shared/inputs/RadioInput.tsx b/app/components-react/shared/inputs/RadioInput.tsx index 53e1852162ba..33c94bfc93e9 100644 --- a/app/components-react/shared/inputs/RadioInput.tsx +++ b/app/components-react/shared/inputs/RadioInput.tsx @@ -19,8 +19,8 @@ interface ICustomRadioGroupProps { label?: string; nolabel?: boolean; nowrap?: boolean; + nomargin?: boolean; options: ICustomRadioOption[]; - buttons?: boolean; icons?: boolean; style?: CSSProperties; value?: string; @@ -28,6 +28,8 @@ interface ICustomRadioGroupProps { disabled?: boolean; className?: string; gapsize?: number; + alignIcons?: 'left' | 'center' | 'right'; + optionType?: 'default' | 'button'; } type TRadioInputProps = TSlobsInputProps; @@ -40,9 +42,21 @@ export const RadioInput = InputComponent((p: TRadioInputProps) => { ...pick(p, 'name'), }; + const optionType = p.optionType ?? 'default'; + return ( - - {p.buttons && ( + + {optionType === 'button' && !p.icons && ( { onChange={e => p.onChange && p.onChange(e.target.value)} options={p.options} optionType="button" - buttonStyle="solid" disabled={p.disabled} className={p.className} style={p?.style} @@ -65,9 +78,13 @@ export const RadioInput = InputComponent((p: TRadioInputProps) => { value={p.value} defaultValue={p.defaultValue} onChange={e => p.onChange && p.onChange(e.target.value)} - className={cx(p.className, styles.iconRadio)} + className={cx(p.className, styles.iconRadio, { + [styles.iconDefault]: optionType === 'default', + [styles.iconButton]: optionType === 'button', + })} style={p?.style} disabled={p.disabled} + optionType={p.optionType} > {p.options.map((option: ICustomRadioOption) => { return ( @@ -89,7 +106,7 @@ export const RadioInput = InputComponent((p: TRadioInputProps) => { })} )} - {!p.icons && !p.buttons && ( + {!p.icons && optionType === 'default' && ( { size={size} {...inputAttrs} ref={p.inputRef} - className={cx(styles.horizontal, styles.horizontalItem, { + className={cx({ + [styles.horizontal]: !p?.nomargin, [styles.checkmark]: p?.checkmark, [styles.secondarySwitch]: p?.color === 'secondary', [styles.noLabel]: p?.nolabel, diff --git a/app/components-react/windows/go-live/AdvancedSettingsSwitch.tsx b/app/components-react/windows/go-live/AdvancedSettingsSwitch.tsx deleted file mode 100644 index fe17f77e8b7d..000000000000 --- a/app/components-react/windows/go-live/AdvancedSettingsSwitch.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import { useGoLiveSettings } from './useGoLiveSettings'; -import { SwitchInput } from '../../shared/inputs'; -import { $t } from '../../../services/i18n'; - -export default function AdvancedSettingsSwitch() { - const { - isAdvancedMode, - canShowAdvancedMode, - switchAdvancedMode, - lifecycle, - isLoading, - } = useGoLiveSettings(); - - const ableToConfirm = ['prepopulate', 'waitForNewSettings'].includes(lifecycle); - const shouldShowAdvancedSwitch = ableToConfirm && canShowAdvancedMode; - - return !shouldShowAdvancedSwitch ? null : ( - - ); -} diff --git a/app/components-react/windows/go-live/AiHighlighterToggle.m.less b/app/components-react/windows/go-live/AiHighlighterToggle.m.less index 1475890cae93..1661c3bec77b 100644 --- a/app/components-react/windows/go-live/AiHighlighterToggle.m.less +++ b/app/components-react/windows/go-live/AiHighlighterToggle.m.less @@ -5,6 +5,48 @@ @paddingRight: 24px; @paddingBottom: 20px; +.highlighter-banner { + display: flex; + align-items: center; + background-color: var(--callout); + border-radius: 4px; + + :global(.ant-alert-message) { + color: var(--section); + font-weight: 500; + + span { + vertical-align: text-top; + } + + a { + color: var(--section); + } + } + + :global(.ant-alert-info) { + background-color: unset; + border: 0px; + } + + :global(.ant-row.ant-form-item) { + margin-top: 0px; + margin-left: 8px; + } + + :global(.ant-switch) { + background-color: var(--button); + } + + :global(.ant-switch-handle::before) { + background-color: var(--title); + } + + :global(.ant-switch-checked) { + background-color: var(--section) !important; + } +} + .ai-highlighter-box { position: relative; overflow: hidden; diff --git a/app/components-react/windows/go-live/AiHighlighterToggle.tsx b/app/components-react/windows/go-live/AiHighlighterToggle.tsx index e3bbabac52cd..6c4d7ac2036d 100644 --- a/app/components-react/windows/go-live/AiHighlighterToggle.tsx +++ b/app/components-react/windows/go-live/AiHighlighterToggle.tsx @@ -1,11 +1,10 @@ import { SwitchInput } from 'components-react/shared/inputs/SwitchInput'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, memo, useMemo } from 'react'; import styles from './AiHighlighterToggle.m.less'; import { Services } from 'components-react/service-provider'; import { useDebounce, useVuex } from 'components-react/hooks'; -import EducationCarousel from 'components-react/highlighter/EducationCarousel'; import { DownOutlined, UpOutlined } from '@ant-design/icons'; -import { Button } from 'antd'; +import { Alert, Button } from 'antd'; import { getConfigByGame, isGameSupported } from 'services/highlighter/models/game-config.models'; import { $t } from 'services/i18n'; import { @@ -14,241 +13,289 @@ import { TikTokLogo, YouTubeLogo, } from 'components-react/highlighter/ImportStream'; +import InputWrapper from 'components-react/shared/inputs/InputWrapper'; +import Translate from 'components-react/shared/Translate'; -export default function AiHighlighterToggle({ - game, - cardIsExpanded, -}: { +export default function AiHighlighterToggle(p: { game: string | undefined; - cardIsExpanded: boolean; + banner?: React.ReactNode; }) { - //TODO M: Probably good way to integrate the highlighter in to GoLiveSettings + const gameIsSupported = useMemo(() => { + return isGameSupported(p.game); + }, [p.game]); + + const toggleHighlighter = useDebounce( + 300, + Services.HighlighterService.actions.toggleAiHighlighter, + ); + + return gameIsSupported ? ( + + {p.banner ? ( + + ) : ( + + )} + + ) : ( + <> + ); +} + +const AIHighlighterBanner = memo( + (p: { game: string | undefined; toggleHighlighter: () => void }) => { + const { HighlighterService } = Services; + const { useHighlighter } = useVuex( + () => ({ + useHighlighter: HighlighterService.views.useAiHighlighter, + }), + false, + ); + + return ( + + + + ); + }, +); + +const AIHighlighterCard = memo((p: { game: string | undefined; toggleHighlighter: () => void }) => { const { HighlighterService } = Services; - const { useHighlighter, highlighterVersion } = useVuex(() => { - return { + const { useHighlighter, highlighterVersion } = useVuex( + () => ({ useHighlighter: HighlighterService.views.useAiHighlighter, highlighterVersion: HighlighterService.views.highlighterVersion, - }; - }); - + }), + false, + ); const [gameIsSupported, setGameIsSupported] = useState(false); - const [gameConfig, setGameConfig] = useState(null); - useEffect(() => { - const supportedGame = isGameSupported(game); - setGameIsSupported(!!supportedGame); - if (supportedGame) { - setIsExpanded(true); - setGameConfig(getConfigByGame(supportedGame)); - } else { - setGameConfig(null); - } - }, [game]); - - function getInitialExpandedState() { + const initialExpandedState = useMemo(() => { if (gameIsSupported) { return true; } else { if (useHighlighter) { return true; } else { - return cardIsExpanded; + return false; } } - } - const initialExpandedState = getInitialExpandedState(); + }, [gameIsSupported, useHighlighter]); + const [isExpanded, setIsExpanded] = useState(initialExpandedState); + const [gameConfig, setGameConfig] = useState(null); - const toggleHighlighter = useDebounce(300, HighlighterService.actions.toggleAiHighlighter); + useEffect(() => { + const supportedGame = isGameSupported(p.game); + setGameIsSupported(!!supportedGame); + if (supportedGame) { + setIsExpanded(true); + setGameConfig(getConfigByGame(supportedGame)); + } else { + setGameConfig(null); + } + }, [p.game]); return ( -
- {gameIsSupported ? ( +
+
+ +
-
+ >
+
+
+
+

setIsExpanded(!isExpanded)}> + {$t('Get stream highlights!')} +

-
-
-
-
-
-

setIsExpanded(!isExpanded)}> - {$t('Get stream highlights!')} -

- - {highlighterVersion !== '' ? ( - - ) : ( - - )} -
-
setIsExpanded(!isExpanded)} style={{ cursor: 'pointer' }}> - {isExpanded ? ( - - ) : ( - - )} -
-
-
-

- {$t('Auto-generate game highlight reels of your stream')} -

-
+ ) : ( +
-
+ {$t('Install AI Highlighter')} + + )}
- {isExpanded && ( - <> -
- {!useHighlighter ? ( -
- {gameConfig?.importModalConfig?.horizontalExampleVideo && - gameConfig?.importModalConfig?.verticalExampleVideo ? ( - <> -
- -
-
- -
+
setIsExpanded(!isExpanded)} style={{ cursor: 'pointer' }}> + {isExpanded ? ( + + ) : ( + + )} +
+
+
+

+ {$t('Auto-generate game highlight reels of your stream')} +

+
+ {$t('Beta')} +
+
+
+ {isExpanded && ( + <> +
+ {!useHighlighter ? ( +
+ {gameConfig?.importModalConfig?.horizontalExampleVideo && + gameConfig?.importModalConfig?.verticalExampleVideo ? ( + <> +
+ +
+
+ +
-
- -
+
+ +
-
- -
-
- -
-
- {' '} - -
- - ) : ( -
- )} -
- ) : ( -
-
- ⚠️ - {$t('Game language must be English')} -
{' '} -
- {' '} - ⚠️ - {$t('Game must be fullscreen')}{' '} +
+
-
- {' '} - ⚠️ - {$t('Game mode must be supported')} +
+
- - {gameConfig?.gameModes && `(${gameConfig?.gameModes})`} - + {' '} +
- {/* */} -
+ + ) : ( +
)} -
- - )} -
-
- ) : ( - <> - )} + ) : ( +
+
+ ⚠️ + {$t('Game language must be English')} +
{' '} +
+ {' '} + ⚠️ + {$t('Game must be fullscreen')}{' '} +
+
+ {' '} + ⚠️ + {$t('Game mode must be supported')} +
+
+ + {gameConfig?.gameModes && `(${gameConfig?.gameModes})`} + +
+ {/* */} +
+ )} + +
+ + )} +
); -} +}); diff --git a/app/components-react/windows/go-live/CommonPlatformFields.tsx b/app/components-react/windows/go-live/CommonPlatformFields.tsx index 1e9ae0633ca1..354278f9bf22 100644 --- a/app/components-react/windows/go-live/CommonPlatformFields.tsx +++ b/app/components-react/windows/go-live/CommonPlatformFields.tsx @@ -1,19 +1,11 @@ import { TPlatform } from '../../../services/platforms'; import { $t } from '../../../services/i18n'; import React, { useMemo } from 'react'; -import { - CheckboxInput, - InputComponent, - TextAreaInput, - TextInput, - TInputLayout, -} from '../../shared/inputs'; -import { assertIsDefined } from '../../../util/properties-type-guards'; -import InputWrapper from '../../shared/inputs/InputWrapper'; -import Animate from 'rc-animate'; +import { InputComponent, TextAreaInput, TextInput, TInputLayout } from '../../shared/inputs'; import { TLayoutMode } from './platforms/PlatformSettingsLayout'; import { Services } from '../../service-provider'; import { Tooltip } from 'antd'; +import AnimatedWrapper from 'components-react/shared/AnimatedWrapper'; interface ICommonPlatformSettings { title: string; @@ -45,29 +37,17 @@ export const CommonPlatformFields = InputComponent((rawProps: IProps) => { const defaultProps = { layoutMode: 'singlePlatform' as TLayoutMode }; const p: IProps = { ...defaultProps, ...rawProps }; - const { HighlighterService } = Services; - function updatePlatform(patch: Partial) { const platformSettings = p.value; p.onChange({ ...platformSettings, ...patch }); } - /** - * Toggle the "Use different title and description " checkbox - **/ - function toggleUseCustom() { - assertIsDefined(p.platform); - const isEnabled = p.value.useCustomFields; - updatePlatform({ useCustomFields: !isEnabled }); - } - function updateCommonField(fieldName: TCustomFieldName, value: string) { updatePlatform({ [fieldName]: value }); } const view = Services.StreamingService.views; - const hasCustomCheckbox = p.layoutMode === 'multiplatformAdvanced'; - const fieldsAreVisible = !hasCustomCheckbox || p.value.useCustomFields; + const fieldsAreVisible = !p.platform || p.value.useCustomFields || false; const descriptionIsRequired = typeof p.descriptionIsRequired === 'boolean' ? p.descriptionIsRequired @@ -79,10 +59,17 @@ export const CommonPlatformFields = InputComponent((rawProps: IProps) => { const fields = p.value; - // find out the best title for common fields - const title = hasDescription - ? $t('Use different title and description') - : $t('Use different title'); + const height = useMemo(() => { + if (!fieldsAreVisible) { + return '0px'; + } + + if (hasDescription) { + return '162px'; + } + + return '71px'; + }, [fieldsAreVisible, hasDescription]); // determine max character length for title by enabled platform limitation let maxCharacters = 120; @@ -93,10 +80,6 @@ export const CommonPlatformFields = InputComponent((rawProps: IProps) => { maxCharacters = 140; } - if (!enabledPlatforms.includes('twitch') && HighlighterService.views.useAiHighlighter) { - HighlighterService.actions.setAiHighlighter(false); - } - const titleTooltip = useMemo(() => { if (enabledPlatforms.includes('tiktok')) { return $t('Only 32 characters of your title will display on TikTok'); @@ -106,63 +89,44 @@ export const CommonPlatformFields = InputComponent((rawProps: IProps) => { }, [enabledPlatforms]); return ( -
- {/* USE CUSTOM CHECKBOX */} - {hasCustomCheckbox && ( - - - + + {/*TITLE*/} + updateCommonField('title', val)} + label={ + titleTooltip ? ( + + {$t('Title')} + + + ) : ( + $t('Title') + ) + } + required={true} + max={maxCharacters} + layout={p.layout} + style={{ marginTop: !p.platform ? '0px' : '10px' }} + size="large" + /> + + {/*DESCRIPTION*/} + {hasDescription && ( + updateCommonField('description', val)} + name="description" + label={$t('Description')} + required={descriptionIsRequired} + layout={p.layout} + /> )} - - - {fieldsAreVisible && ( -
- {/*TITLE*/} - updateCommonField('title', val)} - label={ - titleTooltip ? ( - - {$t('Title')} - - - ) : ( - $t('Title') - ) - } - required={true} - max={maxCharacters} - layout={p.layout} - size="large" - /> - - {/*DESCRIPTION*/} - {hasDescription && ( - updateCommonField('description', val)} - name="description" - label={$t('Description')} - required={descriptionIsRequired} - layout={p.layout} - /> - )} - - {/* {aiHighlighterFeatureEnabled && - enabledPlatforms && - !enabledPlatforms.includes('twitch') && ( - - )} */} -
- )} -
-
+ ); }); diff --git a/app/components-react/windows/go-live/CustomFieldsCheckbox.tsx b/app/components-react/windows/go-live/CustomFieldsCheckbox.tsx new file mode 100644 index 000000000000..8c398a7704e4 --- /dev/null +++ b/app/components-react/windows/go-live/CustomFieldsCheckbox.tsx @@ -0,0 +1,86 @@ +import { TPlatform } from '../../../services/platforms'; +import React, { useMemo, useCallback } from 'react'; +import { InputComponent, TInputLayout } from '../../shared/inputs'; +import { TLayoutMode } from './platforms/PlatformSettingsLayout'; +import InputWrapper from '../../shared/inputs/InputWrapper'; +import { CheckboxInput } from 'components-react/shared/inputs/CheckboxInput'; +import { $t } from 'services/i18n'; +import { assertIsDefined } from 'util/properties-type-guards'; +import { Services } from 'components-react/service-provider'; + +interface ICommonPlatformSettings { + title: string; + description?: string; + useCustomFields?: boolean; +} + +interface IProps { + /** + * if provided then change props only for the provided platform + */ + platform?: TPlatform; + value: ICommonPlatformSettings; + descriptionIsRequired?: boolean; + enabledPlatformsCount?: number; + layout?: TInputLayout; + hasCustomCheckbox?: boolean; + onChange: (newValue: ICommonPlatformSettings) => unknown; +} + +/** + * Component for modifying common platform fields such as "Title" and "Description" + * if "props.platform" is provided it changes props for a single platform + * otherwise it changes props for all enabled platforms + */ +export const CustomFieldsCheckbox = InputComponent((p: IProps) => { + function updatePlatform(patch: Partial) { + const platformSettings = p.value; + p.onChange({ ...platformSettings, ...patch }); + } + + /** + * Toggle the "Use different title and description " checkbox + **/ + const toggleUseCustom = useCallback(() => { + assertIsDefined(p.platform); + const isEnabled = p.value.useCustomFields; + updatePlatform({ useCustomFields: !isEnabled }); + }, [p.platform, p.value.useCustomFields, updatePlatform]); + + // Memoize the values of `hasDescription` to avoid unnecessary re-renders of the component + // but it never needs to be updated after the first render because supported fields for platforms + // can't change without updating the service + const hasDescription = useMemo( + () => + p.platform + ? Services.StreamingService.views.supports('description', [p.platform]) + : Services.StreamingService.views.supports('description'), + [], + ); + + // Same as above for `title` + const title = useMemo( + () => (hasDescription ? $t('Use different title and description') : $t('Use different title')), + [], + ); + + const showCheckbox = useMemo(() => { + return p.enabledPlatformsCount && p.enabledPlatformsCount > 1; + }, [p.enabledPlatformsCount]); + + const layout = p.layout ?? 'vertical'; + + return showCheckbox ? ( + + + + ) : ( + <> + ); +}); diff --git a/app/components-react/windows/go-live/DestinationSwitchers.m.less b/app/components-react/windows/go-live/DestinationSwitchers.m.less index 13cad0d42f3b..7b8e93886123 100644 --- a/app/components-react/windows/go-live/DestinationSwitchers.m.less +++ b/app/components-react/windows/go-live/DestinationSwitchers.m.less @@ -2,66 +2,91 @@ @import '../../../styles/mixins.less'; .destination-logo { - font-size: 34px; + font-size: 16px !important; + margin-top: 4px; } .platform-logo-twitch { - font-size: 34px; + font-size: 16px !important; + margin-top: 5px; } .platform-logo-youtube { - font-size: 31px; - margin-top: 9px; - width: 34px; - height: 34px; + font-size: 15px !important; + margin-top: 4px !important; + width: 16px !important; + height: 16px !important; } .platform-logo-facebook { - font-size: 34px; + font-size: 16px !important; } .platform-logo-tiktok { - font-size: 34px; - margin-left: 5px; - width: 34px; - height: 34px; + font-size: 16px !important; + margin-left: 1px; + width: 16px !important; + height: 16px !important; } .platform-logo-instagram { - font-size: 34px; - width: 34px; - height: 34px; + font-size: 16px !important; + width: 16px !important; + height: 16px !important; } .platform-logo-twitter { - width: 32px; - height: 32px; + width: 15px !important; + height: 15px !important; } .platform-logo-kick { - width: 34px; - height: 34px; + width: 16px !important; + height: 16px !important; + margin-top: 3px; } .platform-logo-trovo { - width: 34px; - height: 34px; + width: 16px !important; + height: 16px !important; + margin-top: 5px; } .platform-logo { - align-self: center; - display: flex; - margin-right: 10px; - min-width: 36px; + margin: 0px 10px 0px 0px; } .platform-switcher { - .radius(); - display: flex; - height: 54px; - background: var(--card) !important; + flex-direction: column; + border-radius: 8px; + background: var(--card-target); transition: transform 0.05s; cursor: pointer; margin-bottom: 15px; padding: 8px 10px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + + .destination-info { + display: flex; + flex-direction: row; + width: 100%; + + :global(.ant-form-item) { + margin: 0px; + } + + :global(.ant-form-item-control-input) { + align-items: flex-start; + } + } + + .col-info { + display: flex; + flex-direction: column; + } + + .col-account { + display: flex; + align-items: center; + } .col-input { - padding-top: 3px; padding-right: 16px; - width: 52px; } .col-primary { @@ -69,23 +94,14 @@ text-align: center; } - .col-account { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - align-self: center; - flex: 1; - } - .platform-name { color: var(--title); + font-weight: 500; + display: flex; + align-items: center; } } -.switch-wrapper { - padding: 0px 15px; -} - .switch-wrapper:last-child { padding-bottom: 55px; } @@ -94,29 +110,20 @@ margin-bottom: 0px; } -.platform-disabled { - background: var(--card-disabled) !important; -} - -.platform-enabled { - transition: 0.3 ease-in-out; - background: var(--card-active) !important; -} - -.platform-name { - font-size: 14px; -} - .platform-handle { font-size: 12px; line-height: 18px; + margin: 5px 0px 3px 0px; } .display-selector-wrapper { - align-self: center; + :global(.ant-form-item) { + margin-bottom: 0px; + } } -.dual-output-display-selector { +.display-selector { + margin-top: 6px; margin-bottom: 0px !important; display: flex; align-self: flex-end; diff --git a/app/components-react/windows/go-live/DestinationSwitchers.tsx b/app/components-react/windows/go-live/DestinationSwitchers.tsx index a96a71af0af3..f8b28b4f823b 100644 --- a/app/components-react/windows/go-live/DestinationSwitchers.tsx +++ b/app/components-react/windows/go-live/DestinationSwitchers.tsx @@ -1,22 +1,35 @@ -import React, { useRef, useMemo, MouseEvent } from 'react'; +import React, { + useRef, + useMemo, + useImperativeHandle, + forwardRef, + MouseEvent, + memo, + useCallback, +} from 'react'; import { getPlatformService, platformLabels, TPlatform } from '../../../services/platforms'; import cx from 'classnames'; import { $t } from '../../../services/i18n'; import styles from './DestinationSwitchers.m.less'; import { ICustomStreamDestination } from '../../../services/settings/streaming'; import { Services } from '../../service-provider'; -import { SwitchInput } from '../../shared/inputs'; import PlatformLogo from '../../shared/PlatformLogo'; import { useDebounce } from '../../hooks'; import { useGoLiveSettings } from './useGoLiveSettings'; import DisplaySelector from 'components-react/shared/DisplaySelector'; -import ConnectButton from 'components-react/shared/ConnectButton'; -import { message, Tooltip } from 'antd'; +import { message } from 'antd'; +import { + SwitcherCard, + ISwitcherCardHandle, + ISwitcherCardHandle as IDestinationSwitcherHandle, +} from './SwitcherCard'; +import AnimatedWrapper from 'components-react/shared/AnimatedWrapper'; +import Utils from 'services/utils'; /** * Allows enabling/disabling platforms and custom destinations for the stream */ -export function DestinationSwitchers() { +export const DestinationSwitchers = memo(() => { const { linkedPlatforms, enabledPlatforms, @@ -27,22 +40,20 @@ export function DestinationSwitchers() { isPlatformLinked, isPrimaryPlatform, isRestreamEnabled, - isDualOutputMode, + isStreamShiftMode, isPrime, alwaysEnabledPlatforms, alwaysShownPlatforms, isTwitchDualStreaming, } = useGoLiveSettings(); - // use these references to apply debounce - // for error handling and switch animation + // Use these references to apply debounce for error handling and switch animation const enabledPlatformsRef = useRef(enabledPlatforms); enabledPlatformsRef.current = enabledPlatforms; const enabledDestRef = useRef(enabledDestinations); enabledDestRef.current = enabledDestinations; - // some platforms are always shown, even if not linked - // add them to the list of platforms to display + // Some platforms are always shown, even if not linked so add them to the list of platforms to display const platforms = useMemo(() => { const unlinkedAlwaysShownPlatforms = alwaysShownPlatforms.filter( platform => !isPlatformLinked(platform), @@ -50,21 +61,15 @@ export function DestinationSwitchers() { return unlinkedAlwaysShownPlatforms.length ? linkedPlatforms.concat(unlinkedAlwaysShownPlatforms) : linkedPlatforms; - }, [linkedPlatforms, enabledPlatformsRef.current, isDualOutputMode, isPrime]); + }, [linkedPlatforms, enabledPlatformsRef.current, enabledPlatforms]); - // Disable custom destination switchers when restream is not available - // or for a non-ultra user is in single output mode. The one exception - // for a non-ultra user in single output mode is if TikTok is the only enabled platform + // Disable custom destination switchers when restream is not available, such as for a non-ultra user. + // The one exception for a non-ultra user in single output mode is if TikTok is the only enabled platform const disableCustomDestinationSwitchers = - !isRestreamEnabled && - !isDualOutputMode && - !isEnabled('tiktok') && - enabledPlatformsRef.current.length > 1; + !isRestreamEnabled && !isEnabled('tiktok') && enabledPlatformsRef.current.length > 1; const disableNonUltraSwitchers = - isDualOutputMode && - !isPrime && - enabledPlatformsRef.current.length + enabledDestRef.current.length >= 2; - const disablePlatformSwitchers = isDualOutputMode && isTwitchDualStreaming; + !isPrime && enabledPlatformsRef.current.length + enabledDestRef.current.length >= 2; + const disablePlatformSwitchers = isTwitchDualStreaming && !Utils.isPreview(); const emitSwitch = useDebounce(500, (ind?: number, enabled?: boolean) => { if (ind !== undefined && enabled !== undefined) { @@ -83,25 +88,17 @@ export function DestinationSwitchers() { } function togglePlatform(platform: TPlatform, enabled: boolean) { - // In dual output mode, only allow non-ultra users to have 2 platforms, or 1 platform and 1 custom destination enabled + // Only allow non-ultra users to have 2 platforms, or 1 platform and 1 custom destination enabled if (!isPrime) { - if (isDualOutputMode) { - if (enabledPlatformsRef.current.length + enabledDestRef.current.length <= 2) { - enabledPlatformsRef.current.push(platform); - } else { - enabledPlatformsRef.current = enabledPlatformsRef.current.filter(p => p !== platform); - } + if (enabled && alwaysEnabledPlatforms.includes(platform)) { + enabledPlatformsRef.current.push(platform); + } else if (enabled) { + enabledPlatformsRef.current = enabledPlatformsRef.current.filter(p => + alwaysEnabledPlatforms.includes(p), + ); + enabledPlatformsRef.current.push(platform); } else { - if (enabled && alwaysEnabledPlatforms.includes(platform)) { - enabledPlatformsRef.current.push(platform); - } else if (enabled) { - enabledPlatformsRef.current = enabledPlatformsRef.current.filter(p => - alwaysEnabledPlatforms.includes(p), - ); - enabledPlatformsRef.current.push(platform); - } else { - enabledPlatformsRef.current = enabledPlatformsRef.current.filter(p => p !== platform); - } + enabledPlatformsRef.current = enabledPlatformsRef.current.filter(p => p !== platform); } if (!enabledPlatformsRef.current.length) { @@ -109,7 +106,7 @@ export function DestinationSwitchers() { } emitSwitch(); - return; + return enabledPlatformsRef.current.includes(platform); } // user can always stream to tiktok in single output mode @@ -133,18 +130,19 @@ export function DestinationSwitchers() { } emitSwitch(); + return enabledPlatformsRef.current.includes(platform); } function toggleDestination(index: number, enabled: boolean) { // In dual output mode, only allow non-ultra users to have 2 platforms, or 1 platform and 1 custom destination enabled - if (isDualOutputMode && !isPrime) { + if (!isPrime) { if (enabledPlatformsRef.current.length + enabledDestRef.current.length < 2) { enabledDestRef.current.push(index); } else { enabledDestRef.current = enabledDestRef.current.filter((dest, i) => i !== index); } emitSwitch(index, enabled); - return; + return enabledDestRef.current.includes(index); } enabledDestRef.current = enabledDestRef.current.filter((dest, i) => i !== index); @@ -154,16 +152,17 @@ export function DestinationSwitchers() { } emitSwitch(index, enabled); + return enabledDestRef.current.includes(index); } return ( -
+
{platforms.map((platform, ind) => ( ))} @@ -187,177 +186,143 @@ export function DestinationSwitchers() { switchDisabled={ disableCustomDestinationSwitchers || (!dest.enabled && disableNonUltraSwitchers) } - isDualOutputMode={isDualOutputMode} + isStreamShiftMode={isStreamShiftMode} index={ind} /> ))}
); -} +}); interface IDestinationSwitcherProps { destination: TPlatform | ICustomStreamDestination; enabled: boolean; - onChange: (enabled: boolean) => unknown; + onChange: (enabled: boolean) => boolean; switchDisabled?: boolean; index: number; - isDualOutputMode: boolean; - isUnlinked?: boolean; + isStreamShiftMode: boolean; showTwitchTooltip?: boolean; } +export type { IDestinationSwitcherHandle }; + /** * Render a single switcher */ // disable `func-call-spacing` and `no-spaced-func` rules // to pass back reference to addClass function // eslint-disable-next-line -const DestinationSwitcher = React.forwardRef<{}, IDestinationSwitcherProps>((p, ref) => { - const switchInputRef = useRef(null); - const containerRef = useRef(null); - const platform = typeof p.destination === 'string' ? (p.destination as TPlatform) : null; - const disabled = p?.switchDisabled; - const label = platform - ? $t('Toggle %{platform}', { platform: platformLabels(platform) }) - : $t('Toggle Destination'); - - function onClickHandler(ev: MouseEvent) { - // If we're disabling the switch we shouldn't be emitting anything past below - if (disabled) { - if (!Services.UserService.state.isPrime) { - message.info({ - key: 'switcher-info-alert', - content: ( -
-
- {$t( - "You've selected the two streaming destinations. Disable a destination to enable a different one. \nYou can always upgrade to Ultra for multistreaming.", - )} -
- - -
- ), - className: styles.infoAlert, - onClick: () => message.destroy('switcher-info-alert'), - }); - } - return; - } +const DestinationSwitcher = memo( + forwardRef((p, ref) => { + const cardRef = useRef(null); + + useImperativeHandle(ref, () => ({ + toggle: () => cardRef.current?.toggle(), + enable: () => cardRef.current?.enable(), + disable: () => cardRef.current?.disable(), + })); + + const platform = typeof p.destination === 'string' ? (p.destination as TPlatform) : null; + const disabled = p?.switchDisabled; + const label = platform + ? $t('Toggle %{platform}', { platform: platformLabels(platform) }) + : $t('Toggle Destination'); + + const onClickHandler = useCallback( + (e: MouseEvent) => { + // If we're disabling the switch we shouldn't be emitting anything past below + if (disabled) { + if (!Services.UserService.state.isPrime) { + message.info({ + key: 'switcher-info-alert', + content: ( +
+
+ {$t( + "You've selected the two streaming destinations. Disable a destination to enable a different one. \nYou can always upgrade to Ultra for multistreaming.", + )} +
+ + +
+ ), + className: styles.infoAlert, + onClick: () => message.destroy('switcher-info-alert'), + }); + } + return p.enabled; + } - const enable = !p.enabled; - p.onChange(enable); - // always proxy the click to the SwitchInput - // so it can play a transition animation - switchInputRef.current?.click(); - } + const enable = !p.enabled; + return p.onChange(enable); + }, + [p.enabled, p.onChange], + ); - const { title, description, Controller, Logo } = (() => { - const { UserService } = Services; + const { title, description, Logo } = (() => { + const { UserService } = Services; - if (platform) { - // define slots for a platform switcher - const service = getPlatformService(platform); - const platformAuthData = UserService.state.auth?.platforms[platform]; - const username = platformAuthData?.username ?? ''; + if (platform) { + // define slots for a platform switcher + const service = getPlatformService(platform); + const platformAuthData = UserService.state.auth?.platforms[platform]; + const username = platformAuthData?.username ?? ''; - return { - title: service.displayName, - description: username, - Logo: () => ( - - ), - Controller: () => ( - - ), - }; - } else { - // define slots for a custom destination switcher - const destination = p.destination as ICustomStreamDestination; - const name = `destination${p?.index}`; - return { - title: destination.name, - description: destination.url, - Logo: () => , - Controller: () => ( - - ), - }; - } - })(); + return { + title: service.displayName, + description: username, + Logo: () => ( + + ), + }; + } else { + // define slots for a custom destination switcher + const destination = p.destination as ICustomStreamDestination; + return { + title: destination.name, + description: destination.url, + Logo: () => , + }; + } + })(); - return ( - -
} + name={platform ?? `destination${p?.index}`} + label={label} + title={title} + description={description} + className={cx({ [styles.disabled]: disabled })} + tooltipTitle={ + p.showTwitchTooltip ? $t('Disable Twitch dual stream to add another platform') : undefined + } > - {/* SWITCH */} -
- -
- - {/* PLATFORM LOGO */} -
- -
- - {/* PLATFORM TITLE AND ACCOUNT/URL */} -
-
{title}
-
{description}
-
- {/* DISPLAY TOGGLES */} - {p.isDualOutputMode && !p?.isUnlinked && ( -
e.stopPropagation()}> - -
- )} - - {/* CONNECT BUTTON */} - {p?.isUnlinked && platform && ( - - )} -
-
- ); -}); + e.stopPropagation()} + height="35px" + > + + + + ); + }), +); diff --git a/app/components-react/windows/go-live/GameSelector.tsx b/app/components-react/windows/go-live/GameSelector.tsx index acd4784cd356..9f6c1ba266f6 100644 --- a/app/components-react/windows/go-live/GameSelector.tsx +++ b/app/components-react/windows/go-live/GameSelector.tsx @@ -10,8 +10,12 @@ import { $t } from '../../../services/i18n'; import { IListOption } from '../../shared/inputs/ListInput'; import { Services } from '../../service-provider'; import { injectState, useModule } from 'slap'; +import { CaretDownOutlined, CaretUpOutlined } from '@ant-design/icons'; -type TProps = TSlobsInputProps<{ platform: TPlatform; layout?: TInputLayout }, string>; +type TProps = TSlobsInputProps< + { platform: TPlatform; layout?: TInputLayout; description?: string | React.ReactNode }, + string +>; export default function GameSelector(p: TProps) { const { platform } = p; @@ -148,6 +152,9 @@ export default function GameSelector(p: TProps) { allowClear layout={p.layout} size="large" + style={p.style} + suffixIcon={isSearching ? : } + description={p.description} /> ); } diff --git a/app/components-react/windows/go-live/GoLive.m.less b/app/components-react/windows/go-live/GoLive.m.less index 774ad0ce362b..f523e83d7405 100644 --- a/app/components-react/windows/go-live/GoLive.m.less +++ b/app/components-react/windows/go-live/GoLive.m.less @@ -1,130 +1,53 @@ @import '../../../styles/index.less'; -.settings-row { - height: 100%; - margin: 0px !important; - - :global(.ant-col) { - padding: 0px; - } -} - -.right-column { +.go-live-settings { height: 100%; background-color: var(--background); - overflow: hidden; - padding: 15px !important; - - :global(label.ant-form-item-required) { - flex-direction: row-reverse; - } - - :global(label.ant-form-item-required::before) { - margin-left: 4px; - } - - :global(.ant-form-item-label > label) { - height: unset; - } -} - -.platform-switcher { - .radius(); - - box-sizing: border-box; - display: flex; - margin: 15px 0px; - padding: 10px; - flex-direction: column; - color: var(--title); - background: var(--dropdown-bg); - transition: transform 0.05s; - cursor: pointer; -} - -.platform-disabled { - opacity: 0.7; - background: rgba(#2b383f, 0.3); } -.switcher-header { - display: flex; - flex: 1; - justify-content: space-between; -} - -.platform-info-wrapper { +.left-column { + height: 100%; width: 100%; - display: flex; - flex-direction: row; -} - -.platform-info { display: flex; flex-direction: column; - flex-grow: 1; - margin: 0px 5px; -} -.platform-name { - font-size: 16px; - font-weight: 600; + > * { + margin: 15px 0px 15px 15px; + } } -.platform-username { - color: var(--link); - overflow-wrap: break-word; - display: inline-block; - word-break: break-word; - white-space: pre-line; +.left-column-scroll { + height: 90%; + padding-right: 12px; } -.platform-switch { - justify-self: flex-end; - color: var(--background); - - :global(.ant-form-item-label) { - display: none; - } - - :global(.ant-switch-small) { - min-width: 36px; - height: 21px; - line-height: 21px; - } - - :global(.ant-switch-small .ant-switch-handle) { - width: 15px; - height: 15px; - top: 3px; - left: 3px; - } +.left-footer { + padding-top: 5px !important; + position: sticky; + bottom: 0px; + background-color: var(--background); +} - :global(.ant-switch-small.ant-switch-checked .ant-switch-handle) { - left: calc(100% - 15px - 3px) !important; - } +.right-column { + height: 100%; + width: 100%; + overflow: hidden; + background-color: var(--background); - :global(.ant-switch-inner i) { - color: var(--background); - display: flex; - align-self: center; + > *:not(:first-child) { + margin: 15px 15px 15px 0px; } } -.platform-display { - display: flex; - flex: 1; - justify-content: space-between; - - :global(.ant-radio-group.ant-radio-group-outline .ant-space) { - gap: 0 !important; - } +.right-column-scroll { + height: calc(100% - 30px); + padding-right: 12px; + margin-right: 8px !important; } -.display-selector { - :global(label.ant-radio-wrapper) { - margin: 0px !important; - } +.destination-mode { + padding-right: 25px !important; + padding-left: 25px !important; } .label { @@ -133,111 +56,18 @@ flex-grow: 1; } -.platforms-selector { - &:global(.ant-select) { - width: 100%; - text-align: left; - margin: 0px; - } - - &:global(.ant-select.dual-output-selector) { - width: calc(100% - 30px) !important; - margin: 0px 15px !important; - } -} - -i.icon { - font-size: 14px; - margin-right: 10px; - color: var(--paragraph); -} - -i.selector-icon { - margin-right: 10px !important; - font-size: 15px !important; - width: 15px !important; - height: 15px !important; - color: var(--section) !important; -} - -.platform-logo { - margin: 5px 5px 0 0; -} - -i.destination-logo { - margin: 5px 5px 0 0; - font-size: 30px; -} - -.option-btn { - border: 1px solid var(--nav-icon-inactive); -} - .go-live-settings { height: 100%; &:global(a) { color: var(--teal); } -} - -.column-padding { - padding: 0px 15px; -} - -.left-column { - height: 100%; - width: 100%; - border-right: 1px solid var(--border); - padding: 0px !important; - display: flex; - flex-direction: column; - background-color: var(--bg-column); - justify-content: space-between; -} - -.left-footer { - border-top: 1px solid var(--border); - margin: 15px 0px 15px 15px; - padding-top: 15px; - width: calc(100% - 30px); -} - -.dual-output-go-live { - height: 100%; - - &:global(a) { - color: var(--teal); - } :global(.ant-modal-body) { padding: 0px; } } -.section { - .margin-bottom(2); - .padding-right(2); - border-top: 1px solid var(--border); - background-color: var(--section); - - &:first-child { - border-top: none; - } - - h2 { - .margin-top(2); - } - - [data-type='bool'] { - width: 100% !important; - } -} - -.section-without-title { - padding-top: 16px; -} - .page { .absolute(0, 0, 0, 0); } @@ -254,38 +84,12 @@ i.destination-logo { } } -.mode-toggle { - display: flex; - margin-top: 6px; - margin-right: 8px; - - [data-type='toggle'] { - margin-top: 2px; - margin-left: 8px; - } -} - -.add-destination { +.add-destination-banner { margin: 0px; } -.add-destination-btn { - width: 100%; - border: 1px dashed var(--border); - border-radius: 4px; - min-height: 58px; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - text-align: center; - position: relative; - transition: border 0.3s, box-shadow 0.3s; - - &:hover { - border-color: (var(--button)); - box-shadow: 0 0 50px rgba(255, 255, 255, 0.1) inset; - } +button.bottom { + margin: 15px 15px 0px 15px !important; } .right-text { @@ -293,26 +97,6 @@ i.destination-logo { text-align: right; } -.success-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; -} - -.spacer { - width: 1px; - height: 32px; -} - -.prime { - position: absolute; - right: 16px; - font-style: italic; - color: var(--prime-hover); -} - .banner-wrapper { flex: 1; display: flex; @@ -326,42 +110,19 @@ i.destination-logo { width: unset !important; } -.destination-mode { - padding-right: 25px !important; - padding-left: 25px !important; -} - -.feature-toggle { - display: flex; - align-items: center; - justify-content: space-between; - - :global(.ant-checkbox-wrapper) { - color: var(--title); - } -} - -.feature-checkbox { - :global(.ant-checkbox-checked .ant-checkbox-inner) { - background-color: var(--secondary-checkbox); - border-color: var(--secondary-checkbox); - } -} - .primary-chat { border: 0px; - padding-bottom: 10px; - font-weight: 500; - color: var(--secondary-bg); - - :global(.ant-select:not(.ant-select-customize-input) .ant-select-selector) { - border: 0px !important; - } + padding-bottom: 5px; + font-size: 15px; :global(.ant-form-item) { margin: 0px; } + :global(label.ant-form-item-no-colon) { + font-size: 14px; + } + :global(.ant-select.ant-select-single.ant-select-show-arrow) { width: unset !important; } @@ -370,14 +131,25 @@ i.destination-logo { color: var(--title); } - :global(.ant-select-selection-item) { - color: var(--title); + :global(.ant-form-item-control-input) { + padding-left: 8px; + } + + :global(.ant-form-item-no-colon::after) { + display: none; } :global(.ant-form-item-label) { - text-align: left; + overflow: visible; + align-self: center; + } + + :global(.ant-select-selector) { + border: 0px; + } + + :global(.ant-select-selection-item) { color: var(--title); - font-weight: 400; } &.disabled { @@ -400,177 +172,64 @@ i.destination-logo { } } -.column-header { - font-size: 16px; +.confirm-btn { + width: 141.25px; +} + +.footer-content { display: flex; flex-direction: row; + justify-content: space-between; + align-items: center; flex: 1; - width: 100%; - padding: 15px 0px 15px 15px; - - :global(.ant-form-item) { - margin-bottom: 0px; - } - - :global(.ant-row) { - align-items: center; - } - - &.ultra-column-header { - padding: 15px; - } -} - -.switcher { - justify-self: flex-end; - color: var(--background); - - :global(.ant-form-item-label) { - display: none; - } - - :global(.ant-switch-small) { - min-width: 36px; - height: 21px; - line-height: 21px; - } - - :global(.ant-switch-small .ant-switch-handle) { - width: 15px; - height: 15px; - top: 3px; - left: 3px; - } - - :global(.ant-switch-checked) { - background: var(--title); - } - - :global(.ant-switch-small.ant-switch-checked .ant-switch-handle) { - left: calc(100% - 15px - 3px) !important; - } - - :global(.ant-switch-inner i) { - color: var(--background); - display: flex; - align-self: center; - } } -.advanced-settings-switch { - justify-self: flex-end; - color: var(--background); +.settings { + background-color: var(--card) !important; + margin-bottom: 8px !important; + padding: 8px !important; + border-radius: 8px !important; + color: var(--paragraph); + font-size: 12px; - :global(.ant-form-item-label) { - display: none; + :global(.ant-form-item) { + margin-bottom: 0px; } - :global(.ant-switch-small) { - min-width: 36px; - height: 21px; - line-height: 21px; - } + :global(.ant-form-item-label > label) { + font-size: 12px; - :global(.ant-switch-small .ant-switch-handle) { - width: 15px; - height: 15px; - top: 3px; - left: 3px; + i { + font-size: 12px; + } } - :global(.ant-switch-checked) { - background: var(--title); + :global(.ant-form-item-required::before) { + font-size: 12px; } - :global(.ant-switch-small.ant-switch-checked .ant-switch-handle) { - left: calc(100% - 15px - 3px) !important; + :global(.ant-input-lg) { + font-size: 14px; + color: var(--paragraph); } - :global(.ant-switch-inner i) { - color: var(--background); - display: flex; - align-self: center; - } -} - -.error-alert { - color: #ff6f5b; - width: 600px; - border-radius: 4px; - margin: 0 auto; - text-align: left !important; - - :global(.ant-message-error) { - display: flex; - align-items: center; + :global(.ant-checkbox-wrapper) { + font-size: 12px; } - :global(.ant-message-error .anticon) { - color: #ff6f5b; - margin-right: 10px; + :global(.ant-select .ant-select-selector) { + background-color: var(--button) !important; + border: 1px solid var(--button) !important; } } -.alert-content { +.go-live-footer { display: flex; - flex-direction: row; + justify-content: space-between; + flex: 1; align-items: center; -} - -.confirm-btn { - width: 141.25px; -} - -.toggle-wrapper { - display: flex; - flex-direction: column; - justify-content: flex-end; - &.shiftEnabled { - min-height: 50px; + :global(.ant-row.ant-form-item) { + margin-top: 0px !important; } } - -.dual-output-alert { - transform: translateY(-100px); - transition: transform 0.3s, visibility 0.3s, opacity 0.3s; - font-size: 13px; - border-radius: 6px; - padding: 8px; - margin: 15px 15px 0px 15px; - background-color: rgba(23, 36, 45, 0); - color: rgba(23, 36, 45, 0); - visibility: hidden; - opacity: 0; - - &.error { - background-color: #ff6f5b14; - color: #ff6f5b; - transform: translateY(0px); - visibility: visible; - opacity: 1; - } -} - -.column-content { - transition: transform 0.3s; - height: 69%; - - &.alert-closed { - transform: translateY(-70px); - z-index: -1; - } - - &.alert-open { - transform: translateY(0px); - z-index: 0; - } - - &.dual-output { - height: 58% !important; - } -} - -.switcher-wrapper { - height: 100%; -} diff --git a/app/components-react/windows/go-live/GoLiveSettings.tsx b/app/components-react/windows/go-live/GoLiveSettings.tsx index 6358e7b83ef6..045ba2805304 100644 --- a/app/components-react/windows/go-live/GoLiveSettings.tsx +++ b/app/components-react/windows/go-live/GoLiveSettings.tsx @@ -6,26 +6,23 @@ import { $t } from 'services/i18n'; import { Row, Col } from 'antd'; import { Section } from './Section'; import PlatformSettings from './PlatformSettings'; -import TwitterInput from './Twitter'; import OptimizedProfileSwitcher from './OptimizedProfileSwitcher'; import Spinner from 'components-react/shared/Spinner'; import GoLiveError from './GoLiveError'; import PrimaryChatSwitcher from './PrimaryChatSwitcher'; import ColorSpaceWarnings from './ColorSpaceWarnings'; -import DualOutputToggle from 'components-react/shared/DualOutputToggle'; import { DestinationSwitchers } from './DestinationSwitchers'; import AddDestinationButton from 'components-react/shared/AddDestinationButton'; import cx from 'classnames'; import StreamShiftToggle from 'components-react/shared/StreamShiftToggle'; import { CaretDownOutlined } from '@ant-design/icons'; -import Tooltip from 'components-react/shared/Tooltip'; import * as remote from '@electron/remote'; import { inject } from 'slap'; import { VideoEncodingOptimizationService } from 'services/video-encoding-optimizations'; import { MagicLinkService } from 'services/magic-link'; import { SettingsService } from 'services/settings'; -import Translate from 'components-react/shared/Translate'; import { maxNumPlatforms } from 'services/platforms'; +import Utils from 'services/utils'; /** * Renders settings for starting the stream @@ -35,26 +32,21 @@ import { maxNumPlatforms } from 'services/platforms'; **/ export default function GoLiveSettings() { const { - isAdvancedMode, protectedModeEnabled, error, isLoading, - isDualOutputMode, - canAddDestinations, canUseOptimizedProfile, - showTweet, hasMultiplePlatforms, - hasMultiplePlatformsLinked, enabledPlatforms, primaryChat, recommendedColorSpaceWarnings, isPrime, isStreamShiftMode, - isStreamShiftDisabled, - isDualOutputSwitchDisabled, - canStreamDualOutput, + showTopAddDestination, + showBottomAddDestination, setPrimaryChat, openPlatformSettings, + showStreamShiftToggle, } = useGoLiveSettings().extend(module => { return { videoEncodingOptimizationService: inject(VideoEncodingOptimizationService), @@ -67,17 +59,13 @@ export default function GoLiveSettings() { return linkedPlatforms.length + customDestinations.length < maxNumPlatforms + 5; }, - showSelector: !module.isPrime && module.isDualOutputMode, - - hasMultiplePlatformsLinked: module.state.linkedPlatforms.length > 1, - - isPrime: module.isPrime, - - showTweet: module.primaryPlatform && module.primaryPlatform !== 'twitter', - - isStreamShiftDisabled: module.isDualOutputMode, + get showTopAddDestination() { + return this.canAddDestinations && module.state.linkedPlatforms.length > 1; + }, - isDualOutputSwitchDisabled: module.isStreamShiftMode && !module.isDualOutputMode, + get showBottomAddDestination() { + return module.state.linkedPlatforms.length < 2; + }, addDestination() { this.settingsService.actions.showSettings('Stream'); @@ -86,7 +74,6 @@ export default function GoLiveSettings() { // temporarily hide the checkbox until streaming and output settings // are migrated to the new API get canUseOptimizedProfile() { - if (module.isDualOutputMode) return false; return ( this.videoEncodingOptimizationService.state.canSeeOptimizedProfile || this.videoEncodingOptimizationService.state.useOptimizedProfile @@ -107,141 +94,88 @@ export default function GoLiveSettings() { console.error('Error generating platform settings magic link', e); } }, + + get showStreamShiftToggle() { + return !(module.canEditLiveOutputs && Utils.isPreview()); + }, }; }); const shouldShowSettings = !error && !isLoading; - const shouldShowLeftCol = isDualOutputMode ? true : protectedModeEnabled; - const shouldShowAddDestButton = canAddDestinations; + const shouldShowLeftCol = isStreamShiftMode ? true : protectedModeEnabled; - const shouldShowPrimaryChatSwitcher = - hasMultiplePlatforms || (isDualOutputMode && hasMultiplePlatformsLinked); + const shouldShowPrimaryChatSwitcher = hasMultiplePlatforms; - const headerText = isDualOutputMode ? $t('Destinations & Outputs:') : $t('Destinations:'); + const headerText = $t('Destinations'); - const featureCheckboxWidth = isPrime ? 140 : 155; + const featureCheckboxWidth = isPrime ? 130 : 135; return ( - + {/*LEFT COLUMN*/} {shouldShowLeftCol && ( - - {isDualOutputMode && ( -
- - - -
- )} -
-
- {headerText} -
- {!isPrime && } - - - - -
- - {shouldShowAddDestButton && ( - + +

{headerText}

+ {!isPrime && ( + )} -
- } - layout="horizontal" - logo={false} - border={false} - disabled={!shouldShowPrimaryChatSwitcher} - /> - -
- - - - - - + + {showTopAddDestination && ( + + )} + + {showBottomAddDestination && ( + + )} +
+ } + layout="horizontal" + logo={false} + border={false} + disabled={!shouldShowPrimaryChatSwitcher} + /> + + {showStreamShiftToggle && ( + + )}
-
+ )} {/*RIGHT COLUMN*/} {shouldShowSettings && ( - <> - - {recommendedColorSpaceWarnings && ( - - )} - {/*PLATFORM SETTINGS*/} - - {/*ADD SOME SPACE IN ADVANCED MODE*/} - {isAdvancedMode &&
} - {/*EXTRAS*/} - {!!canUseOptimizedProfile && ( -
- -
- )} - - {/* Spacer is as scrollable padding-bottom */} -
- - {showTweet && } - + + {recommendedColorSpaceWarnings && ( + + )} + {/*PLATFORM SETTINGS*/} + + {/*EXTRAS*/} + {!!canUseOptimizedProfile && ( +
+ +
+ )} +
)} diff --git a/app/components-react/windows/go-live/GoLiveWindow.tsx b/app/components-react/windows/go-live/GoLiveWindow.tsx index c20d00394348..6787d6442299 100644 --- a/app/components-react/windows/go-live/GoLiveWindow.tsx +++ b/app/components-react/windows/go-live/GoLiveWindow.tsx @@ -19,6 +19,7 @@ import { useGoLiveSettings, useGoLiveSettingsRoot } from './useGoLiveSettings'; import { inject } from 'slap'; import RecordingSwitcher from './RecordingSwitcher'; import { promptAction } from 'components-react/modals'; +import TwitterInput from './Twitter'; export default function GoLiveWindow() { const { lifecycle, form } = useGoLiveSettingsRoot().extend(module => ({ @@ -34,7 +35,7 @@ export default function GoLiveWindow() { const shouldShowChecklist = ['runChecklist', 'live'].includes(lifecycle); return ( - } className={styles.dualOutputGoLive}> + } className={styles.goLiveSettings}>
- {!isDualOutputMode && shouldShowConfirm && } +
+ + {!isDualOutputMode && shouldShowConfirm && } +
{/* CLOSE BUTTON */} diff --git a/app/components-react/windows/go-live/PlatformSettings.tsx b/app/components-react/windows/go-live/PlatformSettings.tsx index 3f8ad38fa7a8..69ef839925ff 100644 --- a/app/components-react/windows/go-live/PlatformSettings.tsx +++ b/app/components-react/windows/go-live/PlatformSettings.tsx @@ -8,26 +8,24 @@ import { Section } from './Section'; import { YoutubeEditStreamInfo } from './platforms/YoutubeEditStreamInfo'; import { TikTokEditStreamInfo } from './platforms/TiktokEditStreamInfo'; import FacebookEditStreamInfo from './platforms/FacebookEditStreamInfo'; -import { IPlatformComponentParams, TLayoutMode } from './platforms/PlatformSettingsLayout'; +import { IPlatformComponentParams } from './platforms/PlatformSettingsLayout'; import { getDefined } from '../../../util/properties-type-guards'; import { TrovoEditStreamInfo } from './platforms/TrovoEditStreamInfo'; import { TwitterEditStreamInfo } from './platforms/TwitterEditStreamInfo'; import { InstagramEditStreamInfo } from './platforms/InstagramEditStreamInfo'; import { KickEditStreamInfo } from './platforms/KickEditStreamInfo'; -import AdvancedSettingsSwitch from './AdvancedSettingsSwitch'; import { TInputLayout } from 'components-react/shared/inputs'; import { inject } from 'slap'; import { HighlighterService } from 'app-services'; +import { SwitcherCard } from './SwitcherCard'; +import UltraIcon from 'components-react/shared/UltraIcon'; +import Utils from 'services/utils'; export default function PlatformSettings() { const { - canShowAdvancedMode, settings, - error, - isAdvancedMode, enabledPlatforms, getPlatformDisplayName, - isLoading, updatePlatform, commonFields, updateCommonFields, @@ -39,6 +37,10 @@ export default function PlatformSettings() { isAiHighlighterEnabled, isStreamShiftMode, enabledPlatformsCount, + isMidStreamMode, + isPrime, + setStreamShift, + canEditLiveOutputs, } = useGoLiveSettings().extend(settings => ({ highlighterService: inject(HighlighterService), @@ -49,11 +51,11 @@ export default function PlatformSettings() { }, get isTikTokConnected() { - return settings.state.isPlatformLinked('tiktok'); + return settings.isPlatformLinked('tiktok'); }, get layout(): TInputLayout { - return settings.isAdvancedMode ? 'horizontal' : 'vertical'; + return 'vertical'; }, get isAiHighlighterEnabled() { @@ -65,14 +67,9 @@ export default function PlatformSettings() { }, })); - const shouldShowSettings = !error && !isLoading; + const layoutMode = 'multiplatformAdvanced'; - let layoutMode: TLayoutMode; - if (canShowAdvancedMode) { - layoutMode = isAdvancedMode ? 'multiplatformAdvanced' : 'multiplatformSimple'; - } else { - layoutMode = 'singlePlatform'; - } + const showLiveSettings = canEditLiveOutputs && Utils.isPreview(); function createPlatformBinding(platform: T): IPlatformComponentParams { return { @@ -81,6 +78,7 @@ export default function PlatformSettings() { isDualOutputMode, isStreamShiftMode, isAiHighlighterEnabled, + isMidStreamMode, enabledPlatformsCount, get value() { return getDefined(settings.platforms[platform]); @@ -93,70 +91,88 @@ export default function PlatformSettings() { return ( // minHeight is required for the loading spinner -
- {shouldShowSettings && ( -
-
-
{$t('Stream Information:')}
- +
+ {showLiveSettings && ( + <> +

{$t('Live Settings')}

+
+ setStreamShift(!isStreamShiftMode)} + value={isStreamShiftMode} + title={ + <> + {$t('Live output editing')} + {!isPrime && } + + } + name="liveOutput" + description={$t('Manage output destinations mid-stream.')} + icon="icon-output" + disabled={!isPrime} + /> + setStreamShift(!isStreamShiftMode)} + value={isStreamShiftMode} + title={ + <> + {$t('Stream Shift')} + {!isPrime && } + + } + name="streamShift" + description={$t('Switch between devices while live.')} + icon="icon-repeat-2" + disabled={!isPrime} + />
+ + )} - {/*COMMON FIELDS*/} - {canShowAdvancedMode && ( -
- -
- )} +

{$t('Channel Settings')}

- {/*SETTINGS FOR EACH ENABLED PLATFORM*/} - {enabledPlatforms.map((platform: TPlatform) => ( -
- {platform === 'twitch' && ( - - )} - {platform === 'facebook' && ( - - )} - {platform === 'youtube' && ( - - )} - {platform === 'tiktok' && isTikTokConnected && ( - - )} - {platform === 'kick' && ( - - )} - {platform === 'trovo' && ( - - )} - {platform === 'twitter' && ( - - )} - {platform === 'instagram' && ( - - )} -
- ))} -
- )} + {/*COMMON FIELDS*/} +
+ +
+ + {/*SETTINGS FOR EACH ENABLED PLATFORM*/} + {enabledPlatforms.map((platform: TPlatform) => ( +
+ {platform === 'twitch' && ( + + )} + {platform === 'facebook' && ( + + )} + {platform === 'youtube' && ( + + )} + {platform === 'tiktok' && isTikTokConnected && ( + + )} + {platform === 'kick' && ( + + )} + {platform === 'trovo' && ( + + )} + {platform === 'twitter' && ( + + )} + {platform === 'instagram' && ( + + )} +
+ ))}
); } diff --git a/app/components-react/windows/go-live/PrimaryChatSwitcher.tsx b/app/components-react/windows/go-live/PrimaryChatSwitcher.tsx index 3752af50bd36..4446990fba24 100644 --- a/app/components-react/windows/go-live/PrimaryChatSwitcher.tsx +++ b/app/components-react/windows/go-live/PrimaryChatSwitcher.tsx @@ -7,7 +7,10 @@ import PlatformLogo from 'components-react/shared/PlatformLogo'; import Tooltip from 'components-react/shared/Tooltip'; import { $t } from 'services/i18n'; import { Services } from 'components-react/service-provider'; +import { IListOption } from 'components-react/shared/inputs/ListInput'; import UltraIcon from 'components-react/shared/UltraIcon'; +import styles from './GoLive.m.less'; +import cx from 'classnames'; interface IPrimaryChatSwitcherProps { enabledPlatforms: TPlatform[]; @@ -40,51 +43,64 @@ export default function PrimaryChatSwitcher({ }: IPrimaryChatSwitcherProps) { const primaryChatOptions = useMemo( () => - enabledPlatforms.map(platform => { - const service = getPlatformService(platform); - return { + enabledPlatforms.reduce((platforms: IListOption[], p) => { + const service = getPlatformService(p); + + if ( + !service.hasLiveDockFeature('chat-streaming') && + !service.hasLiveDockFeature('chat-offline') + ) { + return platforms; + } + platforms.push({ label: service.displayName, - value: platform, - }; - }), + value: p, + }); + return platforms; + }, []), [enabledPlatforms], ); + const switcherDisabled = useMemo(() => { + if (disabled) return false; + return primaryChatOptions.length === 1; + }, [disabled, primaryChatOptions]); + return ( -
- {border && } - - - {`${$t('Primary Chat')}:`} - {!Services.UserService.views.isPrime && - !Services.DualOutputService.views.dualOutputMode ? ( - - ) : ( - - - - )} -
- ) : ( - `${$t('Primary Chat')}:` - ) - } - options={primaryChatOptions} - labelRender={opt => renderPrimaryChatOption(opt, logo)} - optionRender={opt => renderPrimaryChatOption(opt, logo)} - value={primaryChat} - onChange={onSetPrimaryChat} - suffixIcon={suffixIcon} - size={size} - disabled={disabled} - dropdownMatchSelectWidth={false} - /> - -
+
+ + {`${$t('Primary Chat')}:`} + {!Services.UserService.views.isPrime && + !Services.DualOutputService.views.dualOutputMode ? ( + + ) : ( + + + + )} +
+ ) : ( + `${$t('Primary Chat')}:` + ) + } + options={primaryChatOptions} + labelRender={opt => renderPrimaryChatOption(opt, logo)} + optionRender={opt => renderPrimaryChatOption(opt, logo)} + value={primaryChat} + onChange={onSetPrimaryChat} + suffixIcon={suffixIcon} + size={size} + disabled={switcherDisabled} + dropdownMatchSelectWidth={false} + bordered={border} + /> + ); } diff --git a/app/components-react/windows/go-live/RecordingSwitcher.m.less b/app/components-react/windows/go-live/RecordingSwitcher.m.less index 51eb575364fd..fb9ad2d75e5b 100644 --- a/app/components-react/windows/go-live/RecordingSwitcher.m.less +++ b/app/components-react/windows/go-live/RecordingSwitcher.m.less @@ -1,18 +1,14 @@ @import '../../../styles/index.less'; .recording-tooltip { - display: flex; - align-items: center; - i.info { margin-left: 10px; } } .recording-switcher { - display: flex; - justify-content: flex-start; - flex: 1; + color: var(--title); + margin-right: 16px; :global(.ant-switch-checked) { background-color: var(--title); diff --git a/app/components-react/windows/go-live/RecordingSwitcher.tsx b/app/components-react/windows/go-live/RecordingSwitcher.tsx index dedc8a198581..78a6de3f3832 100644 --- a/app/components-react/windows/go-live/RecordingSwitcher.tsx +++ b/app/components-react/windows/go-live/RecordingSwitcher.tsx @@ -46,9 +46,9 @@ export default function RecordingSwitcher(p: IRecordingSettingsProps) { Services.SettingsService.actions.setSettingValue('General', 'RecordWhenStreaming', val); }} uncontrolled - style={{ marginRight: '10px' }} label={v.isDualOutputMode ? $t('Record Stream in') : $t('Record Stream')} layout="horizontal" + nomargin checkmark disabled={v.useAiHighlighter} /> diff --git a/app/components-react/windows/go-live/Section.tsx b/app/components-react/windows/go-live/Section.tsx index ef08fbf14aee..4583e8efee1c 100644 --- a/app/components-react/windows/go-live/Section.tsx +++ b/app/components-react/windows/go-live/Section.tsx @@ -1,33 +1,17 @@ -import styles from './GoLive.m.less'; import React, { HTMLAttributes } from 'react'; -import cx from 'classnames'; -import InputWrapper from '../../shared/inputs/InputWrapper'; interface ISectionProps { title?: string; - isSimpleMode?: boolean; } /** * renders a section wrapper */ export function Section(p: ISectionProps & HTMLAttributes) { - const title = p.title; - - // render header and section wrapper in advanced mode - if (!p.isSimpleMode) { - return ( -
- {title && ( - -

{title}

-
- )} -
{p.children}
-
- ); - } - - // render content only in simple mode - return
{p.children}
; + return ( +
+ {p.title &&

{p.title}

} +
{p.children}
+
+ ); } diff --git a/app/components-react/windows/go-live/SwitcherCard.tsx b/app/components-react/windows/go-live/SwitcherCard.tsx new file mode 100644 index 000000000000..2e996544ec10 --- /dev/null +++ b/app/components-react/windows/go-live/SwitcherCard.tsx @@ -0,0 +1,125 @@ +import React, { + ReactNode, + forwardRef, + MouseEvent, + useEffect, + useImperativeHandle, + useRef, + useState, + useCallback, +} from 'react'; +import cx from 'classnames'; +import styles from './DestinationSwitchers.m.less'; +import { Tooltip } from 'antd'; +import { SwitchInput } from '../../shared/inputs'; + +export interface ISwitcherCardHandle { + toggle: () => void; + enable: () => void; + disable: () => void; +} + +interface ISwitcherCardProps { + children?: ReactNode; + icon?: string | ReactNode; + label?: string; + title: string | ReactNode; + name: string; + description: string; + value: boolean; + onClick: (e: MouseEvent) => boolean | void | unknown; + tooltipTitle?: string; + className?: string; + switchClassName?: string; + disabled?: boolean; +} + +/** + * Render a reusable switcher card shell. + * Pass a `controller` (e.g. a SwitchInput) for the left column and + * any content as `children` for the right column. + */ +export const SwitcherCard = forwardRef((p, ref) => { + const valueRef = useRef(null); + const [displayValue, setDisplayValue] = useState(p.value); + + useImperativeHandle(ref, () => ({ + toggle: () => animateSwitch(!displayValue), + enable: () => animateSwitch(true), + disable: () => animateSwitch(false), + })); + + useEffect(() => { + valueRef.current = null; + setDisplayValue(p.value); + }, [p.value]); + + const animateSwitch = useCallback((nextValue: boolean, resolvedValue = nextValue) => { + valueRef.current = resolvedValue !== nextValue ? resolvedValue : null; + setDisplayValue(nextValue); + }, []); + + const handleTransitionEnd = useCallback((e: React.TransitionEvent) => { + const target = e.target; + + if (!(target instanceof HTMLElement) || valueRef.current === null) { + return; + } + + setDisplayValue(valueRef.current); + valueRef.current = null; + }, []); + + const handleClick = useCallback( + (e: MouseEvent) => { + if (p.disabled) { + p.onClick(e); + return; + } + + const nextValue = !p.value; + const resolvedValue = p.onClick(e); + animateSwitch(nextValue, typeof resolvedValue === 'boolean' ? resolvedValue : nextValue); + }, + [p.disabled, p.value, p.onClick, animateSwitch], + ); + + return ( + +
+
+
+ +
+ +
+
+ {/* PLATFORM LOGO AND NAME*/} + {typeof p.icon === 'string' ? ( + + ) : ( + p.icon + )} + {/* PLATFORM HANDLE */} +
{p.title}
+
+
{p.description}
+ {p?.children} +
+
+
+
+ ); +}); diff --git a/app/components-react/windows/go-live/Twitter.tsx b/app/components-react/windows/go-live/Twitter.tsx index 2c864b953d06..571bdb9454ef 100644 --- a/app/components-react/windows/go-live/Twitter.tsx +++ b/app/components-react/windows/go-live/Twitter.tsx @@ -43,11 +43,7 @@ export default function TwitterInput() { ); return ( - + {$t('Share your stream!')} diff --git a/app/components-react/windows/go-live/platforms/FacebookEditStreamInfo.tsx b/app/components-react/windows/go-live/platforms/FacebookEditStreamInfo.tsx index 7f8e0b2b6227..661dafc7ad95 100644 --- a/app/components-react/windows/go-live/platforms/FacebookEditStreamInfo.tsx +++ b/app/components-react/windows/go-live/platforms/FacebookEditStreamInfo.tsx @@ -25,6 +25,7 @@ import { assertIsDefined } from '../../../../util/properties-type-guards'; import * as remote from '@electron/remote'; import { $t } from 'services/i18n'; import { Services } from '../../../service-provider'; +import { CustomFieldsCheckbox } from '../CustomFieldsCheckbox'; class FacebookEditStreamInfoModule { fbService = inject(FacebookService); @@ -257,10 +258,10 @@ export default function FacebookEditStreamInfo(p: IPlatformComponentParams<'face } - requiredFields={} - optionalFields={} - essentialOptionalFields={} + commonFields={} + requiredFields={} + optionalFields={} + essentialOptionalFields={} /> ); @@ -271,7 +272,6 @@ function CommonFields() { return ( +
{!isUpdateMode && ( <> {shouldShowDestinationType && ( @@ -421,7 +421,7 @@ function OptionalFields() { ); } -function Events() { +function Events(p: IPlatformComponentParams<'facebook'>) { const { bind, shouldShowEvents, @@ -454,6 +454,7 @@ function Events() { size="large" /> )} +
); } diff --git a/app/components-react/windows/go-live/platforms/InstagramEditStreamInfo.tsx b/app/components-react/windows/go-live/platforms/InstagramEditStreamInfo.tsx index 74c0481d9f87..9430809c9891 100644 --- a/app/components-react/windows/go-live/platforms/InstagramEditStreamInfo.tsx +++ b/app/components-react/windows/go-live/platforms/InstagramEditStreamInfo.tsx @@ -42,7 +42,7 @@ export function InstagramEditStreamInfo(p: Props) { /> {!isStreamSettingsWindow && ( } requiredFields={ - +
+ + +
} /> diff --git a/app/components-react/windows/go-live/platforms/PlatformSettingsLayout.tsx b/app/components-react/windows/go-live/platforms/PlatformSettingsLayout.tsx index 3188533e2c9a..df12ac62be79 100644 --- a/app/components-react/windows/go-live/platforms/PlatformSettingsLayout.tsx +++ b/app/components-react/windows/go-live/platforms/PlatformSettingsLayout.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { TPlatform } from '../../../../services/platforms'; import { ITwitchStartStreamOptions } from '../../../../services/platforms/twitch'; import { IYoutubeStartStreamOptions } from '../../../../services/platforms/youtube'; @@ -18,24 +18,11 @@ export default function PlatformSettingsLayout(p: { essentialOptionalFields?: JSX.Element; layout?: TInputLayout; }) { - let layoutItems = []; - switch (p.layoutMode) { - case 'singlePlatform': - layoutItems = [ - p.essentialOptionalFields, - p.commonFields, - p.requiredFields, - p.optionalFields, - p.layout, - ]; - break; - case 'multiplatformSimple': - layoutItems = [p.requiredFields, p.layout]; - return p.requiredFields; - case 'multiplatformAdvanced': - layoutItems = [p.essentialOptionalFields, p.requiredFields, p.optionalFields, p.commonFields]; - break; - } + const layoutItems = useMemo( + () => [p.essentialOptionalFields, p.commonFields, p.requiredFields, p.optionalFields, p.layout], + [p.essentialOptionalFields, p.commonFields, p.requiredFields, p.optionalFields, p.layout], + ); + return <>{layoutItems.map(item => item)}; } @@ -59,4 +46,5 @@ export interface IPlatformComponentParams { isDualOutputMode?: boolean; isAiHighlighterEnabled?: boolean; isStreamShiftMode?: boolean; + isMidStreamMode?: boolean; } diff --git a/app/components-react/windows/go-live/platforms/TiktokEditStreamInfo.tsx b/app/components-react/windows/go-live/platforms/TiktokEditStreamInfo.tsx index 664210d795e3..e4f261bb2418 100644 --- a/app/components-react/windows/go-live/platforms/TiktokEditStreamInfo.tsx +++ b/app/components-react/windows/go-live/platforms/TiktokEditStreamInfo.tsx @@ -1,43 +1,36 @@ -import React from 'react'; +import React, { memo } from 'react'; import Form from '../../../shared/inputs/Form'; import { $t } from '../../../../services/i18n'; import { Services } from '../../../service-provider'; import { Button, Tooltip } from 'antd'; +import Tabs from 'components-react/shared/Tabs'; import InputWrapper from '../../../shared/inputs/InputWrapper'; import PlatformSettingsLayout, { IPlatformComponentParams } from './PlatformSettingsLayout'; import * as remote from '@electron/remote'; import { CommonPlatformFields } from '../CommonPlatformFields'; import { ITikTokStartStreamOptions } from 'services/platforms/tiktok'; import { RadioInput, TextInput, createBinding } from 'components-react/shared/inputs'; -import InfoBanner from 'components-react/shared/InfoBanner'; +import Translate from 'components-react/shared/Translate'; import GameSelector from '../GameSelector'; -import { EDismissable } from 'services/dismissables'; -import styles from './TikTokEditStreamInfo.m.less'; -import cx from 'classnames'; +import { CustomFieldsCheckbox } from '../CustomFieldsCheckbox'; +import InfoBadge from 'components-react/shared/InfoBadge'; /** * @remark The filename for this component is intentionally not consistent with capitalization to preserve the commit history */ export function TikTokEditStreamInfo(p: IPlatformComponentParams<'tiktok'>) { - const { TikTokService } = Services; const ttSettings = p.value; - const approved = TikTokService.scope === 'approved'; - const denied = TikTokService.scope === 'denied'; - const controls = TikTokService.audienceControls; function updateSettings(patch: Partial) { p.onChange({ ...ttSettings, ...patch }); } - - const bind = createBinding(ttSettings, updatedSettings => updateSettings(updatedSettings)); - return (
) { layout={p.layout} /> } - requiredFields={
} + requiredFields={} /> - {approved && ( - - )} - {approved && !controls.disable && ( - - )} - {!approved && } ); } -export function TikTokEnterCredentialsFormInfo( - p: IPlatformComponentParams<'tiktok'> & { denied: boolean }, -) { +const TikTokLiveAccessForm = memo((p: IPlatformComponentParams<'tiktok'>) => { + const { TikTokService } = Services; const bind = createBinding(p.value, updatedSettings => p.onChange({ ...p.value, ...updatedSettings }), ); + const controls = TikTokService.audienceControls; + const approved = TikTokService.scope === 'approved'; + return ( <> + {approved ? ( + + ) : ( + + )} + + ); +}); + +const TikTokStreamKeyForm = memo((p: IPlatformComponentParams<'tiktok'>) => { + const bind = createBinding(p.value, updatedSettings => + p.onChange({ ...p.value, ...updatedSettings }), + ); + + return ( + <> + See Guide', + )} + > + + @@ -86,8 +136,10 @@ export function TikTokEnterCredentialsFormInfo( } required {...bind.serverUrl} - layout={p.layout} + layout="horizontal" size="large" + labelAlign="left" + style={{ marginTop: '24px', fontSize: '14px' }} /> - - {p.denied ? : } -
- } - layout={p.layout} - className={cx({ [styles.hideLabel]: p.layout === 'vertical' })} - > - + + + Click here")} + style={{ fontSize: '12px' }} + > + + +
); -} - -function TikTokDenied() { - return ( - { - openConfirmation(); - Services.UsageStatisticsService.recordAnalyticsEvent('TikTokApplyPrompt', { - component: 'NotGrantedBannerDismissed', - }); - }} - dismissableKey={EDismissable.TikTokRejected} - /> - ); -} +}); function TikTokInfo() { return ( <> - openInfoPage()}> - {$t('Go live to TikTok with a single click. Click here to learn more.')} - - + Learn more', + )} + > + + + ); } @@ -190,6 +239,41 @@ function TikTokButtons(p: { denied: boolean }) { ); } +const TikTokRequired = memo((p: IPlatformComponentParams<'tiktok'>) => { + return ( + <> + + {$t('Streamlabs Access')} + + + ), + id: 'live-access', + content: , + }, + { + label: $t('Stream with TikTok Stream Key'), + id: 'stream-key', + content: , + }, + ]} + /> + + ); +}); + function openInfoPage() { remote.shell.openExternal(Services.TikTokService.infoUrl); } diff --git a/app/components-react/windows/go-live/platforms/TrovoEditStreamInfo.tsx b/app/components-react/windows/go-live/platforms/TrovoEditStreamInfo.tsx index be8167cfabb1..95ceac0eef65 100644 --- a/app/components-react/windows/go-live/platforms/TrovoEditStreamInfo.tsx +++ b/app/components-react/windows/go-live/platforms/TrovoEditStreamInfo.tsx @@ -5,6 +5,7 @@ import { createBinding } from '../../../shared/inputs'; import Form from '../../../shared/inputs/Form'; import { CommonPlatformFields } from '../CommonPlatformFields'; import GameSelector from '../GameSelector'; +import { CustomFieldsCheckbox } from '../CustomFieldsCheckbox'; export function TrovoEditStreamInfo(p: IPlatformComponentParams<'trovo'>) { const trSettings = p.value; @@ -21,7 +22,7 @@ export function TrovoEditStreamInfo(p: IPlatformComponentParams<'trovo'>) { layoutMode={p.layoutMode} commonFields={ ) { /> } requiredFields={ - +
+ + +
} /> diff --git a/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx b/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx index df015cb7a55a..62ef762104bd 100644 --- a/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx +++ b/app/components-react/windows/go-live/platforms/TwitchEditStreamInfo.tsx @@ -1,5 +1,5 @@ import { CommonPlatformFields } from '../CommonPlatformFields'; -import React, { useMemo } from 'react'; +import React, { memo, useMemo } from 'react'; import { $t } from '../../../../services/i18n'; import { TwitchTagsInput } from './TwitchTagsInput'; import GameSelector from '../GameSelector'; @@ -12,8 +12,8 @@ import TwitchContentClassificationInput from './TwitchContentClassificationInput import AiHighlighterToggle from '../AiHighlighterToggle'; import Badge from 'components-react/shared/DismissableBadge'; import { EDismissable } from 'services/dismissables'; -import styles from './TwitchEditStreamInfo.m.less'; -import cx from 'classnames'; +import { CustomFieldsCheckbox } from '../CustomFieldsCheckbox'; +import Utils from 'services/utils'; export function TwitchEditStreamInfo(p: IPlatformComponentParams<'twitch'>) { const twSettings = p.value; @@ -22,6 +22,51 @@ export function TwitchEditStreamInfo(p: IPlatformComponentParams<'twitch'>) { p.onChange({ ...twSettings, ...patch }); } + return ( +
+ + } + requiredFields={} + optionalFields={} + /> + + ); +} + +const TwitchRequiredFields = memo((p: IPlatformComponentParams<'twitch'>) => { + const bind = createBinding(p.value, updatedSettings => + p.onChange({ ...p.value, ...updatedSettings }), + ); + + return ( + <> +
+ + +
+ {p.isAiHighlighterEnabled && ( + + )} + + ); +}); + +const TwitchOptionalFields = memo((p: IPlatformComponentParams<'twitch'>) => { + const twSettings = p.value; + function updateSettings(patch: Partial) { + p.onChange({ ...twSettings, ...patch }); + } + const bind = createBinding(twSettings, updatedSettings => updateSettings(updatedSettings)); const isDualStream = useMemo(() => { @@ -45,6 +90,7 @@ export function TwitchEditStreamInfo(p: IPlatformComponentParams<'twitch'>) { const enhancedBroadcastingEnabled = useMemo(() => { if (isDualStream) return true; + if (Utils.isPreview()) return true; if (multiplePlatformEnabled) return false; if (p.isStreamShiftMode) return false; return twSettings?.isEnhancedBroadcasting; @@ -55,63 +101,38 @@ export function TwitchEditStreamInfo(p: IPlatformComponentParams<'twitch'>) { p.isStreamShiftMode, ]); - const optionalFields = ( -
- + return ( + <> - - - - {process.platform !== 'darwin' && ( - - - +
+ + - )} -
- ); - - return ( -
- - } - requiredFields={ - - - {p.isAiHighlighterEnabled && ( - - )} - - } - optionalFields={optionalFields} - /> - + + {process.platform !== 'darwin' && ( + + + } + /> + + + )} +
+ ); -} +}); diff --git a/app/components-react/windows/go-live/platforms/TwitchTagsInput.tsx b/app/components-react/windows/go-live/platforms/TwitchTagsInput.tsx index 185d97db2c31..b202a5ff95c9 100644 --- a/app/components-react/windows/go-live/platforms/TwitchTagsInput.tsx +++ b/app/components-react/windows/go-live/platforms/TwitchTagsInput.tsx @@ -58,6 +58,7 @@ export function TwitchTagsInput(p: TTwitchTagsInputProps) { dropdownStyle={{ display: 'none' }} layout={p.layout} size="large" + style={{ flex: 1 }} /> ); } diff --git a/app/components-react/windows/go-live/platforms/TwitterEditStreamInfo.tsx b/app/components-react/windows/go-live/platforms/TwitterEditStreamInfo.tsx index 91fc6e4c0b5f..cdc4d70e31ad 100644 --- a/app/components-react/windows/go-live/platforms/TwitterEditStreamInfo.tsx +++ b/app/components-react/windows/go-live/platforms/TwitterEditStreamInfo.tsx @@ -8,6 +8,7 @@ import { ListInput, createBinding } from '../../../shared/inputs'; import Form from '../../../shared/inputs/Form'; import { CommonPlatformFields } from '../CommonPlatformFields'; import { $t } from 'services/i18n'; +import { CustomFieldsCheckbox } from '../CustomFieldsCheckbox'; export function TwitterEditStreamInfo(p: IPlatformComponentParams<'twitter'>) { const twSettings = p.value; @@ -24,7 +25,7 @@ export function TwitterEditStreamInfo(p: IPlatformComponentParams<'twitter'>) { layoutMode={p.layoutMode} commonFields={ ) { /> } requiredFields={ -
+
) { layout={p.layout} size="large" /> +
} /> diff --git a/app/components-react/windows/go-live/platforms/YoutubeEditStreamInfo.tsx b/app/components-react/windows/go-live/platforms/YoutubeEditStreamInfo.tsx index 727db5444702..036d43f811bc 100644 --- a/app/components-react/windows/go-live/platforms/YoutubeEditStreamInfo.tsx +++ b/app/components-react/windows/go-live/platforms/YoutubeEditStreamInfo.tsx @@ -16,8 +16,7 @@ import { IYoutubeStartStreamOptions, YoutubeService } from '../../../../services import PlatformSettingsLayout, { IPlatformComponentParams } from './PlatformSettingsLayout'; import { assertIsDefined } from '../../../../util/properties-type-guards'; import { inject, injectQuery, useModule } from 'slap'; -import styles from './YoutubeEditStreamInfo.m.less'; -import cx from 'classnames'; +import { CustomFieldsCheckbox } from '../CustomFieldsCheckbox'; /*** * Stream Settings for YT @@ -89,7 +88,7 @@ export const YoutubeEditStreamInfo = InputComponent((p: IPlatformComponentParams function renderCommonFields() { return ( +
{!isScheduleMode && ( +
{!isMidStreamMode && ( <> )} - + +
{!isScheduleMode && !isMidStreamMode && ( - + + + )} {!isScheduleMode && ( + + + + )} + + - )} - + {!isMidStreamMode && ( <> - - - {shouldShowSafeForKidsWarn && ( -

- {$t( - "Features like personalized ads and live chat won't be available on live streams made for kids.", - )} -

- )} + + + + + + {shouldShowSafeForKidsWarn && ( +

+ {$t( + "Features like personalized ads and live chat won't be available on live streams made for kids.", + )} +

+ )} +
)} - +
); } diff --git a/app/components-react/windows/go-live/useGoLiveSettings.ts b/app/components-react/windows/go-live/useGoLiveSettings.ts index 802ab3f24758..992577409811 100644 --- a/app/components-react/windows/go-live/useGoLiveSettings.ts +++ b/app/components-react/windows/go-live/useGoLiveSettings.ts @@ -449,6 +449,14 @@ export class GoLiveSettingsModule { return; } + // Disable AI Highlighter if Twitch is not enabled, because it's a Twitch-only feature + if ( + Services.HighlighterService.aiHighlighterFeatureEnabled && + !this.state.isEnabled('twitch') + ) { + Services.HighlighterService.actions.setAiHighlighter(false); + } + try { await getDefined(this.form).validateFields(); return true; diff --git a/app/components/windows/ChildWindow.tsx b/app/components/windows/ChildWindow.tsx index 3850f3b7283d..857d8fa8cbdf 100644 --- a/app/components/windows/ChildWindow.tsx +++ b/app/components/windows/ChildWindow.tsx @@ -19,6 +19,7 @@ export default class ChildWindow extends Vue { components: IWindowOptions[] = []; private refreshingTimeout: number; private modalOptions: IModalOptions = { renderFn: null }; + private isGoLiveActive = false; unbind: () => void; @@ -62,6 +63,14 @@ export default class ChildWindow extends Vue { @Watch('theme') updateAntd(newTheme: Theme, oldTheme: Theme) { + if (this.isGoLiveActive && this.activeGoLiveTheme) { + // Swap to the Go Live variant matching the new app theme + const newGoLiveTheme = ChildWindow.goLiveThemeMap[newTheme] ?? 'golive-night-theme'; + antdThemes[this.activeGoLiveTheme].unuse(); + antdThemes[newGoLiveTheme].use(); + this.activeGoLiveTheme = newGoLiveTheme; + return; + } antdThemes[oldTheme].unuse(); antdThemes[newTheme].use(); } @@ -92,11 +101,14 @@ export default class ChildWindow extends Vue { window.removeEventListener('resize', this.windowSizeHandler); // If the window was closed, just clear the stack if (!options.isShown) { + this.restoreAppTheme(); this.clearComponentStack(); WindowsService.hideModal(); return; } + this.applyGoLiveThemeIfNeeded(options.componentName); + if (options.preservePrevWindow) { this.handlePreservePrevWindow(options); return; @@ -140,6 +152,36 @@ export default class ChildWindow extends Vue { window.addEventListener('resize', this.windowSizeHandler); } + private static readonly goLiveThemeMap: Record = { + 'night-theme': 'golive-night-theme', + 'day-theme': 'golive-day-theme', + 'prime-dark': 'golive-prime-dark', + 'prime-light': 'golive-prime-light', + }; + + private activeGoLiveTheme: Theme | null = null; + + private applyGoLiveThemeIfNeeded(componentName: string | undefined) { + const isGoLive = componentName === 'GoLiveWindow'; + if (isGoLive && !this.isGoLiveActive) { + const goLiveTheme = ChildWindow.goLiveThemeMap[this.theme] ?? 'golive-night-theme'; + antdThemes[this.theme].unuse(); + antdThemes[goLiveTheme].use(); + this.activeGoLiveTheme = goLiveTheme; + this.isGoLiveActive = true; + } else if (!isGoLive && this.isGoLiveActive) { + this.restoreAppTheme(); + } + } + + private restoreAppTheme() { + if (!this.isGoLiveActive || !this.activeGoLiveTheme) return; + antdThemes[this.activeGoLiveTheme].unuse(); + antdThemes[this.theme].use(); + this.activeGoLiveTheme = null; + this.isGoLiveActive = false; + } + render() { return (
diff --git a/app/i18n/en-US/common.json b/app/i18n/en-US/common.json index 078978ca5136..45134f0ebd38 100644 --- a/app/i18n/en-US/common.json +++ b/app/i18n/en-US/common.json @@ -175,5 +175,6 @@ "Upgrade": "Upgrade", "Edit Data": "Edit Data", "Edit Reactive Data": "Edit Reactive Data", - "Reactive Data Editor": "Reactive Data Editor" + "Reactive Data Editor": "Reactive Data Editor", + "Destinations": "Destinations" } diff --git a/app/i18n/en-US/highlighter.json b/app/i18n/en-US/highlighter.json index e68a1048f7b0..3882b63c5bc4 100644 --- a/app/i18n/en-US/highlighter.json +++ b/app/i18n/en-US/highlighter.json @@ -209,5 +209,7 @@ "YouTube Chapter Markers (for recording)": "YouTube Chapter Markers (for recording)", "Export Markers": "Export Markers", "Timeline starts from 01:00:00 (default)": "Timeline starts from 01:00:00 (default)", - "Export full highlight duration as marker range": "Export full highlight duration as marker range" -} \ No newline at end of file + "Export full highlight duration as marker range": "Export full highlight duration as marker range", + "Replay": "Replay", + "Automatically capture highlights of your game with Replay": "Automatically capture highlights of your game with Replay" +} diff --git a/app/i18n/en-US/settings.json b/app/i18n/en-US/settings.json index 19bbb79a478a..db00e5feb2da 100644 --- a/app/i18n/en-US/settings.json +++ b/app/i18n/en-US/settings.json @@ -289,5 +289,7 @@ "Select Codec": "Select Codec", "%{videoCodec} codec is not supported for Multistream. Would you like to proceed with the H.264 codec or select another codec?": "%{videoCodec} codec is not supported for Multistream. Would you like to proceed with the H.264 codec or select another codec?", "%{videoCodec} codec is not supported for Stream Shift. Would you like to proceed with the H.264 codec or select another codec?": "%{videoCodec} codec is not supported for Stream Shift. Would you like to proceed with the H.264 codec or select another codec?", - "%{videoCodec} codec is not supported for Dual Output. Would you like to proceed with the H.264 codec or select another codec?": "%{videoCodec} codec is not supported for Dual Output. Would you like to proceed with the H.264 codec or select another codec?" + "%{videoCodec} codec is not supported for Dual Output. Would you like to proceed with the H.264 codec or select another codec?": "%{videoCodec} codec is not supported for Dual Output. Would you like to proceed with the H.264 codec or select another codec?", + "Live Settings": "Live Settings", + "Channel Settings": "Channel Settings" } diff --git a/app/i18n/en-US/streaming.json b/app/i18n/en-US/streaming.json index 3ae40c7e4317..6178b58dd685 100644 --- a/app/i18n/en-US/streaming.json +++ b/app/i18n/en-US/streaming.json @@ -339,5 +339,8 @@ "Unlock unlimited multistreaming with Ultra and grow your audience faster": "Unlock unlimited multistreaming with Ultra and grow your audience faster", "Dual Output is enabled - you must stream to one horizontal and one vertical platform": "Dual Output is enabled - you must stream to one horizontal and one vertical platform", "Streaming to a custom ingest is advanced functionality. Multistreaming is disabled while streaming to a custom ingest and some features may stop working as expected. Switch to recommended settings to multistream to a custom RTMP destination.": "Streaming to a custom ingest is advanced functionality. Multistreaming is disabled while streaming to a custom ingest and some features may stop working as expected. Switch to recommended settings to multistream to a custom RTMP destination.", - "%{platform} Error": "%{platform} Error" + "%{platform} Error": "%{platform} Error", + "Manage output destinations mid-stream.": "Manage output destinations mid-stream.", + "Switch between devices while live.": "Switch between devices while live.", + "Live output editing": "Live output editing" } diff --git a/app/i18n/en-US/tiktok.json b/app/i18n/en-US/tiktok.json index a3b22c997cd4..6738e67fb528 100644 --- a/app/i18n/en-US/tiktok.json +++ b/app/i18n/en-US/tiktok.json @@ -29,5 +29,13 @@ "Connect your TikTok account to stream to TikTok and one other platform for free. Haven't applied to stream on TikTok Live yet? Start the process here.": "Connect your TikTok account to stream to TikTok and one other platform for free. Haven't applied to stream on TikTok Live yet? Start the process here.", "Reapply for TikTok Live Permission. Reapply here.": "Reapply for TikTok Live Permission. Reapply here.", "Failed to authenticate with TikTok, re-login or re-merge TikTok account": "Failed to authenticate with TikTok, re-login or re-merge TikTok account", - "Stream Shift Error: TikTok is not live": "Stream Shift Error: TikTok is not live" + "Stream Shift Error: TikTok is not live": "Stream Shift Error: TikTok is not live", + "Streamlabs access enabled": "Streamlabs access enabled", + "Streamlabs Access": "Streamlabs Access", + "Stream with TikTok Stream Key": "Stream with TikTok Stream Key", + "Stream at least 50% gaming content to maintain TikTok streaming access. Learn more": "Stream at least 50% gaming content to maintain TikTok streaming access. Learn more", + "Use your TikTok stream key to stream. Stream Keys are granted by TikTok and renew after each session. See Guide": "Use your TikTok stream key to stream. Stream Keys are granted by TikTok and renew after each session. See Guide", + "Can't find your stream key? Click here": "Can't find your stream key? Click here", + "Request streaming access through Streamlabs": "Request streaming access through Streamlabs", + "Request access to stream without a stream key directly through Streamlabs. You must stream at least 50% gaming content continuously. Learn more": "Request access to stream without a stream key directly through Streamlabs. You must stream at least 50% gaming content continuously. Learn more" } diff --git a/app/services/customization.ts b/app/services/customization.ts index 2097bdf9dd51..2ce735a03da8 100644 --- a/app/services/customization.ts +++ b/app/services/customization.ts @@ -11,7 +11,25 @@ import { RealmObject } from './realm'; import { ObjectSchema } from 'realm'; import { Theme } from 'styles/antd'; -export type TApplicationTheme = 'night-theme' | 'day-theme' | 'prime-dark' | 'prime-light'; +export type TGoLiveTheme = + | 'golive-night-theme' + | 'golive-day-theme' + | 'golive-prime-dark' + | 'golive-prime-light'; + +export type TApplicationTheme = + | 'night-theme' + | 'day-theme' + | 'prime-dark' + | 'prime-light' + | TGoLiveTheme; + +const GO_LIVE_THEME_BACKGROUNDS = { + 'golive-night-theme': { r: 0, g: 0, b: 0 }, + 'golive-day-theme': { r: 255, g: 255, b: 255 }, + 'golive-prime-dark': { r: 17, g: 17, b: 17 }, + 'golive-prime-light': { r: 243, g: 243, b: 243 }, +}; // Maps to --background const THEME_BACKGROUNDS = { @@ -19,6 +37,14 @@ const THEME_BACKGROUNDS = { 'prime-dark': { r: 17, g: 17, b: 17 }, 'day-theme': { r: 245, g: 248, b: 250 }, 'prime-light': { r: 243, g: 243, b: 243 }, + ...GO_LIVE_THEME_BACKGROUNDS, +}; + +const GO_LIVE_SECTION_BACKGROUNDS = { + 'golive-night-theme': { r: 0, g: 0, b: 0 }, + 'golive-day-theme': { r: 255, g: 255, b: 255 }, + 'golive-prime-dark': { r: 37, g: 37, b: 37 }, + 'golive-prime-light': { r: 255, g: 255, b: 255 }, }; // Maps to --section @@ -27,6 +53,14 @@ const SECTION_BACKGROUNDS = { 'prime-dark': { r: 0, g: 0, b: 0 }, 'day-theme': { r: 227, g: 232, b: 235 }, 'prime-light': { r: 255, g: 255, b: 255 }, + ...GO_LIVE_SECTION_BACKGROUNDS, +}; + +const GO_LIVE_DISPLAY_BACKGROUNDS = { + 'golive-night-theme': { r: 0, g: 0, b: 0 }, + 'golive-day-theme': { r: 255, g: 255, b: 255 }, + 'golive-prime-dark': { r: 17, g: 17, b: 17 }, + 'golive-prime-light': { r: 243, g: 243, b: 243 }, }; // Doesn't map 1:1 @@ -35,6 +69,7 @@ const DISPLAY_BACKGROUNDS = { 'prime-dark': { r: 37, g: 37, b: 37 }, 'day-theme': { r: 227, g: 232, b: 235 }, 'prime-light': { r: 255, g: 255, b: 255 }, + ...GO_LIVE_DISPLAY_BACKGROUNDS, }; export interface IPinnedStatistics { diff --git a/app/services/incremental-rollout.ts b/app/services/incremental-rollout.ts index 3d42a1eb6eaa..828e18c0ecc8 100644 --- a/app/services/incremental-rollout.ts +++ b/app/services/incremental-rollout.ts @@ -24,6 +24,7 @@ export enum EAvailableFeatures { streamShift = 'slobs--stream-shift', twitchDualStream = 'slobs--twitch-dual-stream', twitchDualStreamPreview = 'slobs--twitch-dual-stream-preview', + liveOutputEditing = 'slobs--live-output-editing', /** * There are two flags because one is used for beta access and diff --git a/app/services/platforms/tiktok.ts b/app/services/platforms/tiktok.ts index 1317e91d647d..8cca682994ac 100644 --- a/app/services/platforms/tiktok.ts +++ b/app/services/platforms/tiktok.ts @@ -118,7 +118,7 @@ export class TikTokService readonly apiBase = 'https://open.tiktokapis.com/v2'; readonly platform = 'tiktok'; readonly displayName = 'TikTok'; - readonly capabilities = new Set(['title', 'game', 'viewerCount']); + readonly capabilities = new Set(['title', 'game', 'viewerCount', 'chat']); readonly liveDockFeatures = new Set([ 'view-stream', 'dashboard', diff --git a/app/services/platforms/twitch.ts b/app/services/platforms/twitch.ts index 7f4bfef70c93..a33adf204475 100644 --- a/app/services/platforms/twitch.ts +++ b/app/services/platforms/twitch.ts @@ -147,7 +147,11 @@ export class TwitchService 'dualStream', ]); - readonly liveDockFeatures = new Set(['chat-offline', 'refresh-chat']); + readonly liveDockFeatures = new Set([ + 'chat-offline', + 'refresh-chat', + 'chat-streaming', + ]); authWindowOptions: Electron.BrowserWindowConstructorOptions = { width: 600, diff --git a/app/services/streaming/streaming-view.ts b/app/services/streaming/streaming-view.ts index bbe8673c2e43..91900a3fa151 100644 --- a/app/services/streaming/streaming-view.ts +++ b/app/services/streaming/streaming-view.ts @@ -11,12 +11,13 @@ import { UserService } from '../user'; import { RestreamService, TStreamShiftStatus } from '../restream'; import { DualOutputService, TDisplayPlatforms, TDisplayDestinations } from '../dual-output'; import { getPlatformService, TPlatform, TPlatformCapability, platformList } from '../platforms'; -import { TwitchService, TwitterService } from '../../app-services'; +import { IncrementalRolloutService, TwitchService, TwitterService } from '../../app-services'; import cloneDeep from 'lodash/cloneDeep'; import difference from 'lodash/difference'; import { Services } from '../../components-react/service-provider'; import { getDefined } from '../../util/properties-type-guards'; import { TDisplayType } from 'services/settings-v2'; +import { EAvailableFeatures } from 'services/incremental-rollout'; /** * The stream info view is responsible for keeping @@ -53,6 +54,10 @@ export class StreamInfoView extends ViewHandler { return this.getServiceViews(DualOutputService); } + private get incrementalRolloutView() { + return this.getServiceViews(IncrementalRolloutService); + } + private get streamingState() { return Services.StreamingService.state; } @@ -513,17 +518,6 @@ export class StreamInfoView extends ViewHandler { }; } - get isAdvancedMode(): boolean { - return (this.isMultiplatformMode || this.isDualOutputMode) && this.settings.advancedMode; - } - - get canShowAdvancedMode() { - if (this.isStreamShiftMode) { - return this.enabledPlatforms.length > 1; - } - return this.isMultiplatformMode || this.isDualOutputMode; - } - /** * Returns common fields for the stream such as title, description, game */ @@ -777,4 +771,8 @@ export class StreamInfoView extends ViewHandler { get selectiveRecording() { return this.streamingState.selectiveRecording; } + + get canEditLiveOutputs() { + return this.incrementalRolloutView.featureIsEnabled(EAvailableFeatures.liveOutputEditing); + } } diff --git a/app/services/streaming/streaming.ts b/app/services/streaming/streaming.ts index 300e517267ab..dfda5d8c84a0 100644 --- a/app/services/streaming/streaming.ts +++ b/app/services/streaming/streaming.ts @@ -1394,8 +1394,8 @@ export class StreamingService * Prefill fields with data if `prepopulateOptions` provided */ showGoLiveWindow(prepopulateOptions?: IGoLiveSettings['prepopulateOptions']) { - const height = 750; - const width = 800; + const height = 800; + const width = 910; this.windowsService.showWindow({ componentName: 'GoLiveWindow', diff --git a/app/styles/antd/golive-day-theme.lazy.less b/app/styles/antd/golive-day-theme.lazy.less new file mode 100644 index 000000000000..1e5f14fac2e2 --- /dev/null +++ b/app/styles/antd/golive-day-theme.lazy.less @@ -0,0 +1,126 @@ +@import '~antd/dist/antd.less'; +@import '../colors'; +@import './antd.less'; +@import '../go-live.less'; + +// ANTD COLOR MAPPING +@primary-color: @teal-light; +@info-color: @yellow-light; +@success-color: @teal-light; +@processing-color: @teal-light; +@error-color: @red-light; +@highlight-color: @purple-light; +@warning-color: @yellow-light; +@normal-color: @light-5; +@disabled-color: fade(@dark-4, 50%); +@disabled-bg: fade(@dark-5, 50%); + +@primary-1: lighten(@teal-light, 4%); +@primary-2: lighten(@teal-light, 4%); +@primary-5: lighten(@teal-light, 4%); +@primary-7: lighten(@teal-light, 4%); + +@text-color: @dark-4; +@text-color-secondary: @dark-4; +@text-color-dark: @dark-4; +@text-color-secondary-dark: @dark-4; +@icon-color: @light-5; +@icon-color-hover: @light-5; +@heading-color: @dark-2; + +@component-background: @light-2; +@popover-background: @light-2; +@background-color-light: @light-2; +@background-color-base: @light-2; +@border-color-split: @light-4; + +@item-active-bg: darken(@light-1, 4%); +@item-hover-bg: darken(@light-1, 4%); + +@link-color: @dark-2; +@link-hover-color: @dark-2; +@link-active-color: @dark-2; + +@border-color-base: @light-4; +@border-color-split: @light-4; +@border-color-inverse: @light-4; + +@shadow-color: rgba(55, 71, 79, 0.12); +@shadow-color-inverse: rgba(55, 71, 79, 0.12); +@btn-shadow: rgba(55, 71, 79, 0.12); +@btn-primary-shadow: rgba(55, 71, 79, 0.12); +@btn-text-shadow: rgba(55, 71, 79, 0.12); + +@btn-danger-color: @red-light; +@btn-danger-bg: fade(@red-light, 28%); +@btn-text-hover-bg: transparent; +@btn-default-bg: @light-4; + +@radio-button-hover-color: @primary-1; + +@checkbox-check-color: @dark-2; + +@radio-dot-disabled-color: @light-2; +@radio-disabled-button-checked-bg: @light-2; + +@select-multiple-item-disabled-color: @dark-4; +@select-item-selected-color: @white; +@select-dropdown-bg: @light-1; +@select-item-active-bg: @light-3; +@select-item-selected-bg: fade(@teal-light, 12%); +@select-background: @light-2; +@select-clear-background: @light-2; +@select-selection-item-bg: @light-3; +@select-selection-item-border-color: @light-4; + +@slider-rail-background-color: @dark-2; +@slider-rail-background-color-hover: @dark-2; +@slider-track-background-color: @teal-light; +@slider-track-background-color-hover: @teal-light; +@slider-handle-background-color: @light-5; +@slider-handle-color: @light-5; +@slider-handle-color-hover: @light-5; +@slider-handle-color-focus: @light-5; +@slider-handle-color-focus-shadow: rgba(55, 71, 79, 0.12); +@slider-handle-color-tooltip-open: @light-5; +@slider-dot-border-color-active: @light-4; + +@input-placeholder-color: @dark-4; +@input-number-handler-active-bg: @dark-4; +@input-icon-hover-color: @light-5; +@input-disabled-color: @disabled-color; + +@layout-body-background: @light-3; +@layout-header-background: @light-3; +@layout-trigger-background: @light-3; +@layout-trigger-color: @dark-4; +@layout-sider-background-light: @light-3; +@layout-trigger-background-light: @light-3; + +@divider-color: @light-4; + +@tooltip-color: @dark-4; +@tooltip-bg: @light-2; +@tooltip-arrow-width: 4px; + +@modal-mask-bg: rgba(0, 0, 0, 0.3); + +@alert-success-border-color: @teal-light; +@alert-success-bg-color: fade(@teal-light, 8%); +@alert-success-icon-color: @teal-light; +@alert-info-border-color: @yellow-light; +@alert-info-bg-color: fade(@yellow-dark, 8%); +@alert-info-icon-color: @yellow-light; +@alert-warning-border-color: @red-light; +@alert-warning-bg-color: fade(@red-light, 28%); +@alert-warning-icon-color: @red-light; +@alert-error-border-color: @red-light; +@alert-error-bg-color: fade(@red-light, 28%); +@alert-error-icon-color: @red-light; +@alert-message-color: @dark-2; + +@image-bg: transparent; +@image-color: transparent; + +@border-radius-base: 4px; +@font-family: 'Roboto', sans-serif; diff --git a/app/styles/antd/golive-night-theme.lazy.less b/app/styles/antd/golive-night-theme.lazy.less new file mode 100644 index 000000000000..697a9b1e0435 --- /dev/null +++ b/app/styles/antd/golive-night-theme.lazy.less @@ -0,0 +1,126 @@ +@import '~antd/dist/antd.dark.less'; +@import '../colors'; +@import './antd.less'; +@import '../go-live.less'; + +// ANTD COLOR MAPPING +@primary-color: @teal-dark; +@info-color: @yellow-dark; +@success-color: @teal-dark; +@processing-color: @teal-dark; +@error-color: @red-dark; +@highlight-color: @purple-dark; +@warning-color: @yellow-dark; +@normal-color: @dark-5; +@disabled-color: fade(@light-4, 50%); +@disabled-bg: fade(@dark-5, 50%); + +@primary-1: darken(@teal-dark, 4%); +@primary-2: darken(@teal-dark, 4%); +@primary-5: darken(@teal-dark, 4%); +@primary-7: darken(@teal-dark, 4%); + +@text-color: @light-4; +@text-color-secondary: @light-4; +@text-color-dark: @light-4; +@text-color-secondary-dark: @light-4; +@icon-color: @light-5; +@icon-color-hover: @light-5; +@heading-color: @light-1; + +@component-background: @dark-3; +@popover-background: @dark-3; +@background-color-light: @dark-3; +@background-color-base: @dark-3; +@border-color-split: @dark-4; + +@item-active-bg: darken(@dark-4, 4%); +@item-hover-bg: darken(@dark-4, 4%); + +@link-color: @light-1; +@link-hover-color: @light-1; +@link-active-color: @light-1; + +@border-color-base: @dark-5; +@border-color-split: @dark-4; +@border-color-inverse: @dark-4; + +@shadow-color: rgba(1, 2, 2, 0.16); +@shadow-color-inverse: rgba(1, 2, 2, 0.16); +@btn-shadow: rgba(1, 2, 2, 0.16); +@btn-primary-shadow: rgba(1, 2, 2, 0.16); +@btn-text-shadow: rgba(1, 2, 2, 0.16); + +@btn-danger-color: @red-dark; +@btn-danger-bg: fade(@red-dark, 28%); +@btn-text-hover-bg: transparent; +@btn-default-bg: @dark-5; +@btn-primary-color: @dark-2; + +@radio-button-hover-color: @primary-1; + +@checkbox-check-color: @light-1; + +@radio-dot-disabled-color: @dark-3; +@radio-disabled-button-checked-bg: @dark-3; + +@select-multiple-item-disabled-color: @light-4; +@select-item-selected-color: @white; +@select-dropdown-bg: @dark-2; +@select-item-active-bg: @dark-4; +@select-item-selected-bg: fade(@teal-dark, 15%); +@select-background: @dark-3; +@select-clear-background: @dark-3; +@select-selection-item-bg: @dark-4; +@select-selection-item-border-color: @dark-5; + +@slider-rail-background-color: @dark-5; +@slider-rail-background-color-hover: @dark-5; +@slider-track-background-color: @teal-dark; +@slider-track-background-color-hover: @teal-dark; +@slider-handle-background-color: @light-5; +@slider-handle-color: @light-5; +@slider-handle-color-hover: @light-5; +@slider-handle-color-focus: @light-5; +@slider-handle-color-focus-shadow: rgba(1, 2, 2, 0.16); +@slider-handle-color-tooltip-open: @light-5; +@slider-dot-border-color-active: @dark-4; + +// FORM +@label-color: @primelight-4; +@input-placeholder-color: @light-4; +@input-number-handler-active-bg: @light-4; +@input-icon-hover-color: @light-5; +@input-disabled-color: @disabled-color; + +@layout-body-background: @dark-4; +@layout-header-background: @dark-4; +@layout-trigger-background: @dark-4; +@layout-trigger-color: @light-4; +@layout-sider-background-light: @dark-4; +@layout-trigger-background-light: @dark-4; + +@divider-color: @dark-4; + +@tooltip-color: @light-4; +@tooltip-bg: @dark-3; +@tooltip-arrow-width: 4px; + +@modal-mask-bg: rgba(0, 0, 0, 0.5); + +@alert-success-border-color: @teal-dark; +@alert-success-bg-color: fade(@teal-dark, 8%); +@alert-success-icon-color: @teal-dark; +@alert-info-border-color: fade(@yellow-dark, 15%); +@alert-info-bg-color: fade(@yellow-dark, 8%); +@alert-info-icon-color: @yellow-dark; +@alert-warning-border-color: @red-dark; +@alert-warning-bg-color: fade(@red-dark, 28%); +@alert-warning-icon-color: @red-dark; +@alert-error-border-color: @red-dark; +@alert-error-bg-color: fade(@red-dark, 28%); +@alert-error-icon-color: @red-dark; +@alert-message-color: @light-1; + +@border-radius-base: 4px; +@font-family: 'Roboto', sans-serif; diff --git a/app/styles/antd/golive-prime-dark.lazy.less b/app/styles/antd/golive-prime-dark.lazy.less new file mode 100644 index 000000000000..0b0ec5e47d23 --- /dev/null +++ b/app/styles/antd/golive-prime-dark.lazy.less @@ -0,0 +1,128 @@ +@import '~antd/dist/antd.dark.less'; +@import '../colors'; +@import './antd.less'; +@import '../go-live.less'; + +// ANTD COLOR MAPPING +@primary-color: @prime-dark; +@info-color: @prime-dark; +@success-color: @prime-dark; +@processing-color: @prime-dark; +@error-color: @red-dark; +@highlight-color: @prime-dark; +@warning-color: @prime-dark; +@normal-color: @primedark-5; +@disabled-color: fade(@primelight-3, 50%); +@disabled-bg: fade(@primedark-5, 50%); + +@primary-1: darken(@prime-dark, 4%); +@primary-2: darken(@prime-dark, 4%); +@primary-5: darken(@prime-dark, 4%); +@primary-7: darken(@prime-dark, 4%); + +@text-color: @primelight-3; +@text-color-secondary: @primelight-3; +@text-color-dark: @primelight-3; +@text-color-secondary-dark: @primelight-3; +@icon-color: @prime-dark; +@icon-color-hover: @prime-dark; +@heading-color: @primelight-1; + +@component-background: @primedark-3; +@popover-background: @primedark-3; +@background-color-light: @primedark-3; +@background-color-base: @primedark-3; + +@item-active-bg: darken(@primedark-4, 4%); +@item-hover-bg: darken(@primedark-4, 4%); + +@link-color: @primelight-1; +@link-hover-color: @primelight-1; +@link-active-color: @primelight-1; + +@border-color-base: @primelight-5; +@border-color-split: @primedark-4; +@border-color-inverse: @primedark-4; + +@shadow-color: rgba(1, 2, 2, 0.16); +@shadow-color-inverse: rgba(1, 2, 2, 0.16); +@btn-shadow: rgba(1, 2, 2, 0.16); +@btn-primary-shadow: rgba(1, 2, 2, 0.16); +@btn-text-shadow: rgba(1, 2, 2, 0.16); + +@btn-danger-color: @red-dark; +@btn-danger-bg: fade(@red-dark, 28%); +@btn-text-hover-bg: transparent; +@btn-default-bg: @primelight-5; +@btn-primary-color: @primedark-2; + +@radio-button-hover-color: @primary-1; + +@checkbox-check-color: @primelight-1; + +@radio-dot-disabled-color: @primedark-3; +@radio-disabled-button-checked-bg: @primedark-3; + +@select-multiple-item-disabled-color: @primelight-3; +@select-item-selected-color: @white; +@select-dropdown-bg: @primedark-2; +@select-item-active-bg: @primedark-4; +@select-item-selected-bg: fade(@prime-dark, 15%); +@select-background: @primedark-3; +@select-clear-background: @primedark-3; +@select-selection-item-bg: @primedark-4; +@select-selection-item-border-color: @primedark-5; + +@slider-rail-background-color: @prime-dark; +@slider-rail-background-color-hover: @prime-dark; +@slider-track-background-color: @prime-dark; +@slider-track-background-color-hover: @prime-dark; +@slider-handle-background-color: @prime-dark; +@slider-handle-color: @prime-dark; +@slider-handle-color-hover: @prime-dark; +@slider-handle-color-focus: @prime-dark; +@slider-handle-color-focus-shadow: rgba(1, 2, 2, 0.16); +@slider-handle-color-tooltip-open: @prime-dark; +@slider-dot-border-color-active: @primedark-4; + +// FORM +@label-color: @primelight-3; +@input-placeholder-color: @primelight-3; +@input-number-handler-active-bg: @primelight-3; +@input-icon-hover-color: @prime-dark; +@input-disabled-color: @disabled-color; + +@layout-body-background: @primedark-4; +@layout-header-background: @primedark-4; +@layout-trigger-background: @primedark-4; +@layout-trigger-color: @primelight-3; +@layout-sider-background-light: @primedark-4; +@layout-trigger-background-light: @primedark-4; + +@divider-color: @primedark-4; + +@tooltip-color: @primelight-3; +@tooltip-bg: @primedark-3; +@tooltip-arrow-width: 4px; + +@modal-mask-bg: rgba(0, 0, 0, 0.5); + +@alert-success-border-color: @prime-dark; +@alert-success-bg-color: fade(@prime-dark, 8%); +@alert-success-icon-color: @prime-dark; +@alert-info-border-color: @prime-dark; +@alert-info-bg-color: fade(@yellow-dark, 8%); +@alert-info-icon-color: @prime-dark; +@alert-warning-border-color: @red-dark; +@alert-warning-bg-color: fade(@red-dark, 28%); +@alert-warning-icon-color: @red-dark; +@alert-error-border-color: @red-dark; +@alert-error-bg-color: fade(@red-dark, 28%); +@alert-error-icon-color: @red-dark; +@alert-message-color: @primelight-1; + +@image-bg: transparent; +@image-color: transparent; + +@border-radius-base: 4px; +@font-family: 'Roboto', sans-serif; diff --git a/app/styles/antd/golive-prime-light.lazy.less b/app/styles/antd/golive-prime-light.lazy.less new file mode 100644 index 000000000000..54b505b911cc --- /dev/null +++ b/app/styles/antd/golive-prime-light.lazy.less @@ -0,0 +1,126 @@ +@import '~antd/dist/antd.less'; +@import '../colors'; +@import './antd.less'; +@import '../go-live.less'; + +// ANTD COLOR MAPPING +@primary-color: @prime-light; +@info-color: @prime-light; +@success-color: @prime-light; +@processing-color: @prime-light; +@error-color: @red-light; +@highlight-color: @prime-light; +@warning-color: @prime-light; +@normal-color: @primelight-5; +@disabled-color: fade(@primedark-4, 50%); +@disabled-bg: fade(@primelight-3, 50%); + +@primary-1: lighten(@prime-light, 4%); +@primary-2: lighten(@prime-light, 4%); +@primary-5: lighten(@prime-light, 4%); +@primary-7: lighten(@prime-light, 4%); + +@text-color: @primedark-4; +@text-color-secondary: @primedark-4; +@text-color-dark: @primedark-4; +@text-color-secondary-dark: @primedark-4; +@icon-color: @prime-light; +@icon-color-hover: @prime-light; +@heading-color: @primedark-2; + +@component-background: @primelight-1; +@popover-background: @primelight-1; +@background-color-light: @primelight-1; +@background-color-base: @primelight-1; +@border-color-split: @primelight-4; + +@item-active-bg: darken(@primelight-1, 4%); +@item-hover-bg: darken(@primelight-1, 4%); + +@link-color: @primedark-2; +@link-hover-color: @primedark-2; +@link-active-color: @primedark-2; + +@border-color-base: @primelight-4; +@border-color-split: @primelight-4; +@border-color-inverse: @primelight-4; + +@shadow-color: rgba(55, 71, 79, 0.12); +@shadow-color-inverse: rgba(55, 71, 79, 0.12); +@btn-shadow: rgba(55, 71, 79, 0.12); +@btn-primary-shadow: rgba(55, 71, 79, 0.12); +@btn-text-shadow: rgba(55, 71, 79, 0.12); + +@btn-danger-color: @red-light; +@btn-danger-bg: fade(@red-light, 28%); +@btn-text-hover-bg: transparent; +@btn-default-bg: @primelight-3; + +@radio-button-hover-color: @primary-1; + +@checkbox-check-color: @primedark-2; + +@radio-dot-disabled-color: @primelight-1; +@radio-disabled-button-checked-bg: @primelight-1; + +@select-multiple-item-disabled-color: @primedark-4; +@select-item-selected-color: @white; +@select-dropdown-bg: @primelight-2; +@select-item-active-bg: @primelight-3; +@select-item-selected-bg: fade(@prime-light, 12%); +@select-background: @primelight-1; +@select-clear-background: @primelight-1; +@select-selection-item-bg: @primelight-3; +@select-selection-item-border-color: @primelight-4; + +@slider-rail-background-color: @prime-light; +@slider-rail-background-color-hover: @prime-light; +@slider-track-background-color: @prime-light; +@slider-track-background-color-hover: @prime-light; +@slider-handle-background-color: @prime-light; +@slider-handle-color: @prime-light; +@slider-handle-color-hover: @prime-light; +@slider-handle-color-focus: @prime-light; +@slider-handle-color-focus-shadow: rgba(55, 71, 79, 0.12); +@slider-handle-color-tooltip-open: @prime-light; +@slider-dot-border-color-active: @primelight-4; + +@input-placeholder-color: @primedark-4; +@input-number-handler-active-bg: @primedark-4; +@input-icon-hover-color: @prime-light; +@input-disabled-color: @disabled-color; + +@layout-body-background: @primelight-3; +@layout-header-background: @primelight-3; +@layout-trigger-background: @primelight-3; +@layout-trigger-color: @primedark-4; +@layout-sider-background-light: @primelight-3; +@layout-trigger-background-light: @primelight-3; + +@divider-color: @primelight-4; + +@tooltip-color: @primedark-4; +@tooltip-bg: @primelight-1; +@tooltip-arrow-width: 4px; + +@modal-mask-bg: rgba(0, 0, 0, 0.5); + +@alert-success-border-color: @prime-light; +@alert-success-bg-color: fade(@prime-light, 8%); +@alert-success-icon-color: @prime-light; +@alert-info-border-color: @prime-light; +@alert-info-bg-color: fade(@yellow-dark, 8%); +@alert-info-icon-color: @prime-light; +@alert-warning-border-color: @red-light; +@alert-warning-bg-color: fade(@red-light, 28%); +@alert-warning-icon-color: @red-light; +@alert-error-border-color: @red-light; +@alert-error-bg-color: fade(@red-light, 28%); +@alert-error-icon-color: @red-light; +@alert-message-color: @primedark-2; + +@image-bg: transparent; +@image-color: transparent; + +@border-radius-base: 4px; +@font-family: 'Roboto', sans-serif; diff --git a/app/styles/antd/index.ts b/app/styles/antd/index.ts index c666b01b23ea..0ecc2d5254f9 100644 --- a/app/styles/antd/index.ts +++ b/app/styles/antd/index.ts @@ -2,12 +2,20 @@ import antdNightTheme from './night-theme.lazy.less'; import antdDayTheme from './day-theme.lazy.less'; import antdPrimeDark from './prime-dark.lazy.less'; import antdPrimeLight from './prime-light.lazy.less'; +import antdGoLiveNightTheme from './golive-night-theme.lazy.less'; +import antdGoLiveDayTheme from './golive-day-theme.lazy.less'; +import antdGoLivePrimeDark from './golive-prime-dark.lazy.less'; +import antdGoLivePrimeLight from './golive-prime-light.lazy.less'; const themes = { ['night-theme']: antdNightTheme, ['day-theme']: antdDayTheme, ['prime-dark']: antdPrimeDark, ['prime-light']: antdPrimeLight, + ['golive-night-theme']: antdGoLiveNightTheme, + ['golive-day-theme']: antdGoLiveDayTheme, + ['golive-prime-dark']: antdGoLivePrimeDark, + ['golive-prime-light']: antdGoLivePrimeLight, }; export type Theme = keyof typeof themes; diff --git a/app/styles/colors.less b/app/styles/colors.less index cf1fbca5149b..f72ac2e5cba4 100644 --- a/app/styles/colors.less +++ b/app/styles/colors.less @@ -1,64 +1,66 @@ // LESS Color Varaibles -- DO NOT REFERENCE DIRECTLY // Brand colors-- postfix indicates with which type of UI the color should be used with -@teal-dark: #80F5D2; +@teal-dark: #80f5d2; @teal-light: #128079; -@red-dark: #F85640; -@red-light: #B14334; -@yellow-dark: #E3973E; -@yellow-light: #A96311; -@blue-dark: #36ADE0; -@blue-light: #2B5BD7; -@blue-2-dark: #72D5FF; -@cyan: #00F2EA; -@pink-dark: #EB7777; -@pink-light: #C22571; -@purple-dark: #C57BFF; -@purple-light: #5E3BEC; -@lavender-dark: #BE99FF; -@lavender-light: #5E3BEC; -@navy: #233A4A; -@dark-navy: #14252E; -@prime-dark: #CAA368; -@prime-light: #8F6F3F; +@red-dark: #f85640; +@red-light: #b14334; +@yellow-dark: #e3973e; +@yellow-light: #a96311; +@blue-dark: #36ade0; +@blue-light: #2b5bd7; +@blue-2-dark: #72d5ff; +@cyan: #00f2ea; +@pink-dark: #eb7777; +@pink-light: #c22571; +@purple-dark: #c57bff; +@purple-light: #5e3bec; +@lavender-dark: #be99ff; +@lavender-light: #5e3bec; +@navy: #233a4a; +@navy-dark: #14252e; +@navy-light: #2d373f; +@prime-dark: #caa368; +@prime-light: #8f6f3f; @black: #000; -@white: #FFF; +@white: #fff; +@ultra: linear-gradient(123.53deg, #2de8b0 0%, #cbe953 48.8%, #ffab48 75.86%, #ff5151 100%); -@video-editor: #FF5151; -@podcast-editor: #5EE57C; -@link-space: #FF9B4A; -@talk-studio: #45D2FF; -@cross-clip: #FF50A4; +@video-editor: #ff5151; +@podcast-editor: #5ee57c; +@link-space: #ff9b4a; +@talk-studio: #45d2ff; +@cross-clip: #ff50a4; @light-1: #ffffff; -@light-2: #F5F8FA; -@light-3: #E3E8EB; -@light-4: #BDC2C4; -@light-5: #91979A; +@light-2: #f5f8fa; +@light-3: #e3e8eb; +@light-4: #bdc2c4; +@light-5: #91979a; -@dark-1: #050D12; -@dark-2: #09161D; -@dark-3: #17242D; -@dark-4: #2B383F; -@dark-5: #4F5E65; +@dark-1: #050d12; +@dark-2: #09161d; +@dark-3: #17242d; +@dark-4: #2b383f; +@dark-5: #4f5e65; -@primedark-1: #0D0D0D; +@primedark-1: #0d0d0d; @primedark-2: #000000; @primedark-3: #111111; @primedark-4: #252525; @primedark-5: #969696; -@primelight-1: #F3F3F3; -@primelight-2: #FFFFFF; -@primelight-3: #D3D3D3; -@primelight-4: #C3C4C6; +@primelight-1: #f3f3f3; +@primelight-2: #ffffff; +@primelight-3: #d3d3d3; +@primelight-4: #c3c4c6; @primelight-5: #767676; -@twitch: #9146FF; -@facebook: #1778F2; -@youtube: #FF0000; -@twitter: #1DA1F2; +@twitch: #9146ff; +@facebook: #1778f2; +@youtube: #ff0000; +@twitter: #1da1f2; @tiktok: white; -@trovo: #19D06D; -@kick: #54FC1F; +@trovo: #19d06d; +@kick: #54fc1f; @instagram: white; diff --git a/app/styles/go-live.less b/app/styles/go-live.less new file mode 100644 index 000000000000..1dd68001a6fa --- /dev/null +++ b/app/styles/go-live.less @@ -0,0 +1,255 @@ +// GO LIVE WINDOW STYLE +h2 { + color: var(--title); + font-size: 16px; + font-weight: 500; + margin-bottom: 16px; + letter-spacing: 0.3px; +} + +h3 { + letter-spacing: 0.3px; + margin-bottom: 0px; +} + +// ADDITIONAL STYLES +.ant-modal-footer { + background-color: var(--section); +} + +// Form inputs +.ant-row.ant-form-item { + margin-top: 8px; + margin-bottom: 0px; +} + +label.ant-form-item-no-colon { + font-size: 12px; +} + +label.ant-form-item-required { + flex-direction: row-reverse; +} + +label.ant-form-item-required::before { + margin-left: 4px; +} + +.ant-form-item-label > label { + height: unset; +} + +.ant-input-group-addon { + background-color: var(--button); +} + +// Checkbox + +label.ant-checkbox-wrapper { + font-size: 13px; +} + +.ant-checkbox-inner { + background-color: var(--section); +} + +.ant-checkbox-checked .ant-checkbox-inner::after { + border: 2px solid @dark-2; + border-top: 0; + border-left: 0; +} + +.ant-checkbox-checked .ant-checkbox-inner { + background-color: var(--secondary-checkbox); + border-color: var(--secondary-checkbox); +} + +.ant-checkbox-input:focus + .ant-checkbox-inner { + color: var(--secondary-checkbox); +} + +.ant-checkbox-wrapper:hover .ant-checkbox-inner, +.ant-checkbox:hover .ant-checkbox-inner { + border-color: var(--teal) !important; +} + +.ant-select-item-option-selected:not(.ant-select-item-option-disabled) { + color: @dark-2; +} + +.ant-select:not(.ant-select-customize-input) .ant-select-selector { + background-color: var(--section-alt); +} + +.ant-select-selection-placeholder { + font-size: 14px; +} + +// make button text not disappear on hover +.ant-btn:hover, +.ant-btn:focus { + color: var(--title); +} +.ant-btn-primary:hover, +.ant-btn-primary:focus { + color: @dark-2; +} + +// Select component styling +.ant-select-selection-item { + color: var(--title); +} + +.ant-select-lg { + font-size: 14px; + color: var(--title); +} + +.ant-select-single.ant-select-lg:not(.ant-select-customize-input) .ant-select-selector { + background-color: var(--button); +} + +.ant-select-arrow { + color: @light-5; +} + +.ant-select-dropdown { + background-color: @btn-primary-color; + border: 1px solid @dark-4; +} + +.ant-select-item { + color: @light-4; +} + +.ant-select-item-option-active:not(.ant-select-item-option-disabled) { + background-color: @dark-4; +} + +// Switch component styling +.ant-form-item-control-input-content { + color: var(--card-text); +} + +.ant-switch-checked { + background-color: var(--title); +} + +.ant-switch-handle::before { + background-color: var(--section); +} + +.ant-select-clear { + border-radius: 50%; +} + +.ant-select-item-option-selected:not(.ant-select-item-option-disabled) { + color: var(--card-text); + font-weight: 500; +} + +// Radio +.ant-form-item-control-input { + min-height: 20px; +} + +.ant-radio-group.ant-radio-group-outline { + margin: 0px !important; +} + +// Tabs +.ant-tabs-nav-list { + width: 100%; + + > *:first-child { + border-radius: 8px 0px 0px 8px !important; + } + + > *:nth-last-child(2) { + border-radius: 0px 8px 8px 0px !important; + } +} + +.ant-tabs-tab { + border: 0px !important; + padding: 8px; + transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); + cursor: pointer; + background-color: var(--section-alt); + flex: 1; + justify-content: center; + margin: 0px !important; +} + +.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab-active { + background-color: var(--button); +} + +.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn { + color: var(--title) !important; + font-weight: 500; + text-shadow: unset; +} + +.ant-tabs-nav-operations { + display: none !important; +} + +.ant-tabs-ink-bar { + display: none !important; +} + +// Modal body +.ant-modal-body { + padding: 16px; +} + +// change color for the search box +#mainWrapper .ant-select-selector input { + color: @light-4; // var(--paragraph) +} + +// scrollbar +.os-scrollbar, +::-webkit-scrollbar { + width: 4px; + height: 4px; + + &:hover, + &:active { + width: 6px; + } +} + +.section { + background-color: var(--card); + padding: 8px; + border-radius: 8px; + color: var(--paragraph); + font-size: 12px; + margin-bottom: 16px; +} + +.flex__horizontal { + display: flex; + flex-direction: row; + align-items: flex-start; + flex-wrap: wrap; + + &.margin > * { + flex: 1; + margin-top: 0px; + margin-left: 8px; + margin-right: 8px; + } + + &.margin > *:last-child { + margin-top: 0px; + margin-right: 0px; + } + + &.margin > *:first-child { + margin-top: 0px; + margin-left: 0px; + } +} diff --git a/app/themes.g.less b/app/themes.g.less index 7ffd3dfa0418..24b68d55b09c 100644 --- a/app/themes.g.less +++ b/app/themes.g.less @@ -37,7 +37,7 @@ --prime-hover: darken(@prime-light, 8%); --new-badge: fade(@purple-light, 15%); --new-badge-text: @purple-light; - --info-badge: @dark-navy; + --info-badge: @navy-dark; --info-badge-text: @blue-dark; --beta-text: @purple-light; --badge: @purple-dark; @@ -92,8 +92,11 @@ --secondary-checkbox: @dark-2; --bg-column: @light-2; --card: @light-3; - --card-active: @light-1; + --card-active: @light-5; + --card-text: @dark-3; + --card-title: @primedark-4; --card-disabled: @light-3; + --card-target: @light-3; // 3rd Party Colors --twitch: @twitch; @@ -146,7 +149,7 @@ --prime-hover: lighten(@prime-dark, 8%); --new-badge: fade(@purple-dark, 15%); --new-badge-text: @purple-dark; - --info-badge: @dark-navy; + --info-badge: @navy-dark; --info-badge-text: @blue-dark; --beta-text: @purple-dark; --badge: @purple-dark; @@ -194,7 +197,10 @@ --bg-column: @dark-2; --card: @dark-4; --card-active: @dark-3; + --card-text: @light-3; + --card-title: @primelight-4; --card-disabled: @dark-4; + --card-target: @navy-light; // 3rd Party Colors --tiktok: @black; @@ -231,7 +237,7 @@ --prime-hover: lighten(@prime-dark, 8%); --new-badge: @prime-dark; --new-badge-text: @primelight-1; - --info-badge: @dark-navy; + --info-badge: @navy-dark; --info-badge-text: @blue-dark; --beta-text: @prime-dark; --badge: @purple-dark; @@ -275,9 +281,12 @@ --secondary-bg: @black; --secondary-checkbox: @light-1; --bg-column: @primedark-2; - --card: @primedark-1; + --card: @primedark-2; + --card-text: @primelight-3; + --card-title: @primelight-4; --card-active: @primedark-4; --card-disabled: @primedark-1; + --card-target: @primedark-2; --ultra: @ultra; --ultra-no-red: @ultra-no-red; @@ -316,7 +325,7 @@ --prime-hover: darken(@prime-light, 8%); --new-badge: @prime-light; --new-badge-text: @primelight-1; - --info-badge: @dark-navy; + --info-badge: @navy-dark; --info-badge-text: @blue-dark; --beta-text: @prime-light; --badge: @purple-dark; @@ -361,9 +370,12 @@ --secondary-bg: @primelight-1; --secondary-checkbox: @dark-2; --bg-column: @primelight-1; - --card: @primelight-3; + --card: darken(@primelight-3, 8%); --card-active: @primelight-2; + --card-title: @primedark-4; + --card-text: @primedark-3; --card-disabled: @primelight-2; + --card-target: @primelight-3; --ultra: @ultra-light; --ultra-no-red: @ultra-no-red-light; diff --git a/test/regular/streaming/dual-output.ts b/test/regular/streaming/dual-output.ts index 993186bf4dc4..9a04db621de8 100644 --- a/test/regular/streaming/dual-output.ts +++ b/test/regular/streaming/dual-output.ts @@ -318,7 +318,7 @@ test('Dual Output Go Live Non-Ultra', async t => { // Clean up the dummy account await showSettingsWindow('Stream', async () => { await waitForDisplayed('h2=Stream Destinations'); - await clickWhenDisplayed('[data-name="instagramUnlink"]'); + await clickWhenDisplayed('[data-name="instagramUnlink"]', { timeout: 3000 }); }); // Vertical display is hidden after logging out diff --git a/test/regular/streaming/multistream.ts b/test/regular/streaming/multistream.ts index e6ea97722806..22d43592f4f8 100644 --- a/test/regular/streaming/multistream.ts +++ b/test/regular/streaming/multistream.ts @@ -32,8 +32,12 @@ import { sleep } from '../../helpers/sleep'; // eslint-disable-next-line react-hooks/rules-of-hooks useWebdriver(); -async function enableAllPlatforms() { +async function enableAllPlatforms(skipYoutube: boolean = false) { for (const platform of ['twitch', 'youtube', 'trovo']) { + if (platform === 'youtube' && skipYoutube) { + continue; + } + await fillForm({ [platform]: true }); await sleep(500); await waitForSettingsWindowLoaded(); @@ -47,7 +51,7 @@ async function goLiveWithMultistream() { // YouTube accounts fail for reasons unrelated to the tests. Check for the bypass prompt, which is // shown when setting up a multistream fails, including for errors from YouTube // Try toggling off YouTube and going live again - const bypassPrompted = await isDisplayed('button=Bypass and Go Live', { timeout: 2000 }); + const bypassPrompted = await isDisplayed('button=Bypass and Go Live', { timeout: 5000 }); if (bypassPrompted) { await clickButton('Close'); @@ -70,30 +74,31 @@ async function goLiveWithStreamShift(t: TExecutionContext, multistream: boolean) await waitForSettingsWindowLoaded(); if (multistream) { - await enableAllPlatforms(); + // Skip streaming to due to limited accounts + await enableAllPlatforms(true); await waitForSettingsWindowLoaded(); await fillForm({ title: 'Test stream', - description: 'Test stream description', twitchGame: 'Fortnite', trovoGame: 'Doom', streamShift: true, }); - await goLiveWithMultistream(); } else { await fillForm({ twitch: true }); await waitForSettingsWindowLoaded(); await fillForm({ title: 'Test stream', twitchGame: 'Fortnite', streamShift: true }); - await waitForSettingsWindowLoaded(); - await submit(); - await waitForDisplayed('span=Configure the Multistream service', { timeout: 10000 }); - await waitForDisplayed("h1=You're live!", { timeout: 60000 }); - // Confirm chat loads - await waitForStreamStart(); - await focusMain(); - await chatIsVisible(); } + await waitForSettingsWindowLoaded(); + await submit(); + await waitForDisplayed('span=Configure the Multistream service', { timeout: 10000 }); + await waitForDisplayed("h1=You're live!", { timeout: 60000 }); + + // Confirm chat loads + await waitForStreamStart(); + await focusMain(); + await chatIsVisible(); + await stopStream(); await waitForStreamStop(); } @@ -174,7 +179,9 @@ test( }, ); -test( +// The current iteration of the go live window only has one mode, so this test is skipped unless +// the advanced mode is reactivated. +test.skip( 'Multistream advanced mode', withUser('twitch', { prime: true, multistream: true }), async t => {