From e49bfee75046e39e68bf7038a146b69161007488 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Fri, 6 Jan 2023 22:57:27 +0800 Subject: [PATCH] refactor --- src/App.jsx | 4 +- src/dialogs/extractFrames.jsx | 108 +++++++++++++++ src/{dialogs.jsx => dialogs/index.jsx} | 183 +------------------------ src/dialogs/parameters.jsx | 78 +++++++++++ 4 files changed, 195 insertions(+), 178 deletions(-) create mode 100644 src/dialogs/extractFrames.jsx rename src/{dialogs.jsx => dialogs/index.jsx} (74%) create mode 100644 src/dialogs/parameters.jsx diff --git a/src/App.jsx b/src/App.jsx index e9c1e9b1..611121e7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -69,7 +69,9 @@ import { } from './util'; import { formatDuration } from './util/duration'; import { adjustRate } from './util/rate-calculator'; -import { askForOutDir, askForInputDir, askForImportChapters, createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog, askForShiftSegments, selectSegmentsByLabelDialog, askExtractFramesAsImages, showRefuseToOverwrite, showParametersDialog, openDirToast, openCutFinishedToast } from './dialogs'; +import { showParametersDialog } from './dialogs/parameters'; +import { askExtractFramesAsImages } from './dialogs/extractFrames'; +import { askForOutDir, askForInputDir, askForImportChapters, createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, promptTimeOffset, askForHtml5ifySpeed, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, labelSegmentDialog, openYouTubeChaptersDialog, openAbout, showEditableJsonDialog, askForShiftSegments, selectSegmentsByLabelDialog, showRefuseToOverwrite, openDirToast, openCutFinishedToast } from './dialogs'; import { openSendReportDialog } from './reporting'; import { fallbackLng } from './i18n'; import { createSegment, getCleanCutSegments, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, convertSegmentsToChapters, hasAnySegmentOverlap, combineOverlappingSegments as combineOverlappingSegments2, isDurationValid } from './segments'; diff --git a/src/dialogs/extractFrames.jsx b/src/dialogs/extractFrames.jsx new file mode 100644 index 00000000..91d963cf --- /dev/null +++ b/src/dialogs/extractFrames.jsx @@ -0,0 +1,108 @@ +import Swal from 'sweetalert2'; +import i18n from 'i18next'; + + +// eslint-disable-next-line import/prefer-default-export +export async function askExtractFramesAsImages({ segmentNumFrames, fps }) { + const { value: captureChoice } = await Swal.fire({ + text: i18n.t('Extract frames of the selected segment as images?'), + icon: 'question', + input: 'radio', + inputValue: 'thumbnailFilter', + showCancelButton: true, + customClass: { input: 'swal2-losslesscut-radio' }, + inputOptions: { + thumbnailFilter: i18n.t('Capture the best image every nth second'), + selectNthSec: i18n.t('Capture exactly one image every nth second'), + selectNthFrame: i18n.t('Capture exactly one image every nth frame'), + selectScene: i18n.t('Capture frames that differ the most from the previous frame'), + everyFrame: i18n.t('Capture every single frame as an image'), + }, + }); + + if (!captureChoice) return undefined; + + let filter; + let estimatedMaxNumFiles = segmentNumFrames; + + if (captureChoice === 'thumbnailFilter') { + const { value } = await Swal.fire({ + text: i18n.t('Capture the best image every nth second'), + icon: 'question', + input: 'text', + inputLabel: i18n.t('Enter a decimal number of seconds'), + inputValue: 5, + showCancelButton: true, + }); + if (value == null) return undefined; + const intervalFrames = Math.round(parseFloat(value) * fps); + if (Number.isNaN(intervalFrames) || intervalFrames < 1 || intervalFrames > 1000) return undefined; // a too large value uses a lot of memory + + filter = `thumbnail=${intervalFrames}`; + estimatedMaxNumFiles = Math.round(segmentNumFrames / intervalFrames); + } + + if (captureChoice === 'selectNthSec' || captureChoice === 'selectNthFrame') { + let nthFrame; + if (captureChoice === 'selectNthFrame') { + const { value } = await Swal.fire({ + text: i18n.t('Capture exactly one image every nth frame'), + icon: 'question', + input: 'number', + inputLabel: i18n.t('Enter an integer number of frames'), + inputValue: 30, + showCancelButton: true, + }); + if (value == null) return undefined; + const intervalFrames = parseInt(value, 10); + if (Number.isNaN(intervalFrames) || intervalFrames < 1) return undefined; + nthFrame = intervalFrames; + } else { + const { value } = await Swal.fire({ + text: i18n.t('Capture exactly one image every nth second'), + icon: 'question', + input: 'text', + inputLabel: i18n.t('Enter a decimal number of seconds'), + inputValue: 5, + showCancelButton: true, + }); + if (value == null) return undefined; + const intervalFrames = Math.round(parseFloat(value) * fps); + if (Number.isNaN(intervalFrames) || intervalFrames < 1) return undefined; + nthFrame = intervalFrames; + } + + filter = `select=not(mod(n\\,${nthFrame}))`; + estimatedMaxNumFiles = Math.round(segmentNumFrames / nthFrame); + } + if (captureChoice === 'selectScene') { + const { value } = await Swal.fire({ + text: i18n.t('Capture frames that differ the most from the previous frame'), + icon: 'question', + input: 'text', + inputLabel: i18n.t('Enter a decimal number between 0 and 1 (sane values are 0.3 - 0.5)'), + inputValue: '0.4', + showCancelButton: true, + }); + if (value == null) return undefined; + const minSceneChange = parseFloat(value); + if (Number.isNaN(minSceneChange) || minSceneChange <= 0 || minSceneChange >= 1) return undefined; + + filter = `select=gt(scene\\,${minSceneChange})`; + // we don't know estimatedMaxNumFiles here + } + + estimatedMaxNumFiles += 1; // just to be sure + + if (estimatedMaxNumFiles > 1000) { + const { isConfirmed } = await Swal.fire({ + icon: 'warning', + text: i18n.t('Note that depending on input parameters, up to {{estimatedMaxNumFiles}} files may be produced!', { estimatedMaxNumFiles }), + showCancelButton: true, + confirmButtonText: i18n.t('Confirm'), + }); + if (!isConfirmed) return undefined; + } + + return { filter, estimatedMaxNumFiles }; +} diff --git a/src/dialogs.jsx b/src/dialogs/index.jsx similarity index 74% rename from src/dialogs.jsx rename to src/dialogs/index.jsx index c71c590a..e98a6f96 100644 --- a/src/dialogs.jsx +++ b/src/dialogs/index.jsx @@ -1,5 +1,5 @@ -import React, { useState, useCallback, useRef, useEffect } from 'react'; -import { HelpIcon, TickCircleIcon, UnorderedList, ListItem, WarningSignIcon, InfoSignIcon, Button, TextInputField, Checkbox, RadioGroup, Paragraph, LinkIcon } from 'evergreen-ui'; +import React, { useState, useCallback } from 'react'; +import { HelpIcon, TickCircleIcon, UnorderedList, ListItem, WarningSignIcon, InfoSignIcon, Checkbox, RadioGroup, Paragraph } from 'evergreen-ui'; import Swal from 'sweetalert2'; import i18n from 'i18next'; import { Trans } from 'react-i18next'; @@ -8,10 +8,10 @@ import SyntaxHighlighter from 'react-syntax-highlighter'; import { tomorrow as style } from 'react-syntax-highlighter/dist/esm/styles/hljs'; import JSON5 from 'json5'; -import { parseDuration, formatDuration } from './util/duration'; -import { swalToastOptions, toast } from './util'; -import { parseYouTube } from './edlFormats'; -import CopyClipboardButton from './components/CopyClipboardButton'; +import { parseDuration, formatDuration } from '../util/duration'; +import { swalToastOptions, toast } from '../util'; +import { parseYouTube } from '../edlFormats'; +import CopyClipboardButton from '../components/CopyClipboardButton'; const { dialog, app } = window.require('@electron/remote'); const { shell } = window.require('electron'); @@ -396,177 +396,6 @@ export async function showCleanupFilesDialog(cleanupChoicesIn = {}) { return undefined; } -const ParametersInput = ({ description, parameters: parametersIn, onChange, onSubmit, docUrl }) => { - const firstInputRef = useRef(); - const [parameters, setParameters] = useState(parametersIn); - - const getParameter = (key) => parameters[key]?.value; - - const handleChange = (key, value) => setParameters((existing) => { - const newParameters = { ...existing, [key]: { ...existing[key], value } }; - onChange(newParameters); - return newParameters; - }); - - const handleSubmit = useCallback((e) => { - e.preventDefault(); - onSubmit(); - }, [onSubmit]); - - useEffect(() => { - firstInputRef.current?.focus?.(); - }, []); - - return ( -
- {description &&

{description}

} - - {docUrl &&

} - -
- {Object.entries(parametersIn).map(([key, parameter], i) => ( - handleChange(key, e.target.value)} hint={parameter.hint} /> - ))} - - - -
- ); -}; - -export async function showParametersDialog({ title, description, parameters: parametersIn, docUrl }) { - let parameters = parametersIn; - let resolve1; - - const promise1 = new Promise((resolve) => { - resolve1 = resolve; - }); - const handleSubmit = () => { - Swal.close(); - resolve1(true); - }; - - const promise2 = (async () => { - const { isConfirmed } = await ReactSwal.fire({ - title, - html: { parameters = newParameters; }} onSubmit={handleSubmit} docUrl={docUrl} />, - confirmButtonText: i18n.t('Confirm'), - showCancelButton: true, - cancelButtonText: i18n.t('Cancel'), - }); - return isConfirmed; - })(); - - const isConfirmed = await Promise.race([promise1, promise2]); - if (!isConfirmed) return undefined; - - return Object.fromEntries(Object.entries(parameters).map(([key, parameter]) => [key, parameter.value])); -} - -export async function askExtractFramesAsImages({ segmentNumFrames, fps }) { - const { value: captureChoice } = await Swal.fire({ - text: i18n.t('Extract frames of the selected segment as images?'), - icon: 'question', - input: 'radio', - inputValue: 'thumbnailFilter', - showCancelButton: true, - customClass: { input: 'swal2-losslesscut-radio' }, - inputOptions: { - thumbnailFilter: i18n.t('Capture the best image every nth second'), - selectNthSec: i18n.t('Capture exactly one image every nth second'), - selectNthFrame: i18n.t('Capture exactly one image every nth frame'), - selectScene: i18n.t('Capture frames that differ the most from the previous frame'), - everyFrame: i18n.t('Capture every single frame as an image'), - }, - }); - - if (!captureChoice) return undefined; - - let filter; - let estimatedMaxNumFiles = segmentNumFrames; - - if (captureChoice === 'thumbnailFilter') { - const { value } = await Swal.fire({ - text: i18n.t('Capture the best image every nth second'), - icon: 'question', - input: 'text', - inputLabel: i18n.t('Enter a decimal number of seconds'), - inputValue: 5, - showCancelButton: true, - }); - if (value == null) return undefined; - const intervalFrames = Math.round(parseFloat(value) * fps); - if (Number.isNaN(intervalFrames) || intervalFrames < 1 || intervalFrames > 1000) return undefined; // a too large value uses a lot of memory - - filter = `thumbnail=${intervalFrames}`; - estimatedMaxNumFiles = Math.round(segmentNumFrames / intervalFrames); - } - - if (captureChoice === 'selectNthSec' || captureChoice === 'selectNthFrame') { - let nthFrame; - if (captureChoice === 'selectNthFrame') { - const { value } = await Swal.fire({ - text: i18n.t('Capture exactly one image every nth frame'), - icon: 'question', - input: 'number', - inputLabel: i18n.t('Enter an integer number of frames'), - inputValue: 30, - showCancelButton: true, - }); - if (value == null) return undefined; - const intervalFrames = parseInt(value, 10); - if (Number.isNaN(intervalFrames) || intervalFrames < 1) return undefined; - nthFrame = intervalFrames; - } else { - const { value } = await Swal.fire({ - text: i18n.t('Capture exactly one image every nth second'), - icon: 'question', - input: 'text', - inputLabel: i18n.t('Enter a decimal number of seconds'), - inputValue: 5, - showCancelButton: true, - }); - if (value == null) return undefined; - const intervalFrames = Math.round(parseFloat(value) * fps); - if (Number.isNaN(intervalFrames) || intervalFrames < 1) return undefined; - nthFrame = intervalFrames; - } - - filter = `select=not(mod(n\\,${nthFrame}))`; - estimatedMaxNumFiles = Math.round(segmentNumFrames / nthFrame); - } - if (captureChoice === 'selectScene') { - const { value } = await Swal.fire({ - text: i18n.t('Capture frames that differ the most from the previous frame'), - icon: 'question', - input: 'text', - inputLabel: i18n.t('Enter a decimal number between 0 and 1 (sane values are 0.3 - 0.5)'), - inputValue: '0.4', - showCancelButton: true, - }); - if (value == null) return undefined; - const minSceneChange = parseFloat(value); - if (Number.isNaN(minSceneChange) || minSceneChange <= 0 || minSceneChange >= 1) return undefined; - - filter = `select=gt(scene\\,${minSceneChange})`; - // we don't know estimatedMaxNumFiles here - } - - estimatedMaxNumFiles += 1; // just to be sure - - if (estimatedMaxNumFiles > 1000) { - const { isConfirmed } = await Swal.fire({ - icon: 'warning', - text: i18n.t('Note that depending on input parameters, up to {{estimatedMaxNumFiles}} files may be produced!', { estimatedMaxNumFiles }), - showCancelButton: true, - confirmButtonText: i18n.t('Confirm'), - }); - if (!isConfirmed) return undefined; - } - - return { filter, estimatedMaxNumFiles }; -} - export async function createFixedDurationSegments(fileDuration) { const segmentDuration = await askForSegmentDuration(fileDuration); if (segmentDuration == null) return undefined; diff --git a/src/dialogs/parameters.jsx b/src/dialogs/parameters.jsx new file mode 100644 index 00000000..651e6db3 --- /dev/null +++ b/src/dialogs/parameters.jsx @@ -0,0 +1,78 @@ +import React, { useState, useCallback, useRef, useEffect } from 'react'; +import { Button, TextInputField, LinkIcon } from 'evergreen-ui'; +import Swal from 'sweetalert2'; +import i18n from 'i18next'; +import withReactContent from 'sweetalert2-react-content'; + +const { shell } = window.require('electron'); + +const ReactSwal = withReactContent(Swal); + + +const ParametersInput = ({ description, parameters: parametersIn, onChange, onSubmit, docUrl }) => { + const firstInputRef = useRef(); + const [parameters, setParameters] = useState(parametersIn); + + const getParameter = (key) => parameters[key]?.value; + + const handleChange = (key, value) => setParameters((existing) => { + const newParameters = { ...existing, [key]: { ...existing[key], value } }; + onChange(newParameters); + return newParameters; + }); + + const handleSubmit = useCallback((e) => { + e.preventDefault(); + onSubmit(); + }, [onSubmit]); + + useEffect(() => { + firstInputRef.current?.focus?.(); + }, []); + + return ( +
+ {description &&

{description}

} + + {docUrl &&

} + +
+ {Object.entries(parametersIn).map(([key, parameter], i) => ( + handleChange(key, e.target.value)} hint={parameter.hint} /> + ))} + + + +
+ ); +}; + +// eslint-disable-next-line import/prefer-default-export +export async function showParametersDialog({ title, description, parameters: parametersIn, docUrl }) { + let parameters = parametersIn; + let resolve1; + + const promise1 = new Promise((resolve) => { + resolve1 = resolve; + }); + const handleSubmit = () => { + Swal.close(); + resolve1(true); + }; + + const promise2 = (async () => { + const { isConfirmed } = await ReactSwal.fire({ + title, + html: { parameters = newParameters; }} onSubmit={handleSubmit} docUrl={docUrl} />, + confirmButtonText: i18n.t('Confirm'), + showCancelButton: true, + cancelButtonText: i18n.t('Cancel'), + }); + return isConfirmed; + })(); + + const isConfirmed = await Promise.race([promise1, promise2]); + if (!isConfirmed) return undefined; + + return Object.fromEntries(Object.entries(parameters).map(([key, parameter]) => [key, parameter.value])); +}