Merge pull request #80 from mifi/master

[pull] master from mifi:master
3.66.1.1
pull[bot] 9 months ago committed by GitHub
commit 00df0135ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -163,6 +163,7 @@ const defaults: Config = {
waveformMode: undefined,
thumbnailsEnabled: false,
keyframesEnabled: true,
reducedMotion: 'user',
};
const configFileName = 'config.json'; // note: this is also hard-coded inside electron-store

@ -1,7 +1,7 @@
import { memo, useEffect, useState, useCallback, useRef, useMemo, CSSProperties, ReactEventHandler, FocusEventHandler, DragEventHandler } from 'react';
import { FaAngleLeft, FaRegTimesCircle } from 'react-icons/fa';
import { MdRotate90DegreesCcw } from 'react-icons/md';
import { AnimatePresence } from 'framer-motion';
import { AnimatePresence, MotionConfig } from 'framer-motion';
import { ThemeProvider } from 'evergreen-ui';
import i18n from 'i18next';
import { useTranslation } from 'react-i18next';
@ -176,7 +176,7 @@ function App() {
const allUserSettings = useUserSettingsRoot();
const {
captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMetadata, preserveChapters, preserveMovData, movFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, 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, toggleDarkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames, cutToAdjustmentFrames, waveformMode: waveformModePreference, setWaveformMode, thumbnailsEnabled, setThumbnailsEnabled, keyframesEnabled, setKeyframesEnabled,
captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMetadata, preserveChapters, preserveMovData, movFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, 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, toggleDarkMode, preferStrongColors, outputFileNameMinZeroPadding, cutFromAdjustmentFrames, cutToAdjustmentFrames, waveformMode: waveformModePreference, setWaveformMode, thumbnailsEnabled, setThumbnailsEnabled, keyframesEnabled, setKeyframesEnabled, prefersReducedMotion, reducedMotion,
} = allUserSettings;
// Note that each action may be multiple key bindings and this will only be the first binding for each action
@ -2513,13 +2513,20 @@ function App() {
const baseColorStyle = useMemo(() => ({ color: 'var(--gray-12)', background: 'var(--gray-1)', colorScheme: darkMode ? 'only dark' : 'only light' }), [darkMode]);
const rootClass = useMemo(() => [
...(darkMode ? ['dark-theme'] : []),
...(prefersReducedMotion ? ['no-animations'] : []),
].join(' '), [darkMode, prefersReducedMotion]);
const rootStyle = useMemo<CSSProperties>(() => ({ ...baseColorStyle, display: 'flex', flexDirection: 'column', height: '100vh', transition: darkModeTransition }), [baseColorStyle]);
return (
<>
<MotionConfig reducedMotion={reducedMotion}>
<AppContext.Provider value={appContext}>
<SegColorsContext.Provider value={segColorsContext}>
<UserSettingsContext.Provider value={userSettingsContext}>
<ThemeProvider value={theme}>
<div className={darkMode ? 'dark-theme' : undefined} style={{ ...baseColorStyle, display: 'flex', flexDirection: 'column', height: '100vh', transition: darkModeTransition }}>
<div className={rootClass} style={rootStyle}>
<TopMenu
filePath={filePath}
fileFormat={fileFormat}
@ -2834,7 +2841,7 @@ function App() {
</AppContext.Provider>
<div id="swal2-container-wrapper" className={darkMode ? 'dark-theme' : undefined} style={baseColorStyle} />
</>
</MotionConfig>
);
}

@ -2,7 +2,6 @@ import { memo } from 'react';
import { motion } from 'framer-motion';
import { FaTrashAlt, FaSave } from 'react-icons/fa';
import { mySpring } from './animations';
import { saveColor } from './colors';
import useUserSettings from './hooks/useUserSettings';
@ -15,7 +14,7 @@ function BetweenSegments({ start, end, fileDurationNonZero, invertCutSegments }:
}) {
const left = `${(start / fileDurationNonZero) * 100}%`;
const { effectiveExportMode } = useUserSettings();
const { effectiveExportMode, prefersReducedMotion, springAnimation } = useUserSettings();
return (
<motion.div
@ -35,8 +34,8 @@ function BetweenSegments({ start, end, fileDurationNonZero, invertCutSegments }:
left,
width: `${((end - start) / fileDurationNonZero) * 100}%`,
}}
layout
transition={mySpring}
layout={!prefersReducedMotion}
transition={springAnimation}
>
<div style={{ flexGrow: 1, borderBottom: '1px dashed var(--gray-10)', marginLeft: 5, marginRight: 5 }} />
{/* https://github.com/mifi/lossless-cut/issues/2157 */}

@ -15,7 +15,6 @@ import useContextMenu from './hooks/useContextMenu';
import useUserSettings from './hooks/useUserSettings';
import { saveColor, controlsBackground, primaryTextColor, darkModeTransition } from './colors';
import { useSegColors } from './contexts';
import { mySpring } from './animations';
import { getSegmentTags } from './segments';
import TagEditor from './components/TagEditor';
import { ContextMenuTemplate, DefiniteSegmentBase, FormatTimecode, GetFrameCount, InverseCutSegment, SegmentBase, SegmentTags, StateSegment } from './types';
@ -356,7 +355,7 @@ function SegmentList({
const { getSegColor } = useSegColors();
const [draggingId, setDraggingId] = useState<UniqueIdentifier | undefined>();
const { invertCutSegments, simpleMode, darkMode } = useUserSettings();
const { invertCutSegments, simpleMode, darkMode, springAnimation } = useUserSettings();
const getButtonColor = useCallback((seg: StateSegment | undefined, next?: boolean) => getSegColor(seg ? { segColorIndex: next ? seg.segColorIndex + 1 : seg.segColorIndex } : undefined).desaturate(0.3).lightness(darkMode ? 45 : 55).string(), [darkMode, getSegColor]);
const currentSegColor = useMemo(() => getButtonColor(currentCutSeg), [currentCutSeg, getButtonColor]);
@ -586,7 +585,7 @@ function SegmentList({
initial={{ x: width }}
animate={{ x: 0 }}
exit={{ x: width }}
transition={mySpring}
transition={springAnimation}
>
<div style={{ fontSize: 14, padding: '0 5px', color: 'var(--gray-12)' }} className="no-user-select">
<FaAngleRight

@ -152,7 +152,7 @@ function Timeline({
}) {
const { t } = useTranslation();
const { invertCutSegments } = useUserSettings();
const { invertCutSegments, springAnimation } = useUserSettings();
const timelineScrollerRef = useRef<HTMLDivElement>(null);
const timelineScrollerSkipEventRef = useRef<boolean>(false);
@ -427,7 +427,7 @@ function Timeline({
))}
{currentTimePercent !== undefined && (
<motion.div transition={{ type: 'spring', damping: 70, stiffness: 800 }} animate={{ left: currentTimePercent }} style={{ position: 'absolute', bottom: 0, top: 0, backgroundColor: 'var(--gray-12)', width: currentTimeWidth, pointerEvents: 'none' }} />
<motion.div transition={springAnimation} animate={{ left: currentTimePercent }} style={{ position: 'absolute', bottom: 0, top: 0, backgroundColor: 'var(--gray-12)', width: currentTimeWidth, pointerEvents: 'none' }} />
)}
{commandedTimePercent !== undefined && (
<CommandedTime commandedTimePercent={commandedTimePercent} />

@ -3,7 +3,6 @@ import { motion, AnimatePresence, MotionStyle } from 'framer-motion';
import { FaTrashAlt } from 'react-icons/fa';
import Color from 'color';
import { mySpring } from './animations';
import useUserSettings from './hooks/useUserSettings';
import { useSegColors } from './contexts';
import { FormatTimecode, StateSegment } from './types';
@ -23,7 +22,7 @@ function Marker({
getTimePercent: (a: number) => string,
formatTimecode: FormatTimecode,
}) {
const { darkMode } = useUserSettings();
const { darkMode, prefersReducedMotion, springAnimation } = useUserSettings();
const pinColor = darkMode ? color.saturate(0.2).lightness(40).string() : color.desaturate(0.2).lightness(50).string();
@ -59,8 +58,8 @@ function Marker({
return (
<motion.div
style={style}
layout
transition={mySpring}
layout={!prefersReducedMotion}
transition={springAnimation}
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: selected ? 1 : 0.5, scale: 1 }}
exit={{ opacity: 0, scale: 0 }}
@ -92,7 +91,7 @@ function Segment({
formatTimecode: FormatTimecode,
invertCutSegments: boolean,
}) {
const { darkMode } = useUserSettings();
const { darkMode, prefersReducedMotion, springAnimation } = useUserSettings();
const { name } = seg;
const border = useMemo(() => {
@ -164,8 +163,8 @@ function Segment({
return (
<motion.div
style={wrapperStyle}
layout
transition={mySpring}
layout={!prefersReducedMotion}
transition={springAnimation}
initial={{ opacity: 0, scaleX: 0 }}
animate={{ opacity: 1, scaleX: 1, backgroundColor }}
exit={{ opacity: 0, scaleX: 0 }}

@ -1,2 +1,10 @@
// eslint-disable-next-line import/prefer-default-export
export const mySpring = { type: 'spring', damping: 50, stiffness: 700 };
let prefersReducedMotionValue = false;
export function setPrefersReducedMotion(v: boolean) {
prefersReducedMotionValue = v;
}
export const prefersReducedMotion = () => prefersReducedMotionValue;

@ -10,8 +10,8 @@ import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import BatchFile from './BatchFile';
import { controlsBackground, darkModeTransition, primaryColor } from '../colors';
import { mySpring } from '../animations';
import { BatchFile as BatchFileType } from '../types';
import useUserSettings from '../hooks/useUserSettings';
const iconStyle = {
@ -37,6 +37,7 @@ function BatchFilesList({ selectedBatchFiles, filePath, width, batchFiles, setBa
onDrop: DragEventHandler<HTMLDivElement>,
}) {
const { t } = useTranslation();
const { springAnimation } = useUserSettings();
const [sortDesc, setSortDesc] = useState<boolean>();
const [draggingId, setDraggingId] = useState<UniqueIdentifier | undefined>();
@ -86,7 +87,7 @@ function BatchFilesList({ selectedBatchFiles, filePath, width, batchFiles, setBa
initial={{ x: -width }}
animate={{ x: 0 }}
exit={{ x: -width }}
transition={mySpring}
transition={springAnimation}
onDrop={onDrop}
>
<div style={{ fontSize: 14, paddingBottom: 3, paddingTop: 0, paddingLeft: 10, display: 'flex', alignItems: 'center', justifyContent: 'flex-end', flexWrap: 'wrap' }}>

@ -11,7 +11,7 @@ import Switch from './Switch';
import useUserSettings from '../hooks/useUserSettings';
import { askForFfPath } from '../dialogs';
import { isMasBuild, isStoreBuild } from '../util';
import { LanguageKey, ModifierKey, TimecodeFormat, langNames } from '../../../../types';
import { Config, LanguageKey, ModifierKey, TimecodeFormat, langNames } from '../../../../types';
import styles from './Settings.module.css';
import Select from './Select';
@ -74,7 +74,7 @@ function Settings({
const { t } = useTranslation();
const [showAdvanced, setShowAdvanced] = useState(!simpleMode);
const { customOutDir, changeOutDir, keyframeCut, toggleKeyframeCut, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, autoSaveProjectFile, setAutoSaveProjectFile, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, hideNotifications, setHideNotifications, hideOsNotifications, setHideOsNotifications, autoLoadTimecode, setAutoLoadTimecode, enableAutoHtml5ify, setEnableAutoHtml5ify, customFfPath, setCustomFfPath, storeProjectInWorkingDir, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, setMouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, setMouseWheelKeyframeSeekModifierKey, captureFrameMethod, setCaptureFrameMethod, captureFrameQuality, setCaptureFrameQuality, captureFrameFileNameFormat, setCaptureFrameFileNameFormat, enableNativeHevc, setEnableNativeHevc, enableUpdateCheck, setEnableUpdateCheck, allowMultipleInstances, setAllowMultipleInstances, preferStrongColors, setPreferStrongColors, treatInputFileModifiedTimeAsStart, setTreatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, setTreatOutputFileModifiedTimeAsStart, exportConfirmEnabled, toggleExportConfirmEnabled, storeWindowBounds, setStoreWindowBounds } = useUserSettings();
const { customOutDir, changeOutDir, keyframeCut, toggleKeyframeCut, timecodeFormat, setTimecodeFormat, invertCutSegments, setInvertCutSegments, askBeforeClose, setAskBeforeClose, enableAskForImportChapters, setEnableAskForImportChapters, enableAskForFileOpenAction, setEnableAskForFileOpenAction, autoSaveProjectFile, setAutoSaveProjectFile, invertTimelineScroll, setInvertTimelineScroll, language, setLanguage, hideNotifications, setHideNotifications, hideOsNotifications, setHideOsNotifications, autoLoadTimecode, setAutoLoadTimecode, enableAutoHtml5ify, setEnableAutoHtml5ify, customFfPath, setCustomFfPath, storeProjectInWorkingDir, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, setMouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, setMouseWheelKeyframeSeekModifierKey, captureFrameMethod, setCaptureFrameMethod, captureFrameQuality, setCaptureFrameQuality, captureFrameFileNameFormat, setCaptureFrameFileNameFormat, enableNativeHevc, setEnableNativeHevc, enableUpdateCheck, setEnableUpdateCheck, allowMultipleInstances, setAllowMultipleInstances, preferStrongColors, setPreferStrongColors, treatInputFileModifiedTimeAsStart, setTreatInputFileModifiedTimeAsStart, treatOutputFileModifiedTimeAsStart, setTreatOutputFileModifiedTimeAsStart, exportConfirmEnabled, toggleExportConfirmEnabled, storeWindowBounds, setStoreWindowBounds, reducedMotion, setReducedMotion } = useUserSettings();
const onLangChange = useCallback<ChangeEventHandler<HTMLSelectElement>>((e) => {
const { value } = e.target;
@ -482,6 +482,17 @@ function Settings({
</td>
</Row>
<Row>
<KeyCell>{t('Reduce motion in user interface')}</KeyCell>
<td>
<Select value={reducedMotion} onChange={(e) => setReducedMotion(e.target.value as Config['reducedMotion'])}>
{Object.entries({ user: t('System default'), always: t('Yes'), never: t('No') } satisfies Record<Config['reducedMotion'], string>).map(([key, value]) => (
<option key={key} value={key}>{value}</option>
))}
</Select>
</td>
</Row>
<Row>
<KeyCell>{t('In timecode show')}</KeyCell>
<td>

@ -1,11 +1,17 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import i18n from 'i18next';
import { Transition } from 'framer-motion';
import { Config } from '../../../../types';
import { errorToast } from '../swal';
import isDev from '../isDev';
import { mySpring, setPrefersReducedMotion } from '../animations';
const { configStore } = window.require('@electron/remote').require('./index.js');
const { systemPreferences } = window.require('@electron/remote');
const animationSettings = systemPreferences.getAnimationSettings();
export default () => {
const firstUpdateRef = useRef(true);
@ -175,6 +181,8 @@ export default () => {
useEffect(() => safeSetConfig({ thumbnailsEnabled }), [thumbnailsEnabled]);
const [keyframesEnabled, setKeyframesEnabled] = useState(safeGetConfigInitial('keyframesEnabled'));
useEffect(() => safeSetConfig({ keyframesEnabled }), [keyframesEnabled]);
const [reducedMotion, setReducedMotion] = useState(safeGetConfigInitial('reducedMotion'));
useEffect(() => safeSetConfig({ reducedMotion }), [reducedMotion]);
const resetKeyBindings = useCallback(() => {
@ -192,6 +200,19 @@ export default () => {
const toggleDarkMode = useCallback(() => setDarkMode((v) => !v), []);
const prefersReducedMotion = useMemo(() => {
if (reducedMotion !== 'user') return reducedMotion === 'always';
// fallback to electron detected system setting
// note: user has to restart app for changes here to be detected
return animationSettings.prefersReducedMotion;
}, [reducedMotion]);
useEffect(() => {
setPrefersReducedMotion(prefersReducedMotion);
}, [prefersReducedMotion]);
const springAnimation = useMemo<Transition>(() => (prefersReducedMotion ? { duration: 0 } : mySpring), [prefersReducedMotion]);
return {
captureFormat,
setCaptureFormat,
@ -326,5 +347,9 @@ export default () => {
setThumbnailsEnabled,
keyframesEnabled,
setKeyframesEnabled,
reducedMotion,
prefersReducedMotion,
setReducedMotion,
springAnimation,
};
};

@ -1,6 +1,5 @@
import { Suspense, StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { MotionConfig } from 'framer-motion';
import { enableMapSet } from 'immer';
import * as Electron from 'electron';
import Remote from '@electron/remote';
@ -77,9 +76,7 @@ root.render(
<StrictMode>
<ErrorBoundary>
<Suspense fallback={<div />}>
<MotionConfig reducedMotion="user">
<App />
</MotionConfig>
<App />
</Suspense>
</ErrorBoundary>
</StrictMode>,

@ -125,5 +125,10 @@ input:focus-visible, select:focus-visible {
/* because some people have a hard time finding the button */
.export-animation {
animation: pulse 2s linear infinite
animation: pulse 2s linear infinite;
}
.no-animations * {
transition: none !important;
animation: none !important;
}

@ -1,18 +1,16 @@
import SwalRaw from 'sweetalert2/dist/sweetalert2.js';
import type { SweetAlertOptions } from 'sweetalert2';
import withReactContent from 'sweetalert2-react-content';
import i18n from './i18n';
const { systemPreferences } = window.require('@electron/remote');
import i18n from './i18n';
import { prefersReducedMotion } from './animations';
const animationSettings = systemPreferences.getAnimationSettings();
let commonSwalOptions: SweetAlertOptions = {
target: '#swal2-container-wrapper',
};
if (animationSettings.prefersReducedMotion) {
if (prefersReducedMotion()) {
commonSwalOptions = {
...commonSwalOptions,
showClass: {

@ -13,6 +13,7 @@ import { ffmpegExtractWindow } from './util/constants';
import { appName } from '../../main/common';
import { DirectoryAccessDeclinedError, UnsupportedFileError } from '../errors';
import { Html5ifyMode } from '../../../types';
import { prefersReducedMotion } from './animations';
const { dirname, parse: parsePath, join, extname, isAbsolute, resolve, basename } = window.require('path');
const fsExtra = window.require('fs-extra');
@ -531,6 +532,8 @@ export const mediaSourceQualities = ['HD', 'SD', 'OG']; // OG is original
export const splitKeyboardKeys = (keys: string) => keys.split('+');
export function shootConfetti(options?: confetti.Options) {
if (prefersReducedMotion()) return;
confetti({
particleCount: 30,
angle: 110,

@ -120,6 +120,7 @@ export interface Config {
waveformMode: WaveformMode | undefined,
thumbnailsEnabled: boolean,
keyframesEnabled: boolean,
reducedMotion: 'always' | 'never' | 'user',
}
export interface Waveform {

Loading…
Cancel
Save