reduce react prop drilling

also fix issue with keyframe cut button not working
pull/901/head
Mikael Finstad 4 years ago
parent 70438f44fb
commit 402b8290af
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -3,7 +3,7 @@ import { unstable_batchedUpdates as batchedUpdates } from 'react-dom';
import { FaAngleLeft, FaWindowClose } from 'react-icons/fa';
import { MdRotate90DegreesCcw } from 'react-icons/md';
import { AnimatePresence, motion } from 'framer-motion';
import { Table, SideSheet, Button, Position, ForkIcon, DisableIcon, ThemeProvider } from 'evergreen-ui';
import { Table, SideSheet, Position, ThemeProvider } from 'evergreen-ui';
import { useStateWithHistory } from 'react-use/lib/useStateWithHistory';
import useDebounceOld from 'react-use/lib/useDebounce'; // Want to phase out this
import { useDebounce } from 'use-debounce';
@ -18,13 +18,15 @@ import isEqual from 'lodash/isEqual';
import theme from './theme';
import useTimelineScroll from './hooks/useTimelineScroll';
import useUserPreferences from './hooks/useUserPreferences';
import useUserSettingsRoot from './hooks/useUserSettingsRoot';
import useFfmpegOperations from './hooks/useFfmpegOperations';
import useKeyframes from './hooks/useKeyframes';
import useWaveform from './hooks/useWaveform';
import useKeyboard from './hooks/useKeyboard';
import useFileFormatState from './hooks/useFileFormatState';
import UserSettingsContext from './contexts/UserSettingsContext';
import NoFileLoaded from './NoFileLoaded';
import Canvas from './Canvas';
import TopMenu from './TopMenu';
@ -36,7 +38,7 @@ import Settings from './Settings';
import Timeline from './Timeline';
import BottomBar from './BottomBar';
import ExportConfirm from './ExportConfirm';
import ValueTuner from './components/ValueTuner';
import ValueTuners from './components/ValueTuners';
import VolumeControl from './components/VolumeControl';
import SubtitleControl from './components/SubtitleControl';
import BatchFilesList from './components/BatchFilesList';
@ -59,7 +61,7 @@ import { shouldCopyStreamByDefault, getAudioStreams, getRealVideoStreams, defaul
import { exportEdlFile, readEdlFile, saveLlcProject, loadLlcProject, askForEdlImport } from './edlStore';
import { formatYouTube, getTimeFromFrameNum as getTimeFromFrameNumRaw, getFrameCountRaw } from './edlFormats';
import {
getOutPath, toast, errorToast, handleError, setFileNameTitle, getOutDir, getFileDir, withBlur,
getOutPath, toast, errorToast, handleError, setFileNameTitle, getOutDir, getFileDir,
checkDirWriteAccess, dirExists, openDirToast, isMasBuild, isStoreBuild, dragPreventer,
isDurationValid, filenamify, getOutFileExtension, generateSegFileName, defaultOutSegTemplate,
havePermissionToReadFile, resolvePathIfNeeded, getPathReadAccessError, html5ifiedPrefix, html5dummySuffix, findExistingHtml5FriendlyFile,
@ -196,7 +198,7 @@ const App = memo(() => {
const {
captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, setAvoidNegativeTs, autoMerge, setAutoMerge, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, setAutoExportExtraStreams, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, setAutoSaveProjectFile, wheelSensitivity, setWheelSensitivity, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, ffmpegExperimental, setFfmpegExperimental, hideNotifications, setHideNotifications, autoLoadTimecode, setAutoLoadTimecode, autoDeleteMergedSegments, setAutoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, simpleMode, setSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, setKeyboardSeekAccFactor, keyboardNormalSeekSpeed, setKeyboardNormalSeekSpeed, enableTransferTimestamps, setEnableTransferTimestamps, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, setEnableAutoHtml5ify, segmentsToChaptersOnly, setSegmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, setEnableSmartCut,
} = useUserPreferences();
} = useUserSettingsRoot();
const {
concatFiles, html5ifyDummy, cutMultiple, autoConcatCutSegments, html5ify, fixInvalidDuration,
@ -214,8 +216,6 @@ const App = memo(() => {
const isFileOpened = !!filePath;
const onOutFormatLockedClick = () => setOutFormatLocked((v) => (v ? undefined : fileFormat));
const onOutputFormatUserChange = useCallback((newFormat) => {
setFileFormat(newFormat);
if (outFormatLocked) {
@ -740,6 +740,10 @@ const App = memo(() => {
return !v;
}), [hideAllNotifications, setSimpleMode]);
const userSettingsContext = useMemo(() => ({
captureFormat, setCaptureFormat, toggleCaptureFormat, customOutDir, setCustomOutDir, changeOutDir, keyframeCut, setKeyframeCut, toggleKeyframeCut, preserveMovData, setPreserveMovData, togglePreserveMovData, movFastStart, setMovFastStart, toggleMovFastStart, avoidNegativeTs, setAvoidNegativeTs, autoMerge, setAutoMerge, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, setAutoExportExtraStreams, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, setAutoSaveProjectFile, wheelSensitivity, setWheelSensitivity, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, ffmpegExperimental, setFfmpegExperimental, hideNotifications, setHideNotifications, autoLoadTimecode, setAutoLoadTimecode, autoDeleteMergedSegments, setAutoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, toggleSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, togglePreserveMetadataOnMerge, simpleMode, setSimpleMode, toggleSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, setKeyboardSeekAccFactor, keyboardNormalSeekSpeed, setKeyboardNormalSeekSpeed, enableTransferTimestamps, setEnableTransferTimestamps, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, toggleSafeOutputFileName, enableAutoHtml5ify, setEnableAutoHtml5ify, segmentsToChaptersOnly, setSegmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, setEnableSmartCut,
}), [askBeforeClose, autoDeleteMergedSegments, autoExportExtraStreams, autoLoadTimecode, autoMerge, autoSaveProjectFile, avoidNegativeTs, captureFormat, changeOutDir, customOutDir, enableAskForFileOpenAction, enableAskForImportChapters, enableAutoHtml5ify, enableSmartCut, enableTransferTimestamps, exportConfirmEnabled, ffmpegExperimental, hideNotifications, invertCutSegments, invertTimelineScroll, keyBindings, keyboardNormalSeekSpeed, keyboardSeekAccFactor, keyframeCut, language, movFastStart, outFormatLocked, outSegTemplate, playbackVolume, preserveMetadataOnMerge, preserveMovData, resetKeyBindings, safeOutputFileName, segmentsToChapters, segmentsToChaptersOnly, setAskBeforeClose, setAutoDeleteMergedSegments, setAutoExportExtraStreams, setAutoLoadTimecode, setAutoMerge, setAutoSaveProjectFile, setAvoidNegativeTs, setCaptureFormat, setCustomOutDir, setEnableAskForFileOpenAction, setEnableAskForImportChapters, setEnableAutoHtml5ify, setEnableSmartCut, setEnableTransferTimestamps, setExportConfirmEnabled, setFfmpegExperimental, setHideNotifications, setInvertCutSegments, setInvertTimelineScroll, setKeyBindings, setKeyboardNormalSeekSpeed, setKeyboardSeekAccFactor, setKeyframeCut, setLanguage, setMovFastStart, setOutFormatLocked, setOutSegTemplate, setPlaybackVolume, setPreserveMetadataOnMerge, setPreserveMovData, setSafeOutputFileName, setSegmentsToChapters, setSegmentsToChaptersOnly, setSimpleMode, setTimecodeFormat, setWheelSensitivity, simpleMode, timecodeFormat, toggleCaptureFormat, toggleExportConfirmEnabled, toggleKeyframeCut, toggleMovFastStart, togglePreserveMetadataOnMerge, togglePreserveMovData, toggleSafeOutputFileName, toggleSegmentsToChapters, toggleSimpleMode, wheelSensitivity]);
const isCopyingStreamId = useCallback((path, streamId) => (
!!(copyStreamIdsByFile[path] || {})[streamId]
), [copyStreamIdsByFile]);
@ -2109,23 +2113,6 @@ const App = memo(() => {
<OutputFormatSelect style={style} detectedFileFormat={detectedFileFormat} fileFormat={fileFormat} onOutputFormatUserChange={onOutputFormatUserChange} />
), [detectedFileFormat, fileFormat, onOutputFormatUserChange]);
const renderCaptureFormatButton = useCallback((props) => (
<Button
title={i18n.t('Capture frame format')}
onClick={withBlur(toggleCaptureFormat)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{captureFormat}
</Button>
), [captureFormat, toggleCaptureFormat]);
const AutoExportToggler = useCallback(() => (
<Button intent={autoExportExtraStreams ? 'success' : 'danger'} iconBefore={autoExportExtraStreams ? ForkIcon : DisableIcon} onClick={() => setAutoExportExtraStreams(!autoExportExtraStreams)}>
{autoExportExtraStreams ? i18n.t('Extract') : i18n.t('Discard')}
</Button>
), [autoExportExtraStreams, setAutoExportExtraStreams]);
const onTunerRequested = useCallback((type) => {
setSettingsVisible(false);
setTunerVisible(type);
@ -2163,39 +2150,6 @@ const App = memo(() => {
const { t } = useTranslation();
function renderTuner(type) {
// NOTE default values are duplicated in public/configStore.js
const types = {
wheelSensitivity: {
title: t('Timeline trackpad/wheel sensitivity'),
value: wheelSensitivity,
setValue: setWheelSensitivity,
default: 0.2,
},
keyboardNormalSeekSpeed: {
title: t('Timeline keyboard seek speed'),
value: keyboardNormalSeekSpeed,
setValue: setKeyboardNormalSeekSpeed,
min: 0,
max: 100,
default: 1,
},
keyboardSeekAccFactor: {
title: t('Timeline keyboard seek acceleration'),
value: keyboardSeekAccFactor,
setValue: setKeyboardSeekAccFactor,
min: 1,
max: 2,
default: 1.03,
},
};
const { title, value, setValue, min, max, default: defaultValue } = types[type];
const resetToDefault = () => setValue(defaultValue);
return <ValueTuner title={title} value={value} setValue={setValue} onFinished={() => setTunerVisible()} max={max} min={min} resetToDefault={resetToDefault} />;
}
function renderSubtitles() {
if (!activeSubtitle) return null;
return <track default kind="subtitles" label={activeSubtitle.lang} srcLang="en" src={activeSubtitle.url} />;
@ -2204,328 +2158,276 @@ const App = memo(() => {
// throw new Error('Test error boundary');
return (
<ThemeProvider value={theme}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<TopMenu
filePath={filePath}
copyAnyAudioTrack={copyAnyAudioTrack}
toggleStripAudio={toggleStripAudio}
customOutDir={customOutDir}
changeOutDir={changeOutDir}
clearOutDir={clearOutDir}
isCustomFormatSelected={isCustomFormatSelected}
renderOutFmt={renderOutFmt}
toggleHelp={toggleHelp}
toggleSettings={toggleSettings}
numStreamsToCopy={numStreamsToCopy}
numStreamsTotal={numStreamsTotal}
setStreamsSelectorShown={setStreamsSelectorShown}
enabledSegments={enabledSegments}
autoMerge={autoMerge}
setAutoMerge={setAutoMerge}
autoDeleteMergedSegments={autoDeleteMergedSegments}
setAutoDeleteMergedSegments={setAutoDeleteMergedSegments}
outFormatLocked={outFormatLocked}
onOutFormatLockedClick={onOutFormatLockedClick}
simpleMode={simpleMode}
segmentsToChaptersOnly={segmentsToChaptersOnly}
setSegmentsToChaptersOnly={setSegmentsToChaptersOnly}
/>
<div style={{ flexGrow: 1, display: 'flex', overflowY: 'hidden' }}>
<AnimatePresence>
{showLeftBar && (
<BatchFilesList
selectedBatchFiles={selectedBatchFiles}
filePath={filePath}
width={leftBarWidth}
batchFiles={batchFiles}
setBatchFiles={setBatchFiles}
onBatchFileSelect={onBatchFileSelect}
batchRemoveFile={batchRemoveFile}
closeBatch={closeBatch}
onMergeFilesClick={concatCurrentBatch}
onBatchConvertToSupportedFormatClick={convertFormatBatch}
/>
)}
</AnimatePresence>
{/* Middle part: */}
<div style={{ position: 'relative', flexGrow: 1 }}>
{!isFileOpened && <NoFileLoaded mifiLink={mifiLink} toggleHelp={toggleHelp} currentCutSeg={currentCutSeg} simpleMode={simpleMode} toggleSimpleMode={toggleSimpleMode} />}
<div className="no-user-select" style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, visibility: !isFileOpened ? 'hidden' : undefined }} onWheel={onTimelineWheel}>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video
muted={playbackVolume === 0}
ref={videoRef}
style={videoStyle}
src={fileUri}
onPlay={onSartPlaying}
onPause={onStopPlaying}
onDurationChange={onDurationChange}
onTimeUpdate={onTimeUpdate}
onError={onVideoError}
>
{renderSubtitles()}
</video>
{canvasPlayerEnabled && <Canvas rotate={effectiveRotation} filePath={filePath} width={mainVideoStream.width} height={mainVideoStream.height} streamIndex={mainVideoStream.index} playerTime={playerTime} commandedTime={commandedTime} playing={playing} />}
</div>
{isRotationSet && !hideCanvasPreview && (
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, marginTop: '1em', marginLeft: '1em', color: 'white', display: 'flex', alignItems: 'center' }}>
<MdRotate90DegreesCcw size={26} style={{ marginRight: 5 }} />
{t('Rotation preview')}
{!canvasPlayerRequired && <FaWindowClose role="button" style={{ cursor: 'pointer', verticalAlign: 'middle', padding: 10 }} onClick={() => setHideCanvasPreview(true)} />}
</div>
)}
{isFileOpened && (
<div className="no-user-select" style={{ position: 'absolute', right: 0, bottom: 0, marginBottom: 10, color: 'rgba(255,255,255,0.7)', display: 'flex', alignItems: 'center' }}>
<VolumeControl playbackVolume={playbackVolume} setPlaybackVolume={setPlaybackVolume} usingDummyVideo={usingDummyVideo} />
<UserSettingsContext.Provider value={userSettingsContext}>
<ThemeProvider value={theme}>
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
<TopMenu
filePath={filePath}
fileFormat={fileFormat}
copyAnyAudioTrack={copyAnyAudioTrack}
toggleStripAudio={toggleStripAudio}
clearOutDir={clearOutDir}
isCustomFormatSelected={isCustomFormatSelected}
renderOutFmt={renderOutFmt}
toggleHelp={toggleHelp}
toggleSettings={toggleSettings}
numStreamsToCopy={numStreamsToCopy}
numStreamsTotal={numStreamsTotal}
setStreamsSelectorShown={setStreamsSelectorShown}
enabledSegments={enabledSegments}
/>
{subtitleStreams.length > 0 && <SubtitleControl subtitleStreams={subtitleStreams} activeSubtitleStreamIndex={activeSubtitleStreamIndex} onActiveSubtitleChange={onActiveSubtitleChange} />}
<div style={{ flexGrow: 1, display: 'flex', overflowY: 'hidden' }}>
<AnimatePresence>
{showLeftBar && (
<BatchFilesList
selectedBatchFiles={selectedBatchFiles}
filePath={filePath}
width={leftBarWidth}
batchFiles={batchFiles}
setBatchFiles={setBatchFiles}
onBatchFileSelect={onBatchFileSelect}
batchRemoveFile={batchRemoveFile}
closeBatch={closeBatch}
onMergeFilesClick={concatCurrentBatch}
onBatchConvertToSupportedFormatClick={convertFormatBatch}
/>
)}
</AnimatePresence>
{!showRightBar && (
<FaAngleLeft
title={t('Show sidebar')}
size={30}
role="button"
style={{ marginRight: 10 }}
onClick={toggleSegmentsList}
/>
)}
{/* Middle part: */}
<div style={{ position: 'relative', flexGrow: 1 }}>
{!isFileOpened && <NoFileLoaded mifiLink={mifiLink} toggleHelp={toggleHelp} currentCutSeg={currentCutSeg} />}
<div className="no-user-select" style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, visibility: !isFileOpened ? 'hidden' : undefined }} onWheel={onTimelineWheel}>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video
muted={playbackVolume === 0}
ref={videoRef}
style={videoStyle}
src={fileUri}
onPlay={onSartPlaying}
onPause={onStopPlaying}
onDurationChange={onDurationChange}
onTimeUpdate={onTimeUpdate}
onError={onVideoError}
>
{renderSubtitles()}
</video>
{canvasPlayerEnabled && <Canvas rotate={effectiveRotation} filePath={filePath} width={mainVideoStream.width} height={mainVideoStream.height} streamIndex={mainVideoStream.index} playerTime={playerTime} commandedTime={commandedTime} playing={playing} />}
</div>
)}
{isRotationSet && !hideCanvasPreview && (
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, marginTop: '1em', marginLeft: '1em', color: 'white', display: 'flex', alignItems: 'center' }}>
<MdRotate90DegreesCcw size={26} style={{ marginRight: 5 }} />
{t('Rotation preview')}
{!canvasPlayerRequired && <FaWindowClose role="button" style={{ cursor: 'pointer', verticalAlign: 'middle', padding: 10 }} onClick={() => setHideCanvasPreview(true)} />}
</div>
)}
{isFileOpened && (
<div className="no-user-select" style={{ position: 'absolute', right: 0, bottom: 0, marginBottom: 10, color: 'rgba(255,255,255,0.7)', display: 'flex', alignItems: 'center' }}>
<VolumeControl playbackVolume={playbackVolume} setPlaybackVolume={setPlaybackVolume} usingDummyVideo={usingDummyVideo} />
{subtitleStreams.length > 0 && <SubtitleControl subtitleStreams={subtitleStreams} activeSubtitleStreamIndex={activeSubtitleStreamIndex} onActiveSubtitleChange={onActiveSubtitleChange} />}
{!showRightBar && (
<FaAngleLeft
title={t('Show sidebar')}
size={30}
role="button"
style={{ marginRight: 10 }}
onClick={toggleSegmentsList}
/>
)}
</div>
)}
<AnimatePresence>
{working && <Loading text={working} cutProgress={cutProgress} />}
</AnimatePresence>
{tunerVisible && <ValueTuners type={tunerVisible} onFinished={() => setTunerVisible()} />}
</div>
<AnimatePresence>
{working && <Loading text={working} cutProgress={cutProgress} />}
{showRightBar && isFileOpened && (
<motion.div
className="no-user-select"
style={{ width: rightBarWidth, background: controlsBackground, color: 'rgba(255,255,255,0.7)', display: 'flex', flexDirection: 'column', overflowY: 'hidden' }}
initial={{ x: rightBarWidth }}
animate={{ x: 0 }}
exit={{ x: rightBarWidth }}
>
<SegmentList
currentSegIndex={currentSegIndexSafe}
apparentCutSegments={apparentCutSegments}
inverseCutSegments={inverseCutSegments}
getFrameCount={getFrameCount}
formatTimecode={formatTimecode}
onSegClick={setCurrentSegIndex}
updateSegOrder={updateSegOrder}
updateSegOrders={updateSegOrders}
onLabelSegmentPress={onLabelSegmentPress}
currentCutSeg={currentCutSeg}
segmentAtCursor={segmentAtCursor}
addCutSegment={addCutSegment}
removeCutSegment={removeCutSegment}
toggleSegmentsList={toggleSegmentsList}
splitCurrentSegment={splitCurrentSegment}
enabledSegmentsRaw={enabledSegmentsRaw}
enabledSegments={enabledSegments}
onExportSingleSegmentClick={enableOnlySegment}
onExportSegmentEnabledToggle={toggleSegmentEnabled}
onExportSegmentDisableAll={disableAllSegments}
onExportSegmentEnableAll={enableAllSegments}
jumpSegStart={jumpSegStart}
jumpSegEnd={jumpSegEnd}
onViewSegmentTagsPress={onViewSegmentTagsPress}
/>
</motion.div>
)}
</AnimatePresence>
{tunerVisible && renderTuner(tunerVisible)}
</div>
<AnimatePresence>
{showRightBar && isFileOpened && (
<motion.div
className="no-user-select"
style={{ width: rightBarWidth, background: controlsBackground, color: 'rgba(255,255,255,0.7)', display: 'flex', flexDirection: 'column', overflowY: 'hidden' }}
initial={{ x: rightBarWidth }}
animate={{ x: 0 }}
exit={{ x: rightBarWidth }}
>
<SegmentList
simpleMode={simpleMode}
currentSegIndex={currentSegIndexSafe}
apparentCutSegments={apparentCutSegments}
inverseCutSegments={inverseCutSegments}
getFrameCount={getFrameCount}
formatTimecode={formatTimecode}
invertCutSegments={invertCutSegments}
onSegClick={setCurrentSegIndex}
updateSegOrder={updateSegOrder}
updateSegOrders={updateSegOrders}
onLabelSegmentPress={onLabelSegmentPress}
currentCutSeg={currentCutSeg}
segmentAtCursor={segmentAtCursor}
addCutSegment={addCutSegment}
removeCutSegment={removeCutSegment}
toggleSegmentsList={toggleSegmentsList}
splitCurrentSegment={splitCurrentSegment}
enabledSegmentsRaw={enabledSegmentsRaw}
enabledSegments={enabledSegments}
onExportSingleSegmentClick={enableOnlySegment}
onExportSegmentEnabledToggle={toggleSegmentEnabled}
onExportSegmentDisableAll={disableAllSegments}
onExportSegmentEnableAll={enableAllSegments}
jumpSegStart={jumpSegStart}
jumpSegEnd={jumpSegEnd}
onViewSegmentTagsPress={onViewSegmentTagsPress}
<motion.div className="no-user-select" style={{ background: controlsBackground }}>
<Timeline
shouldShowKeyframes={shouldShowKeyframes}
waveforms={waveforms}
shouldShowWaveform={shouldShowWaveform}
waveformEnabled={waveformEnabled}
thumbnailsEnabled={thumbnailsEnabled}
neighbouringKeyFrames={neighbouringKeyFrames}
thumbnails={thumbnailsSorted}
getCurrentTime={getCurrentTime}
commandedTimeRef={commandedTimeRef}
startTimeOffset={startTimeOffset}
playerTime={playerTime}
commandedTime={commandedTime}
zoom={zoom}
seekAbs={seekAbs}
durationSafe={durationSafe}
apparentCutSegments={apparentCutSegments}
setCurrentSegIndex={setCurrentSegIndex}
currentSegIndexSafe={currentSegIndexSafe}
inverseCutSegments={inverseCutSegments}
formatTimecode={formatTimecode}
onZoomWindowStartTimeChange={setZoomWindowStartTime}
playing={playing}
isFileOpened={isFileOpened}
onWheel={onTimelineWheel}
goToTimecode={goToTimecode}
/>
<BottomBar
zoom={zoom}
setZoom={setZoom}
timelineToggleComfortZoom={timelineToggleComfortZoom}
hasVideo={hasVideo}
isRotationSet={isRotationSet}
rotation={rotation}
areWeCutting={areWeCutting}
increaseRotation={increaseRotation}
cleanupFilesDialog={cleanupFilesDialog}
captureSnapshot={captureSnapshot}
onExportPress={onExportPress}
enabledSegments={enabledSegments}
seekAbs={seekAbs}
currentSegIndexSafe={currentSegIndexSafe}
cutSegments={cutSegments}
currentCutSeg={currentCutSeg}
setCutStart={setCutStart}
setCutEnd={setCutEnd}
setCurrentSegIndex={setCurrentSegIndex}
cutStartTimeManual={cutStartTimeManual}
setCutStartTimeManual={setCutStartTimeManual}
cutEndTimeManual={cutEndTimeManual}
setCutEndTimeManual={setCutEndTimeManual}
jumpCutEnd={jumpCutEnd}
jumpCutStart={jumpCutStart}
jumpTimelineStart={jumpTimelineStart}
jumpTimelineEnd={jumpTimelineEnd}
startTimeOffset={startTimeOffset}
setCutTime={setCutTime}
currentApparentCutSeg={currentApparentCutSeg}
playing={playing}
shortStep={shortStep}
seekClosestKeyframe={seekClosestKeyframe}
togglePlay={togglePlay}
setTimelineMode={setTimelineMode}
timelineMode={timelineMode}
hasAudio={hasAudio}
keyframesEnabled={keyframesEnabled}
toggleKeyframesEnabled={toggleKeyframesEnabled}
detectedFps={detectedFps}
/>
</motion.div>
<SideSheet
width={700}
containerProps={{ style: { maxWidth: '100%' } }}
position={Position.LEFT}
isShown={streamsSelectorShown}
onCloseComplete={() => setStreamsSelectorShown(false)}
>
<StreamsSelector
mainFilePath={filePath}
mainFileFormatData={mainFileFormatData}
mainFileChapters={mainFileChapters}
allFilesMeta={allFilesMeta}
externalFilesMeta={externalFilesMeta}
setExternalFilesMeta={setExternalFilesMeta}
showAddStreamSourceDialog={showAddStreamSourceDialog}
streams={mainStreams}
isCopyingStreamId={isCopyingStreamId}
toggleCopyStreamId={toggleCopyStreamId}
setCopyStreamIdsForPath={setCopyStreamIdsForPath}
onExtractAllStreamsPress={extractAllStreams}
onExtractStreamPress={extractSingleStream}
areWeCutting={areWeCutting}
shortestFlag={shortestFlag}
setShortestFlag={setShortestFlag}
nonCopiedExtraStreams={nonCopiedExtraStreams}
customTagsByFile={customTagsByFile}
setCustomTagsByFile={setCustomTagsByFile}
customTagsByStreamId={customTagsByStreamId}
setCustomTagsByStreamId={setCustomTagsByStreamId}
dispositionByStreamId={dispositionByStreamId}
setDispositionByStreamId={setDispositionByStreamId}
/>
</SideSheet>
<ExportConfirm filePath={filePath} areWeCutting={areWeCutting} enabledSegments={enabledSegments} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} setStreamsSelectorShown={setStreamsSelectorShown} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} getOutSegError={getOutSegError} />
<HelpSheet
visible={helpVisible}
onTogglePress={toggleHelp}
ffmpegCommandLog={ffmpegCommandLog}
onKeyboardShortcutsDialogRequested={onKeyboardShortcutsDialogRequested}
/>
<Sheet visible={settingsVisible} onClosePress={toggleSettings} style={{ background: 'white', color: 'black' }}>
<Table style={{ marginTop: 40 }}>
<Table.Head>
<Table.TextHeaderCell>{t('Settings')}</Table.TextHeaderCell>
<Table.TextHeaderCell>{t('Current setting')}</Table.TextHeaderCell>
</Table.Head>
<Table.Body>
<Settings
onTunerRequested={onTunerRequested}
onKeyboardShortcutsDialogRequested={onKeyboardShortcutsDialogRequested}
/>
</motion.div>
)}
</AnimatePresence>
</div>
</Table.Body>
</Table>
</Sheet>
<motion.div className="no-user-select" style={{ background: controlsBackground }}>
<Timeline
shouldShowKeyframes={shouldShowKeyframes}
waveforms={waveforms}
shouldShowWaveform={shouldShowWaveform}
waveformEnabled={waveformEnabled}
thumbnailsEnabled={thumbnailsEnabled}
neighbouringKeyFrames={neighbouringKeyFrames}
thumbnails={thumbnailsSorted}
getCurrentTime={getCurrentTime}
commandedTimeRef={commandedTimeRef}
startTimeOffset={startTimeOffset}
playerTime={playerTime}
commandedTime={commandedTime}
zoom={zoom}
seekAbs={seekAbs}
durationSafe={durationSafe}
apparentCutSegments={apparentCutSegments}
setCurrentSegIndex={setCurrentSegIndex}
currentSegIndexSafe={currentSegIndexSafe}
invertCutSegments={invertCutSegments}
inverseCutSegments={inverseCutSegments}
formatTimecode={formatTimecode}
onZoomWindowStartTimeChange={setZoomWindowStartTime}
playing={playing}
isFileOpened={isFileOpened}
onWheel={onTimelineWheel}
goToTimecode={goToTimecode}
/>
<ConcatDialog isShown={batchFiles.length > 0 && concatDialogVisible} onHide={() => setConcatDialogVisible(false)} initialPaths={batchFilePaths} onConcat={userConcatFiles} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} />
<BottomBar
zoom={zoom}
setZoom={setZoom}
invertCutSegments={invertCutSegments}
setInvertCutSegments={setInvertCutSegments}
timelineToggleComfortZoom={timelineToggleComfortZoom}
simpleMode={simpleMode}
toggleSimpleMode={toggleSimpleMode}
hasVideo={hasVideo}
isRotationSet={isRotationSet}
rotation={rotation}
areWeCutting={areWeCutting}
autoMerge={autoMerge}
increaseRotation={increaseRotation}
cleanupFilesDialog={cleanupFilesDialog}
renderCaptureFormatButton={renderCaptureFormatButton}
captureSnapshot={captureSnapshot}
onExportPress={onExportPress}
enabledSegments={enabledSegments}
exportConfirmEnabled={exportConfirmEnabled}
toggleExportConfirmEnabled={toggleExportConfirmEnabled}
seekAbs={seekAbs}
currentSegIndexSafe={currentSegIndexSafe}
cutSegments={cutSegments}
currentCutSeg={currentCutSeg}
setCutStart={setCutStart}
setCutEnd={setCutEnd}
setCurrentSegIndex={setCurrentSegIndex}
cutStartTimeManual={cutStartTimeManual}
setCutStartTimeManual={setCutStartTimeManual}
cutEndTimeManual={cutEndTimeManual}
setCutEndTimeManual={setCutEndTimeManual}
jumpCutEnd={jumpCutEnd}
jumpCutStart={jumpCutStart}
jumpTimelineStart={jumpTimelineStart}
jumpTimelineEnd={jumpTimelineEnd}
startTimeOffset={startTimeOffset}
setCutTime={setCutTime}
currentApparentCutSeg={currentApparentCutSeg}
playing={playing}
shortStep={shortStep}
seekClosestKeyframe={seekClosestKeyframe}
togglePlay={togglePlay}
setTimelineMode={setTimelineMode}
timelineMode={timelineMode}
hasAudio={hasAudio}
keyframesEnabled={keyframesEnabled}
toggleKeyframesEnabled={toggleKeyframesEnabled}
detectedFps={detectedFps}
/>
</motion.div>
<SideSheet
width={700}
containerProps={{ style: { maxWidth: '100%' } }}
position={Position.LEFT}
isShown={streamsSelectorShown}
onCloseComplete={() => setStreamsSelectorShown(false)}
>
<StreamsSelector
mainFilePath={filePath}
mainFileFormatData={mainFileFormatData}
mainFileChapters={mainFileChapters}
allFilesMeta={allFilesMeta}
externalFilesMeta={externalFilesMeta}
setExternalFilesMeta={setExternalFilesMeta}
showAddStreamSourceDialog={showAddStreamSourceDialog}
streams={mainStreams}
isCopyingStreamId={isCopyingStreamId}
toggleCopyStreamId={toggleCopyStreamId}
setCopyStreamIdsForPath={setCopyStreamIdsForPath}
onExtractAllStreamsPress={extractAllStreams}
onExtractStreamPress={extractSingleStream}
areWeCutting={areWeCutting}
shortestFlag={shortestFlag}
setShortestFlag={setShortestFlag}
nonCopiedExtraStreams={nonCopiedExtraStreams}
AutoExportToggler={AutoExportToggler}
customTagsByFile={customTagsByFile}
setCustomTagsByFile={setCustomTagsByFile}
customTagsByStreamId={customTagsByStreamId}
setCustomTagsByStreamId={setCustomTagsByStreamId}
dispositionByStreamId={dispositionByStreamId}
setDispositionByStreamId={setDispositionByStreamId}
/>
</SideSheet>
<ExportConfirm filePath={filePath} autoMerge={autoMerge} setAutoMerge={setAutoMerge} areWeCutting={areWeCutting} enabledSegments={enabledSegments} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} keyframeCut={keyframeCut} toggleKeyframeCut={toggleKeyframeCut} renderOutFmt={renderOutFmt} preserveMovData={preserveMovData} togglePreserveMovData={togglePreserveMovData} movFastStart={movFastStart} toggleMovFastStart={toggleMovFastStart} avoidNegativeTs={avoidNegativeTs} setAvoidNegativeTs={setAvoidNegativeTs} changeOutDir={changeOutDir} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} setStreamsSelectorShown={setStreamsSelectorShown} exportConfirmEnabled={exportConfirmEnabled} toggleExportConfirmEnabled={toggleExportConfirmEnabled} segmentsToChapters={segmentsToChapters} toggleSegmentsToChapters={toggleSegmentsToChapters} outFormat={fileFormat} preserveMetadataOnMerge={preserveMetadataOnMerge} togglePreserveMetadataOnMerge={togglePreserveMetadataOnMerge} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} getOutSegError={getOutSegError} autoDeleteMergedSegments={autoDeleteMergedSegments} setAutoDeleteMergedSegments={setAutoDeleteMergedSegments} safeOutputFileName={safeOutputFileName} toggleSafeOutputFileName={toggleSafeOutputFileName} segmentsToChaptersOnly={segmentsToChaptersOnly} setSegmentsToChaptersOnly={setSegmentsToChaptersOnly} enableSmartCut={enableSmartCut} setEnableSmartCut={setEnableSmartCut} />
<HelpSheet
visible={helpVisible}
onTogglePress={toggleHelp}
ffmpegCommandLog={ffmpegCommandLog}
onKeyboardShortcutsDialogRequested={onKeyboardShortcutsDialogRequested}
/>
<Sheet visible={settingsVisible} onClosePress={toggleSettings} style={{ background: 'white', color: 'black' }}>
<Table style={{ marginTop: 40 }}>
<Table.Head>
<Table.TextHeaderCell>{t('Settings')}</Table.TextHeaderCell>
<Table.TextHeaderCell>{t('Current setting')}</Table.TextHeaderCell>
</Table.Head>
<Table.Body>
<Settings
changeOutDir={changeOutDir}
customOutDir={customOutDir}
keyframeCut={keyframeCut}
setKeyframeCut={setKeyframeCut}
invertCutSegments={invertCutSegments}
setInvertCutSegments={setInvertCutSegments}
autoSaveProjectFile={autoSaveProjectFile}
setAutoSaveProjectFile={setAutoSaveProjectFile}
timecodeFormat={timecodeFormat}
setTimecodeFormat={setTimecodeFormat}
askBeforeClose={askBeforeClose}
setAskBeforeClose={setAskBeforeClose}
enableAskForImportChapters={enableAskForImportChapters}
setEnableAskForImportChapters={setEnableAskForImportChapters}
enableAskForFileOpenAction={enableAskForFileOpenAction}
setEnableAskForFileOpenAction={setEnableAskForFileOpenAction}
ffmpegExperimental={ffmpegExperimental}
setFfmpegExperimental={setFfmpegExperimental}
invertTimelineScroll={invertTimelineScroll}
setInvertTimelineScroll={setInvertTimelineScroll}
language={language}
setLanguage={setLanguage}
hideNotifications={hideNotifications}
setHideNotifications={setHideNotifications}
autoLoadTimecode={autoLoadTimecode}
setAutoLoadTimecode={setAutoLoadTimecode}
enableTransferTimestamps={enableTransferTimestamps}
setEnableTransferTimestamps={setEnableTransferTimestamps}
enableAutoHtml5ify={enableAutoHtml5ify}
setEnableAutoHtml5ify={setEnableAutoHtml5ify}
AutoExportToggler={AutoExportToggler}
renderCaptureFormatButton={renderCaptureFormatButton}
onTunerRequested={onTunerRequested}
onKeyboardShortcutsDialogRequested={onKeyboardShortcutsDialogRequested}
/>
</Table.Body>
</Table>
</Sheet>
<ConcatDialog isShown={batchFiles.length > 0 && concatDialogVisible} onHide={() => setConcatDialogVisible(false)} initialPaths={batchFilePaths} onConcat={userConcatFiles} segmentsToChapters={segmentsToChapters} setSegmentsToChapters={setSegmentsToChapters} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} preserveMetadataOnMerge={preserveMetadataOnMerge} setPreserveMetadataOnMerge={setPreserveMetadataOnMerge} preserveMovData={preserveMovData} setPreserveMovData={setPreserveMovData} />
<KeyboardShortcuts isShown={keyboardShortcutsVisible} onHide={() => setKeyboardShortcutsVisible(false)} keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} />
</div>
</ThemeProvider>
<KeyboardShortcuts isShown={keyboardShortcutsVisible} onHide={() => setKeyboardShortcutsVisible(false)} keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} />
</div>
</ThemeProvider>
</UserSettingsContext.Provider>
);
});

@ -13,11 +13,13 @@ import SegmentCutpointButton from './components/SegmentCutpointButton';
import SetCutpointButton from './components/SetCutpointButton';
import ExportButton from './components/ExportButton';
import ToggleExportConfirm from './components/ToggleExportConfirm';
import CaptureFormatButton from './components/CaptureFormatButton';
import SimpleModeButton from './components/SimpleModeButton';
import { withBlur, toast, mirrorTransform } from './util';
import { getSegColor } from './util/colors';
import { formatDuration, parseDuration } from './util/duration';
import useUserSettings from './hooks/useUserSettings';
const isDev = window.require('electron-is-dev');
@ -27,9 +29,9 @@ const zoomOptions = Array(13).fill().map((unused, z) => 2 ** z);
const leftRightWidth = 100;
const BottomBar = memo(({
zoom, setZoom, invertCutSegments, setInvertCutSegments, timelineToggleComfortZoom, simpleMode, toggleSimpleMode,
isRotationSet, rotation, areWeCutting, increaseRotation, cleanupFilesDialog, renderCaptureFormatButton,
captureSnapshot, onExportPress, enabledSegments, hasVideo, autoMerge, exportConfirmEnabled, toggleExportConfirmEnabled,
zoom, setZoom, timelineToggleComfortZoom,
isRotationSet, rotation, areWeCutting, increaseRotation, cleanupFilesDialog,
captureSnapshot, onExportPress, enabledSegments, hasVideo,
seekAbs, currentSegIndexSafe, cutSegments, currentCutSeg, setCutStart, setCutEnd,
setCurrentSegIndex, cutStartTimeManual, setCutStartTimeManual, cutEndTimeManual, setCutEndTimeManual,
jumpTimelineStart, jumpTimelineEnd, jumpCutEnd, jumpCutStart, startTimeOffset, setCutTime, currentApparentCutSeg,
@ -38,6 +40,8 @@ const BottomBar = memo(({
}) => {
const { t } = useTranslation();
const { invertCutSegments, setInvertCutSegments, simpleMode, toggleSimpleMode } = useUserSettings();
const onYinYangClick = useCallback(() => {
setInvertCutSegments(v => {
const newVal = !v;
@ -281,7 +285,7 @@ const BottomBar = memo(({
className="no-user-select"
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '3px 4px' }}
>
<SimpleModeButton simpleMode={simpleMode} toggleSimpleMode={toggleSimpleMode} style={{ flexShrink: 0 }} />
<SimpleModeButton style={{ flexShrink: 0 }} />
{simpleMode && <div role="button" onClick={toggleSimpleMode} style={{ marginLeft: 5, fontSize: '90%' }}>{t('Toggle advanced view')}</div>}
@ -344,7 +348,7 @@ const BottomBar = memo(({
{hasVideo && (
<>
{!simpleMode && renderCaptureFormatButton({ height: 20 })}
{!simpleMode && <CaptureFormatButton height={20} />}
<IoIosCamera
style={{ paddingLeft: 5, paddingRight: 15 }}
@ -355,9 +359,9 @@ const BottomBar = memo(({
</>
)}
{!simpleMode && <ToggleExportConfirm style={{ marginRight: 5 }} exportConfirmEnabled={exportConfirmEnabled} toggleExportConfirmEnabled={toggleExportConfirmEnabled} />}
{!simpleMode && <ToggleExportConfirm style={{ marginRight: 5 }} />}
<ExportButton size={1.3} enabledSegments={enabledSegments} areWeCutting={areWeCutting} autoMerge={autoMerge} onClick={onExportPress} />
<ExportButton size={1.3} enabledSegments={enabledSegments} areWeCutting={areWeCutting} onClick={onExportPress} />
</div>
</>
);

@ -16,6 +16,7 @@ import HighlightedText from './components/HighlightedText';
import { withBlur, toast } from './util';
import { isMov as ffmpegIsMov } from './ffmpeg';
import useUserSettings from './hooks/useUserSettings';
const sheetStyle = {
position: 'fixed',
@ -39,16 +40,17 @@ const warningStyle = { color: '#faa', fontSize: '80%' };
const HelpIcon = ({ onClick }) => <IoIosHelpCircle size={20} role="button" onClick={withBlur(onClick)} style={{ cursor: 'pointer', verticalAlign: 'middle', marginLeft: 5 }} />;
const ExportConfirm = memo(({
autoMerge, areWeCutting, enabledSegments, willMerge, visible, onClosePress, onExportConfirm, keyframeCut, toggleKeyframeCut,
setAutoMerge, renderOutFmt, preserveMovData, togglePreserveMovData, movFastStart, toggleMovFastStart, avoidNegativeTs, setAvoidNegativeTs,
changeOutDir, outputDir, numStreamsTotal, numStreamsToCopy, setStreamsSelectorShown,
exportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, toggleSegmentsToChapters, outFormat,
preserveMetadataOnMerge, togglePreserveMetadataOnMerge, outSegTemplate, setOutSegTemplate, generateOutSegFileNames,
filePath, currentSegIndexSafe, getOutSegError, autoDeleteMergedSegments, setAutoDeleteMergedSegments,
safeOutputFileName, toggleSafeOutputFileName, segmentsToChaptersOnly, setSegmentsToChaptersOnly, enableSmartCut, setEnableSmartCut,
areWeCutting, enabledSegments, willMerge, visible, onClosePress, onExportConfirm,
renderOutFmt,
outputDir, numStreamsTotal, numStreamsToCopy, setStreamsSelectorShown,
outFormat,
outSegTemplate, setOutSegTemplate, generateOutSegFileNames,
filePath, currentSegIndexSafe, getOutSegError,
}) => {
const { t } = useTranslation();
const { changeOutDir, keyframeCut, preserveMovData, movFastStart, avoidNegativeTs, setAvoidNegativeTs, autoDeleteMergedSegments, exportConfirmEnabled, toggleExportConfirmEnabled, segmentsToChapters, toggleSegmentsToChapters, preserveMetadataOnMerge, togglePreserveMetadataOnMerge, enableSmartCut, setEnableSmartCut } = useUserSettings();
const isMov = ffmpegIsMov(outFormat);
const isIpod = outFormat === 'ipod';
@ -121,7 +123,7 @@ const ExportConfirm = memo(({
<h2 style={{ marginTop: 0 }}>{t('Export options')}</h2>
<ul>
{enabledSegments.length >= 2 && <li>{t('Merge {{segments}} cut segments to one file?', { segments: enabledSegments.length })} <MergeExportButton autoMerge={autoMerge} enabledSegments={enabledSegments} setAutoMerge={setAutoMerge} autoDeleteMergedSegments={autoDeleteMergedSegments} setAutoDeleteMergedSegments={setAutoDeleteMergedSegments} segmentsToChaptersOnly={segmentsToChaptersOnly} setSegmentsToChaptersOnly={setSegmentsToChaptersOnly} /></li>}
{enabledSegments.length >= 2 && <li>{t('Merge {{segments}} cut segments to one file?', { segments: enabledSegments.length })} <MergeExportButton enabledSegments={enabledSegments} /></li>}
<li>
{t('Output container format:')} {renderOutFmt({ height: 20, maxWidth: 150 })}
<HelpIcon onClick={onOutFmtHelpPress} />
@ -135,7 +137,7 @@ const ExportConfirm = memo(({
</li>
{canEditTemplate && (
<li>
<OutSegTemplateEditor filePath={filePath} helpIcon={outSegTemplateHelpIcon} outSegTemplate={outSegTemplate} setOutSegTemplate={setOutSegTemplate} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} getOutSegError={getOutSegError} safeOutputFileName={safeOutputFileName} toggleSafeOutputFileName={toggleSafeOutputFileName} />
<OutSegTemplateEditor filePath={filePath} helpIcon={outSegTemplateHelpIcon} outSegTemplate={outSegTemplate} setOutSegTemplate={setOutSegTemplate} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} getOutSegError={getOutSegError} />
</li>
)}
</ul>
@ -166,7 +168,7 @@ const ExportConfirm = memo(({
</li>
{!enableSmartCut && (
<li>
{t('Cut mode:')} <KeyframeCutButton keyframeCut={keyframeCut} onClick={withBlur(() => toggleKeyframeCut(false))} />
{t('Cut mode:')} <KeyframeCutButton />
<HelpIcon onClick={onKeyframeCutHelpPress} /> {!keyframeCut && <span style={warningStyle}>{t('Note: Keyframe cut is recommended for most common files')}</span>}
</li>
)}
@ -176,11 +178,11 @@ const ExportConfirm = memo(({
{isMov && (
<>
<li>
{t('Enable MOV Faststart?')} <MovFastStartButton movFastStart={movFastStart} toggleMovFastStart={toggleMovFastStart} />
{t('Enable MOV Faststart?')} <MovFastStartButton />
<HelpIcon onClick={onMovFastStartHelpPress} /> {isIpod && !movFastStart && <span style={warningStyle}>{t('For the ipod format, it is recommended to activate this option')}</span>}
</li>
<li>
{t('Preserve all MP4/MOV metadata?')} <PreserveMovDataButton preserveMovData={preserveMovData} togglePreserveMovData={togglePreserveMovData} />
{t('Preserve all MP4/MOV metadata?')} <PreserveMovDataButton />
<HelpIcon onClick={onPreserveMovDataHelpPress} /> {isIpod && preserveMovData && <span style={warningStyle}>{t('For the ipod format, it is recommended to deactivate this option')}</span>}
</li>
</>
@ -211,7 +213,7 @@ const ExportConfirm = memo(({
transition={{ duration: 0.4, easings: ['easeOut'] }}
style={{ display: 'flex', alignItems: 'flex-end' }}
>
<ToggleExportConfirm size={25} exportConfirmEnabled={exportConfirmEnabled} toggleExportConfirmEnabled={toggleExportConfirmEnabled} />
<ToggleExportConfirm size={25} />
<div style={{ fontSize: 13, marginLeft: 3, marginRight: 7, maxWidth: 120, lineHeight: '100%', color: exportConfirmEnabled ? 'white' : 'rgba(255,255,255,0.3)', cursor: 'pointer' }} role="button" onClick={toggleExportConfirmEnabled}>{t('Show this page before exporting?')}</div>
</motion.div>
@ -222,7 +224,7 @@ const ExportConfirm = memo(({
exit={{ scale: 0.7, opacity: 0 }}
transition={{ duration: 0.4, easings: ['easeOut'] }}
>
<ExportButton enabledSegments={enabledSegments} areWeCutting={areWeCutting} autoMerge={autoMerge} onClick={() => onExportConfirm()} size={1.7} />
<ExportButton enabledSegments={enabledSegments} areWeCutting={areWeCutting} onClick={() => onExportConfirm()} size={1.7} />
</motion.div>
</div>
</>

@ -5,11 +5,13 @@ import { useTranslation, Trans } from 'react-i18next';
import SetCutpointButton from './components/SetCutpointButton';
import SimpleModeButton from './components/SimpleModeButton';
import useUserSettings from './hooks/useUserSettings';
const electron = window.require('electron');
const NoFileLoaded = memo(({ mifiLink, toggleHelp, currentCutSeg, simpleMode, toggleSimpleMode }) => {
const NoFileLoaded = memo(({ mifiLink, toggleHelp, currentCutSeg }) => {
const { t } = useTranslation();
const { simpleMode, toggleSimpleMode } = useUserSettings();
return (
<div className="no-user-select" style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, border: '2vmin dashed #252525', color: '#505050', margin: '5vmin', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', whiteSpace: 'nowrap' }}>
@ -24,7 +26,7 @@ const NoFileLoaded = memo(({ mifiLink, toggleHelp, currentCutSeg, simpleMode, to
</div>
<div style={{ fontSize: '3vmin', color: '#ccc', cursor: 'pointer' }} role="button" onClick={toggleSimpleMode}>
<SimpleModeButton simpleMode={simpleMode} toggleSimpleMode={toggleSimpleMode} style={{ verticalAlign: 'middle' }} size={16} /> {simpleMode ? i18n.t('to show advanced view') : i18n.t('to show simple view')}
<SimpleModeButton style={{ verticalAlign: 'middle' }} size={16} /> {simpleMode ? i18n.t('to show advanced view') : i18n.t('to show simple view')}
</div>

@ -10,6 +10,7 @@ import useDebounce from 'react-use/lib/useDebounce';
import scrollIntoView from 'scroll-into-view-if-needed';
import useContextMenu from './hooks/useContextMenu';
import useUserSettings from './hooks/useUserSettings';
import { saveColor } from './colors';
import { getSegColor } from './util/colors';
@ -124,14 +125,16 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou
const SegmentList = memo(({
formatTimecode, apparentCutSegments, inverseCutSegments, getFrameCount, onSegClick,
currentSegIndex, invertCutSegments,
currentSegIndex,
updateSegOrder, updateSegOrders, addCutSegment, removeCutSegment,
onLabelSegmentPress, currentCutSeg, segmentAtCursor, toggleSegmentsList, splitCurrentSegment,
enabledSegments, enabledSegmentsRaw, onExportSingleSegmentClick, onExportSegmentEnabledToggle, onExportSegmentDisableAll, onExportSegmentEnableAll,
jumpSegStart, jumpSegEnd, simpleMode, onViewSegmentTagsPress,
jumpSegStart, jumpSegEnd, onViewSegmentTagsPress,
}) => {
const { t } = useTranslation();
const { invertCutSegments, simpleMode } = useUserSettings();
const segments = invertCutSegments ? inverseCutSegments : apparentCutSegments;
const sortableList = segments.map((seg) => ({ id: seg.segId, seg }));

@ -2,6 +2,9 @@ import React, { memo, useCallback, useMemo } from 'react';
import { FaYinYang, FaKeyboard } from 'react-icons/fa';
import { Button, Table, NumericalIcon, KeyIcon, FolderCloseIcon, DocumentIcon, TimeIcon, Checkbox, Select } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
import CaptureFormatButton from './components/CaptureFormatButton';
import AutoExportToggler from './components/AutoExportToggler';
import useUserSettings from './hooks/useUserSettings';
// https://www.electronjs.org/docs/api/locales
@ -36,17 +39,13 @@ const Row = (props) => <Table.Row height="auto" paddingY={12} {...props} />;
const KeyCell = (props) => <Table.TextCell textProps={{ whiteSpace: 'auto' }} {...props} />;
const Settings = memo(({
changeOutDir, customOutDir, keyframeCut, setKeyframeCut, invertCutSegments, setInvertCutSegments,
autoSaveProjectFile, setAutoSaveProjectFile, timecodeFormat, setTimecodeFormat, askBeforeClose, setAskBeforeClose,
AutoExportToggler, renderCaptureFormatButton, onTunerRequested, language, setLanguage,
invertTimelineScroll, setInvertTimelineScroll, ffmpegExperimental, setFfmpegExperimental,
enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction,
hideNotifications, setHideNotifications, autoLoadTimecode, setAutoLoadTimecode,
enableTransferTimestamps, setEnableTransferTimestamps, enableAutoHtml5ify, setEnableAutoHtml5ify,
onTunerRequested,
onKeyboardShortcutsDialogRequested,
}) => {
const { t } = useTranslation();
const { customOutDir, changeOutDir, keyframeCut, toggleKeyframeCut, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, autoSaveProjectFile, setAutoSaveProjectFile, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, ffmpegExperimental, setFfmpegExperimental, hideNotifications, setHideNotifications, autoLoadTimecode, setAutoLoadTimecode, enableTransferTimestamps, setEnableTransferTimestamps, enableAutoHtml5ify, setEnableAutoHtml5ify } = useUserSettings();
const onLangChange = useCallback((e) => {
const { value } = e.target;
const l = value !== '' ? value : undefined;
@ -115,8 +114,8 @@ const Settings = memo(({
<b>{t('Normal cut')}</b>: {t('Accurate time but could leave an empty portion at the beginning of the video. Equiv to')} <i>ffmpeg -i -ss ...</i><br />
</KeyCell>
<Table.TextCell>
<Button iconBefore={keyframeCut === 'keyframe' ? KeyIcon : undefined} onClick={() => setKeyframeCut(keyframeCut === 'keyframe' ? 'normal' : 'keyframe')}>
{keyframeCut === 'keyframe' ? t('Keyframe cut') : t('Normal cut')}
<Button iconBefore={keyframeCut ? KeyIcon : undefined} onClick={() => toggleKeyframeCut()}>
{keyframeCut ? t('Keyframe cut') : t('Normal cut')}
</Button>
</Table.TextCell>
</Row>
@ -174,7 +173,7 @@ const Settings = memo(({
{t('Snapshot capture format')}
</KeyCell>
<Table.TextCell>
{renderCaptureFormatButton()}
<CaptureFormatButton showIcon />
</Table.TextCell>
</Row>

@ -7,6 +7,7 @@ import { MdSubtitles } from 'react-icons/md';
import { BookIcon, Paragraph, TextInput, MoreIcon, Position, Popover, Menu, TrashIcon, EditIcon, InfoSignIcon, IconButton, Select, Heading, SortAscIcon, SortDescIcon, Dialog, Button, PlusIcon, Pane, ForkIcon, Alert } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
import AutoExportToggler from './components/AutoExportToggler';
import { askForMetadataKey, showJson5Dialog } from './dialogs';
import { formatDuration } from './util/duration';
import { getStreamFps } from './ffmpeg';
@ -325,7 +326,7 @@ const StreamsSelector = memo(({
mainFilePath, mainFileFormatData, streams: mainFileStreams, mainFileChapters, isCopyingStreamId, toggleCopyStreamId,
setCopyStreamIdsForPath, onExtractStreamPress, onExtractAllStreamsPress, allFilesMeta, externalFilesMeta, setExternalFilesMeta,
showAddStreamSourceDialog, shortestFlag, setShortestFlag, nonCopiedExtraStreams,
AutoExportToggler, customTagsByFile, setCustomTagsByFile, customTagsByStreamId, setCustomTagsByStreamId,
customTagsByFile, setCustomTagsByFile, customTagsByStreamId, setCustomTagsByStreamId,
dispositionByStreamId, setDispositionByStreamId,
}) => {
const [editingFile, setEditingFile] = useState();

@ -8,6 +8,7 @@ import { FaCaretDown, FaCaretUp } from 'react-icons/fa';
import TimelineSeg from './TimelineSeg';
import BetweenSegments from './BetweenSegments';
import useContextMenu from './hooks/useContextMenu';
import useUserSettings from './hooks/useUserSettings';
import { timelineBackground } from './colors';
@ -59,13 +60,15 @@ const CommandedTime = memo(({ commandedTimePercent }) => {
const Timeline = memo(({
durationSafe, getCurrentTime, startTimeOffset, playerTime, commandedTime,
zoom, neighbouringKeyFrames, seekAbs, apparentCutSegments,
setCurrentSegIndex, currentSegIndexSafe, invertCutSegments, inverseCutSegments, formatTimecode,
setCurrentSegIndex, currentSegIndexSafe, inverseCutSegments, formatTimecode,
waveforms, shouldShowWaveform, shouldShowKeyframes, timelineHeight = 36, thumbnails,
onZoomWindowStartTimeChange, waveformEnabled, thumbnailsEnabled,
playing, isFileOpened, onWheel, commandedTimeRef, goToTimecode,
}) => {
const { t } = useTranslation();
const { invertCutSegments } = useUserSettings();
const timelineScrollerRef = useRef();
const timelineScrollerSkipEventRef = useRef();
const timelineScrollerSkipEventDebounce = useRef();

@ -1,4 +1,4 @@
import React, { memo } from 'react';
import React, { memo, useCallback } from 'react';
import { IoIosHelpCircle, IoIosSettings } from 'react-icons/io';
import { FaLock, FaUnlock } from 'react-icons/fa';
import { IconButton, Button, CrossIcon, ListIcon, VolumeUpIcon, VolumeOffIcon } from 'evergreen-ui';
@ -8,15 +8,18 @@ import MergeExportButton from './components/MergeExportButton';
import { withBlur, isMasBuild } from './util';
import { primaryTextColor, controlsBackground } from './colors';
import useUserSettings from './hooks/useUserSettings';
const TopMenu = memo(({
filePath, copyAnyAudioTrack, toggleStripAudio, customOutDir, changeOutDir,
filePath, fileFormat, copyAnyAudioTrack, toggleStripAudio,
renderOutFmt, toggleHelp, numStreamsToCopy, numStreamsTotal, setStreamsSelectorShown, toggleSettings,
enabledSegments, autoMerge, setAutoMerge, autoDeleteMergedSegments, setAutoDeleteMergedSegments, isCustomFormatSelected, onOutFormatLockedClick, simpleMode, outFormatLocked, clearOutDir,
segmentsToChaptersOnly, setSegmentsToChaptersOnly,
enabledSegments, isCustomFormatSelected, clearOutDir,
}) => {
const { t } = useTranslation();
const { customOutDir, changeOutDir, simpleMode, outFormatLocked, setOutFormatLocked } = useUserSettings();
const onOutFormatLockedClick = useCallback(() => setOutFormatLocked((v) => (v ? undefined : fileFormat)), [fileFormat, setOutFormatLocked]);
// We cannot allow exporting to a directory which has not yet been confirmed by an open dialog because of sandox restrictions
const showClearWorkingDirButton = customOutDir && !isMasBuild;
@ -75,7 +78,7 @@ const TopMenu = memo(({
{!simpleMode && (isCustomFormatSelected || outFormatLocked) && renderFormatLock()}
<MergeExportButton autoMerge={autoMerge} enabledSegments={enabledSegments} setAutoMerge={setAutoMerge} autoDeleteMergedSegments={autoDeleteMergedSegments} setAutoDeleteMergedSegments={setAutoDeleteMergedSegments} segmentsToChaptersOnly={segmentsToChaptersOnly} setSegmentsToChaptersOnly={setSegmentsToChaptersOnly} />
<MergeExportButton enabledSegments={enabledSegments} />
</>
)}

@ -0,0 +1,18 @@
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, ForkIcon, DisableIcon } from 'evergreen-ui';
import useUserSettings from '../hooks/useUserSettings';
const AutoExportToggler = memo(() => {
const { t } = useTranslation();
const { autoExportExtraStreams, setAutoExportExtraStreams } = useUserSettings();
return (
<Button intent={autoExportExtraStreams ? 'success' : 'danger'} iconBefore={autoExportExtraStreams ? ForkIcon : DisableIcon} onClick={() => setAutoExportExtraStreams(!autoExportExtraStreams)}>
{autoExportExtraStreams ? t('Extract') : t('Discard')}
</Button>
);
});
export default AutoExportToggler;

@ -0,0 +1,25 @@
import React, { memo } from 'react';
import { Button } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
import { FaImage } from 'react-icons/fa';
import useUserSettings from '../hooks/useUserSettings';
import { withBlur } from '../util';
const CaptureFormatButton = memo(({ showIcon = false, ...props }) => {
const { t } = useTranslation();
const { captureFormat, toggleCaptureFormat } = useUserSettings();
return (
<Button
iconBefore={showIcon ? <FaImage /> : undefined}
title={t('Capture frame format')}
onClick={withBlur(toggleCaptureFormat)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{captureFormat}
</Button>
);
});
export default CaptureFormatButton;

@ -6,6 +6,7 @@ import { AiOutlineMergeCells } from 'react-icons/ai';
import { readFileMeta, getSmarterOutFormat } from '../ffmpeg';
import useFileFormatState from '../hooks/useFileFormatState';
import OutputFormatSelect from './OutputFormatSelect';
import useUserSettings from '../hooks/useUserSettings';
const { basename } = window.require('path');
@ -17,12 +18,10 @@ const rowStyle = {
const ConcatDialog = memo(({
isShown, onHide, initialPaths, onConcat,
segmentsToChapters, setSegmentsToChapters,
alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles,
preserveMetadataOnMerge, setPreserveMetadataOnMerge,
preserveMovData, setPreserveMovData,
}) => {
const { t } = useTranslation();
const { preserveMovData, setPreserveMovData, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge } = useUserSettings();
const [paths, setPaths] = useState(initialPaths);
const [includeAllStreams, setIncludeAllStreams] = useState(false);

@ -1,23 +0,0 @@
import React, { memo } from 'react';
import { Button, FolderOpenIcon } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
import { withBlur } from '../util';
const CustomOutDirButton = memo(({ customOutDir, changeOutDir }) => {
const { t } = useTranslation();
return (
<Button
iconBefore={customOutDir ? FolderOpenIcon : undefined}
height={20}
onClick={withBlur(changeOutDir)}
title={customOutDir}
>
{customOutDir ? t('Working dir set') : t('Working dir unset')}
</Button>
);
});
export default CustomOutDirButton;

@ -4,13 +4,16 @@ import { FaFileExport } from 'react-icons/fa';
import { useTranslation } from 'react-i18next';
import { primaryColor } from '../colors';
import useUserSettings from '../hooks/useUserSettings';
const ExportButton = memo(({ enabledSegments, areWeCutting, autoMerge, onClick, size = 1 }) => {
const ExportButton = memo(({ enabledSegments, areWeCutting, onClick, size = 1 }) => {
const CutIcon = areWeCutting ? FiScissors : FaFileExport;
const { t } = useTranslation();
const { autoMerge } = useUserSettings();
let exportButtonTitle = t('Export');
if (enabledSegments.length === 1) {
exportButtonTitle = t('Export selection');

@ -3,17 +3,19 @@ import { Button, KeyIcon } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
import { withBlur } from '../util';
import useUserSettings from '../hooks/useUserSettings';
const KeyframeCutButton = memo(({ keyframeCut, onClick }) => {
const KeyframeCutButton = memo(() => {
const { t } = useTranslation();
const { keyframeCut, toggleKeyframeCut } = useUserSettings();
return (
<Button
height={20}
iconBefore={keyframeCut ? KeyIcon : undefined}
title={`${t('Cut mode is:')} ${keyframeCut ? t('Keyframe cut') : t('Normal cut')}`}
onClick={withBlur(onClick)}
onClick={withBlur(() => toggleKeyframeCut(false))}
>
{keyframeCut ? t('Keyframe cut') : t('Normal cut')}
</Button>

@ -4,11 +4,14 @@ import { useTranslation } from 'react-i18next';
import { MdCallSplit, MdCallMerge } from 'react-icons/md';
import { withBlur } from '../util';
import useUserSettings from '../hooks/useUserSettings';
const MergeExportButton = memo(({ autoMerge, enabledSegments, setAutoMerge, autoDeleteMergedSegments, setAutoDeleteMergedSegments, segmentsToChaptersOnly, setSegmentsToChaptersOnly }) => {
const MergeExportButton = memo(({ enabledSegments }) => {
const { t } = useTranslation();
const { autoMerge, setAutoMerge, autoDeleteMergedSegments, setAutoDeleteMergedSegments, segmentsToChaptersOnly, setSegmentsToChaptersOnly } = useUserSettings();
let AutoMergeIcon;
let effectiveMode;

@ -3,10 +3,12 @@ import { Button } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
import { withBlur } from '../util';
import useUserSettings from '../hooks/useUserSettings';
const MovFastStartButton = memo(({ movFastStart, toggleMovFastStart }) => {
const MovFastStartButton = memo(() => {
const { t } = useTranslation();
const { movFastStart, toggleMovFastStart } = useUserSettings();
return (
<Button height={20} onClick={withBlur(toggleMovFastStart)}>

@ -8,13 +8,16 @@ import withReactContent from 'sweetalert2-react-content';
import HighlightedText from './HighlightedText';
import { defaultOutSegTemplate } from '../util';
import useUserSettings from '../hooks/useUserSettings';
const ReactSwal = withReactContent(Swal);
const inputStyle = { flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em' };
const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe, getOutSegError, safeOutputFileName, toggleSafeOutputFileName }) => {
const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe, getOutSegError }) => {
const { safeOutputFileName, toggleSafeOutputFileName } = useUserSettings();
const [text, setText] = useState(outSegTemplate);
const [debouncedText] = useDebounce(text, 500);
const [validText, setValidText] = useState();

@ -3,10 +3,12 @@ import { Button } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
import { withBlur } from '../util';
import useUserSettings from '../hooks/useUserSettings';
const PreserveMovDataButton = memo(({ preserveMovData, togglePreserveMovData }) => {
const PreserveMovDataButton = memo(() => {
const { t } = useTranslation();
const { preserveMovData, togglePreserveMovData } = useUserSettings();
return (
<Button height={20} onClick={withBlur(togglePreserveMovData)}>

@ -3,10 +3,12 @@ import { useTranslation } from 'react-i18next';
import { FaBaby } from 'react-icons/fa';
import { primaryTextColor } from '../colors';
import useUserSettings from '../hooks/useUserSettings';
const SimpleModeButton = memo(({ simpleMode, toggleSimpleMode, size = 20, style }) => {
const SimpleModeButton = memo(({ size = 20, style }) => {
const { t } = useTranslation();
const { simpleMode, toggleSimpleMode } = useUserSettings();
return (
<FaBaby

@ -4,10 +4,12 @@ import { useTranslation } from 'react-i18next';
import { MdEventNote } from 'react-icons/md';
import { primaryTextColor } from '../colors';
import useUserSettings from '../hooks/useUserSettings';
const ToggleExportConfirm = memo(({ exportConfirmEnabled, toggleExportConfirmEnabled, size = 23, style }) => {
const ToggleExportConfirm = memo(({ size = 23, style }) => {
const { t } = useTranslation();
const { exportConfirmEnabled, toggleExportConfirmEnabled } = useUserSettings();
return (
<MdEventNote style={{ color: exportConfirmEnabled ? primaryTextColor : 'rgba(255,255,255,0.3)', ...style }} size={size} title={t('Show export options screen before exporting?')} role="button" onClick={toggleExportConfirmEnabled} />

@ -0,0 +1,44 @@
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import ValueTuner from './ValueTuner';
import useUserSettings from '../hooks/useUserSettings';
const ValueTuners = memo(({ type, onFinished }) => {
const { t } = useTranslation();
const { wheelSensitivity, setWheelSensitivity, keyboardNormalSeekSpeed, setKeyboardNormalSeekSpeed, keyboardSeekAccFactor, setKeyboardSeekAccFactor } = useUserSettings();
// NOTE default values are duplicated in public/configStore.js
const types = {
wheelSensitivity: {
title: t('Timeline trackpad/wheel sensitivity'),
value: wheelSensitivity,
setValue: setWheelSensitivity,
default: 0.2,
},
keyboardNormalSeekSpeed: {
title: t('Timeline keyboard seek speed'),
value: keyboardNormalSeekSpeed,
setValue: setKeyboardNormalSeekSpeed,
min: 0,
max: 100,
default: 1,
},
keyboardSeekAccFactor: {
title: t('Timeline keyboard seek acceleration'),
value: keyboardSeekAccFactor,
setValue: setKeyboardSeekAccFactor,
min: 1,
max: 2,
default: 1.03,
},
};
const { title, value, setValue, min, max, default: defaultValue } = types[type];
const resetToDefault = () => setValue(defaultValue);
return <ValueTuner title={title} value={value} setValue={setValue} onFinished={onFinished} max={max} min={min} resetToDefault={resetToDefault} />;
});
export default ValueTuners;

@ -0,0 +1,3 @@
import React from 'react';
export default React.createContext();

@ -0,0 +1,5 @@
import { useContext } from 'react';
import UserSettingsContext from '../contexts/UserSettingsContext';
export default () => useContext(UserSettingsContext);
Loading…
Cancel
Save