mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-22 02:12:30 +01:00
parent
0155d4c567
commit
a32a1a35de
@ -361,7 +361,7 @@ const App = memo(() => {
|
||||
}, [isFileOpened]);
|
||||
|
||||
const {
|
||||
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, onViewSegmentTags, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, createNumSegments, createFixedDurationSegments, createRandomSegments, apparentCutSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, currentApparentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, onSelectSegmentsByTag, toggleSegmentSelected, selectOnlySegment, getApparentCutSegmentById, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment,
|
||||
cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, createNumSegments, createFixedDurationSegments, createRandomSegments, apparentCutSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, currentApparentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, onSelectSegmentsByTag, toggleSegmentSelected, selectOnlySegment, getApparentCutSegmentById, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex,
|
||||
} = useSegments({ filePath, workingRef, setWorking, setCutProgress, mainVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly });
|
||||
|
||||
const jumpSegStart = useCallback((index) => userSeekAbs(apparentCutSegments[index].start), [apparentCutSegments, userSeekAbs]);
|
||||
@ -2418,10 +2418,10 @@ const App = memo(() => {
|
||||
onExtractSegmentFramesAsImages={extractSegmentFramesAsImages}
|
||||
jumpSegStart={jumpSegStart}
|
||||
jumpSegEnd={jumpSegEnd}
|
||||
onViewSegmentTags={onViewSegmentTags}
|
||||
onSelectSegmentsByLabel={onSelectSegmentsByLabel}
|
||||
onSelectSegmentsByTag={onSelectSegmentsByTag}
|
||||
onLabelSelectedSegments={onLabelSelectedSegments}
|
||||
updateSegAtIndex={updateSegAtIndex}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { memo, useMemo, useRef, useCallback } from 'react';
|
||||
import React, { memo, useMemo, useRef, useCallback, useState } from 'react';
|
||||
import { FaYinYang, FaSave, FaPlus, FaMinus, FaTag, FaSortNumericDown, FaAngleRight, FaRegCheckCircle, FaRegCircle } from 'react-icons/fa';
|
||||
import { AiOutlineSplitCells } from 'react-icons/ai';
|
||||
import { motion } from 'framer-motion';
|
||||
@ -7,6 +7,7 @@ import { ReactSortable } from 'react-sortablejs';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import { Dialog } from 'evergreen-ui';
|
||||
|
||||
import Swal from './swal';
|
||||
import useContextMenu from './hooks/useContextMenu';
|
||||
@ -15,6 +16,7 @@ import { saveColor, controlsBackground, primaryTextColor, darkModeTransition } f
|
||||
import { useSegColors } from './contexts';
|
||||
import { mySpring } from './animations';
|
||||
import { getSegmentTags } from './segments';
|
||||
import TagEditor from './components/TagEditor';
|
||||
|
||||
const buttonBaseStyle = {
|
||||
margin: '0 3px', borderRadius: 3, color: 'white', cursor: 'pointer',
|
||||
@ -23,7 +25,7 @@ const buttonBaseStyle = {
|
||||
const neutralButtonColor = 'var(--gray8)';
|
||||
|
||||
|
||||
const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelected, onLabelSelectedSegments, onReorderPress, onLabelPress, selected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onViewSegmentTags, onExtractSegmentFramesAsImages, onInvertSelectedSegments, onDuplicateSegmentClick }) => {
|
||||
const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelected, onLabelSelectedSegments, onReorderPress, onLabelPress, selected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onEditSegmentTags, onExtractSegmentFramesAsImages, onInvertSelectedSegments, onDuplicateSegmentClick }) => {
|
||||
const { t } = useTranslation();
|
||||
const { getSegColor } = useSegColors();
|
||||
|
||||
@ -64,10 +66,10 @@ const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, g
|
||||
|
||||
{ type: 'separator' },
|
||||
|
||||
{ label: t('Segment tags'), click: () => onViewSegmentTags(index) },
|
||||
{ label: t('Segment tags'), click: () => onEditSegmentTags(index) },
|
||||
{ label: t('Extract frames as image files'), click: () => onExtractSegmentFramesAsImages([seg.segId]) },
|
||||
];
|
||||
}, [invertCutSegments, t, jumpSegStart, jumpSegEnd, addSegment, onLabelPress, onRemovePress, onLabelSelectedSegments, onRemoveSelected, onReorderPress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onInvertSelectedSegments, updateOrder, onViewSegmentTags, index, onExtractSegmentFramesAsImages]);
|
||||
}, [invertCutSegments, t, jumpSegStart, jumpSegEnd, addSegment, onLabelPress, onRemovePress, onLabelSelectedSegments, onRemoveSelected, onReorderPress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onInvertSelectedSegments, updateOrder, onEditSegmentTags, index, onExtractSegmentFramesAsImages]);
|
||||
|
||||
useContextMenu(ref, contextMenuTemplate);
|
||||
|
||||
@ -159,7 +161,7 @@ const SegmentList = memo(({
|
||||
updateSegOrder, updateSegOrders, addSegment, removeCutSegment, onRemoveSelected,
|
||||
onLabelSegment, currentCutSeg, segmentAtCursor, toggleSegmentsList, splitCurrentSegment,
|
||||
selectedSegments, isSegmentSelected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onExtractSegmentFramesAsImages, onLabelSelectedSegments, onInvertSelectedSegments, onDuplicateSegmentClick,
|
||||
jumpSegStart, jumpSegEnd, onViewSegmentTags,
|
||||
jumpSegStart, jumpSegEnd, updateSegAtIndex,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { getSegColor } = useSegColors();
|
||||
@ -269,69 +271,111 @@ const SegmentList = memo(({
|
||||
);
|
||||
}
|
||||
|
||||
const [editingSegmentTagsSegmentIndex, setEditingSegmentTagsSegmentIndex] = useState();
|
||||
const [editingSegmentTags, setEditingSegmentTags] = useState();
|
||||
const [editingTag, setEditingTag] = useState();
|
||||
|
||||
const onTagChange = useCallback((tag, value) => setEditingSegmentTags((existingTags) => ({
|
||||
...existingTags,
|
||||
[tag]: value,
|
||||
})), []);
|
||||
|
||||
const onTagReset = useCallback((tag) => setEditingSegmentTags(({ [tag]: deleted, ...rest }) => rest), []);
|
||||
|
||||
const onEditSegmentTags = useCallback((index) => {
|
||||
setEditingSegmentTagsSegmentIndex(index);
|
||||
setEditingSegmentTags(getSegmentTags(apparentCutSegments[index]));
|
||||
}, [apparentCutSegments]);
|
||||
|
||||
const onSegmentTagsCloseComplete = useCallback(() => {
|
||||
setEditingSegmentTagsSegmentIndex();
|
||||
setEditingSegmentTags();
|
||||
}, []);
|
||||
|
||||
const onSegmentTagsConfirm = useCallback(() => {
|
||||
updateSegAtIndex(editingSegmentTagsSegmentIndex, { tags: editingSegmentTags });
|
||||
onSegmentTagsCloseComplete();
|
||||
}, [editingSegmentTags, editingSegmentTagsSegmentIndex, onSegmentTagsCloseComplete, updateSegAtIndex]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
style={{ width, background: controlsBackground, borderLeft: '1px solid var(--gray7)', color: 'var(--gray11)', transition: darkModeTransition, display: 'flex', flexDirection: 'column', overflowY: 'hidden' }}
|
||||
initial={{ x: width }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: width }}
|
||||
transition={mySpring}
|
||||
>
|
||||
<div style={{ fontSize: 14, padding: '0 5px', color: 'var(--gray12)' }} className="no-user-select">
|
||||
<FaAngleRight
|
||||
title={t('Close sidebar')}
|
||||
size={20}
|
||||
style={{ verticalAlign: 'middle', color: 'var(--gray11)', cursor: 'pointer', padding: 2 }}
|
||||
role="button"
|
||||
onClick={toggleSegmentsList}
|
||||
/>
|
||||
<>
|
||||
<Dialog
|
||||
title={t('Edit segment tags')}
|
||||
isShown={editingSegmentTagsSegmentIndex != null}
|
||||
hasCancel={false}
|
||||
isConfirmDisabled={editingTag != null}
|
||||
confirmLabel={t('Save')}
|
||||
onConfirm={onSegmentTagsConfirm}
|
||||
onCloseComplete={onSegmentTagsCloseComplete}
|
||||
>
|
||||
<div style={{ color: 'black' }}>
|
||||
<TagEditor customTags={editingSegmentTags} editingTag={editingTag} setEditingTag={setEditingTag} onTagChange={onTagChange} onTagReset={onTagReset} addTagTitle={t('Add segment tag')} addTagText={t('Enter tag key')} />
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
{header}
|
||||
</div>
|
||||
<motion.div
|
||||
style={{ width, background: controlsBackground, borderLeft: '1px solid var(--gray7)', color: 'var(--gray11)', transition: darkModeTransition, display: 'flex', flexDirection: 'column', overflowY: 'hidden' }}
|
||||
initial={{ x: width }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: width }}
|
||||
transition={mySpring}
|
||||
>
|
||||
<div style={{ fontSize: 14, padding: '0 5px', color: 'var(--gray12)' }} className="no-user-select">
|
||||
<FaAngleRight
|
||||
title={t('Close sidebar')}
|
||||
size={20}
|
||||
style={{ verticalAlign: 'middle', color: 'var(--gray11)', cursor: 'pointer', padding: 2 }}
|
||||
role="button"
|
||||
onClick={toggleSegmentsList}
|
||||
/>
|
||||
|
||||
<div style={{ padding: '0 10px', overflowY: 'scroll', flexGrow: 1 }} className="hide-scrollbar">
|
||||
<ReactSortable list={sortableList} setList={setSortableList} disabled={!!invertCutSegments} handle=".segment-handle">
|
||||
{sortableList.map(({ id, seg }, index) => {
|
||||
const selected = !invertCutSegments && isSegmentSelected({ segId: seg.segId });
|
||||
return (
|
||||
<Segment
|
||||
key={id}
|
||||
darkMode={darkMode}
|
||||
seg={seg}
|
||||
index={index}
|
||||
selected={selected}
|
||||
onClick={onSegClick}
|
||||
addSegment={addSegment}
|
||||
onRemoveSelected={onRemoveSelected}
|
||||
onRemovePress={() => removeCutSegment(index)}
|
||||
onReorderPress={() => onReorderSegs(index)}
|
||||
onLabelPress={() => onLabelSegment(index)}
|
||||
jumpSegStart={() => jumpSegStart(index)}
|
||||
jumpSegEnd={() => jumpSegEnd(index)}
|
||||
updateOrder={(dir) => updateSegOrder(index, index + dir)}
|
||||
getFrameCount={getFrameCount}
|
||||
formatTimecode={formatTimecode}
|
||||
currentSegIndex={currentSegIndex}
|
||||
invertCutSegments={invertCutSegments}
|
||||
onSelectSingleSegment={onSelectSingleSegment}
|
||||
onToggleSegmentSelected={onToggleSegmentSelected}
|
||||
onDeselectAllSegments={onDeselectAllSegments}
|
||||
onSelectAllSegments={onSelectAllSegments}
|
||||
onViewSegmentTags={onViewSegmentTags}
|
||||
onSelectSegmentsByLabel={onSelectSegmentsByLabel}
|
||||
onSelectSegmentsByTag={onSelectSegmentsByTag}
|
||||
onExtractSegmentFramesAsImages={onExtractSegmentFramesAsImages}
|
||||
onLabelSelectedSegments={onLabelSelectedSegments}
|
||||
onInvertSelectedSegments={onInvertSelectedSegments}
|
||||
onDuplicateSegmentClick={onDuplicateSegmentClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ReactSortable>
|
||||
</div>
|
||||
{header}
|
||||
</div>
|
||||
|
||||
{segments.length > 0 && renderFooter()}
|
||||
</motion.div>
|
||||
<div style={{ padding: '0 10px', overflowY: 'scroll', flexGrow: 1 }} className="hide-scrollbar">
|
||||
<ReactSortable list={sortableList} setList={setSortableList} disabled={!!invertCutSegments} handle=".segment-handle">
|
||||
{sortableList.map(({ id, seg }, index) => {
|
||||
const selected = !invertCutSegments && isSegmentSelected({ segId: seg.segId });
|
||||
return (
|
||||
<Segment
|
||||
key={id}
|
||||
darkMode={darkMode}
|
||||
seg={seg}
|
||||
index={index}
|
||||
selected={selected}
|
||||
onClick={onSegClick}
|
||||
addSegment={addSegment}
|
||||
onRemoveSelected={onRemoveSelected}
|
||||
onRemovePress={() => removeCutSegment(index)}
|
||||
onReorderPress={() => onReorderSegs(index)}
|
||||
onLabelPress={() => onLabelSegment(index)}
|
||||
jumpSegStart={() => jumpSegStart(index)}
|
||||
jumpSegEnd={() => jumpSegEnd(index)}
|
||||
updateOrder={(dir) => updateSegOrder(index, index + dir)}
|
||||
getFrameCount={getFrameCount}
|
||||
formatTimecode={formatTimecode}
|
||||
currentSegIndex={currentSegIndex}
|
||||
invertCutSegments={invertCutSegments}
|
||||
onSelectSingleSegment={onSelectSingleSegment}
|
||||
onToggleSegmentSelected={onToggleSegmentSelected}
|
||||
onDeselectAllSegments={onDeselectAllSegments}
|
||||
onSelectAllSegments={onSelectAllSegments}
|
||||
onEditSegmentTags={onEditSegmentTags}
|
||||
onSelectSegmentsByLabel={onSelectSegmentsByLabel}
|
||||
onSelectSegmentsByTag={onSelectSegmentsByTag}
|
||||
onExtractSegmentFramesAsImages={onExtractSegmentFramesAsImages}
|
||||
onLabelSelectedSegments={onLabelSelectedSegments}
|
||||
onInvertSelectedSegments={onInvertSelectedSegments}
|
||||
onDuplicateSegmentClick={onDuplicateSegmentClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ReactSortable>
|
||||
</div>
|
||||
|
||||
{segments.length > 0 && renderFooter()}
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1,117 +1,28 @@
|
||||
import React, { memo, useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||
import React, { memo, useState, useMemo, useCallback } from 'react';
|
||||
|
||||
import { FaImage, FaCheckCircle, FaPaperclip, FaVideo, FaVideoSlash, FaFileImport, FaVolumeUp, FaVolumeMute, FaBan, FaFileExport } from 'react-icons/fa';
|
||||
import { GoFileBinary } from 'react-icons/go';
|
||||
import { FiEdit, FiCheck, FiTrash } from 'react-icons/fi';
|
||||
import { MdSubtitles } from 'react-icons/md';
|
||||
import { Checkbox, BookIcon, TextInput, MoreIcon, Position, Popover, Menu, TrashIcon, EditIcon, InfoSignIcon, IconButton, Heading, SortAscIcon, SortDescIcon, Dialog, Button, PlusIcon, ForkIcon, WarningSignIcon } from 'evergreen-ui';
|
||||
import { Checkbox, BookIcon, MoreIcon, Position, Popover, Menu, TrashIcon, EditIcon, InfoSignIcon, IconButton, Heading, SortAscIcon, SortDescIcon, Dialog, Button, ForkIcon, WarningSignIcon } from 'evergreen-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
import AutoExportToggler from './components/AutoExportToggler';
|
||||
import Select from './components/Select';
|
||||
import { askForMetadataKey, showJson5Dialog } from './dialogs';
|
||||
import { showJson5Dialog } from './dialogs';
|
||||
import { formatDuration } from './util/duration';
|
||||
import { getStreamFps } from './ffmpeg';
|
||||
import { deleteDispositionValue } from './util';
|
||||
import { getActiveDisposition, attachedPicDisposition } from './util/streams';
|
||||
import TagEditor from './components/TagEditor';
|
||||
|
||||
|
||||
const activeColor = '#429777';
|
||||
|
||||
const dispositionOptions = ['default', 'dub', 'original', 'comment', 'lyrics', 'karaoke', 'forced', 'hearing_impaired', 'visual_impaired', 'clean_effects', 'attached_pic', 'captions', 'descriptions', 'dependent', 'metadata'];
|
||||
const unchangedDispositionValue = 'llc_disposition_unchanged';
|
||||
|
||||
const TagEditor = memo(({ existingTags, customTags, editingTag, setEditingTag, onTagChange, onTagReset }) => {
|
||||
|
||||
const EditFileDialog = memo(({ editingFile, allFilesMeta, customTagsByFile, setCustomTagsByFile, editingTag, setEditingTag }) => {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef();
|
||||
|
||||
const [editingTagVal, setEditingTagVal] = useState();
|
||||
const [newTag, setNewTag] = useState();
|
||||
|
||||
const mergedTags = useMemo(() => ({ ...existingTags, ...customTags, ...(newTag ? { [newTag]: '' } : {}) }), [customTags, existingTags, newTag]);
|
||||
|
||||
const onResetClick = useCallback(() => {
|
||||
onTagReset(editingTag);
|
||||
setEditingTag();
|
||||
setNewTag();
|
||||
}, [editingTag, onTagReset, setEditingTag]);
|
||||
|
||||
function onEditClick(tag) {
|
||||
if (newTag) {
|
||||
onTagChange(editingTag, editingTagVal);
|
||||
setEditingTag();
|
||||
setNewTag();
|
||||
} else if (editingTag != null) {
|
||||
if (editingTagVal !== existingTags[editingTag]) {
|
||||
onTagChange(editingTag, editingTagVal);
|
||||
setEditingTag();
|
||||
} else { // If not actually changed, no need to update
|
||||
onResetClick();
|
||||
}
|
||||
} else {
|
||||
setEditingTag(tag);
|
||||
setEditingTagVal(mergedTags[tag]);
|
||||
}
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
e.preventDefault();
|
||||
onEditClick();
|
||||
}
|
||||
|
||||
const onAddPress = useCallback(async (e) => {
|
||||
e.preventDefault();
|
||||
e.target.blur();
|
||||
const tag = await askForMetadataKey();
|
||||
if (!tag || Object.keys(mergedTags).includes(tag)) return;
|
||||
setEditingTag(tag);
|
||||
setEditingTagVal('');
|
||||
setNewTag(tag);
|
||||
}, [mergedTags, setEditingTag]);
|
||||
|
||||
useEffect(() => {
|
||||
ref.current?.focus();
|
||||
}, [editingTag]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<table style={{ color: 'black' }}>
|
||||
<tbody>
|
||||
{Object.keys(mergedTags).map((tag) => {
|
||||
const editingThis = tag === editingTag;
|
||||
const Icon = editingThis ? FiCheck : FiEdit;
|
||||
const thisTagCustom = customTags[tag] != null;
|
||||
const thisTagNew = existingTags[tag] == null;
|
||||
|
||||
return (
|
||||
<tr key={tag}>
|
||||
<td style={{ paddingRight: 20, color: thisTagNew ? activeColor : 'rgba(0,0,0,0.6)' }}>{tag}</td>
|
||||
|
||||
<td style={{ paddingTop: 5, paddingBottom: 5 }}>
|
||||
{editingThis ? (
|
||||
<form style={{ display: 'inline' }} onSubmit={onSubmit}>
|
||||
<TextInput ref={ref} placeholder={t('Enter value')} value={editingTagVal || ''} onChange={(e) => setEditingTagVal(e.target.value)} />
|
||||
</form>
|
||||
) : (
|
||||
<span style={{ color: thisTagCustom ? activeColor : undefined, fontWeight: thisTagCustom ? 'bold' : undefined }}>{mergedTags[tag]}</span>
|
||||
)}
|
||||
{(editingTag == null || editingThis) && <Icon title={t('Edit')} role="button" size={17} style={{ paddingLeft: 5, verticalAlign: 'middle', color: activeColor }} onClick={() => onEditClick(tag)} />}
|
||||
{editingThis && <FiTrash title={t('Reset')} role="button" size={18} style={{ paddingLeft: 5, verticalAlign: 'middle' }} onClick={onResetClick} />}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Button style={{ marginTop: 10 }} iconBefore={PlusIcon} onClick={onAddPress}>{t('Add metadata')}</Button>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const EditFileDialog = memo(({ editingFile, allFilesMeta, customTagsByFile, setCustomTagsByFile }) => {
|
||||
const [editingTag, setEditingTag] = useState();
|
||||
|
||||
const { formatData } = allFilesMeta[editingFile];
|
||||
const existingTags = formatData.tags || {};
|
||||
@ -128,7 +39,7 @@ const EditFileDialog = memo(({ editingFile, allFilesMeta, customTagsByFile, setC
|
||||
});
|
||||
}, [editingFile, setCustomTagsByFile]);
|
||||
|
||||
return <TagEditor existingTags={existingTags} customTags={customTags} editingTag={editingTag} setEditingTag={setEditingTag} onTagChange={onTagChange} onTagReset={onTagReset} />;
|
||||
return <TagEditor existingTags={existingTags} customTags={customTags} editingTag={editingTag} setEditingTag={setEditingTag} onTagChange={onTagChange} onTagReset={onTagReset} addTagTitle={t('Add metadata')} addTagText={t('Enter metadata key')} />;
|
||||
});
|
||||
|
||||
const getStreamDispositionsObj = (stream) => ((stream && stream.disposition) || {});
|
||||
@ -208,7 +119,7 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat
|
||||
<Heading>Parameters</Heading>
|
||||
<StreamParametersEditor stream={editingStream} streamParams={streamParams} updateStreamParams={(setter) => updateStreamParams(editingFile, editingStreamId, setter)} />
|
||||
<Heading>Tags</Heading>
|
||||
<TagEditor existingTags={existingTags} customTags={customTags} editingTag={editingTag} setEditingTag={setEditingTag} onTagChange={onTagChange} onTagReset={onTagReset} />
|
||||
<TagEditor existingTags={existingTags} customTags={customTags} editingTag={editingTag} setEditingTag={setEditingTag} onTagChange={onTagChange} onTagReset={onTagReset} addTagTitle={t('Add metadata')} addTagText={t('Enter metadata key')} />
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
@ -387,6 +298,7 @@ const StreamsSelector = memo(({
|
||||
const [editingFile, setEditingFile] = useState();
|
||||
const [editingStream, setEditingStream] = useState();
|
||||
const { t } = useTranslation();
|
||||
const [editingTag, setEditingTag] = useState();
|
||||
|
||||
function getFormatDuration(formatData) {
|
||||
if (!formatData || !formatData.duration) return undefined;
|
||||
@ -507,8 +419,9 @@ const StreamsSelector = memo(({
|
||||
hasCancel={false}
|
||||
confirmLabel={t('Done')}
|
||||
onCloseComplete={() => setEditingFile()}
|
||||
isConfirmDisabled={editingTag != null}
|
||||
>
|
||||
<EditFileDialog editingFile={editingFile} allFilesMeta={allFilesMeta} customTagsByFile={customTagsByFile} setCustomTagsByFile={setCustomTagsByFile} />
|
||||
<EditFileDialog editingFile={editingFile} editingTag={editingTag} setEditingTag={setEditingTag} allFilesMeta={allFilesMeta} customTagsByFile={customTagsByFile} setCustomTagsByFile={setCustomTagsByFile} />
|
||||
</Dialog>
|
||||
|
||||
{editingStream != null && (
|
||||
|
100
src/components/TagEditor.jsx
Normal file
100
src/components/TagEditor.jsx
Normal file
@ -0,0 +1,100 @@
|
||||
import React, { memo, useRef, useState, useMemo, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, TrashIcon, TickIcon, EditIcon, PlusIcon, Button, IconButton } from 'evergreen-ui';
|
||||
|
||||
import { askForMetadataKey } from '../dialogs';
|
||||
|
||||
|
||||
const activeColor = '#429777';
|
||||
|
||||
const emptyObject = {};
|
||||
|
||||
function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editingTag, setEditingTag, onTagChange, onTagReset, addTagTitle, addTagText }) {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef();
|
||||
|
||||
const [editingTagVal, setEditingTagVal] = useState();
|
||||
const [newTag, setNewTag] = useState();
|
||||
|
||||
const mergedTags = useMemo(() => ({ ...existingTags, ...customTags, ...(newTag ? { [newTag]: '' } : {}) }), [customTags, existingTags, newTag]);
|
||||
|
||||
const onResetClick = useCallback(() => {
|
||||
onTagReset(editingTag);
|
||||
setEditingTag();
|
||||
setNewTag();
|
||||
}, [editingTag, onTagReset, setEditingTag]);
|
||||
|
||||
function onEditClick(tag) {
|
||||
if (newTag) {
|
||||
onTagChange(editingTag, editingTagVal);
|
||||
setEditingTag();
|
||||
setNewTag();
|
||||
} else if (editingTag != null) {
|
||||
if (editingTagVal !== existingTags[editingTag]) {
|
||||
onTagChange(editingTag, editingTagVal);
|
||||
setEditingTag();
|
||||
} else { // If not actually changed, no need to update
|
||||
onResetClick();
|
||||
}
|
||||
} else {
|
||||
setEditingTag(tag);
|
||||
setEditingTagVal(mergedTags[tag]);
|
||||
}
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
e.preventDefault();
|
||||
onEditClick();
|
||||
}
|
||||
|
||||
const onAddPress = useCallback(async (e) => {
|
||||
e.preventDefault();
|
||||
e.target.blur();
|
||||
const tag = await askForMetadataKey({ title: addTagTitle, text: addTagText });
|
||||
if (!tag || Object.keys(mergedTags).includes(tag)) return;
|
||||
setEditingTag(tag);
|
||||
setEditingTagVal('');
|
||||
setNewTag(tag);
|
||||
}, [addTagText, addTagTitle, mergedTags, setEditingTag]);
|
||||
|
||||
useEffect(() => {
|
||||
ref.current?.focus();
|
||||
}, [editingTag]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<table style={{ color: 'black' }}>
|
||||
<tbody>
|
||||
{Object.keys(mergedTags).map((tag) => {
|
||||
const editingThis = tag === editingTag;
|
||||
const Icon = editingThis ? TickIcon : EditIcon;
|
||||
const thisTagCustom = customTags[tag] != null;
|
||||
const thisTagNew = existingTags[tag] == null;
|
||||
|
||||
return (
|
||||
<tr key={tag}>
|
||||
<td style={{ paddingRight: 20, color: thisTagNew ? activeColor : 'rgba(0,0,0,0.6)' }}>{tag}</td>
|
||||
|
||||
<td style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{editingThis ? (
|
||||
<form style={{ display: 'inline' }} onSubmit={onSubmit}>
|
||||
<TextInput ref={ref} placeholder={t('Enter value')} value={editingTagVal || ''} onChange={(e) => setEditingTagVal(e.target.value)} />
|
||||
</form>
|
||||
) : (
|
||||
<span style={{ padding: '.5em 0', color: thisTagCustom ? activeColor : undefined, fontWeight: thisTagCustom ? 'bold' : undefined }}>{mergedTags[tag] || `<${t('empty')}>`}</span>
|
||||
)}
|
||||
{(editingTag == null || editingThis) && <IconButton icon={Icon} title={t('Edit')} appearance="minimal" style={{ marginLeft: '.4em' }} onClick={() => onEditClick(tag)} intent={editingThis ? 'success' : 'none'} />}
|
||||
{editingThis && <IconButton icon={TrashIcon} title={thisTagCustom ? t('Delete') : t('Reset')} appearance="minimal" onClick={onResetClick} intent="danger" />}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<Button style={{ marginTop: 10 }} iconBefore={PlusIcon} onClick={onAddPress}>{t('Add metadata')}</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(TagEditor);
|
@ -310,13 +310,13 @@ export async function askForAlignSegments() {
|
||||
};
|
||||
}
|
||||
|
||||
export async function askForMetadataKey() {
|
||||
export async function askForMetadataKey({ title, text }) {
|
||||
const { value } = await Swal.fire({
|
||||
title: i18n.t('Add metadata'),
|
||||
text: i18n.t('Enter metadata key'),
|
||||
title,
|
||||
text,
|
||||
input: 'text',
|
||||
showCancelButton: true,
|
||||
inputPlaceholder: 'metadata_key',
|
||||
inputPlaceholder: 'key',
|
||||
inputValidator: (v) => v.includes('=') && i18n.t('Invalid character(s) found in key'),
|
||||
});
|
||||
return value;
|
||||
@ -525,20 +525,6 @@ export async function selectSegmentsByTagDialog() {
|
||||
return { tagName: value1, tagValue: value2 };
|
||||
}
|
||||
|
||||
export async function showEditableJsonDialog({ text, title, inputLabel, inputValue, inputValidator }) {
|
||||
const { value } = await Swal.fire({
|
||||
input: 'textarea',
|
||||
inputLabel,
|
||||
text,
|
||||
title,
|
||||
inputPlaceholder: JSON5.stringify({ exampleTag: 'Example value' }, null, 2),
|
||||
inputValue,
|
||||
showCancelButton: true,
|
||||
inputValidator,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
export function showJson5Dialog({ title, json }) {
|
||||
const html = (
|
||||
<SyntaxHighlighter language="javascript" style={style} customStyle={{ textAlign: 'left', maxHeight: 300, overflowY: 'auto', fontSize: 14 }}>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useCallback, useRef, useMemo, useState } from 'react';
|
||||
import { useStateWithHistory } from 'react-use/lib/useStateWithHistory';
|
||||
import i18n from 'i18next';
|
||||
import JSON5 from 'json5';
|
||||
import pMap from 'p-map';
|
||||
|
||||
import sortBy from 'lodash/sortBy';
|
||||
@ -10,7 +9,7 @@ import { detectSceneChanges as ffmpegDetectSceneChanges, readFrames, mapTimesToS
|
||||
import { handleError, shuffleArray } from '../util';
|
||||
import { errorToast } from '../swal';
|
||||
import { showParametersDialog } from '../dialogs/parameters';
|
||||
import { createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, showEditableJsonDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, selectSegmentsByTagDialog } from '../dialogs';
|
||||
import { createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, selectSegmentsByTagDialog } from '../dialogs';
|
||||
import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2 } from '../segments';
|
||||
import * as ffmpegParameters from '../ffmpeg-parameters';
|
||||
import { maxSegmentsAllowed } from '../util/constants';
|
||||
@ -273,22 +272,6 @@ export default ({
|
||||
}
|
||||
}, [filePath, mainVideoStream, modifySelectedSegmentTimes, setWorking, workingRef]);
|
||||
|
||||
const onViewSegmentTags = useCallback(async (index) => {
|
||||
const segment = cutSegments[index];
|
||||
function inputValidator(jsonStr) {
|
||||
try {
|
||||
const json = JSON5.parse(jsonStr);
|
||||
if (!(typeof json === 'object' && Object.values(json).every((val) => typeof val === 'string'))) throw new Error();
|
||||
return undefined;
|
||||
} catch (err) {
|
||||
return i18n.t('Invalid JSON');
|
||||
}
|
||||
}
|
||||
const tags = getSegmentTags(segment);
|
||||
const newTagsStr = await showEditableJsonDialog({ title: i18n.t('Segment tags'), text: i18n.t('View and edit segment tags in JSON5 format:'), inputValue: Object.keys(tags).length > 0 ? JSON5.stringify(tags, null, 2) : '', inputValidator });
|
||||
if (newTagsStr != null) updateSegAtIndex(index, { tags: JSON5.parse(newTagsStr) });
|
||||
}, [cutSegments, updateSegAtIndex]);
|
||||
|
||||
const updateSegOrder = useCallback((index, newOrder) => {
|
||||
if (newOrder > cutSegments.length - 1 || newOrder < 0) return;
|
||||
const newSegments = [...cutSegments];
|
||||
@ -465,7 +448,6 @@ export default ({
|
||||
enableSegments(segmentsToEnable);
|
||||
}, [cutSegments, enableSegments]);
|
||||
|
||||
|
||||
const onLabelSelectedSegments = useCallback(async () => {
|
||||
if (selectedSegmentsRaw.length < 1) return;
|
||||
const { name } = selectedSegmentsRaw[0];
|
||||
@ -517,7 +499,6 @@ export default ({
|
||||
combineSelectedSegments,
|
||||
shiftAllSegmentTimes,
|
||||
alignSegmentTimesToKeyframes,
|
||||
onViewSegmentTags,
|
||||
updateSegOrder,
|
||||
updateSegOrders,
|
||||
reorderSegsByStartTime,
|
||||
@ -561,5 +542,6 @@ export default ({
|
||||
toggleSegmentSelected,
|
||||
selectOnlySegment,
|
||||
setCutTime,
|
||||
updateSegAtIndex,
|
||||
};
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user