Add partial support for segment tags #427 #814

allows importing from cue files
does not support loading/saving
pull/841/head
Mikael Finstad 5 years ago
parent 0e6f69ac1d
commit faf70568dc
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -57,10 +57,10 @@ import {
hasDuplicates, havePermissionToReadFile, isMac, getFileBaseName, hasDuplicates, havePermissionToReadFile, isMac, getFileBaseName,
} from './util'; } from './util';
import { formatDuration } from './util/duration'; import { formatDuration } from './util/duration';
import { askForOutDir, askForImportChapters, createNumSegments, createFixedDurationSegments, promptTimeOffset, askForHtml5ifySpeed, askForYouTubeInput, askForFileOpenAction, confirmExtractAllStreamsDialog, cleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, showMergeDialog, showOpenAndMergeDialog, openAbout } from './dialogs'; import { askForOutDir, askForImportChapters, createNumSegments, createFixedDurationSegments, promptTimeOffset, askForHtml5ifySpeed, askForYouTubeInput, askForFileOpenAction, confirmExtractAllStreamsDialog, cleanupFilesDialog, showDiskFull, showCutFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, showMergeDialog, showOpenAndMergeDialog, openAbout, showJson5Dialog } from './dialogs';
import { openSendReportDialog } from './reporting'; import { openSendReportDialog } from './reporting';
import { fallbackLng } from './i18n'; import { fallbackLng } from './i18n';
import { createSegment, createInitialCutSegments, getCleanCutSegments, getSegApparentStart, findSegmentsAtCursor, sortSegments, invertSegments } from './segments'; import { createSegment, createInitialCutSegments, getCleanCutSegments, getSegApparentStart, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags } from './segments';
import loadingLottie from './7077-magic-flow.json'; import loadingLottie from './7077-magic-flow.json';
@ -370,6 +370,10 @@ const App = memo(() => {
if (value != null) updateSegAtIndex(index, { name: value }); if (value != null) updateSegAtIndex(index, { name: value });
}, [cutSegments, updateSegAtIndex]); }, [cutSegments, updateSegAtIndex]);
const onViewSegmentTagsPress = useCallback((segment) => {
showJson5Dialog({ title: 'Segment tags', json: getSegmentTags(segment) });
}, []);
const updateSegOrder = useCallback((index, newOrder) => { const updateSegOrder = useCallback((index, newOrder) => {
if (newOrder > cutSegments.length - 1 || newOrder < 0) return; if (newOrder > cutSegments.length - 1 || newOrder < 0) return;
const newSegments = [...cutSegments]; const newSegments = [...cutSegments];
@ -899,10 +903,12 @@ const App = memo(() => {
const filenamifyOrNot = useCallback((name) => (safeOutputFileName ? filenamify(name) : name), [safeOutputFileName]); const filenamifyOrNot = useCallback((name) => (safeOutputFileName ? filenamify(name) : name), [safeOutputFileName]);
const generateOutSegFileNames = useCallback(({ segments = enabledOutSegments, template }) => ( const generateOutSegFileNames = useCallback(({ segments = enabledOutSegments, template }) => (
segments.map(({ start, end, name = '' }, i) => { segments.map((segment, i) => {
const { start, end, name = '' } = segment;
const cutFromStr = formatDuration({ seconds: start, fileNameFriendly: true }); const cutFromStr = formatDuration({ seconds: start, fileNameFriendly: true });
const cutToStr = formatDuration({ seconds: end, fileNameFriendly: true }); const cutToStr = formatDuration({ seconds: end, fileNameFriendly: true });
const segNum = i + 1; const segNum = i + 1;
const tags = getSegmentTags(segment);
// https://github.com/mifi/lossless-cut/issues/583 // https://github.com/mifi/lossless-cut/issues/583
let segSuffix = ''; let segSuffix = '';
@ -913,8 +919,8 @@ const App = memo(() => {
const { name: fileNameWithoutExt } = parsePath(filePath); const { name: fileNameWithoutExt } = parsePath(filePath);
const generated = generateSegFileName({ template, segSuffix, inputFileNameWithoutExt: fileNameWithoutExt, ext, segNum, segLabel: filenamifyOrNot(name), cutFrom: cutFromStr, cutTo: cutToStr });
return generated.substr(0, 200); // Just to be sure return generated.substr(0, 200); // Just to be sure
const generated = generateSegFileName({ template, segSuffix, inputFileNameWithoutExt: fileNameWithoutExt, ext, segNum, segLabel: filenamifyOrNot(name), cutFrom: cutFromStr, cutTo: cutToStr, tags });
}) })
), [fileFormat, filePath, isCustomFormatSelected, enabledOutSegments, filenamifyOrNot]); ), [fileFormat, filePath, isCustomFormatSelected, enabledOutSegments, filenamifyOrNot]);
@ -2198,6 +2204,7 @@ const App = memo(() => {
onExportSegmentEnableAll={onExportSegmentEnableAll} onExportSegmentEnableAll={onExportSegmentEnableAll}
jumpSegStart={jumpSegStart} jumpSegStart={jumpSegStart}
jumpSegEnd={jumpSegEnd} jumpSegEnd={jumpSegEnd}
onViewSegmentTagsPress={onViewSegmentTagsPress}
/> />
</motion.div> </motion.div>
)} )}

@ -10,6 +10,7 @@ import isEqual from 'lodash/isEqual';
import useDebounce from 'react-use/lib/useDebounce'; import useDebounce from 'react-use/lib/useDebounce';
import scrollIntoView from 'scroll-into-view-if-needed'; import scrollIntoView from 'scroll-into-view-if-needed';
import { getSegmentTags } from './segments';
import useContextMenu from './hooks/useContextMenu'; import useContextMenu from './hooks/useContextMenu';
import { saveColor } from './colors'; import { saveColor } from './colors';
import { getSegColors } from './util/colors'; import { getSegColors } from './util/colors';
@ -21,7 +22,7 @@ const buttonBaseStyle = {
const neutralButtonColor = 'rgba(255, 255, 255, 0.2)'; const neutralButtonColor = 'rgba(255, 255, 255, 0.2)';
const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onReorderPress, onLabelPress, enabled, onExportSingleSegmentClick, onExportSegmentEnabledToggle, onExportSegmentDisableAll, onExportSegmentEnableAll, jumpSegStart, jumpSegEnd, addCutSegment }) => { const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onReorderPress, onLabelPress, enabled, onExportSingleSegmentClick, onExportSegmentEnabledToggle, onExportSegmentDisableAll, onExportSegmentEnableAll, jumpSegStart, jumpSegEnd, addCutSegment, onViewSegmentTagsPress }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const ref = useRef(); const ref = useRef();
@ -48,6 +49,10 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou
{ label: enabled ? t('Exclude this segment from export') : t('Include this segment in export'), click: () => onExportSegmentEnabledToggle(seg) }, { label: enabled ? t('Exclude this segment from export') : t('Include this segment in export'), click: () => onExportSegmentEnabledToggle(seg) },
{ label: t('Include all segments in export'), click: () => onExportSegmentEnableAll(seg) }, { label: t('Include all segments in export'), click: () => onExportSegmentEnableAll(seg) },
{ label: t('Exclude all segments from export'), click: () => onExportSegmentDisableAll(seg) }, { label: t('Exclude all segments from export'), click: () => onExportSegmentDisableAll(seg) },
{ type: 'separator' },
{ label: t('View tags'), enabled: Object.keys(getSegmentTags(seg)).length > 0, click: () => onViewSegmentTagsPress(seg) },
]); ]);
const duration = seg.end - seg.start; const duration = seg.end - seg.start;
@ -120,7 +125,7 @@ const SegmentList = memo(({
updateSegOrder, updateSegOrders, addCutSegment, removeCutSegment, updateSegOrder, updateSegOrders, addCutSegment, removeCutSegment,
onLabelSegmentPress, currentCutSeg, segmentAtCursor, toggleSideBar, splitCurrentSegment, onLabelSegmentPress, currentCutSeg, segmentAtCursor, toggleSideBar, splitCurrentSegment,
enabledOutSegments, enabledOutSegmentsRaw, onExportSingleSegmentClick, onExportSegmentEnabledToggle, onExportSegmentDisableAll, onExportSegmentEnableAll, enabledOutSegments, enabledOutSegmentsRaw, onExportSingleSegmentClick, onExportSegmentEnabledToggle, onExportSegmentDisableAll, onExportSegmentEnableAll,
jumpSegStart, jumpSegEnd, simpleMode, jumpSegStart, jumpSegEnd, simpleMode, onViewSegmentTagsPress,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -266,6 +271,7 @@ const SegmentList = memo(({
onExportSegmentEnabledToggle={onExportSegmentEnabledToggle} onExportSegmentEnabledToggle={onExportSegmentEnabledToggle}
onExportSegmentDisableAll={onExportSegmentDisableAll} onExportSegmentDisableAll={onExportSegmentDisableAll}
onExportSegmentEnableAll={onExportSegmentEnableAll} onExportSegmentEnableAll={onExportSegmentEnableAll}
onViewSegmentTagsPress={onViewSegmentTagsPress}
/> />
); );
})} })}

@ -4,20 +4,13 @@ import { FaVideo, FaVideoSlash, FaFileExport, FaFileImport, FaVolumeUp, FaVolume
import { GoFileBinary } from 'react-icons/go'; import { GoFileBinary } from 'react-icons/go';
import { FiEdit, FiCheck, FiTrash } from 'react-icons/fi'; import { FiEdit, FiCheck, FiTrash } from 'react-icons/fi';
import { MdSubtitles } from 'react-icons/md'; import { MdSubtitles } from 'react-icons/md';
import Swal from 'sweetalert2';
import { SegmentedControl, Dialog, Button } from 'evergreen-ui'; import { SegmentedControl, Dialog, Button } from 'evergreen-ui';
import withReactContent from 'sweetalert2-react-content';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { tomorrow as style } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import JSON5 from 'json5';
import { askForMetadataKey } from './dialogs'; import { askForMetadataKey, showJson5Dialog } from './dialogs';
import { formatDuration } from './util/duration'; import { formatDuration } from './util/duration';
import { getStreamFps } from './ffmpeg'; import { getStreamFps } from './ffmpeg';
const ReactSwal = withReactContent(Swal);
const activeColor = '#9f5f80'; const activeColor = '#9f5f80';
@ -159,18 +152,8 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat
return <TagEditor existingTags={existingTags} customTags={customTags} onTagChange={onTagChange} onTagReset={onTagReset} />; return <TagEditor existingTags={existingTags} customTags={customTags} onTagChange={onTagChange} onTagReset={onTagReset} />;
}); });
function onInfoClick(s, title) { function onInfoClick(json, title) {
const html = ( showJson5Dialog({ title, json });
<SyntaxHighlighter language="javascript" style={style} customStyle={{ textAlign: 'left', maxHeight: 300, overflowY: 'auto', fontSize: 14 }}>
{JSON5.stringify(s, null, 2)}
</SyntaxHighlighter>
);
ReactSwal.fire({
showCloseButton: true,
title,
html,
});
} }
const Stream = memo(({ filePath, stream, onToggle, copyStream, fileDuration, setEditingStream, onExtractStreamPress }) => { const Stream = memo(({ filePath, stream, onToggle, copyStream, fileDuration, setEditingStream, onExtractStreamPress }) => {

@ -81,7 +81,7 @@ const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate
<div> <div>
{error != null && <Alert intent="danger" appearance="card">{`${i18n.t('There is an error in the file name template:')} ${error}`}</Alert>} {error != null && <Alert intent="danger" appearance="card">{`${i18n.t('There is an error in the file name template:')} ${error}`}</Alert>}
{/* eslint-disable-next-line no-template-curly-in-string */} {/* eslint-disable-next-line no-template-curly-in-string */}
<div style={{ fontSize: '.8em', color: 'rgba(255,255,255,0.7)' }}>{`${i18n.t('Variables')}`} {'${FILENAME} ${CUT_FROM} ${CUT_TO} ${SEG_NUM} ${SEG_LABEL} ${SEG_SUFFIX} ${EXT}'}</div> <div style={{ fontSize: '.8em', color: 'rgba(255,255,255,0.7)' }}>{`${i18n.t('Variables')}`} {'${FILENAME} ${CUT_FROM} ${CUT_TO} ${SEG_NUM} ${SEG_LABEL} ${SEG_SUFFIX} ${EXT} ${SEG_TAGS.XX}'}</div>
</div> </div>
</> </>
)} )}

@ -4,6 +4,9 @@ import Swal from 'sweetalert2';
import i18n from 'i18next'; import i18n from 'i18next';
import { Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import withReactContent from 'sweetalert2-react-content'; import withReactContent from 'sweetalert2-react-content';
import SyntaxHighlighter from 'react-syntax-highlighter';
import { tomorrow as style } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import JSON5 from 'json5';
import { parseDuration } from './util/duration'; import { parseDuration } from './util/duration';
import { parseYouTube } from './edlFormats'; import { parseYouTube } from './edlFormats';
@ -370,3 +373,17 @@ export async function showOpenAndMergeDialog({ defaultPath, onMergeClick }) {
if (canceled) return; if (canceled) return;
showMergeDialog(filePaths, onMergeClick); showMergeDialog(filePaths, onMergeClick);
} }
export function showJson5Dialog({ title, json }) {
const html = (
<SyntaxHighlighter language="javascript" style={style} customStyle={{ textAlign: 'left', maxHeight: 300, overflowY: 'auto', fontSize: 14 }}>
{JSON5.stringify(json, null, 2)}
</SyntaxHighlighter>
);
ReactSwal.fire({
showCloseButton: true,
title,
html,
});
}

@ -70,7 +70,7 @@ export function parseCuesheet(cuesheet) {
const nextTrack = tracks[i + 1]; const nextTrack = tracks[i + 1];
const end = nextTrack && parseTime(nextTrack); const end = nextTrack && parseTime(nextTrack);
return { name: track.title, start: parseTime(track), end }; return { name: track.title, start: parseTime(track), end, tags: { performer: track.performer, title: track.title } };
}); });
} }

@ -3,12 +3,13 @@ import sortBy from 'lodash/sortBy';
import { generateColor } from './util/colors'; import { generateColor } from './util/colors';
export const createSegment = ({ start, end, name } = {}) => ({ export const createSegment = ({ start, end, name, tags } = {}) => ({
start, start,
end, end,
name: name || '', name: name || '',
color: generateColor(), color: generateColor(),
segId: uuidv4(), segId: uuidv4(),
tags, // tags is an optional object (key-value)
}); });
export const createInitialCutSegments = () => [createSegment()]; export const createInitialCutSegments = () => [createSegment()];
@ -24,6 +25,7 @@ export const getCleanCutSegments = (cs) => cs.map((seg) => ({
start: seg.start, start: seg.start,
end: seg.end, end: seg.end,
name: seg.name, name: seg.name,
tags: seg.tags,
})); }));
export function findSegmentsAtCursor(apparentSegments, currentTime) { export function findSegmentsAtCursor(apparentSegments, currentTime) {
@ -34,6 +36,8 @@ export function findSegmentsAtCursor(apparentSegments, currentTime) {
return indexes; return indexes;
} }
export const getSegmentTags = (segment) => (segment.tags || {});
export const sortSegments = (segments) => sortBy(segments, 'start'); export const sortSegments = (segments) => sortBy(segments, 'start');
export function invertSegments(sortedCutSegments, duration) { export function invertSegments(sortedCutSegments, duration) {

@ -148,9 +148,19 @@ export function getOutFileExtension({ isCustomFormatSelected, outFormat, filePat
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
export const defaultOutSegTemplate = '${FILENAME}-${CUT_FROM}-${CUT_TO}${SEG_SUFFIX}${EXT}'; export const defaultOutSegTemplate = '${FILENAME}-${CUT_FROM}-${CUT_TO}${SEG_SUFFIX}${EXT}';
export function generateSegFileName({ template, inputFileNameWithoutExt, segSuffix, ext, segNum, segLabel, cutFrom, cutTo }) { export function generateSegFileName({ template, inputFileNameWithoutExt, segSuffix, ext, segNum, segLabel, cutFrom, cutTo, tags }) {
const compiled = lodashTemplate(template); const compiled = lodashTemplate(template);
return compiled({ FILENAME: inputFileNameWithoutExt, SEG_SUFFIX: segSuffix, EXT: ext, SEG_NUM: segNum, SEG_LABEL: segLabel, CUT_FROM: cutFrom, CUT_TO: cutTo }); const data = {
FILENAME: inputFileNameWithoutExt,
SEG_SUFFIX: segSuffix,
EXT: ext,
SEG_NUM: segNum,
SEG_LABEL: segLabel,
CUT_FROM: cutFrom,
CUT_TO: cutTo,
SEG_TAGS: Object.fromEntries(Object.entries(tags).map(([key, value]) => [`${key.toLocaleUpperCase('en-US')}`, value])),
};
return compiled(data);
} }
export const hasDuplicates = (arr) => new Set(arr).size !== arr.length; export const hasDuplicates = (arr) => new Set(arr).size !== arr.length;

Loading…
Cancel
Save