Skip to content

Commit ba3ed75

Browse files
committed
Merge branch 'master' into preview
2 parents ef5b105 + a23f1f3 commit ba3ed75

File tree

6 files changed

+284
-71
lines changed

6 files changed

+284
-71
lines changed

app/components-react/highlighter/ClipsViewModal.tsx

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Modal, Alert, Button } from 'antd';
99
import ExportModal from 'components-react/highlighter/Export/ExportModal';
1010
import { $t } from 'services/i18n';
1111
import PreviewModal from './PreviewModal';
12+
import RemoveModal from './RemoveModal';
1213

1314
export default function ClipsViewModal({
1415
streamId,
@@ -49,7 +50,7 @@ export default function ClipsViewModal({
4950
trim: '60%',
5051
preview: '700px',
5152
export: 'fit-content',
52-
remove: '400px',
53+
remove: '280px',
5354
}[modal],
5455
);
5556
}
@@ -89,47 +90,15 @@ export default function ClipsViewModal({
8990
/>
9091
)}
9192
{inspectedClip && showModal === 'remove' && (
92-
<RemoveClip
93+
<RemoveModal
94+
key={`remove-${inspectedClip.path}`}
9395
close={closeModal}
9496
clip={inspectedClip}
9597
streamId={streamId}
9698
deleteClip={deleteClip}
99+
removeType={'clip'}
97100
/>
98101
)}
99102
</Modal>
100103
);
101104
}
102-
103-
function RemoveClip(p: {
104-
clip: TClip;
105-
streamId: string | undefined;
106-
close: () => void;
107-
deleteClip: (clipPath: string, streamId: string | undefined) => void;
108-
}) {
109-
const { HighlighterService } = Services;
110-
111-
return (
112-
<div style={{ textAlign: 'center' }}>
113-
<h2>{$t('Remove the clip?')}</h2>
114-
<p>
115-
{$t(
116-
'Are you sure you want to remove the clip? You will need to manually import it again to reverse this action.',
117-
)}
118-
</p>
119-
<Button style={{ marginRight: 8 }} onClick={p.close}>
120-
{$t('Cancel')}
121-
</Button>
122-
<Button
123-
type="primary"
124-
danger
125-
onClick={() => {
126-
HighlighterService.actions.removeClip(p.clip.path, p.streamId);
127-
p.deleteClip(p.clip.path, p.streamId);
128-
p.close();
129-
}}
130-
>
131-
{$t('Remove')}
132-
</Button>
133-
</div>
134-
);
135-
}

app/components-react/highlighter/Export/ExportModal.tsx

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ import { formatSecondsToHMS } from '../ClipPreview';
2626
import { set } from 'lodash';
2727
import PlatformSelect from './Platform';
2828
import cx from 'classnames';
29+
import { getVideoResolution } from 'services/highlighter/cut-highlight-clips';
2930

3031
type TSetting = { name: string; fps: TFPS; resolution: TResolution; preset: TPreset };
3132
const settings: TSetting[] = [
3233
{ name: 'Standard', fps: 30, resolution: 1080, preset: 'fast' },
3334
{ name: 'Best', fps: 60, resolution: 1080, preset: 'slow' },
35+
{ name: 'Fast', fps: 30, resolution: 720, preset: 'ultrafast' },
3436
{ name: 'Custom', fps: 30, resolution: 720, preset: 'ultrafast' },
3537
];
3638
class ExportController {
@@ -60,6 +62,14 @@ class ExportController {
6062
return getCombinedClipsDuration(this.getClips(streamId));
6163
}
6264

65+
async getClipResolution(streamId?: string) {
66+
const firstClipPath = this.getClips(streamId).find(clip => clip.enabled)?.path;
67+
if (!firstClipPath) {
68+
return undefined;
69+
}
70+
return await getVideoResolution(firstClipPath);
71+
}
72+
6373
dismissError() {
6474
return this.service.actions.dismissError();
6575
}
@@ -134,7 +144,7 @@ function ExportModal({ close, streamId }: { close: () => void; streamId: string
134144
// Clear all errors when this component unmounts
135145
useEffect(() => unmount, []);
136146

137-
if (!exportInfo.exported || exportInfo.exporting) {
147+
if (!exportInfo.exported || exportInfo.exporting || exportInfo.error) {
138148
return (
139149
<ExportFlow
140150
isExporting={exportInfo.exporting}
@@ -176,6 +186,7 @@ function ExportFlow({
176186
getClips,
177187
getDuration,
178188
getClipThumbnail,
189+
getClipResolution,
179190
} = useController(ExportModalCtx);
180191

181192
const [currentFormat, setCurrentFormat] = useState<TOrientation>(EOrientation.HORIZONTAL);
@@ -201,14 +212,47 @@ function ExportFlow({
201212
};
202213
}
203214

204-
const [currentSetting, setSetting] = useState<TSetting>(
205-
settingMatcher({
206-
name: 'from default',
207-
fps: exportInfo.fps,
208-
resolution: exportInfo.resolution,
209-
preset: exportInfo.preset,
210-
}),
211-
);
215+
const [currentSetting, setSetting] = useState<TSetting | null>(null);
216+
const [isLoadingResolution, setIsLoadingResolution] = useState(true);
217+
218+
useEffect(() => {
219+
setIsLoadingResolution(true);
220+
221+
async function initializeSettings() {
222+
try {
223+
const resolution = await getClipResolution(streamId);
224+
let setting: TSetting;
225+
if (resolution?.height === 720 && exportInfo.resolution !== 720) {
226+
setting = settings.find(s => s.resolution === 720) || settings[settings.length - 1];
227+
} else if (resolution?.height === 1080 && exportInfo.resolution !== 1080) {
228+
setting = settings.find(s => s.resolution === 1080) || settings[settings.length - 1];
229+
} else {
230+
setting = settingMatcher({
231+
name: 'from default',
232+
fps: exportInfo.fps,
233+
resolution: exportInfo.resolution,
234+
preset: exportInfo.preset,
235+
});
236+
}
237+
238+
setSetting(setting);
239+
} catch (error: unknown) {
240+
console.error('Failed to detect clip resolution, setting default. Error: ', error);
241+
setSetting(
242+
settingMatcher({
243+
name: 'from default',
244+
fps: exportInfo.fps,
245+
resolution: exportInfo.resolution,
246+
preset: exportInfo.preset,
247+
}),
248+
);
249+
} finally {
250+
setIsLoadingResolution(false);
251+
}
252+
}
253+
254+
initializeSettings();
255+
}, [streamId]);
212256

213257
// Video name and export file are kept in sync
214258
const [exportFile, setExportFile] = useState<string>(getExportFileFromVideoName(videoName));
@@ -302,7 +346,6 @@ function ExportFlow({
302346
buttonContent={<i className="icon-edit" />}
303347
/>
304348
</div>
305-
306349
<div
307350
className={cx(styles.thumbnail, isExporting && styles.thumbnailInProgress)}
308351
style={
@@ -341,7 +384,6 @@ function ExportFlow({
341384
}
342385
/>
343386
</div>
344-
345387
<div className={styles.clipInfoWrapper}>
346388
<div
347389
className={cx(isExporting && styles.isDisabled)}
@@ -365,27 +407,33 @@ function ExportFlow({
365407
emitState={format => setCurrentFormat(format)}
366408
/>
367409
</div>
368-
369410
<div
370411
style={{
371412
display: 'flex',
372413
justifyContent: 'space-between',
373414
}}
374415
>
375-
<CustomDropdownWrapper
376-
initialSetting={currentSetting}
377-
disabled={isExporting}
378-
emitSettings={setting => {
379-
setSetting(setting);
380-
if (setting.name !== 'Custom') {
381-
setFps(setting.fps.toString());
382-
setResolution(setting.resolution.toString());
383-
setPreset(setting.preset);
384-
}
385-
}}
386-
/>
416+
{isLoadingResolution ? (
417+
<div className={styles.innerDropdownWrapper}>
418+
<div className={styles.dropdownText}>Loading settings...</div>
419+
<i className="icon-down"></i>
420+
</div>
421+
) : (
422+
<CustomDropdownWrapper
423+
initialSetting={currentSetting!}
424+
disabled={isExporting || isLoadingResolution}
425+
emitSettings={setting => {
426+
setSetting(setting);
427+
if (setting.name !== 'Custom') {
428+
setFps(setting.fps.toString());
429+
setResolution(setting.resolution.toString());
430+
setPreset(setting.preset);
431+
}
432+
}}
433+
/>
434+
)}
387435
</div>
388-
{currentSetting.name === 'Custom' && (
436+
{currentSetting?.name === 'Custom' && (
389437
<div className={`${styles.customSection} ${isExporting ? styles.isDisabled : ''}`}>
390438
<div className={styles.customItemWrapper}>
391439
<p>{$t('Resolution')}</p>
@@ -431,10 +479,8 @@ function ExportFlow({
431479
</div>
432480
</div>
433481
)}
434-
435482
{exportInfo.error && (
436483
<Alert
437-
style={{ marginBottom: 24 }}
438484
message={exportInfo.error}
439485
type="error"
440486
closable
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
.thumbnail {
2+
top: 14px;
3+
left: 15%;
4+
position: absolute;
5+
border-radius: 8px;
6+
overflow: hidden;
7+
max-height: 390px;
8+
margin: 0 auto;
9+
overflow-x: clip;
10+
background: var(--Day-Colors-Dark-4, #4f5e65);
11+
}
12+
13+
.thumbnail-in-progress img {
14+
position: relative;
15+
filter: blur(10px);
16+
}
17+
18+
.thumbnail img {
19+
height: 100%;
20+
}
21+
22+
.thumbnail::before {
23+
content: '';
24+
border-style: solid;
25+
border-width: 1px;
26+
border-color: #ffffff29;
27+
border-radius: 8px;
28+
position: absolute;
29+
width: 100%;
30+
height: 100%;
31+
}
32+
33+
.selected-for-deletion {
34+
position: absolute;
35+
text-decoration: underline;
36+
text-wrap: nowrap;
37+
opacity: 0.3;
38+
bottom: 22px;
39+
transform: translateX(-50%);
40+
left: 50%;
41+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { Button, Checkbox } from 'antd';
2+
import { Services } from 'components-react/service-provider';
3+
import React, { useState } from 'react';
4+
import { TClip } from 'services/highlighter/models/highlighter.models';
5+
import { $t } from 'services/i18n';
6+
import styles from './RemoveModal.m.less';
7+
import { SCRUB_HEIGHT, SCRUB_WIDTH } from 'services/highlighter/constants';
8+
9+
export default function RemoveModal(p: {
10+
removeType: 'clip' | 'stream';
11+
clip: TClip;
12+
streamId: string | undefined;
13+
close: () => void;
14+
deleteClip: (clipPath: string, streamId: string | undefined) => void;
15+
}) {
16+
const { HighlighterService } = Services;
17+
const [deleteAllSelected, setDeleteAllSelected] = useState<boolean>(false);
18+
const [clipsToDelete, setClipsToDelete] = useState<TClip[]>([p.clip]);
19+
20+
function getClipsToDelete(): TClip[] {
21+
return HighlighterService.getClips(HighlighterService.views.clips, p.streamId).filter(
22+
clip => clip.path !== p.clip.path && clip.enabled,
23+
);
24+
}
25+
26+
return (
27+
<div style={{ textAlign: 'center' }}>
28+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
29+
<h2>{p.removeType === 'stream' ? $t('Delete stream') : $t('Delete clips')}</h2>
30+
<Button type="text" onClick={p.close}>
31+
<i className="icon-close" style={{ margin: 0 }}></i>
32+
</Button>
33+
</div>
34+
35+
<div style={{ position: 'relative', height: `${SCRUB_HEIGHT}px`, marginTop: '32px' }}>
36+
{clipsToDelete.slice(0, 3).map((clip, index) => (
37+
<React.Fragment key={clip.path}>
38+
<div
39+
className={styles.thumbnail}
40+
style={{
41+
width: `${SCRUB_WIDTH / 2}px`,
42+
height: `${SCRUB_HEIGHT / 2}px`,
43+
rotate: `${clipsToDelete.length !== 1 ? (index - 1) * 6 : 0}deg`,
44+
scale: '1.2',
45+
transform: `translate(${clipsToDelete.length > 1 ? (index - 1) * 9 : 0}px, ${
46+
index === 1 ? 0 + 4 : 2 + 4
47+
}px)`,
48+
zIndex: index === 1 ? 10 : 0,
49+
}}
50+
>
51+
<img src={clip.scrubSprite} />{' '}
52+
</div>
53+
</React.Fragment>
54+
))}
55+
{clipsToDelete.length > 1 && (
56+
<span className={styles.selectedForDeletion}>
57+
{$t('%{clipsAmountToDelete} clips selected for deletion', {
58+
clipsAmountToDelete: clipsToDelete.length,
59+
})}
60+
</span>
61+
)}
62+
</div>
63+
64+
<div>
65+
<Checkbox
66+
style={{ marginBottom: '24px' }}
67+
checked={deleteAllSelected}
68+
onChange={e => {
69+
setDeleteAllSelected(e.target.checked);
70+
if (e.target.checked) {
71+
setClipsToDelete([...getClipsToDelete(), p.clip]);
72+
} else {
73+
setClipsToDelete([p.clip]);
74+
}
75+
}}
76+
>
77+
{$t('Delete all selected clips')}
78+
</Checkbox>
79+
<div>
80+
<Button
81+
type="primary"
82+
style={{ width: '100%' }}
83+
danger
84+
onClick={() => {
85+
const clipsToDelete: TClip[] = [p.clip];
86+
if (deleteAllSelected) {
87+
const selectedClipsToDelete = HighlighterService.getClips(
88+
HighlighterService.views.clips,
89+
p.streamId,
90+
).filter(clip => clip.path !== p.clip.path && clip.enabled); // prevent adding the same clip twice
91+
clipsToDelete.push(...selectedClipsToDelete);
92+
}
93+
94+
clipsToDelete.forEach(clip => {
95+
HighlighterService.actions.removeClip(clip.path, p.streamId);
96+
});
97+
98+
p.deleteClip(p.clip.path, p.streamId);
99+
p.close();
100+
}}
101+
>
102+
{$t('Delete')}
103+
</Button>
104+
</div>
105+
</div>
106+
</div>
107+
);
108+
}

0 commit comments

Comments
 (0)