Skip to content

Commit

Permalink
Merge branch 'master' into staging
Browse files Browse the repository at this point in the history
  • Loading branch information
SLOBS-Release committed Jul 13, 2022
2 parents 56660d3 + 27e8946 commit 65afd0a
Show file tree
Hide file tree
Showing 19 changed files with 346 additions and 360 deletions.
5 changes: 5 additions & 0 deletions app/components-react/editor/elements/SceneSelector.m.less
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@
padding: 8px;
border-radius: 4px;
height: 100%;

.whisper {
font-size: 10px;
opacity: 0.7;
}
}

.dropdown-item {
Expand Down
15 changes: 5 additions & 10 deletions app/components-react/editor/elements/SceneSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,12 @@ function SceneSelector() {

const DropdownMenu = (
<div className={cx(styles.dropdownContainer, 'react')}>
<TextInput
placeholder={$t('Search')}
value={searchQuery}
onChange={setSearchQuery}
nowrap
uncontrolled={false}
/>
<div className="link link--pointer" onClick={manageCollections} style={{ marginTop: '6px' }}>
{$t('Manage All')}
<div className={styles.dropdownItem} onClick={manageCollections} style={{ marginTop: '6px' }}>
<i className="icon-edit" />
{$t('Manage Scene Collections')}
</div>
<hr style={{ borderColor: 'var(--border)' }} />
<span className={styles.whisper}>{$t('Your Scene Collections')}</span>
<Scrollable style={{ height: 'calc(100% - 60px)' }}>
{filteredCollections().map(collection => (
<div
Expand Down Expand Up @@ -188,7 +183,7 @@ function SceneSelector() {
<i className="icon-subtract icon-button icon-button--lg" onClick={removeScene} />
</Tooltip>
<Tooltip title={$t('Edit Scene Transitions.')} placement="bottom">
<i className="icon-settings icon-button icon-button--lg" onClick={showTransitions} />
<i className="icon-transition icon-button icon-button--lg" onClick={showTransitions} />
</Tooltip>
</div>
<Scrollable style={{ height: '100%' }} className={styles.scenesContainer}>
Expand Down
2 changes: 2 additions & 0 deletions app/components-react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import NotificationsArea from './root/NotificationsArea';
import StudioEditor from './root/StudioEditor';
import SharedComponentsLibrary from './windows/sharedComponentsLibrary/SharedComponentsLibrary';
import { ObsSettings } from './windows/settings/ObsSettings';
import ManageSceneCollections from './windows/ManageSceneCollections';
import ThemeAudit from './pages/ThemeAudit';
import { WidgetWindow } from './widgets/common/WidgetWindow';
import SafeMode from './windows/SafeMode';
Expand Down Expand Up @@ -74,6 +75,7 @@ export const components = {
GuestCamProperties,
News,
PerformanceMetrics,
ManageSceneCollections,
PatchNotes,
Display,
TitleBar,
Expand Down
4 changes: 2 additions & 2 deletions app/components-react/modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function alertAsync(p: Omit<ModalFuncProps, 'afterClose'> | string): Prom
*
*/
export function promptAsync(
p: (ModalFuncProps & { placeholder: string }) | string,
p: (ModalFuncProps & { placeholder?: string }) | string,
value: string = '',
): Promise<string> {
const { WindowsService } = Services;
Expand Down Expand Up @@ -142,7 +142,7 @@ export function DefaultPromptForm(

return (
<Form name="prompt" form={form}>
<TextInput name="prompt" value={p.values.prompt} onChange={onChange} />
<TextInput name="prompt" value={p.values.prompt} onChange={onChange} nowrap />
</Form>
);
}
Expand Down
8 changes: 5 additions & 3 deletions app/components-react/root/StudioFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,13 +254,15 @@ function RecordingTimer() {
}));

useEffect(() => {
let recordingTimeout: number | undefined;
if (isRecording) {
const recordingTimeout = window.setTimeout(() => {
recordingTimeout = window.setTimeout(() => {
setRecordingTime(StreamingService.formattedDurationInCurrentRecordingState);
}, 1000);

return () => clearTimeout(recordingTimeout);
} else if (recordingTime) {
setRecordingTime('');
}
return () => clearTimeout(recordingTimeout);
}, [isRecording, recordingTime]);

if (!isRecording) return <></>;
Expand Down
5 changes: 3 additions & 2 deletions app/components-react/shared/ModalLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type TProps = {
hideFooter?: boolean;
scrollable?: boolean;
wrapperStyle?: React.CSSProperties;
} & Pick<ModalProps, 'footer' | 'onOk' | 'okText' | 'bodyStyle' | 'confirmLoading'>;
} & Pick<ModalProps, 'footer' | 'onOk' | 'okText' | 'bodyStyle' | 'confirmLoading' | 'onCancel'>;

// calculate OS dependent styles
const titleHeight = getOS() === OS.Mac ? 22 : 30;
Expand Down Expand Up @@ -60,9 +60,10 @@ export function ModalLayout(p: TProps) {
// render a default footer with action buttons
function DefaultFooter() {
const okText = p.okText || $t('Done');
const closeFunc = p.onCancel || close;
return (
<>
<Button onClick={close}>{$t('Close')}</Button>
<Button onClick={closeFunc}>{$t('Close')}</Button>
{p.onOk && (
<Button onClick={p.onOk} type="primary" disabled={p.confirmLoading}>
{p.confirmLoading && (
Expand Down
91 changes: 91 additions & 0 deletions app/components-react/windows/ManageSceneCollections.m.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
.button-container {
display: grid;
grid-template-columns: 50% 50%;
grid-template-rows: 150px 150px;
gap: 16px;
padding-right: 20px;

.button {
background: var(--button);
color: var(--paragraph);
padding: 8px;

i {
display: block;
font-size: 24px;
margin-right: 0;
}

&:hover {
cursor: pointer;
opacity: 0.8;
color: var(--title);
}
}

.button.lg {
grid-row: 2 / span 1;
grid-column: 1 / span 2;
display: flex;
justify-content: space-evenly;
align-items: center;

img {
width: 140px;
height: auto;
margin-left: 16px;
}
}
}

.collection-node {
display: flex;
padding: 4px;
border-radius: 2px;
border: 1px solid transparent;
align-items: center;
margin-top: 4px;
width: 96%;

i {
margin-right: 4px;
}

span {
overflow: hidden;
display: block;
text-overflow: ellipsis;
white-space: nowrap;
}

.edit-icons {
opacity: 0;
margin-left: auto;

i {
&:hover {
color: var(--title);
}
}
}

&:hover {
cursor: pointer;
border: 1px solid var(--border);

.edit-icons {
opacity: 1;
}
}

&.active {
border: 1px solid var(--teal);
}

.whisper {
opacity: 0.7;
margin-left: 6px;
font-style: italic;
font-size: 11px;
}
}
190 changes: 190 additions & 0 deletions app/components-react/windows/ManageSceneCollections.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import React, { useEffect, useState } from 'react';
import { Layout, Input, Tooltip } from 'antd';
import Fuse from 'fuse.js';
import moment from 'moment';
import cx from 'classnames';
import { ModalLayout } from 'components-react/shared/ModalLayout';
import { Services } from 'components-react/service-provider';
import { confirmAsync, promptAsync } from 'components-react/modals';
import Scrollable from 'components-react/shared/Scrollable';
import { $t } from 'services/i18n';
import { $i } from 'services/utils';
import { ISceneCollectionsManifestEntry } from 'services/scene-collections';
import { byOS, getOS, OS } from 'util/operating-systems';
import styles from './ManageSceneCollections.m.less';
import { TextInput } from 'components-react/shared/inputs';
import { useVuex } from 'components-react/hooks';

const { Sider, Content } = Layout;
const { Search } = Input;

export default function ManageSceneCollections() {
const {
WindowsService,
SceneCollectionsService,
ObsImporterService,
NavigationService,
} = Services;
const [query, setQuery] = useState('');

const { collections } = useVuex(() => ({ collections: SceneCollectionsService.collections }));

function close() {
SceneCollectionsService.stateService.flushManifestFile();
WindowsService.actions.closeChildWindow();
}

async function create() {
const name = await promptAsync(
{ title: $t('Enter a Scene Collection Name'), closable: true },
SceneCollectionsService.suggestName('Scenes'),
);
SceneCollectionsService.actions.create({ name });
}

function importFromObs() {
ObsImporterService.actions.import();
}

function filteredCollections() {
const list = collections.sort((a, b) => (a.modified > b.modified ? -1 : 1));

if (query) {
const fuse = new Fuse(list, { shouldSort: true, keys: ['name'] });
return fuse.search(query);
}

return list;
}

function goToThemes() {
NavigationService.actions.navigate('BrowseOverlays');
WindowsService.actions.closeChildWindow();
}

return (
<ModalLayout onCancel={close}>
<Layout style={{ height: '100%' }}>
<Sider width={300}>
<div>{$t('Your Scene Collections:')}</div>
<div style={{ width: '96%' }}>
<TextInput
placeholder={$t('Search Scene Collections')}
onChange={setQuery}
uncontrolled={false}
addonBefore={<i className="icon-search" />}
nowrap
/>
</div>
<Scrollable style={{ height: 'calc(100% - 32px)' }}>
{filteredCollections().map((collection, i) => (
<CollectionNode collection={collection} recentlyUpdated={i < 2} key={collection.id} />
))}
</Scrollable>
</Sider>
<Content style={{ paddingLeft: '24px' }}>
<div>{$t('Add New Scene Collection:')}</div>
<div className={styles.buttonContainer}>
<button onClick={create} className={cx('button', styles.button)}>
<i className="icon-stream-labels" />
<strong>{$t('New')}</strong>
<p>{$t('Start fresh and build from scratch')}</p>
</button>
<button onClick={importFromObs} className={cx('button', styles.button)}>
<i className="icon-cloud-backup" />
<strong>{$t('Import')}</strong>
<p>{$t('Load existing scenes from OBS')}</p>
</button>
<button onClick={goToThemes} className={cx('button', styles.button, styles.lg)}>
<div>
<strong>{$t('Template')}</strong>
<p>{$t('Choose a template from our theme library')}</p>
</div>
<img src={$i('images/prime-themes.png')} />
</button>
</div>
</Content>
</Layout>
</ModalLayout>
);
}

function CollectionNode(p: {
collection: ISceneCollectionsManifestEntry;
recentlyUpdated: boolean;
}) {
const { SceneCollectionsService } = Services;
const [duplicating, setDuplicating] = useState(false);
const modified = moment(p.collection.modified).fromNow();
const isActive = p.collection.id === SceneCollectionsService.activeCollection?.id;

useEffect(onNeedsRenamedChanged, [p.collection.needsRename]);

function onNeedsRenamedChanged() {
if (p.collection.needsRename) rename();
}

function makeActive() {
if (p.collection.operatingSystem !== getOS()) return;
SceneCollectionsService.actions.load(p.collection.id);
}

function duplicate() {
setDuplicating(true);

setTimeout(() => {
SceneCollectionsService.actions.return
.duplicate(p.collection.name, p.collection.id)
.finally(() => setDuplicating(false));
}, 500);
}

async function rename() {
const newName = await promptAsync(
{ title: $t('Enter a Scene Collection Name'), closable: true },
p.collection.name,
);
SceneCollectionsService.actions.rename(newName, p.collection.id);
}

async function remove() {
const deleteConfirmed = await confirmAsync(
$t('Are you sure you want to remove %{collectionName}?', {
collectionName: p.collection.name,
}),
);
if (deleteConfirmed) SceneCollectionsService.actions.delete(p.collection.id);
}

return (
<div
onDoubleClick={makeActive}
className={cx(styles.collectionNode, { [styles.active]: isActive })}
>
<span>
<i
className={cx(
'fab',
p.collection.operatingSystem === OS.Windows ? 'fa-windows' : 'fa-apple',
)}
/>
{p.collection.name}
</span>
{p.recentlyUpdated && <span className={styles.whisper}>Updated {modified}</span>}
<div className={styles.editIcons}>
<Tooltip title={$t('Rename')}>
<i className="icon-edit" onClick={rename} />
</Tooltip>
{!duplicating && (
<Tooltip title={$t('Duplicate')}>
<i className="icon-copy" onClick={duplicate} />
</Tooltip>
)}
{duplicating && <i className="fa fa-spinner fa-pulse" />}
<Tooltip title={$t('Delete')}>
<i className="icon-trash" onClick={remove} />
</Tooltip>
</div>
</div>
);
}
Loading

0 comments on commit 65afd0a

Please sign in to comment.