From faf70568dce420ed116abea2839060e625bf218c Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Tue, 24 Aug 2021 00:44:11 +0700 Subject: [PATCH] Add partial support for segment tags #427 #814 allows importing from cue files does not support loading/saving --- src/App.jsx | 15 +++++++++++---- src/SegmentList.jsx | 10 ++++++++-- src/StreamsSelector.jsx | 23 +++-------------------- src/components/OutSegTemplateEditor.jsx | 2 +- src/dialogs.jsx | 17 +++++++++++++++++ src/edlFormats.js | 2 +- src/segments.js | 6 +++++- src/util.js | 14 ++++++++++++-- 8 files changed, 58 insertions(+), 31 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index e0b45cf5..da0fd795 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -57,10 +57,10 @@ import { hasDuplicates, havePermissionToReadFile, isMac, getFileBaseName, } from './util'; 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 { 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'; @@ -370,6 +370,10 @@ const App = memo(() => { if (value != null) updateSegAtIndex(index, { name: value }); }, [cutSegments, updateSegAtIndex]); + const onViewSegmentTagsPress = useCallback((segment) => { + showJson5Dialog({ title: 'Segment tags', json: getSegmentTags(segment) }); + }, []); + const updateSegOrder = useCallback((index, newOrder) => { if (newOrder > cutSegments.length - 1 || newOrder < 0) return; const newSegments = [...cutSegments]; @@ -899,10 +903,12 @@ const App = memo(() => { const filenamifyOrNot = useCallback((name) => (safeOutputFileName ? filenamify(name) : name), [safeOutputFileName]); 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 cutToStr = formatDuration({ seconds: end, fileNameFriendly: true }); const segNum = i + 1; + const tags = getSegmentTags(segment); // https://github.com/mifi/lossless-cut/issues/583 let segSuffix = ''; @@ -913,8 +919,8 @@ const App = memo(() => { 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 + const generated = generateSegFileName({ template, segSuffix, inputFileNameWithoutExt: fileNameWithoutExt, ext, segNum, segLabel: filenamifyOrNot(name), cutFrom: cutFromStr, cutTo: cutToStr, tags }); }) ), [fileFormat, filePath, isCustomFormatSelected, enabledOutSegments, filenamifyOrNot]); @@ -2198,6 +2204,7 @@ const App = memo(() => { onExportSegmentEnableAll={onExportSegmentEnableAll} jumpSegStart={jumpSegStart} jumpSegEnd={jumpSegEnd} + onViewSegmentTagsPress={onViewSegmentTagsPress} /> )} diff --git a/src/SegmentList.jsx b/src/SegmentList.jsx index b76e23d7..58eeeb50 100644 --- a/src/SegmentList.jsx +++ b/src/SegmentList.jsx @@ -10,6 +10,7 @@ import isEqual from 'lodash/isEqual'; import useDebounce from 'react-use/lib/useDebounce'; import scrollIntoView from 'scroll-into-view-if-needed'; +import { getSegmentTags } from './segments'; import useContextMenu from './hooks/useContextMenu'; import { saveColor } from './colors'; import { getSegColors } from './util/colors'; @@ -21,7 +22,7 @@ const buttonBaseStyle = { 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 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: t('Include all segments in export'), click: () => onExportSegmentEnableAll(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; @@ -120,7 +125,7 @@ const SegmentList = memo(({ updateSegOrder, updateSegOrders, addCutSegment, removeCutSegment, onLabelSegmentPress, currentCutSeg, segmentAtCursor, toggleSideBar, splitCurrentSegment, enabledOutSegments, enabledOutSegmentsRaw, onExportSingleSegmentClick, onExportSegmentEnabledToggle, onExportSegmentDisableAll, onExportSegmentEnableAll, - jumpSegStart, jumpSegEnd, simpleMode, + jumpSegStart, jumpSegEnd, simpleMode, onViewSegmentTagsPress, }) => { const { t } = useTranslation(); @@ -266,6 +271,7 @@ const SegmentList = memo(({ onExportSegmentEnabledToggle={onExportSegmentEnabledToggle} onExportSegmentDisableAll={onExportSegmentDisableAll} onExportSegmentEnableAll={onExportSegmentEnableAll} + onViewSegmentTagsPress={onViewSegmentTagsPress} /> ); })} diff --git a/src/StreamsSelector.jsx b/src/StreamsSelector.jsx index eb704f69..b5d5d43e 100644 --- a/src/StreamsSelector.jsx +++ b/src/StreamsSelector.jsx @@ -4,20 +4,13 @@ import { FaVideo, FaVideoSlash, FaFileExport, FaFileImport, FaVolumeUp, FaVolume import { GoFileBinary } from 'react-icons/go'; import { FiEdit, FiCheck, FiTrash } from 'react-icons/fi'; import { MdSubtitles } from 'react-icons/md'; -import Swal from 'sweetalert2'; import { SegmentedControl, Dialog, Button } from 'evergreen-ui'; -import withReactContent from 'sweetalert2-react-content'; 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 { getStreamFps } from './ffmpeg'; -const ReactSwal = withReactContent(Swal); - const activeColor = '#9f5f80'; @@ -159,18 +152,8 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat return ; }); -function onInfoClick(s, title) { - const html = ( - - {JSON5.stringify(s, null, 2)} - - ); - - ReactSwal.fire({ - showCloseButton: true, - title, - html, - }); +function onInfoClick(json, title) { + showJson5Dialog({ title, json }); } const Stream = memo(({ filePath, stream, onToggle, copyStream, fileDuration, setEditingStream, onExtractStreamPress }) => { diff --git a/src/components/OutSegTemplateEditor.jsx b/src/components/OutSegTemplateEditor.jsx index 67d48e6a..b7bf021a 100644 --- a/src/components/OutSegTemplateEditor.jsx +++ b/src/components/OutSegTemplateEditor.jsx @@ -81,7 +81,7 @@ const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate
{error != null && {`${i18n.t('There is an error in the file name template:')} ${error}`}} {/* eslint-disable-next-line no-template-curly-in-string */} -
{`${i18n.t('Variables')}`} {'${FILENAME} ${CUT_FROM} ${CUT_TO} ${SEG_NUM} ${SEG_LABEL} ${SEG_SUFFIX} ${EXT}'}
+
{`${i18n.t('Variables')}`} {'${FILENAME} ${CUT_FROM} ${CUT_TO} ${SEG_NUM} ${SEG_LABEL} ${SEG_SUFFIX} ${EXT} ${SEG_TAGS.XX}'}
)} diff --git a/src/dialogs.jsx b/src/dialogs.jsx index c2155f80..25e9eaef 100644 --- a/src/dialogs.jsx +++ b/src/dialogs.jsx @@ -4,6 +4,9 @@ import Swal from 'sweetalert2'; import i18n from 'i18next'; import { Trans } from 'react-i18next'; 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 { parseYouTube } from './edlFormats'; @@ -370,3 +373,17 @@ export async function showOpenAndMergeDialog({ defaultPath, onMergeClick }) { if (canceled) return; showMergeDialog(filePaths, onMergeClick); } + +export function showJson5Dialog({ title, json }) { + const html = ( + + {JSON5.stringify(json, null, 2)} + + ); + + ReactSwal.fire({ + showCloseButton: true, + title, + html, + }); +} diff --git a/src/edlFormats.js b/src/edlFormats.js index 0bf7af6e..80859461 100644 --- a/src/edlFormats.js +++ b/src/edlFormats.js @@ -70,7 +70,7 @@ export function parseCuesheet(cuesheet) { const nextTrack = tracks[i + 1]; 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 } }; }); } diff --git a/src/segments.js b/src/segments.js index 5cdbea8e..a6452a7d 100644 --- a/src/segments.js +++ b/src/segments.js @@ -3,12 +3,13 @@ import sortBy from 'lodash/sortBy'; import { generateColor } from './util/colors'; -export const createSegment = ({ start, end, name } = {}) => ({ +export const createSegment = ({ start, end, name, tags } = {}) => ({ start, end, name: name || '', color: generateColor(), segId: uuidv4(), + tags, // tags is an optional object (key-value) }); export const createInitialCutSegments = () => [createSegment()]; @@ -24,6 +25,7 @@ export const getCleanCutSegments = (cs) => cs.map((seg) => ({ start: seg.start, end: seg.end, name: seg.name, + tags: seg.tags, })); export function findSegmentsAtCursor(apparentSegments, currentTime) { @@ -34,6 +36,8 @@ export function findSegmentsAtCursor(apparentSegments, currentTime) { return indexes; } +export const getSegmentTags = (segment) => (segment.tags || {}); + export const sortSegments = (segments) => sortBy(segments, 'start'); export function invertSegments(sortedCutSegments, duration) { diff --git a/src/util.js b/src/util.js index d193f42c..9cbfe2f4 100644 --- a/src/util.js +++ b/src/util.js @@ -148,9 +148,19 @@ export function getOutFileExtension({ isCustomFormatSelected, outFormat, filePat // eslint-disable-next-line no-template-curly-in-string 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); - 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;