diff --git a/src/main/configStore.ts b/src/main/configStore.ts index 9180b1a2..d0aa8e13 100644 --- a/src/main/configStore.ts +++ b/src/main/configStore.ts @@ -108,6 +108,7 @@ const defaults: Config = { playbackVolume: 1, autoSaveProjectFile: true, wheelSensitivity: 0.2, + waveformHeight: 40, language: undefined, ffmpegExperimental: false, preserveChapters: true, diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 10547cff..376d4d24 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -174,7 +174,7 @@ function App() { const allUserSettings = useUserSettingsRoot(); const { - captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMetadata, preserveChapters, preserveMovData, movFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, hideOsNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, preserveMetadataOnMerge, simpleMode, setSimpleMode, outSegTemplate, setOutSegTemplate, mergedFileTemplate, setMergedFileTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, darkMode, setDarkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames, cutToAdjustmentFrames, + captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMetadata, preserveChapters, preserveMovData, movFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, waveformHeight, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, hideOsNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, preserveMetadataOnMerge, simpleMode, setSimpleMode, outSegTemplate, setOutSegTemplate, mergedFileTemplate, setMergedFileTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, keyboardSeekSpeed2, keyboardSeekSpeed3, treatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, darkMode, setDarkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames, cutToAdjustmentFrames, } = allUserSettings; const { working, setWorking, workingRef, abortWorking } = useLoading(); @@ -261,8 +261,6 @@ function App() { const toggleWaveformMode = useCallback(() => { // eslint-disable-next-line unicorn/prefer-switch if (waveformMode === 'waveform') { - setWaveformMode('waveform-tall'); - } else if (waveformMode === 'waveform-tall') { setWaveformMode('big-waveform'); } else if (waveformMode === 'big-waveform') { setWaveformMode(undefined); @@ -2513,8 +2511,6 @@ function App() { {working && } - - {tunerVisible && setTunerVisible(undefined)} />} @@ -2571,8 +2567,8 @@ function App() { waveforms={waveforms} overviewWaveform={overviewWaveform} shouldShowWaveform={shouldShowWaveform} - waveformMode={waveformMode} waveformEnabled={waveformEnabled} + waveformHeight={waveformHeight} showThumbnails={showThumbnails} neighbouringKeyFrames={neighbouringKeyFrames} thumbnails={thumbnailsSorted} @@ -2653,6 +2649,10 @@ function App() { /> + {tunerVisible != null && setTunerVisible(undefined)} />} + + {/* Dialogs */} + setStreamsSelectorShown(false)} maxWidth={1000}> diff --git a/src/renderer/src/BottomBar.tsx b/src/renderer/src/BottomBar.tsx index cb00d6ae..92bd03da 100644 --- a/src/renderer/src/BottomBar.tsx +++ b/src/renderer/src/BottomBar.tsx @@ -527,7 +527,7 @@ function BottomBar({ {hasVideo && ( <> - {!simpleMode && } + {!simpleMode && } string | undefined; @@ -49,16 +48,16 @@ const Waveform = memo(({ waveform, calculateTimelinePercent, fileDurationNonZero }); // eslint-disable-next-line react/display-name -const Waveforms = memo(({ calculateTimelinePercent, fileDurationNonZero, waveforms, overviewWaveform, zoom, darkMode, waveformMode }: { +const Waveforms = memo(({ calculateTimelinePercent, fileDurationNonZero, waveforms, overviewWaveform, zoom, darkMode, height }: { calculateTimelinePercent: CalculateTimelinePercent, fileDurationNonZero: number, waveforms: WaveformSlice[], overviewWaveform: OverviewWaveform | undefined, zoom: number, darkMode: boolean, - waveformMode: WaveformMode | undefined, + height: number, }) => ( -
+
{zoom === 1 && overviewWaveform != null ? ( ) : waveforms.map((waveform) => ( @@ -108,8 +107,8 @@ function Timeline({ zoomWindowEndTime, onZoomWindowStartTimeChange, onGenerateOverviewWaveformClick, - waveformMode, waveformEnabled, + waveformHeight, showThumbnails, playing, isFileOpened, @@ -142,8 +141,8 @@ function Timeline({ zoomWindowEndTime: number | undefined, onZoomWindowStartTimeChange: (a: number) => void, onGenerateOverviewWaveformClick: () => void, - waveformMode: WaveformMode | undefined, waveformEnabled: boolean, + waveformHeight: number, showThumbnails: boolean, playing: boolean, isFileOpened: boolean, @@ -375,7 +374,7 @@ function Timeline({ overviewWaveform={overviewWaveform} zoom={zoom} darkMode={darkMode} - waveformMode={waveformMode} + height={waveformHeight} /> )} diff --git a/src/renderer/src/components/BigWaveform.tsx b/src/renderer/src/components/BigWaveform.tsx index 175f5c4b..af37cc27 100644 --- a/src/renderer/src/components/BigWaveform.tsx +++ b/src/renderer/src/components/BigWaveform.tsx @@ -1,4 +1,4 @@ -import { memo, useEffect, useState, useCallback, useRef, CSSProperties, MouseEventHandler, WheelEventHandler } from 'react'; +import { memo, useEffect, useState, useCallback, useRef, CSSProperties, MouseEventHandler, WheelEventHandler, useMemo } from 'react'; import { Spinner } from 'evergreen-ui'; import { ffmpegExtractWindow } from '../util/constants'; @@ -17,7 +17,7 @@ function BigWaveform({ waveforms, relevantTime, playing, fileDurationNonZero, zo const windowSize = ffmpegExtractWindow * 2; const windowStart = Math.max(0, relevantTime - windowSize); const windowEnd = relevantTime + windowSize; - const filtered = waveforms.filter((waveform) => waveform.from >= windowStart && waveform.to <= windowEnd); + const filtered = useMemo(() => waveforms.filter((waveform) => waveform.from >= windowStart && waveform.to <= windowEnd), [waveforms, windowEnd, windowStart]); const scaleFactor = zoom; diff --git a/src/renderer/src/components/CaptureFormatButton.tsx b/src/renderer/src/components/CaptureFormatButton.tsx index 644c15a0..dd6c383e 100644 --- a/src/renderer/src/components/CaptureFormatButton.tsx +++ b/src/renderer/src/components/CaptureFormatButton.tsx @@ -1,24 +1,24 @@ import { memo } from 'react'; -import { Button, ButtonProps } from 'evergreen-ui'; import { useTranslation } from 'react-i18next'; import { FaImage } from 'react-icons/fa'; import useUserSettings from '../hooks/useUserSettings'; import { withBlur } from '../util'; +import Button from './Button'; -function CaptureFormatButton({ showIcon = false, ...props }: { showIcon?: boolean } & ButtonProps) { +function CaptureFormatButton({ showIcon = false, ...props }: { showIcon?: boolean } & Parameters[0]) { const { t } = useTranslation(); const { captureFormat, toggleCaptureFormat } = useUserSettings(); return ( ); } diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index 2cac009e..ee25bcbf 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -329,7 +329,7 @@ function Settings({ {t('Snapshot capture format')} - + @@ -437,6 +437,15 @@ function Settings({ )} + {showAdvanced && ( + + {t('Waveform height')} + + + + + )} + {showAdvanced && ( {t('Auto load timecode from file as an offset in the timeline?')} @@ -455,7 +464,6 @@ function Settings({ )} - {showAdvanced && ( {t('Try to automatically convert to supported format when opening unsupported file?')} diff --git a/src/renderer/src/components/ValueTuner.module.css b/src/renderer/src/components/ValueTuner.module.css new file mode 100644 index 00000000..81f89cda --- /dev/null +++ b/src/renderer/src/components/ValueTuner.module.css @@ -0,0 +1,17 @@ +.value-tuner { + min-width: 20em; + position: absolute; + bottom: 0; + padding: 2em; + margin: 1.3em; + border-radius: 1em; + color: var(--gray12); + background: var(--whiteA9); + box-shadow: 0 0 .3em .3em var(--whiteA8); + backdrop-filter: blur(30px); +} + +:global(.dark-theme) .value-tuner { + background: var(--blackA9); + box-shadow: 0 0 .3em .3em var(--blackA8); +} diff --git a/src/renderer/src/components/ValueTuner.tsx b/src/renderer/src/components/ValueTuner.tsx index 19050b9a..a291e39b 100644 --- a/src/renderer/src/components/ValueTuner.tsx +++ b/src/renderer/src/components/ValueTuner.tsx @@ -1,17 +1,19 @@ -import { memo, useState, useCallback, CSSProperties, ChangeEventHandler } from 'react'; +import { memo, useState, useCallback, ChangeEventHandler } from 'react'; import { Button } from 'evergreen-ui'; import { useTranslation } from 'react-i18next'; +import styles from './ValueTuner.module.css'; + import Switch from './Switch'; -function ValueTuner({ style, title, value, setValue, onFinished, resolution = 1000, min: minIn = 0, max: maxIn = 1, resetToDefault }: { - style?: CSSProperties, +function ValueTuner({ title, value, setValue, onFinished, resolution, decimals, min: minIn = 0, max: maxIn = 1, resetToDefault }: { title: string, value: number, setValue: (v: number) => void, onFinished: () => void, - resolution?: number, + resolution: number, + decimals: number, min?: number, max?: number, resetToDefault: () => void, @@ -28,22 +30,31 @@ function ValueTuner({ style, title, value, setValue, onFinished, resolution = 10 const isZoomed = !(min === minIn && max === maxIn); + const resetZoom = useCallback(() => { + setMin(minIn); + setMax(maxIn); + }, [maxIn, minIn]); + const toggleZoom = useCallback(() => { if (isZoomed) { - setMin(minIn); - setMax(maxIn); + resetZoom(); } else { const zoomWindow = (maxIn - minIn) / 100; setMin(Math.max(minIn, value - zoomWindow)); setMax(Math.min(maxIn, value + zoomWindow)); } - }, [isZoomed, maxIn, minIn, value]); + }, [isZoomed, maxIn, minIn, resetZoom, value]); + + const handleResetToDefaultClick = useCallback(() => { + resetToDefault(); + resetZoom(); + }, [resetToDefault, resetZoom]); return ( -
-
+
+
{title}
-
{value.toFixed(4)}
+
{value.toFixed(decimals)}
{t('Precise')}
@@ -52,7 +63,7 @@ function ValueTuner({ style, title, value, setValue, onFinished, resolution = 10
- +
diff --git a/src/renderer/src/components/ValueTuners.tsx b/src/renderer/src/components/ValueTuners.tsx index 8d6296d3..f6800ee6 100644 --- a/src/renderer/src/components/ValueTuners.tsx +++ b/src/renderer/src/components/ValueTuners.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import ValueTuner from './ValueTuner'; @@ -8,7 +8,7 @@ import { TunerType } from '../types'; function ValueTuners({ type, onFinished }: { type: TunerType, onFinished: () => void }) { const { t } = useTranslation(); - const { wheelSensitivity, setWheelSensitivity, keyboardNormalSeekSpeed, keyboardSeekSpeed2, setKeyboardSeekSpeed2, keyboardSeekSpeed3, setKeyboardSeekSpeed3, setKeyboardNormalSeekSpeed, keyboardSeekAccFactor, setKeyboardSeekAccFactor } = useUserSettings(); + const { wheelSensitivity, setWheelSensitivity, keyboardNormalSeekSpeed, keyboardSeekSpeed2, setKeyboardSeekSpeed2, keyboardSeekSpeed3, setKeyboardSeekSpeed3, setKeyboardNormalSeekSpeed, keyboardSeekAccFactor, setKeyboardSeekAccFactor, waveformHeight, setWaveformHeight } = useUserSettings(); // NOTE default values are duplicated in src/main/configStore.js const types = { @@ -18,14 +18,28 @@ function ValueTuners({ type, onFinished }: { type: TunerType, onFinished: () => setValue: setWheelSensitivity, min: 0, max: 4, + resolution: 1000, + decimals: 4, default: 0.2, }, + waveformHeight: { + title: t('Waveform height'), + value: waveformHeight, + setValue: setWaveformHeight, + min: 20, + max: 1000, + resolution: 1000 - 20, + decimals: 0, + default: 40, + }, keyboardNormalSeekSpeed: { title: t('Timeline keyboard seek interval'), value: keyboardNormalSeekSpeed, setValue: setKeyboardNormalSeekSpeed, min: 0, max: 120, + resolution: 1000, + decimals: 4, default: 1, }, keyboardSeekSpeed2: { @@ -34,6 +48,8 @@ function ValueTuners({ type, onFinished }: { type: TunerType, onFinished: () => setValue: setKeyboardSeekSpeed2, min: 0, max: 600, + resolution: 1000, + decimals: 4, default: 10, }, keyboardSeekSpeed3: { @@ -42,6 +58,8 @@ function ValueTuners({ type, onFinished }: { type: TunerType, onFinished: () => setValue: setKeyboardSeekSpeed3, min: 0, max: 3600, + resolution: 1000, + decimals: 4, default: 60, }, keyboardSeekAccFactor: { @@ -50,14 +68,16 @@ function ValueTuners({ type, onFinished }: { type: TunerType, onFinished: () => setValue: setKeyboardSeekAccFactor, min: 1, max: 2, + resolution: 1000, + decimals: 4, default: 1.03, }, }; - const { title, value, setValue, min, max, default: defaultValue } = types[type]; + const { title, value, setValue, min, max, resolution, decimals, default: defaultValue } = types[type]; - const resetToDefault = () => setValue(defaultValue); + const resetToDefault = useCallback(() => setValue(defaultValue), [defaultValue, setValue]); - return ; + return ; } diff --git a/src/renderer/src/hooks/useUserSettingsRoot.ts b/src/renderer/src/hooks/useUserSettingsRoot.ts index d9542667..764168dd 100644 --- a/src/renderer/src/hooks/useUserSettingsRoot.ts +++ b/src/renderer/src/hooks/useUserSettingsRoot.ts @@ -79,6 +79,8 @@ export default () => { useEffect(() => safeSetConfig({ autoSaveProjectFile }), [autoSaveProjectFile]); const [wheelSensitivity, setWheelSensitivity] = useState(safeGetConfigInitial('wheelSensitivity')); useEffect(() => safeSetConfig({ wheelSensitivity }), [wheelSensitivity]); + const [waveformHeight, setWaveformHeight] = useState(safeGetConfigInitial('waveformHeight')); + useEffect(() => safeSetConfig({ waveformHeight }), [waveformHeight]); const [invertTimelineScroll, setInvertTimelineScroll] = useState(safeGetConfigInitial('invertTimelineScroll')); useEffect(() => safeSetConfig({ invertTimelineScroll }), [invertTimelineScroll]); const [language, setLanguage] = useState(safeGetConfigInitial('language')); @@ -220,6 +222,8 @@ export default () => { setAutoSaveProjectFile, wheelSensitivity, setWheelSensitivity, + waveformHeight, + setWaveformHeight, invertTimelineScroll, setInvertTimelineScroll, language, diff --git a/src/renderer/src/styles.ts b/src/renderer/src/styles.ts index 129521a3..14570b84 100644 --- a/src/renderer/src/styles.ts +++ b/src/renderer/src/styles.ts @@ -3,4 +3,4 @@ import { controlsBackground, darkModeTransition } from './colors'; export const videoStyle: CSSProperties = { width: '100%', height: '100%', objectFit: 'contain' }; -export const bottomStyle: CSSProperties = { background: controlsBackground, transition: darkModeTransition }; +export const bottomStyle: CSSProperties = { background: controlsBackground, transition: darkModeTransition, flexShrink: 0 }; diff --git a/src/renderer/src/types.ts b/src/renderer/src/types.ts index 62d91879..91b11714 100644 --- a/src/renderer/src/types.ts +++ b/src/renderer/src/types.ts @@ -85,7 +85,7 @@ export type EdlImportType = 'youtube' | EdlFileType; export type EdlExportType = 'csv' | 'tsv-human' | 'csv-human' | 'csv-frames' | 'srt' | 'llc'; -export type TunerType = 'wheelSensitivity' | 'keyboardNormalSeekSpeed' | 'keyboardSeekSpeed2' | 'keyboardSeekSpeed3' | 'keyboardSeekAccFactor'; +export type TunerType = 'wheelSensitivity' | 'waveformHeight' | 'keyboardNormalSeekSpeed' | 'keyboardSeekSpeed2' | 'keyboardSeekSpeed3' | 'keyboardSeekAccFactor'; export interface WaveformBase { createdAt: Date, diff --git a/types.ts b/types.ts index 79565437..4e17e5d9 100644 --- a/types.ts +++ b/types.ts @@ -65,6 +65,7 @@ export interface Config { playbackVolume: number, autoSaveProjectFile: boolean, wheelSensitivity: number, + waveformHeight: number, language: LanguageKey | undefined, ffmpegExperimental: boolean, preserveChapters: boolean, @@ -127,7 +128,7 @@ export interface ApiActionRequest { export type Html5ifyMode = 'fastest' | 'fast-audio-remux' | 'fast-audio' | 'fast' | 'slow' | 'slow-audio' | 'slowest'; -export type WaveformMode = 'big-waveform' | 'waveform' | 'waveform-tall'; +export type WaveformMode = 'big-waveform' | 'waveform'; // This is the contract with the user, see https://github.com/mifi/lossless-cut/blob/master/expressions.md export interface ScopeSegment {