allow adjusting waveform height

also improve value tuner
improve capture format button
pull/2233/head
Mikael Finstad 1 year ago
parent 4827471e3f
commit 63f9969eda
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26

@ -108,6 +108,7 @@ const defaults: Config = {
playbackVolume: 1,
autoSaveProjectFile: true,
wheelSensitivity: 0.2,
waveformHeight: 40,
language: undefined,
ffmpegExperimental: false,
preserveChapters: true,

@ -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() {
<AnimatePresence>
{working && <Working text={working.text} progress={progress} onAbortClick={abortWorking} />}
</AnimatePresence>
{tunerVisible && <ValueTuners type={tunerVisible} onFinished={() => setTunerVisible(undefined)} />}
</div>
<AnimatePresence>
@ -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() {
/>
</div>
{tunerVisible != null && <ValueTuners type={tunerVisible} onFinished={() => setTunerVisible(undefined)} />}
{/* Dialogs */}
<ExportConfirm areWeCutting={areWeCutting} nonFilteredSegmentsOrInverse={nonFilteredSegmentsOrInverse} selectedSegments={selectedSegmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} onShowStreamsSelectorClick={handleShowStreamsSelectorClick} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} mergedFileTemplate={mergedFileTemplateOrDefault} setMergedFileTemplate={setMergedFileTemplate} generateOutSegFileNames={generateOutSegFileNames} generateMergedFileNames={generateMergedFileNames} currentSegIndexSafe={currentSegIndexSafe} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} needSmartCut={needSmartCut} smartCutBitrate={smartCutBitrate} setSmartCutBitrate={setSmartCutBitrate} toggleSettings={toggleSettings} outputPlaybackRate={outputPlaybackRate} />
<Sheet visible={streamsSelectorShown} onClosePress={() => setStreamsSelectorShown(false)} maxWidth={1000}>

@ -527,7 +527,7 @@ function BottomBar({
{hasVideo && (
<>
{!simpleMode && <CaptureFormatButton height={20} />}
{!simpleMode && <CaptureFormatButton style={{ width: '3.7em', textAlign: 'center' }} />}
<IoIosCamera
style={{ paddingLeft: 5, paddingRight: 15 }}

@ -17,7 +17,6 @@ import { timelineBackground, darkModeTransition } from './colors';
import { Frame } from './ffmpeg';
import { FormatTimecode, InverseCutSegment, OverviewWaveform, RenderableWaveform, WaveformSlice, StateSegment, Thumbnail } from './types';
import Button from './components/Button';
import { WaveformMode } from '../../../types';
type CalculateTimelinePercent = (time: number) => 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,
}) => (
<div style={{ height: waveformMode === 'waveform-tall' ? '15vh' : 40, width: `${zoom * 100}%`, position: 'relative' }}>
<div style={{ height, width: `${zoom * 100}%`, position: 'relative' }}>
{zoom === 1 && overviewWaveform != null ? (
<Waveform waveform={overviewWaveform} calculateTimelinePercent={calculateTimelinePercent} fileDurationNonZero={fileDurationNonZero} darkMode={darkMode} />
) : 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}
/>
)}

@ -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;

@ -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<typeof Button>[0]) {
const { t } = useTranslation();
const { captureFormat, toggleCaptureFormat } = useUserSettings();
return (
<Button
iconBefore={showIcon ? <FaImage /> : null}
title={t('Capture frame format')}
onClick={withBlur(toggleCaptureFormat)}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{captureFormat}
{showIcon && <FaImage style={{ verticalAlign: 'middle', fontSize: '1.3em', marginRight: '.7em' }} />}
{captureFormat.toUpperCase()}
</Button>
);
}

@ -329,7 +329,7 @@ function Settings({
{t('Snapshot capture format')}
</KeyCell>
<td>
<CaptureFormatButton showIcon />
<CaptureFormatButton showIcon style={{ padding: '.5em 1em' }} />
</td>
</Row>
@ -437,6 +437,15 @@ function Settings({
</Row>
)}
{showAdvanced && (
<Row>
<KeyCell>{t('Waveform height')}</KeyCell>
<td>
<Button iconBefore={CogIcon} onClick={() => onTunerRequested('waveformHeight')}>{t('Change value')}</Button>
</td>
</Row>
)}
{showAdvanced && (
<Row>
<KeyCell>{t('Auto load timecode from file as an offset in the timeline?')}</KeyCell>
@ -455,7 +464,6 @@ function Settings({
</Row>
)}
{showAdvanced && (
<Row>
<KeyCell>{t('Try to automatically convert to supported format when opening unsupported file?')}</KeyCell>

@ -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);
}

@ -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 toggleZoom = useCallback(() => {
if (isZoomed) {
const resetZoom = useCallback(() => {
setMin(minIn);
setMax(maxIn);
}, [maxIn, minIn]);
const toggleZoom = useCallback(() => {
if (isZoomed) {
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 (
<div style={{ background: 'var(--gray1)', color: 'var(--gray12)', position: 'absolute', bottom: 0, padding: 10, margin: 10, borderRadius: 10, ...style }}>
<div style={{ display: 'flex', alignItems: 'center', flexBasis: 400, marginBottom: '.2em' }}>
<div className={styles['value-tuner']}>
<div style={{ display: 'flex', alignItems: 'center', flexBasis: 400, marginBottom: '.3em' }}>
<div>{title}</div>
<div style={{ marginLeft: '.5em', fontWeight: 'bold', marginRight: '.5em', textDecoration: 'underline', fontFamily: 'monospace', width: '5em' }}>{value.toFixed(4)}</div>
<div style={{ marginLeft: '.6em', fontSize: '1.3em', marginRight: '.5em', fontFamily: 'monospace', width: '5.5em' }}>{value.toFixed(decimals)}</div>
<Switch checked={isZoomed} onCheckedChange={toggleZoom} style={{ flexShrink: 0 }} /><span style={{ marginLeft: '.3em' }}>{t('Precise')}</span>
</div>
@ -52,7 +63,7 @@ function ValueTuner({ style, title, value, setValue, onFinished, resolution = 10
</div>
<div style={{ textAlign: 'right' }}>
<Button height={20} onClick={resetToDefault}>{t('Default')}</Button>
<Button height={20} onClick={handleResetToDefaultClick}>{t('Default')}</Button>
<Button height={20} intent="success" onClick={onFinished}>{t('Done')}</Button>
</div>
</div>

@ -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 <ValueTuner title={title} value={value} setValue={setValue} onFinished={onFinished} max={max} min={min} resetToDefault={resetToDefault} />;
return <ValueTuner title={title} value={value} setValue={setValue} onFinished={onFinished} max={max} min={min} resetToDefault={resetToDefault} resolution={resolution} decimals={decimals} />;
}

@ -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,

@ -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 };

@ -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,

@ -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 {

Loading…
Cancel
Save