mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-25 03:33:14 +01:00
parent
759c079747
commit
9b027bc762
@ -43,6 +43,7 @@
|
||||
"license": "GPL-2.0-only",
|
||||
"devDependencies": {
|
||||
"@fontsource/open-sans": "^4.5.14",
|
||||
"@radix-ui/react-switch": "^1.0.1",
|
||||
"@types/sortablejs": "^1.15.0",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"color": "^3.1.0",
|
||||
@ -96,6 +97,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.0.9",
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"cue-parser": "^0.3.0",
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
|
@ -122,6 +122,7 @@ const defaults = {
|
||||
trashTmpFiles: true, askForCleanup: true,
|
||||
},
|
||||
allowMultipleInstances: false,
|
||||
darkMode: true,
|
||||
};
|
||||
|
||||
// For portable app: https://github.com/mifi/lossless-cut/issues/645
|
||||
|
@ -20,7 +20,7 @@ const { checkNewVersion } = require('./update-checker');
|
||||
|
||||
require('./i18n');
|
||||
|
||||
const { app, ipcMain, shell, BrowserWindow } = electron;
|
||||
const { app, ipcMain, shell, BrowserWindow, nativeTheme } = electron;
|
||||
|
||||
remote.initialize();
|
||||
|
||||
@ -71,6 +71,10 @@ function getSizeOptions() {
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const darkMode = configStore.get('darkMode');
|
||||
// todo follow darkMode setting when user switches
|
||||
if (darkMode) nativeTheme.themeSource = 'dark';
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
...getSizeOptions(),
|
||||
darkTheme: true,
|
||||
@ -81,6 +85,7 @@ function createWindow() {
|
||||
// https://github.com/electron/electron/issues/5107
|
||||
webSecurity: !isDev,
|
||||
},
|
||||
backgroundColor: darkMode ? '#333' : '#fff',
|
||||
});
|
||||
|
||||
remote.enable(mainWindow.webContents);
|
||||
|
34
src/App.jsx
34
src/App.jsx
@ -2,7 +2,7 @@ import React, { memo, useEffect, useState, useCallback, useRef, useMemo } from '
|
||||
import { FaAngleLeft, FaWindowClose } from 'react-icons/fa';
|
||||
import { MdRotate90DegreesCcw } from 'react-icons/md';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { Heading, InlineAlert, Table, SideSheet, Position, ThemeProvider } from 'evergreen-ui';
|
||||
import { SideSheet, Position, ThemeProvider } from 'evergreen-ui';
|
||||
import useDebounceOld from 'react-use/lib/useDebounce'; // Want to phase out this
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import i18n from 'i18next';
|
||||
@ -31,14 +31,14 @@ import UserSettingsContext from './contexts/UserSettingsContext';
|
||||
import NoFileLoaded from './NoFileLoaded';
|
||||
import Canvas from './Canvas';
|
||||
import TopMenu from './TopMenu';
|
||||
import Sheet from './Sheet';
|
||||
import Sheet from './components/Sheet';
|
||||
import LastCommandsSheet from './LastCommandsSheet';
|
||||
import StreamsSelector from './StreamsSelector';
|
||||
import SegmentList from './SegmentList';
|
||||
import Settings from './Settings';
|
||||
import Settings from './components/Settings';
|
||||
import Timeline from './Timeline';
|
||||
import BottomBar from './BottomBar';
|
||||
import ExportConfirm from './ExportConfirm';
|
||||
import ExportConfirm from './components/ExportConfirm';
|
||||
import ValueTuners from './components/ValueTuners';
|
||||
import VolumeControl from './components/VolumeControl';
|
||||
import SubtitleControl from './components/SubtitleControl';
|
||||
@ -49,7 +49,7 @@ import Working from './components/Working';
|
||||
import OutputFormatSelect from './components/OutputFormatSelect';
|
||||
|
||||
import { loadMifiLink, runStartupCheck } from './mifi';
|
||||
import { controlsBackground } from './colors';
|
||||
import { controlsBackground, darkModeTransition } from './colors';
|
||||
import {
|
||||
getStreamFps, isCuttingStart, isCuttingEnd,
|
||||
readFileMeta, getSmarterOutFormat, renderThumbnails as ffmpegRenderThumbnails,
|
||||
@ -96,7 +96,7 @@ const calcShouldShowKeyframes = (zoomedDuration) => (zoomedDuration != null && z
|
||||
|
||||
|
||||
const videoStyle = { width: '100%', height: '100%', objectFit: 'contain' };
|
||||
const bottomStyle = { background: controlsBackground };
|
||||
const bottomStyle = { background: controlsBackground, transition: darkModeTransition };
|
||||
|
||||
let lastOpenedPath;
|
||||
const hevcPlaybackSupportedPromise = doesPlayerSupportHevcPlayback();
|
||||
@ -174,7 +174,7 @@ const App = memo(() => {
|
||||
const allUserSettings = useUserSettingsRoot();
|
||||
|
||||
const {
|
||||
captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, setSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, enableTransferTimestamps, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices,
|
||||
captureFormat, setCaptureFormat, customOutDir, setCustomOutDir, keyframeCut, setKeyframeCut, preserveMovData, setPreserveMovData, movFastStart, setMovFastStart, avoidNegativeTs, autoMerge, timecodeFormat, invertCutSegments, setInvertCutSegments, autoExportExtraStreams, askBeforeClose, enableAskForImportChapters, enableAskForFileOpenAction, playbackVolume, setPlaybackVolume, autoSaveProjectFile, wheelSensitivity, invertTimelineScroll, language, ffmpegExperimental, hideNotifications, autoLoadTimecode, autoDeleteMergedSegments, exportConfirmEnabled, setExportConfirmEnabled, segmentsToChapters, setSegmentsToChapters, preserveMetadataOnMerge, setPreserveMetadataOnMerge, setSimpleMode, outSegTemplate, setOutSegTemplate, keyboardSeekAccFactor, keyboardNormalSeekSpeed, enableTransferTimestamps, outFormatLocked, setOutFormatLocked, safeOutputFileName, setSafeOutputFileName, enableAutoHtml5ify, segmentsToChaptersOnly, keyBindings, setKeyBindings, resetKeyBindings, enableSmartCut, customFfPath, storeProjectInWorkingDir, setStoreProjectInWorkingDir, enableOverwriteOutput, mouseWheelZoomModifierKey, captureFrameMethod, captureFrameQuality, captureFrameFileNameFormat, enableNativeHevc, cleanupChoices, setCleanupChoices, darkMode, setDarkMode,
|
||||
} = allUserSettings;
|
||||
|
||||
useEffect(() => {
|
||||
@ -2160,7 +2160,7 @@ const App = memo(() => {
|
||||
return (
|
||||
<UserSettingsContext.Provider value={userSettingsContext}>
|
||||
<ThemeProvider value={theme}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
|
||||
<div className={darkMode ? 'dark-theme' : undefined} style={{ display: 'flex', flexDirection: 'column', height: '100vh', color: 'var(--gray12)', background: 'var(--gray1)', transition: darkModeTransition }}>
|
||||
<TopMenu
|
||||
filePath={filePath}
|
||||
fileFormat={fileFormat}
|
||||
@ -2226,7 +2226,7 @@ const App = memo(() => {
|
||||
)}
|
||||
|
||||
{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' }}>
|
||||
<div className="no-user-select" style={{ position: 'absolute', right: 0, bottom: 0, marginBottom: 10, display: 'flex', alignItems: 'center' }}>
|
||||
<VolumeControl playbackVolume={playbackVolume} setPlaybackVolume={setPlaybackVolume} usingDummyVideo={usingDummyVideo} />
|
||||
|
||||
{subtitleStreams.length > 0 && <SubtitleControl subtitleStreams={subtitleStreams} activeSubtitleStreamIndex={activeSubtitleStreamIndex} onActiveSubtitleChange={onActiveSubtitleChange} />}
|
||||
@ -2236,7 +2236,7 @@ const App = memo(() => {
|
||||
title={t('Show sidebar')}
|
||||
size={30}
|
||||
role="button"
|
||||
style={{ marginRight: 10 }}
|
||||
style={{ marginRight: 10, color: 'var(--gray12)', opacity: 0.7 }}
|
||||
onClick={toggleSegmentsList}
|
||||
/>
|
||||
)}
|
||||
@ -2359,6 +2359,8 @@ const App = memo(() => {
|
||||
detectedFps={detectedFps}
|
||||
toggleLoopSelectedSegments={toggleLoopSelectedSegments}
|
||||
isFileOpened={isFileOpened}
|
||||
darkMode={darkMode}
|
||||
setDarkMode={setDarkMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -2406,23 +2408,13 @@ const App = memo(() => {
|
||||
ffmpegCommandLog={ffmpegCommandLog}
|
||||
/>
|
||||
|
||||
<Sheet visible={settingsVisible} onClosePress={toggleSettings} style={{ background: 'white', color: 'black' }}>
|
||||
<Heading>{t('Keyboard & mouse shortcuts')}</Heading>
|
||||
<InlineAlert marginTop={20}>{t('Hover mouse over buttons in the main interface to see which function they have')}</InlineAlert>
|
||||
<Table style={{ marginTop: 20 }}>
|
||||
<Table.Head>
|
||||
<Table.TextHeaderCell>{t('Settings')}</Table.TextHeaderCell>
|
||||
<Table.TextHeaderCell>{t('Current setting')}</Table.TextHeaderCell>
|
||||
</Table.Head>
|
||||
<Table.Body>
|
||||
<Sheet visible={settingsVisible} onClosePress={toggleSettings} style={{ padding: '1em 0' }}>
|
||||
<Settings
|
||||
onTunerRequested={onTunerRequested}
|
||||
onKeyboardShortcutsDialogRequested={toggleKeyboardShortcuts}
|
||||
askForCleanupChoices={askForCleanupChoices}
|
||||
toggleStoreProjectInWorkingDir={toggleStoreProjectInWorkingDir}
|
||||
/>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Sheet>
|
||||
|
||||
<ConcatDialog isShown={batchFiles.length > 0 && concatDialogVisible} onHide={() => setConcatDialogVisible(false)} paths={batchFilePaths} onConcat={userConcatFiles} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} />
|
||||
|
@ -29,13 +29,13 @@ const BetweenSegments = memo(({ start, end, duration, invertCutSegments }) => {
|
||||
layout
|
||||
transition={mySpring}
|
||||
>
|
||||
<div style={{ flexGrow: 1, borderBottom: '1px dashed rgba(255, 255, 255, 0.3)', marginLeft: 5, marginRight: 5 }} />
|
||||
<div style={{ flexGrow: 1, borderBottom: '1px dashed var(--gray10)', marginLeft: 5, marginRight: 5 }} />
|
||||
{invertCutSegments ? (
|
||||
<FaSave style={{ color: saveColor }} size={16} />
|
||||
) : (
|
||||
<FaTrashAlt style={{ color: 'rgba(255, 255, 255, 0.3)' }} size={16} />
|
||||
<FaTrashAlt style={{ color: 'var(--gray10)' }} size={16} />
|
||||
)}
|
||||
<div style={{ flexGrow: 1, borderBottom: '1px dashed rgba(255, 255, 255, 0.3)', marginLeft: 5, marginRight: 5 }} />
|
||||
<div style={{ flexGrow: 1, borderBottom: '1px dashed var(--gray10)', marginLeft: 5, marginRight: 5 }} />
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
|
@ -1,19 +1,19 @@
|
||||
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Select } from 'evergreen-ui';
|
||||
import { motion } from 'framer-motion';
|
||||
import { MdRotate90DegreesCcw } from 'react-icons/md';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IoIosCamera, IoMdKey } from 'react-icons/io';
|
||||
import { FaYinYang, FaTrashAlt, FaStepBackward, FaStepForward, FaCaretLeft, FaCaretRight, FaPause, FaPlay, FaImages, FaKey } from 'react-icons/fa';
|
||||
import { FaYinYang, FaTrashAlt, FaStepBackward, FaStepForward, FaCaretLeft, FaCaretRight, FaPause, FaPlay, FaImages, FaKey, FaSun } from 'react-icons/fa';
|
||||
import { GiSoundWaves } from 'react-icons/gi';
|
||||
// import useTraceUpdate from 'use-trace-update';
|
||||
|
||||
import { primaryTextColor, primaryColor } from './colors';
|
||||
import { primaryTextColor, primaryColor, darkModeTransition } from './colors';
|
||||
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 Select from './components/Select';
|
||||
|
||||
import SimpleModeButton from './components/SimpleModeButton';
|
||||
import { withBlur, mirrorTransform, checkAppPath } from './util';
|
||||
@ -29,7 +29,7 @@ const zoomOptions = Array(13).fill().map((unused, z) => 2 ** z);
|
||||
|
||||
const leftRightWidth = 100;
|
||||
|
||||
const CutTimeInput = memo(({ cutTime, setCutTime, startTimeOffset, seekAbs, currentCutSeg, currentApparentCutSeg, isStart }) => {
|
||||
const CutTimeInput = memo(({ darkMode, cutTime, setCutTime, startTimeOffset, seekAbs, currentCutSeg, currentApparentCutSeg, isStart }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [cutTimeManual, setCutTimeManual] = useState();
|
||||
@ -41,10 +41,10 @@ const CutTimeInput = memo(({ cutTime, setCutTime, startTimeOffset, seekAbs, curr
|
||||
|
||||
const isCutTimeManualSet = () => cutTimeManual !== undefined;
|
||||
|
||||
const border = `1px solid ${getSegColor(currentCutSeg).alpha(0.8).string()}`;
|
||||
const border = `.15em solid ${getSegColor(currentCutSeg, darkMode).alpha(0.8).string()}`;
|
||||
|
||||
const cutTimeInputStyle = {
|
||||
background: 'white', border, borderRadius: 5, color: 'rgba(0, 0, 0, 0.7)', fontSize: 13, textAlign: 'center', padding: '1px 5px', marginTop: 0, marginBottom: 0, marginLeft: isStart ? 0 : 5, marginRight: isStart ? 5 : 0, boxSizing: 'border-box', fontFamily: 'inherit', width: 90, outline: 'none',
|
||||
border, borderRadius: 5, backgroundColor: 'var(--gray5)', transition: darkModeTransition, fontSize: 13, textAlign: 'center', padding: '1px 5px', marginTop: 0, marginBottom: 0, marginLeft: isStart ? 0 : 5, marginRight: isStart ? 5 : 0, boxSizing: 'border-box', fontFamily: 'inherit', width: 90, outline: 'none',
|
||||
};
|
||||
|
||||
const trySetTime = useCallback((timeWithOffset) => {
|
||||
@ -113,7 +113,7 @@ const CutTimeInput = memo(({ cutTime, setCutTime, startTimeOffset, seekAbs, curr
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
style={{ ...cutTimeInputStyle, color: isCutTimeManualSet() ? '#dc1d1d' : undefined }}
|
||||
style={{ ...cutTimeInputStyle, color: isCutTimeManualSet() ? 'var(--red11)' : 'var(--gray12)' }}
|
||||
type="text"
|
||||
title={isStart ? t('Manually input current segment\'s start time') : t('Manually input current segment\'s end time')}
|
||||
onChange={e => handleCutTimeInput(e.target.value)}
|
||||
@ -137,6 +137,7 @@ const BottomBar = memo(({
|
||||
jumpTimelineStart, jumpTimelineEnd, jumpCutEnd, jumpCutStart, startTimeOffset, setCutTime, currentApparentCutSeg,
|
||||
playing, shortStep, togglePlay, toggleLoopSelectedSegments, toggleTimelineMode, hasAudio, timelineMode,
|
||||
keyframesEnabled, toggleKeyframesEnabled, seekClosestKeyframe, detectedFps, isFileOpened, selectedSegments,
|
||||
darkMode, setDarkMode,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -146,7 +147,7 @@ const BottomBar = memo(({
|
||||
const selectedSegmentsSafe = (selectedSegments.length > 1 ? selectedSegments : [selectedSegments[0], selectedSegments[0]]).slice(0, 10);
|
||||
|
||||
const gradientColors = selectedSegmentsSafe.map((seg, i) => {
|
||||
const segColor = getSegColor(seg);
|
||||
const segColor = getSegColor(seg, darkMode);
|
||||
// make colors stronger, the more segments
|
||||
return `${segColor.alpha(Math.max(0.4, Math.min(0.8, selectedSegmentsSafe.length / 3))).string()} ${((i / (selectedSegmentsSafe.length - 1)) * 100).toFixed(1)}%`;
|
||||
}).join(', ');
|
||||
@ -155,7 +156,8 @@ const BottomBar = memo(({
|
||||
paddingLeft: 2,
|
||||
backgroundOffset: 30,
|
||||
background: `linear-gradient(90deg, ${gradientColors})`,
|
||||
border: '1px solid rgb(200,200,200)',
|
||||
border: '1px solid var(--gray8)',
|
||||
color: 'white',
|
||||
margin: '2px 4px 0 0px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@ -164,7 +166,7 @@ const BottomBar = memo(({
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
};
|
||||
}, [selectedSegments]);
|
||||
}, [darkMode, selectedSegments]);
|
||||
|
||||
const { invertCutSegments, setInvertCutSegments, simpleMode, toggleSimpleMode, exportConfirmEnabled } = useUserSettings();
|
||||
|
||||
@ -187,8 +189,8 @@ const BottomBar = memo(({
|
||||
const newIndex = currentSegIndexSafe + direction;
|
||||
const seg = cutSegments[newIndex];
|
||||
|
||||
const backgroundColor = seg && getSegColor(seg).alpha(0.5).string();
|
||||
const opacity = seg ? undefined : 0.3;
|
||||
const backgroundColor = seg && getSegColor(seg, darkMode).alpha(0.5).string();
|
||||
const opacity = seg ? undefined : 0.5;
|
||||
const text = seg ? `${newIndex + 1}` : '-';
|
||||
const wide = text.length > 1;
|
||||
const segButtonStyle = {
|
||||
@ -215,10 +217,12 @@ const BottomBar = memo(({
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexBasis: leftRightWidth }}>
|
||||
{!simpleMode && (
|
||||
<>
|
||||
<FaSun color="var(--gray12)" role="button" onClick={() => setDarkMode((v) => !v)} style={{ padding: '0 .2em 0 .3em' }} />
|
||||
|
||||
{hasAudio && (
|
||||
<GiSoundWaves
|
||||
size={24}
|
||||
style={{ padding: '0 5px', color: timelineMode === 'waveform' ? primaryTextColor : undefined }}
|
||||
style={{ padding: '0 .1em', color: timelineMode === 'waveform' ? primaryTextColor : undefined }}
|
||||
role="button"
|
||||
title={t('Show waveform')}
|
||||
onClick={() => toggleTimelineMode('waveform')}
|
||||
@ -228,7 +232,7 @@ const BottomBar = memo(({
|
||||
<>
|
||||
<FaImages
|
||||
size={20}
|
||||
style={{ padding: '0 5px', color: timelineMode === 'thumbnails' ? primaryTextColor : undefined }}
|
||||
style={{ padding: '0 .2em', color: timelineMode === 'thumbnails' ? primaryTextColor : undefined }}
|
||||
role="button"
|
||||
title={t('Show thumbnails')}
|
||||
onClick={() => toggleTimelineMode('thumbnails')}
|
||||
@ -236,7 +240,7 @@ const BottomBar = memo(({
|
||||
|
||||
<FaKey
|
||||
size={16}
|
||||
style={{ padding: '0 5px', color: keyframesEnabled ? primaryTextColor : undefined }}
|
||||
style={{ padding: '0 .2em', color: keyframesEnabled ? primaryTextColor : undefined }}
|
||||
role="button"
|
||||
title={t('Show keyframes')}
|
||||
onClick={toggleKeyframesEnabled}
|
||||
@ -267,7 +271,7 @@ const BottomBar = memo(({
|
||||
|
||||
<SetCutpointButton currentCutSeg={currentCutSeg} side="start" onClick={setCutStart} title={t('Start current segment at current time')} style={{ marginRight: 5 }} />
|
||||
|
||||
{!simpleMode && <CutTimeInput currentCutSeg={currentCutSeg} currentApparentCutSeg={currentApparentCutSeg} startTimeOffset={startTimeOffset} seekAbs={seekAbs} cutTime={currentApparentCutSeg.start} setCutTime={setCutTime} isStart />}
|
||||
{!simpleMode && <CutTimeInput darkMode={darkMode} currentCutSeg={currentCutSeg} currentApparentCutSeg={currentApparentCutSeg} startTimeOffset={startTimeOffset} seekAbs={seekAbs} cutTime={currentApparentCutSeg.start} setCutTime={setCutTime} isStart />}
|
||||
|
||||
<IoMdKey
|
||||
size={25}
|
||||
@ -287,7 +291,7 @@ const BottomBar = memo(({
|
||||
/>
|
||||
)}
|
||||
|
||||
<div role="button" onClick={() => togglePlay()} style={{ background: primaryColor, margin: '2px 5px 0 5px', display: 'flex', alignItems: 'center', justifyContent: 'center', width: 34, height: 34, borderRadius: 17 }}>
|
||||
<div role="button" onClick={() => togglePlay()} style={{ background: primaryColor, margin: '2px 5px 0 5px', display: 'flex', alignItems: 'center', justifyContent: 'center', width: 34, height: 34, borderRadius: 17, color: 'white' }}>
|
||||
<PlayPause
|
||||
style={{ paddingLeft: playing ? 0 : 2 }}
|
||||
size={16}
|
||||
@ -318,7 +322,7 @@ const BottomBar = memo(({
|
||||
onClick={() => seekClosestKeyframe(1)}
|
||||
/>
|
||||
|
||||
{!simpleMode && <CutTimeInput currentCutSeg={currentCutSeg} currentApparentCutSeg={currentApparentCutSeg} startTimeOffset={startTimeOffset} seekAbs={seekAbs} cutTime={currentApparentCutSeg.end} setCutTime={setCutTime} />}
|
||||
{!simpleMode && <CutTimeInput darkMode={darkMode} currentCutSeg={currentCutSeg} currentApparentCutSeg={currentApparentCutSeg} startTimeOffset={startTimeOffset} seekAbs={seekAbs} cutTime={currentApparentCutSeg.end} setCutTime={setCutTime} />}
|
||||
|
||||
<SetCutpointButton currentCutSeg={currentCutSeg} side="end" onClick={setCutEnd} title={t('End current segment at current time')} style={{ marginLeft: 5 }} />
|
||||
|
||||
@ -371,14 +375,14 @@ const BottomBar = memo(({
|
||||
|
||||
<div role="button" style={{ marginRight: 5, marginLeft: 10 }} title={t('Zoom')} onClick={timelineToggleComfortZoom}>{Math.floor(zoom)}x</div>
|
||||
|
||||
<Select height={20} style={{ flexBasis: 85, flexGrow: 0 }} value={zoomOptions.includes(zoom) ? zoom.toString() : ''} title={t('Zoom')} onChange={withBlur(e => setZoom(parseInt(e.target.value, 10)))}>
|
||||
<Select style={{ height: 20, flexBasis: 85, flexGrow: 0 }} value={zoomOptions.includes(zoom) ? zoom.toString() : ''} title={t('Zoom')} onChange={withBlur(e => setZoom(parseInt(e.target.value, 10)))}>
|
||||
<option key="" value="" disabled>{t('Zoom')}</option>
|
||||
{zoomOptions.map(val => (
|
||||
<option key={val} value={String(val)}>{t('Zoom')} {val}x</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{detectedFps != null && <div title={t('Video FPS')} style={{ color: 'rgba(255,255,255,0.6)', fontSize: '.7em', marginLeft: 6 }}>{detectedFps.toFixed(3)}</div>}
|
||||
{detectedFps != null && <div title={t('Video FPS')} style={{ color: 'var(--gray11)', fontSize: '.7em', marginLeft: 6 }}>{detectedFps.toFixed(3)}</div>}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -3,13 +3,13 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Heading } from 'evergreen-ui';
|
||||
|
||||
import CopyClipboardButton from './components/CopyClipboardButton';
|
||||
import Sheet from './Sheet';
|
||||
import Sheet from './components/Sheet';
|
||||
|
||||
const LastCommandsSheet = memo(({ visible, onTogglePress, ffmpegCommandLog }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Sheet visible={visible} onClosePress={onTogglePress} style={{ background: '#6b6b6b', color: 'white' }}>
|
||||
<Sheet visible={visible} onClosePress={onTogglePress} style={{ paddingTop: '2em' }}>
|
||||
<Heading color="white">{t('Last ffmpeg commands')}</Heading>
|
||||
|
||||
{ffmpegCommandLog.length > 0 ? (
|
||||
|
@ -11,21 +11,21 @@ const electron = window.require('electron');
|
||||
|
||||
const NoFileLoaded = memo(({ mifiLink, currentCutSeg }) => {
|
||||
const { t } = useTranslation();
|
||||
const { simpleMode, toggleSimpleMode } = useUserSettings();
|
||||
const { simpleMode } = 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' }}>
|
||||
<div className="no-user-select" style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, border: '2vmin dashed var(--gray5)', color: 'var(--gray12)', margin: '5vmin', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', whiteSpace: 'nowrap' }}>
|
||||
<div style={{ fontSize: '6vmin', textTransform: 'uppercase' }}>{t('DROP FILE(S)')}</div>
|
||||
|
||||
<div style={{ fontSize: '3vmin', color: '#777', marginBottom: '.3em' }}>
|
||||
<div style={{ fontSize: '3vmin', color: 'var(--gray11)', marginBottom: '.3em' }}>
|
||||
<Trans>See <b>Help</b> menu for help</Trans>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '3vmin', color: '#ccc' }}>
|
||||
<div style={{ fontSize: '3vmin', color: 'var(--gray11)' }}>
|
||||
<Trans><SetCutpointButton currentCutSeg={currentCutSeg} side="start" style={{ verticalAlign: 'middle' }} /> <SetCutpointButton currentCutSeg={currentCutSeg} side="end" style={{ verticalAlign: 'middle' }} /> or <kbd>I</kbd> <kbd>O</kbd> to set cutpoints</Trans>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '3vmin', color: '#ccc', cursor: 'pointer' }} role="button" onClick={toggleSimpleMode}>
|
||||
<div style={{ fontSize: '3vmin', color: 'var(--gray11)' }} role="button">
|
||||
<SimpleModeButton style={{ verticalAlign: 'middle' }} size={16} /> {simpleMode ? i18n.t('to show advanced view') : i18n.t('to show simple view')}
|
||||
</div>
|
||||
|
||||
|
@ -11,7 +11,7 @@ import scrollIntoView from 'scroll-into-view-if-needed';
|
||||
import Swal from './swal';
|
||||
import useContextMenu from './hooks/useContextMenu';
|
||||
import useUserSettings from './hooks/useUserSettings';
|
||||
import { saveColor, controlsBackground, primaryTextColor } from './colors';
|
||||
import { saveColor, controlsBackground, primaryTextColor, darkModeTransition } from './colors';
|
||||
import { getSegColor } from './util/colors';
|
||||
import { mySpring } from './animations';
|
||||
|
||||
@ -19,10 +19,10 @@ const buttonBaseStyle = {
|
||||
margin: '0 3px', borderRadius: 3, color: 'white', cursor: 'pointer',
|
||||
};
|
||||
|
||||
const neutralButtonColor = 'rgba(255, 255, 255, 0.2)';
|
||||
const neutralButtonColor = 'var(--gray8)';
|
||||
|
||||
|
||||
const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelected, onLabelSelectedSegments, onReorderPress, onLabelPress, selected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onViewSegmentTags, onExtractSegmentFramesAsImages, onInvertSelectedSegments }) => {
|
||||
const Segment = memo(({ darkMode, seg, index, currentSegIndex, formatTimecode, getFrameCount, updateOrder, invertCutSegments, onClick, onRemovePress, onRemoveSelected, onLabelSelectedSegments, onReorderPress, onLabelPress, selected, onSelectSingleSegment, onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectAllSegments, jumpSegStart, jumpSegEnd, addSegment, onViewSegmentTags, onExtractSegmentFramesAsImages, onInvertSelectedSegments }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const ref = useRef();
|
||||
@ -79,7 +79,7 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou
|
||||
function renderNumber() {
|
||||
if (invertCutSegments) return <FaSave style={{ color: saveColor, marginRight: 5, verticalAlign: 'middle' }} size={14} />;
|
||||
|
||||
const segColor = getSegColor(seg);
|
||||
const segColor = getSegColor(seg, darkMode);
|
||||
|
||||
return <b style={{ cursor: 'grab', color: 'white', padding: '0 4px', marginRight: 3, marginLeft: -3, background: segColor.alpha(0.5).string(), border: `1px solid ${isActive ? segColor.lighten(0.3).string() : 'transparent'}`, borderRadius: 10, fontSize: 12 }}>{index + 1}</b>;
|
||||
}
|
||||
@ -114,18 +114,18 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou
|
||||
onClick={() => !invertCutSegments && onClick(index)}
|
||||
onDoubleClick={onDoubleClick}
|
||||
layout
|
||||
style={{ originY: 0, margin: '5px 0', background: 'rgba(0,0,0,0.1)', border: `1px solid rgba(255,255,255,${isActive ? 1 : 0.3})`, padding: 5, borderRadius: 5, position: 'relative' }}
|
||||
style={{ originY: 0, margin: '5px 0', background: 'var(--gray2)', border: isActive ? '1px solid var(--gray10)' : '1px solid transparent', padding: 5, borderRadius: 5, position: 'relative' }}
|
||||
initial={{ scaleY: 0 }}
|
||||
animate={{ scaleY: 1, opacity: !selected && !invertCutSegments ? 0.5 : undefined }}
|
||||
exit={{ scaleY: 0 }}
|
||||
className="segment-list-entry"
|
||||
>
|
||||
<div className="segment-handle" style={{ cursor, color: 'white', marginBottom: 3, display: 'flex', alignItems: 'center', height: 16 }}>
|
||||
<div className="segment-handle" style={{ cursor, color: 'var(--gray12)', marginBottom: 3, display: 'flex', alignItems: 'center', height: 16 }}>
|
||||
{renderNumber()}
|
||||
<span style={{ cursor, fontSize: Math.min(310 / timeStr.length, 14), whiteSpace: 'nowrap' }}>{timeStr}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 12, color: 'white' }}>{seg.name}</div>
|
||||
<div style={{ fontSize: 12, color: primaryTextColor }}>{seg.name}</div>
|
||||
<div style={{ fontSize: 13 }}>
|
||||
{t('Duration')} {formatTimecode({ seconds: duration, shorten: true })}
|
||||
</div>
|
||||
@ -135,7 +135,7 @@ const Segment = memo(({ seg, index, currentSegIndex, formatTimecode, getFrameCou
|
||||
|
||||
{!invertCutSegments && (
|
||||
<div style={{ position: 'absolute', right: 3, bottom: 3 }}>
|
||||
<CheckIcon className="enabled" size={20} color="white" onClick={onToggleSegmentSelectedClick} />
|
||||
<CheckIcon className="enabled" size={20} color="var(--gray12)" onClick={onToggleSegmentSelectedClick} />
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
@ -152,7 +152,7 @@ const SegmentList = memo(({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { invertCutSegments, simpleMode } = useUserSettings();
|
||||
const { invertCutSegments, simpleMode, darkMode } = useUserSettings();
|
||||
|
||||
const segments = invertCutSegments ? inverseCutSegments : apparentCutSegments;
|
||||
|
||||
@ -195,14 +195,14 @@ const SegmentList = memo(({
|
||||
}
|
||||
|
||||
function renderFooter() {
|
||||
const currentSegColor = getSegColor(currentCutSeg).alpha(0.5).string();
|
||||
const segAtCursorColor = getSegColor(segmentAtCursor).alpha(0.5).string();
|
||||
const currentSegColor = getSegColor(currentCutSeg, darkMode).alpha(0.5).string();
|
||||
const segAtCursorColor = getSegColor(segmentAtCursor, darkMode).alpha(0.5).string();
|
||||
|
||||
const segmentsTotal = selectedSegments.reduce((acc, { start, end }) => (end - start) + acc, 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', padding: '5px 0', alignItems: 'center', justifyContent: 'center', borderBottom: '1px solid grey' }}>
|
||||
<div style={{ display: 'flex', padding: '5px 0', alignItems: 'center', justifyContent: 'center', borderBottom: '1px solid var(gray6)' }}>
|
||||
<FaPlus
|
||||
size={24}
|
||||
style={{ ...buttonBaseStyle, background: neutralButtonColor }}
|
||||
@ -248,7 +248,7 @@ const SegmentList = memo(({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '5px 10px', boxSizing: 'border-box', borderBottom: '1px solid grey', borderTop: '1px solid grey', display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
|
||||
<div style={{ padding: '5px 10px', boxSizing: 'border-box', borderBottom: '1px solid var(gray6)', borderTop: '1px solid var(gray6)', display: 'flex', justifyContent: 'space-between', fontSize: 13 }}>
|
||||
<div>{t('Segments total:')}</div>
|
||||
<div>{formatTimecode({ seconds: segmentsTotal })}</div>
|
||||
</div>
|
||||
@ -258,17 +258,17 @@ const SegmentList = memo(({
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
style={{ width, background: controlsBackground, color: 'rgba(255,255,255,0.7)', display: 'flex', flexDirection: 'column', overflowY: 'hidden' }}
|
||||
style={{ width, background: controlsBackground, color: 'var(--gray11)', transition: darkModeTransition, display: 'flex', flexDirection: 'column', overflowY: 'hidden' }}
|
||||
initial={{ x: width }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: width }}
|
||||
transition={mySpring}
|
||||
>
|
||||
<div style={{ fontSize: 14, padding: '0 5px' }} className="no-user-select">
|
||||
<div style={{ fontSize: 14, padding: '0 5px', color: 'var(--gray12)' }} className="no-user-select">
|
||||
<FaAngleRight
|
||||
title={t('Close sidebar')}
|
||||
size={20}
|
||||
style={{ verticalAlign: 'middle', color: 'white', cursor: 'pointer', padding: 2 }}
|
||||
style={{ verticalAlign: 'middle', color: 'var(--gray11)', cursor: 'pointer', padding: 2 }}
|
||||
role="button"
|
||||
onClick={toggleSegmentsList}
|
||||
/>
|
||||
@ -283,6 +283,7 @@ const SegmentList = memo(({
|
||||
return (
|
||||
<Segment
|
||||
key={id}
|
||||
darkMode={darkMode}
|
||||
seg={seg}
|
||||
index={index}
|
||||
selected={selected}
|
||||
|
408
src/Settings.jsx
408
src/Settings.jsx
@ -1,408 +0,0 @@
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { FaYinYang, FaKeyboard } from 'react-icons/fa';
|
||||
import { GlobeIcon, CleanIcon, CogIcon, 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';
|
||||
import { askForFfPath } from './dialogs';
|
||||
import { isMasBuild, isStoreBuild } from './util';
|
||||
import { langNames } from './util/constants';
|
||||
|
||||
import { getModifierKeyNames } from './hooks/useTimelineScroll';
|
||||
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
const Row = (props) => <Table.Row height="auto" paddingY={12} {...props} />;
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
const KeyCell = (props) => <Table.TextCell textProps={{ whiteSpace: 'auto' }} {...props} />;
|
||||
|
||||
const Header = ({ title }) => (
|
||||
<Row backgroundColor="rgba(0,0,0,0.05)">
|
||||
<Table.TextCell><b>{title}</b></Table.TextCell>
|
||||
<Table.TextCell />
|
||||
</Row>
|
||||
);
|
||||
|
||||
const Settings = memo(({
|
||||
onTunerRequested,
|
||||
onKeyboardShortcutsDialogRequested,
|
||||
askForCleanupChoices,
|
||||
toggleStoreProjectInWorkingDir,
|
||||
}) => {
|
||||
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, customFfPath, setCustomFfPath, storeProjectInWorkingDir, enableOverwriteOutput, setEnableOverwriteOutput, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey, captureFrameMethod, setCaptureFrameMethod, captureFrameQuality, setCaptureFrameQuality, captureFrameFileNameFormat, setCaptureFrameFileNameFormat, enableNativeHevc, setEnableNativeHevc, enableUpdateCheck, setEnableUpdateCheck, allowMultipleInstances, setAllowMultipleInstances } = useUserSettings();
|
||||
|
||||
const onLangChange = useCallback((e) => {
|
||||
const { value } = e.target;
|
||||
const l = value !== '' ? value : undefined;
|
||||
setLanguage(l);
|
||||
}, [setLanguage]);
|
||||
|
||||
const timecodeFormatOptions = useMemo(() => ({
|
||||
frameCount: t('Frame counts'),
|
||||
timecodeWithDecimalFraction: t('Millisecond fractions'),
|
||||
timecodeWithFramesFraction: t('Frame fractions'),
|
||||
}), [t]);
|
||||
|
||||
const onTimecodeFormatClick = useCallback(() => {
|
||||
const keys = Object.keys(timecodeFormatOptions);
|
||||
let index = keys.indexOf(timecodeFormat);
|
||||
if (index === -1 || index >= keys.length - 1) index = 0;
|
||||
else index += 1;
|
||||
setTimecodeFormat(keys[index]);
|
||||
}, [setTimecodeFormat, timecodeFormat, timecodeFormatOptions]);
|
||||
|
||||
const changeCustomFfPath = useCallback(async () => {
|
||||
const newCustomFfPath = await askForFfPath(customFfPath);
|
||||
setCustomFfPath(newCustomFfPath);
|
||||
}, [customFfPath, setCustomFfPath]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<KeyCell><GlobeIcon style={{ verticalAlign: 'middle', marginRight: '.5em' }} /> App language</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Select value={language || ''} onChange={onLangChange}>
|
||||
<option key="" value="">{t('System language')}</option>
|
||||
{Object.keys(langNames).map((lang) => <option key={lang} value={lang}>{langNames[lang]}</option>)}
|
||||
</Select>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Choose cutting mode: Remove or keep selected segments from video when exporting?')}<br />
|
||||
<b>{t('Keep')}</b>: {t('The video inside segments will be kept, while the video outside will be discarded.')}<br />
|
||||
<b>{t('Remove')}</b>: {t('The video inside segments will be discarded, while the video surrounding them will be kept.')}
|
||||
</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Button iconBefore={FaYinYang} appearance={invertCutSegments ? 'default' : 'primary'} intent="success" onClick={() => setInvertCutSegments((v) => !v)}>
|
||||
{invertCutSegments ? t('Remove') : t('Keep')}
|
||||
</Button>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Working directory')}<br />
|
||||
{t('This is where working files and exported files are stored.')}
|
||||
</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Button iconBefore={customOutDir ? FolderCloseIcon : DocumentIcon} onClick={changeOutDir}>
|
||||
{customOutDir ? t('Custom working directory') : t('Same directory as input file')}...
|
||||
</Button>
|
||||
<div>{customOutDir}</div>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Auto save project file?')}<br />
|
||||
</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Checkbox
|
||||
label={t('Auto save project')}
|
||||
checked={autoSaveProjectFile}
|
||||
onChange={e => setAutoSaveProjectFile(e.target.checked)}
|
||||
/>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Store project file (.llc) in the working directory or next to loaded media file?')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Button iconBefore={storeProjectInWorkingDir ? FolderCloseIcon : DocumentIcon} disabled={!autoSaveProjectFile} onClick={toggleStoreProjectInWorkingDir}>
|
||||
{storeProjectInWorkingDir ? t('Store in working directory') : t('Store next to media file')}
|
||||
</Button>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Header title={t('Keyboard, mouse and input')} />
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Keyboard & mouse shortcuts')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Button iconBefore={<FaKeyboard />} onClick={onKeyboardShortcutsDialogRequested}>{t('Keyboard & mouse shortcuts')}</Button>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Mouse wheel zoom modifier key')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Select value={mouseWheelZoomModifierKey} onChange={(e) => setMouseWheelZoomModifierKey(e.target.value)}>
|
||||
{Object.entries(getModifierKeyNames()).map(([key, value]) => (
|
||||
<option key={key} value={key}>{value}</option>
|
||||
))}
|
||||
</Select>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Timeline trackpad/wheel sensitivity')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Button onClick={() => onTunerRequested('wheelSensitivity')}>{t('Change value')}</Button>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Timeline keyboard seek speed')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Button onClick={() => onTunerRequested('keyboardNormalSeekSpeed')}>{t('Change value')}</Button>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Timeline keyboard seek acceleration')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Button onClick={() => onTunerRequested('keyboardSeekAccFactor')}>{t('Change value')}</Button>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Invert timeline trackpad/wheel direction?')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Checkbox
|
||||
label={t('Invert direction')}
|
||||
checked={invertTimelineScroll}
|
||||
onChange={e => setInvertTimelineScroll(e.target.checked)}
|
||||
/>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Header title={t('Options affecting exported files')} />
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Set file modification date/time of output files to:')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Button iconBefore={enableTransferTimestamps ? DocumentIcon : TimeIcon} onClick={() => setEnableTransferTimestamps((v) => !v)}>
|
||||
{enableTransferTimestamps ? t('Source file\'s time') : t('Current time')}
|
||||
</Button>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Keyframe cut mode')}<br />
|
||||
<b>{t('Keyframe cut')}</b>: {t('Cut at the nearest keyframe (not accurate time.) Equiv to')} <i>ffmpeg -ss -i ...</i><br />
|
||||
<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 ? KeyIcon : undefined} onClick={() => toggleKeyframeCut()}>
|
||||
{keyframeCut ? t('Keyframe cut') : t('Normal cut')}
|
||||
</Button>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Overwrite files when exporting, if a file with the same name as the output file name exists?')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Checkbox
|
||||
label={t('Overwrite existing files')}
|
||||
checked={enableOverwriteOutput}
|
||||
onChange={e => setEnableOverwriteOutput(e.target.checked)}
|
||||
/>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Cleanup files after export?')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Button iconBefore={<CleanIcon />} onClick={askForCleanupChoices}>{t('Change preferences')}</Button>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Header title={t('Snapshots and frame extraction')} />
|
||||
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Snapshot capture format')}
|
||||
</KeyCell>
|
||||
<Table.TextCell>
|
||||
<CaptureFormatButton showIcon />
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Snapshot capture method')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Button onClick={() => setCaptureFrameMethod((existing) => (existing === 'ffmpeg' ? 'videotag' : 'ffmpeg'))}>
|
||||
{captureFrameMethod === 'ffmpeg' ? t('FFmpeg') : t('HTML video tag')}
|
||||
</Button>
|
||||
{captureFrameMethod === 'ffmpeg' && <div style={{ whiteSpace: 'initial' }}>{t('FFmpeg capture method might sometimes capture more correct colors, but the captured snapshot might be off by one or more frames, relative to the preview.')}</div>}
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Snapshot capture quality')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<input type="range" min={1} max={1000} style={{ width: 200 }} value={Math.round(captureFrameQuality * 1000)} onChange={(e) => setCaptureFrameQuality(Math.max(Math.min(1, parseInt(e.target.value, 10) / 1000)), 0)} /><br />
|
||||
{Math.round(captureFrameQuality * 100)}%
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('File names of extracted video frames')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Button iconBefore={captureFrameFileNameFormat === 'timestamp' ? TimeIcon : NumericalIcon} onClick={() => setCaptureFrameFileNameFormat((existing) => (existing === 'timestamp' ? 'index' : 'timestamp'))}>
|
||||
{captureFrameFileNameFormat === 'timestamp' ? t('Frame timestamp') : t('File number')}
|
||||
</Button>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('In timecode show')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Button iconBefore={timecodeFormat === 'frameCount' ? NumericalIcon : TimeIcon} onClick={onTimecodeFormatClick}>
|
||||
{timecodeFormatOptions[timecodeFormat]}
|
||||
</Button>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Header title={t('Prompts and dialogs')} />
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Hide informational notifications?')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Checkbox
|
||||
label={t('Check to hide notifications')}
|
||||
checked={!!hideNotifications}
|
||||
onChange={e => setHideNotifications(e.target.checked ? 'all' : undefined)}
|
||||
/>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Ask about what to do when opening a new file when another file is already already open?')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Checkbox
|
||||
label={t('Ask on file open')}
|
||||
checked={enableAskForFileOpenAction}
|
||||
onChange={e => setEnableAskForFileOpenAction(e.target.checked)}
|
||||
/>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Ask for confirmation when closing app or file?')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Checkbox
|
||||
label={t('Ask before closing')}
|
||||
checked={askBeforeClose}
|
||||
onChange={e => setAskBeforeClose(e.target.checked)}
|
||||
/>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Ask about importing chapters from opened file?')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Checkbox
|
||||
label={t('Ask about chapters')}
|
||||
checked={enableAskForImportChapters}
|
||||
onChange={e => setEnableAskForImportChapters(e.target.checked)}
|
||||
/>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Header title={t('Advanced options')} />
|
||||
|
||||
{!isMasBuild && (
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Custom FFmpeg directory (experimental)')}<br />
|
||||
{t('This allows you to specify custom FFmpeg and FFprobe binaries to use. Make sure the "ffmpeg" and "ffprobe" executables exist in the same directory, and then select the directory.')}
|
||||
</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Button iconBefore={CogIcon} onClick={changeCustomFfPath}>
|
||||
{customFfPath ? t('Using external ffmpeg') : t('Using built-in ffmpeg')}
|
||||
</Button>
|
||||
<div>{customFfPath}</div>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{!isStoreBuild && (
|
||||
<Row>
|
||||
<KeyCell>{t('Check for updates on startup?')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Checkbox
|
||||
label={t('Check for updates')}
|
||||
checked={enableUpdateCheck}
|
||||
onChange={e => setEnableUpdateCheck(e.target.checked)}
|
||||
/>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Allow multiple instances of LosslessCut to run concurrently? (experimental)')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Checkbox
|
||||
label={t('Allow multiple instances')}
|
||||
checked={allowMultipleInstances}
|
||||
onChange={e => setAllowMultipleInstances(e.target.checked)}
|
||||
/>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Enable HEVC / H265 hardware decoding (you may need to turn this off if you have problems with HEVC files)')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Checkbox
|
||||
label={t('Enable HEVC hardware decoding')}
|
||||
checked={enableNativeHevc}
|
||||
onChange={e => setEnableNativeHevc(e.target.checked)}
|
||||
/>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Enable experimental ffmpeg features flag?')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Checkbox
|
||||
label={t('Experimental flag')}
|
||||
checked={ffmpegExperimental}
|
||||
onChange={e => setFfmpegExperimental(e.target.checked)}
|
||||
/>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Auto load timecode from file as an offset in the timeline?')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Checkbox
|
||||
label={t('Auto load timecode')}
|
||||
checked={autoLoadTimecode}
|
||||
onChange={e => setAutoLoadTimecode(e.target.checked)}
|
||||
/>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Try to automatically convert to supported format when opening unsupported file?')}</KeyCell>
|
||||
<Table.TextCell>
|
||||
<Checkbox
|
||||
label={t('Auto convert to supported format')}
|
||||
checked={enableAutoHtml5ify}
|
||||
onChange={e => setEnableAutoHtml5ify(e.target.checked)}
|
||||
/>
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Extract unprocessable tracks to separate files or discard them?')}<br />
|
||||
{t('(data tracks such as GoPro GPS, telemetry etc. are not copied over by default because ffmpeg cannot cut them, thus they will cause the media duration to stay the same after cutting video/audio)')}
|
||||
</KeyCell>
|
||||
<Table.TextCell>
|
||||
<AutoExportToggler />
|
||||
</Table.TextCell>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Settings;
|
@ -4,11 +4,12 @@ import { FaImage, FaCheckCircle, FaPaperclip, FaVideo, FaVideoSlash, FaFileImpor
|
||||
import { GoFileBinary } from 'react-icons/go';
|
||||
import { FiEdit, FiCheck, FiTrash } from 'react-icons/fi';
|
||||
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 { BookIcon, Paragraph, TextInput, MoreIcon, Position, Popover, Menu, TrashIcon, EditIcon, InfoSignIcon, IconButton, Heading, SortAscIcon, SortDescIcon, Dialog, Button, PlusIcon, Pane, ForkIcon, Alert } from 'evergreen-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
import AutoExportToggler from './components/AutoExportToggler';
|
||||
import Select from './components/Select';
|
||||
import { askForMetadataKey, showJson5Dialog } from './dialogs';
|
||||
import { formatDuration } from './util/duration';
|
||||
import { getStreamFps } from './ffmpeg';
|
||||
@ -254,7 +255,7 @@ const Stream = memo(({ dispositionByStreamId, setDispositionByStreamId, filePath
|
||||
<td style={{ maxWidth: '2.5em', overflow: 'hidden' }} title={language}>{language}</td>
|
||||
<td>{stream.width && stream.height && `${stream.width}x${stream.height}`} {stream.channels && `${stream.channels}c`} {stream.channel_layout} {streamFps && `${streamFps.toFixed(2)}fps`}</td>
|
||||
<td>
|
||||
<Select width={100} value={effectiveDisposition || unchangedDispositionValue} onChange={onDispositionChange}>
|
||||
<Select style={{ width: 100 }} value={effectiveDisposition || unchangedDispositionValue} onChange={onDispositionChange}>
|
||||
<option value="" disabled>{t('Disposition')}</option>
|
||||
<option value={unchangedDispositionValue}>{t('Unchanged')}</option>
|
||||
<option value={deleteDispositionValue}>{t('Remove')}</option>
|
||||
|
@ -10,7 +10,7 @@ import useContextMenu from './hooks/useContextMenu';
|
||||
import useUserSettings from './hooks/useUserSettings';
|
||||
|
||||
|
||||
import { timelineBackground } from './colors';
|
||||
import { timelineBackground, darkModeTransition } from './colors';
|
||||
|
||||
import { getSegColor } from './util/colors';
|
||||
|
||||
@ -43,7 +43,7 @@ const Waveforms = memo(({ calculateTimelinePercent, durationSafe, waveforms, zoo
|
||||
));
|
||||
|
||||
const CommandedTime = memo(({ commandedTimePercent }) => {
|
||||
const color = 'white';
|
||||
const color = 'var(--gray12)';
|
||||
const commonStyle = { left: commandedTimePercent, position: 'absolute', zIndex: 4, pointerEvents: 'none' };
|
||||
return (
|
||||
<>
|
||||
@ -64,7 +64,7 @@ const Timeline = memo(({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { invertCutSegments } = useUserSettings();
|
||||
const { invertCutSegments, darkMode } = useUserSettings();
|
||||
|
||||
const timelineScrollerRef = useRef();
|
||||
const timelineScrollerSkipEventRef = useRef();
|
||||
@ -276,18 +276,18 @@ const Timeline = memo(({
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{ height: timelineHeight, width: `${zoom * 100}%`, position: 'relative', backgroundColor: timelineBackground }}
|
||||
style={{ height: timelineHeight, width: `${zoom * 100}%`, position: 'relative', backgroundColor: timelineBackground, transition: darkModeTransition }}
|
||||
ref={timelineWrapperRef}
|
||||
>
|
||||
{currentTimePercent !== undefined && (
|
||||
<motion.div transition={{ type: 'spring', damping: 70, stiffness: 800 }} animate={{ left: currentTimePercent }} style={{ position: 'absolute', bottom: 0, top: 0, zIndex: 3, backgroundColor: 'rgba(255,255,255,0.6)', width: currentTimeWidth, pointerEvents: 'none' }} />
|
||||
<motion.div transition={{ type: 'spring', damping: 70, stiffness: 800 }} animate={{ left: currentTimePercent }} style={{ position: 'absolute', bottom: 0, top: 0, zIndex: 3, backgroundColor: 'var(--gray12)', width: currentTimeWidth, pointerEvents: 'none' }} />
|
||||
)}
|
||||
{commandedTimePercent !== undefined && (
|
||||
<CommandedTime commandedTimePercent={commandedTimePercent} />
|
||||
)}
|
||||
|
||||
{apparentCutSegments.map((seg, i) => {
|
||||
const segColor = getSegColor(seg);
|
||||
const segColor = getSegColor(seg, darkMode);
|
||||
|
||||
if (seg.start === 0 && seg.end === 0) return null; // No video loaded
|
||||
|
||||
@ -322,13 +322,13 @@ const Timeline = memo(({
|
||||
))}
|
||||
|
||||
{shouldShowKeyframes && !areKeyframesTooClose && neighbouringKeyFrames.map((f) => (
|
||||
<div key={f.time} style={{ position: 'absolute', top: 0, bottom: 0, left: `${(f.time / durationSafe) * 100}%`, marginLeft: -1, width: 1, background: 'rgba(0,0,0,0.4)', pointerEvents: 'none' }} />
|
||||
<div key={f.time} style={{ position: 'absolute', top: 0, bottom: 0, left: `${(f.time / durationSafe) * 100}%`, marginLeft: -1, width: 1, background: 'var(--gray9)', mixBlendMode: 'difference', pointerEvents: 'none' }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(waveformEnabled && !thumbnailsEnabled && !shouldShowWaveform) && (
|
||||
<div style={{ position: 'absolute', pointerEvents: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', height: timelineHeight, bottom: timelineHeight, left: 0, right: 0, color: 'rgba(255,255,255,0.6)' }}>
|
||||
<div style={{ position: 'absolute', pointerEvents: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', height: timelineHeight, bottom: timelineHeight, left: 0, right: 0, color: 'var(--gray11)' }}>
|
||||
{t('Zoom in more to view waveform')}
|
||||
</div>
|
||||
)}
|
||||
|
@ -33,6 +33,7 @@ const TimelineSeg = memo(({
|
||||
justifyContent: 'space-between',
|
||||
originX: 0,
|
||||
boxSizing: 'border-box',
|
||||
color: 'white',
|
||||
|
||||
borderLeft: markerBorder,
|
||||
borderTopLeftRadius: markerBorderRadius,
|
||||
@ -72,7 +73,7 @@ const TimelineSeg = memo(({
|
||||
style={{ width: 16, height: 16, flexShrink: 1 }}
|
||||
>
|
||||
<FaTrashAlt
|
||||
style={{ width: '100%', color: 'rgba(255,255,255,0.8)' }}
|
||||
style={{ width: '100%', color: 'var(--gray12)' }}
|
||||
size={16}
|
||||
/>
|
||||
</motion.div>
|
||||
|
@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import ExportModeButton from './components/ExportModeButton';
|
||||
|
||||
import { withBlur } from './util';
|
||||
import { primaryTextColor, controlsBackground } from './colors';
|
||||
import { primaryTextColor, controlsBackground, darkModeTransition } from './colors';
|
||||
import useUserSettings from './hooks/useUserSettings';
|
||||
|
||||
|
||||
@ -31,7 +31,7 @@ const TopMenu = memo(({
|
||||
return (
|
||||
<div
|
||||
className="no-user-select"
|
||||
style={{ background: controlsBackground, display: 'flex', alignItems: 'center', padding: '3px 5px', justifyContent: 'space-between', flexWrap: 'wrap' }}
|
||||
style={{ background: controlsBackground, transition: darkModeTransition, display: 'flex', alignItems: 'center', padding: '3px 5px', justifyContent: 'space-between', flexWrap: 'wrap' }}
|
||||
>
|
||||
{filePath && (
|
||||
<>
|
||||
|
@ -1,6 +1,8 @@
|
||||
export const saveColor = 'hsl(158, 100%, 43%)';
|
||||
export const primaryColor = 'hsl(194, 78%, 47%)';
|
||||
export const primaryTextColor = 'hsla(194, 100%, 66%, 1)';
|
||||
export const saveColor = 'var(--green11)';
|
||||
export const primaryColor = 'var(--cyan9)';
|
||||
export const primaryTextColor = 'var(--cyan11)';
|
||||
// todo darkMode:
|
||||
export const waveformColor = '#ffffff'; // Must be hex because used by ffmpeg
|
||||
export const controlsBackground = '#6b6b6b';
|
||||
export const timelineBackground = '#444';
|
||||
export const controlsBackground = 'var(--gray4)';
|
||||
export const timelineBackground = 'var(--gray2)';
|
||||
export const darkModeTransition = 'background .5s';
|
||||
|
@ -16,12 +16,12 @@ const BatchFile = memo(({ path, isOpen, isSelected, name, onSelect, onDelete })
|
||||
useContextMenu(ref, contextMenuTemplate);
|
||||
|
||||
return (
|
||||
<div ref={ref} role="button" style={{ background: isSelected ? 'rgba(255,255,255,0.15)' : undefined, fontSize: 13, padding: '3px 6px', display: 'flex', alignItems: 'center', alignContent: 'flex-start' }} title={path} onClick={() => onSelect(path)}>
|
||||
<div ref={ref} role="button" style={{ background: isSelected ? 'var(--gray6)' : undefined, fontSize: 13, padding: '3px 6px', display: 'flex', alignItems: 'center', alignContent: 'flex-start' }} title={path} onClick={() => onSelect(path)}>
|
||||
<FaFile size={14} style={{ color: primaryTextColor, flexShrink: 0 }} />
|
||||
<div style={{ flexBasis: 4, flexShrink: 0 }} />
|
||||
<div style={{ whiteSpace: 'nowrap', cursor: 'pointer', overflow: 'hidden' }}>{name}</div>
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
{isOpen && <FaAngleRight size={14} style={{ color: 'white', marginRight: -5, flexShrink: 0 }} />}
|
||||
{isOpen && <FaAngleRight size={14} style={{ color: 'var(--gray9)', marginRight: -5, flexShrink: 0 }} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
@ -7,13 +7,13 @@ import { ReactSortable } from 'react-sortablejs';
|
||||
import { SortAlphabeticalIcon, SortAlphabeticalDescIcon } from 'evergreen-ui';
|
||||
|
||||
import BatchFile from './BatchFile';
|
||||
import { timelineBackground, controlsBackground } from '../colors';
|
||||
import { controlsBackground, darkModeTransition } from '../colors';
|
||||
import { mySpring } from '../animations';
|
||||
|
||||
|
||||
const iconStyle = {
|
||||
flexShrink: 0,
|
||||
color: 'white',
|
||||
color: 'var(--gray12)',
|
||||
cursor: 'pointer',
|
||||
paddingTop: 3,
|
||||
paddingBottom: 3,
|
||||
@ -46,19 +46,19 @@ const BatchFilesList = memo(({ selectedBatchFiles, filePath, width, batchFiles,
|
||||
return (
|
||||
<motion.div
|
||||
className="no-user-select"
|
||||
style={{ width, background: timelineBackground, color: 'rgba(255,255,255,0.7)', display: 'flex', flexDirection: 'column', overflowY: 'hidden', overflowX: 'hidden', resize: 'horizontal' }}
|
||||
style={{ width, background: controlsBackground, color: 'var(--gray12)', transition: darkModeTransition, display: 'flex', flexDirection: 'column', overflowY: 'hidden', overflowX: 'hidden', resize: 'horizontal' }}
|
||||
initial={{ x: -width }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: -width }}
|
||||
transition={mySpring}
|
||||
>
|
||||
<div style={{ background: controlsBackground, fontSize: 14, paddingBottom: 3, paddingTop: 0, paddingLeft: 10, display: 'flex', alignItems: 'center', justifyContent: 'flex-end', flexWrap: 'wrap' }}>
|
||||
{t('Batch file list')}
|
||||
<div style={{ fontSize: 14, paddingBottom: 3, paddingTop: 0, paddingLeft: 10, display: 'flex', alignItems: 'center', justifyContent: 'flex-end', flexWrap: 'wrap' }}>
|
||||
<div>{t('Batch file list')}</div>
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
<FaHatWizard size={17} role="button" title={`${t('Convert to supported format')}...`} style={iconStyle} onClick={onBatchConvertToSupportedFormatClick} />
|
||||
<AiOutlineMergeCells size={20} role="button" title={`${t('Merge/concatenate files')}...`} style={iconStyle} onClick={onMergeFilesClick} />
|
||||
<SortIcon size={25} role="button" title={t('Sort items')} style={iconStyle} onClick={onSortClick} />
|
||||
<FaTimes size={20} role="button" title={t('Close batch')} style={iconStyle} onClick={closeBatch} />
|
||||
<FaTimes size={20} role="button" title={t('Close batch')} style={{ ...iconStyle, color: 'var(--gray11)' }} onClick={closeBatch} />
|
||||
</div>
|
||||
|
||||
<div style={{ overflowX: 'hidden', overflowY: 'auto' }}>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { memo, useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TextInput, IconButton, Alert, Checkbox, Dialog, Button, Paragraph } from 'evergreen-ui';
|
||||
import { TextInput, IconButton, Alert, Checkbox, Dialog, Button, Paragraph, CogIcon } from 'evergreen-ui';
|
||||
import { AiOutlineMergeCells } from 'react-icons/ai';
|
||||
import { FaQuestionCircle, FaCheckCircle, FaExclamationTriangle } from 'react-icons/fa';
|
||||
import { FaQuestionCircle, FaExclamationTriangle } from 'react-icons/fa';
|
||||
import i18n from 'i18next';
|
||||
import withReactContent from 'sweetalert2-react-content';
|
||||
|
||||
@ -170,9 +170,9 @@ const ConcatDialog = memo(({
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'flex-end' }}>
|
||||
<Checkbox checked={enableReadFileMeta} onChange={(e) => setEnableReadFileMeta(e.target.checked)} label={t('Check compatibility')} marginLeft={10} marginRight={10} />
|
||||
<Button iconBefore={FaCheckCircle} onClick={() => setSettingsVisible(true)}>{t('Options')}</Button>
|
||||
<Button iconBefore={CogIcon} onClick={() => setSettingsVisible(true)}>{t('Options')}</Button>
|
||||
{fileFormat && detectedFileFormat ? (
|
||||
<OutputFormatSelect style={{ maxWidth: 180 }} detectedFileFormat={detectedFileFormat} fileFormat={fileFormat} onOutputFormatUserChange={onOutputFormatUserChange} />
|
||||
<OutputFormatSelect style={{ height: 30, maxWidth: 180 }} detectedFileFormat={detectedFileFormat} fileFormat={fileFormat} onOutputFormatUserChange={onOutputFormatUserChange} />
|
||||
) : (
|
||||
<Button disabled isLoading>{t('Loading')}</Button>
|
||||
)}
|
||||
|
@ -25,7 +25,7 @@ const ExportButton = memo(({ segmentsToExport, areWeCutting, onClick, size = 1 }
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ cursor: 'pointer', background: primaryColor, borderRadius: size * 5, paddingTop: size * 1, paddingBottom: size * 2.5, paddingLeft: size * 7, paddingRight: size * 7, fontSize: size * 13, whiteSpace: 'nowrap', opacity: segmentsToExport.length === 0 ? 0.5 : undefined }}
|
||||
style={{ cursor: 'pointer', background: primaryColor, color: 'white', borderRadius: size * 5, paddingTop: size * 1, paddingBottom: size * 2.5, paddingLeft: size * 7, paddingRight: size * 7, fontSize: size * 13, whiteSpace: 'nowrap', opacity: segmentsToExport.length === 0 ? 0.5 : undefined }}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
role="button"
|
||||
|
@ -1,45 +1,36 @@
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { WarningSignIcon, Button, Select, CrossIcon } from 'evergreen-ui';
|
||||
import { WarningSignIcon, Button, CrossIcon } from 'evergreen-ui';
|
||||
import { FaRegCheckCircle } from 'react-icons/fa';
|
||||
import i18n from 'i18next';
|
||||
import { useTranslation, Trans } from 'react-i18next';
|
||||
import { IoIosHelpCircle } from 'react-icons/io';
|
||||
|
||||
import KeyframeCutButton from './components/KeyframeCutButton';
|
||||
import ExportButton from './components/ExportButton';
|
||||
import ExportModeButton from './components/ExportModeButton';
|
||||
import PreserveMovDataButton from './components/PreserveMovDataButton';
|
||||
import MovFastStartButton from './components/MovFastStartButton';
|
||||
import ToggleExportConfirm from './components/ToggleExportConfirm';
|
||||
import OutSegTemplateEditor from './components/OutSegTemplateEditor';
|
||||
import HighlightedText from './components/HighlightedText';
|
||||
import KeyframeCutButton from './KeyframeCutButton';
|
||||
import ExportButton from './ExportButton';
|
||||
import ExportModeButton from './ExportModeButton';
|
||||
import PreserveMovDataButton from './PreserveMovDataButton';
|
||||
import MovFastStartButton from './MovFastStartButton';
|
||||
import ToggleExportConfirm from './ToggleExportConfirm';
|
||||
import OutSegTemplateEditor from './OutSegTemplateEditor';
|
||||
import HighlightedText, { highlightedTextStyle } from './HighlightedText';
|
||||
import Select from './Select';
|
||||
|
||||
import { withBlur } from './util';
|
||||
import { toast } from './swal';
|
||||
import { isMov as ffmpegIsMov } from './util/streams';
|
||||
import useUserSettings from './hooks/useUserSettings';
|
||||
import { primaryTextColor } from '../colors';
|
||||
import { withBlur } from '../util';
|
||||
import { toast } from '../swal';
|
||||
import { isMov as ffmpegIsMov } from '../util/streams';
|
||||
import useUserSettings from '../hooks/useUserSettings';
|
||||
import styles from './ExportConfirm.module.css';
|
||||
|
||||
const sheetStyle = {
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
zIndex: 10,
|
||||
background: 'rgba(105, 105, 105, 0.7)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
overflowY: 'scroll',
|
||||
display: 'flex',
|
||||
};
|
||||
|
||||
const boxStyle = { margin: '15px 15px 50px 15px', background: 'rgba(25, 25, 25, 0.6)', borderRadius: 10, padding: '10px 20px', minHeight: 500, position: 'relative' };
|
||||
const boxStyle = { margin: '15px 15px 50px 15px', borderRadius: 10, padding: '10px 20px', minHeight: 500, position: 'relative' };
|
||||
|
||||
const outDirStyle = { background: 'rgb(193, 98, 0)', borderRadius: '.4em', padding: '0 .3em', wordBreak: 'break-all', cursor: 'pointer' };
|
||||
const outDirStyle = { ...highlightedTextStyle, wordBreak: 'break-all', cursor: 'pointer' };
|
||||
|
||||
const warningStyle = { color: '#faa', fontSize: '80%' };
|
||||
const warningStyle = { color: 'var(--red11)', fontSize: '80%' };
|
||||
|
||||
const HelpIcon = ({ onClick, style }) => <IoIosHelpCircle size={20} role="button" onClick={withBlur(onClick)} style={{ cursor: 'pointer', verticalAlign: 'middle', marginLeft: 5, ...style }} />;
|
||||
const HelpIcon = ({ onClick, style }) => <IoIosHelpCircle size={20} role="button" onClick={withBlur(onClick)} style={{ cursor: 'pointer', color: primaryTextColor, verticalAlign: 'middle', marginLeft: 5, ...style }} />;
|
||||
|
||||
const ExportConfirm = memo(({
|
||||
areWeCutting, selectedSegments, segmentsToExport, willMerge, visible, onClosePress, onExportConfirm,
|
||||
@ -128,11 +119,11 @@ const ExportConfirm = memo(({
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
style={sheetStyle}
|
||||
className={styles.sheet}
|
||||
transition={{ duration: 0.3, easings: ['easeOut'] }}
|
||||
>
|
||||
<div style={{ margin: 'auto' }}>
|
||||
<div style={boxStyle}>
|
||||
<div style={boxStyle} className={styles.box}>
|
||||
<CrossIcon size={24} style={{ position: 'absolute', right: 0, top: 0, padding: 15, boxSizing: 'content-box', cursor: 'pointer' }} role="button" onClick={onClosePress} />
|
||||
|
||||
<h2 style={{ marginTop: 0, marginBottom: '.5em' }}>{t('Export options')}</h2>
|
||||
@ -213,7 +204,7 @@ const ExportConfirm = memo(({
|
||||
{!needSmartCut && (
|
||||
<li>
|
||||
"avoid_negative_ts"
|
||||
<Select height={20} value={avoidNegativeTs} onChange={(e) => setAvoidNegativeTs(e.target.value)} style={{ marginLeft: 5 }}>
|
||||
<Select value={avoidNegativeTs} onChange={(e) => setAvoidNegativeTs(e.target.value)} style={{ height: 20, marginLeft: 5 }}>
|
||||
<option value="auto">auto</option>
|
||||
<option value="make_zero">make_zero</option>
|
||||
<option value="make_non_negative">make_non_negative</option>
|
||||
@ -237,7 +228,7 @@ const ExportConfirm = memo(({
|
||||
style={{ display: 'flex', alignItems: 'flex-end' }}
|
||||
>
|
||||
<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>
|
||||
<div style={{ fontSize: 13, marginLeft: 3, marginRight: 7, maxWidth: 120, lineHeight: '100%', color: exportConfirmEnabled ? 'var(--gray12)' : 'var(--gray11)', cursor: 'pointer' }} role="button" onClick={toggleExportConfirmEnabled}>{t('Show this page before exporting?')}</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
26
src/components/ExportConfirm.module.css
Normal file
26
src/components/ExportConfirm.module.css
Normal file
@ -0,0 +1,26 @@
|
||||
.sheet {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
background: var(--whiteA10);
|
||||
color: var(--gray12);
|
||||
backdrop-filter: blur(30px);
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:global(.dark-theme) .sheet {
|
||||
background: var(--blackA11);
|
||||
}
|
||||
|
||||
.box {
|
||||
background: var(--whiteA8);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .box {
|
||||
background: var(--blackA8);
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { Select } from 'evergreen-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { withBlur } from '../util';
|
||||
import useUserSettings from '../hooks/useUserSettings';
|
||||
import Select from './Select';
|
||||
|
||||
const ExportModeButton = memo(({ selectedSegments, style }) => {
|
||||
const { t } = useTranslation();
|
||||
@ -50,8 +50,7 @@ const ExportModeButton = memo(({ selectedSegments, style }) => {
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<Select
|
||||
height={20}
|
||||
style={style}
|
||||
style={{ height: 20, ...style }}
|
||||
value={effectiveExportMode}
|
||||
onChange={withBlur((e) => onChange(e.target.value))}
|
||||
>
|
||||
|
@ -1,6 +1,10 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import { primaryTextColor } from '../colors';
|
||||
|
||||
export const highlightedTextStyle = { textDecoration: 'underline', textUnderlineOffset: '.2em', textDecorationColor: primaryTextColor, color: 'var(--gray12)', borderRadius: '.4em', padding: '0 .3em' };
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
const HighlightedText = memo(({ children, style, ...props }) => <span {...props} style={{ background: 'rgb(193, 98, 0)', borderRadius: '.4em', padding: '0 .3em', ...style }}>{children}</span>);
|
||||
const HighlightedText = memo(({ children, style, ...props }) => <span {...props} style={{ ...highlightedTextStyle, ...style }}>{children}</span>);
|
||||
|
||||
export default HighlightedText;
|
||||
|
@ -15,7 +15,7 @@ const ReactSwal = withReactContent(Swal);
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
const extVar = '${EXT}';
|
||||
|
||||
const inputStyle = { flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em' };
|
||||
const inputStyle = { flexGrow: 1, fontFamily: 'inherit', fontSize: '.8em', backgroundColor: 'var(--gray3)', color: 'var(--gray12)', appearance: 'none', border: 'none' };
|
||||
|
||||
const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate, generateOutSegFileNames, currentSegIndexSafe, getOutSegError }) => {
|
||||
const { safeOutputFileName, toggleSafeOutputFileName } = useUserSettings();
|
||||
@ -86,8 +86,10 @@ const OutSegTemplateEditor = memo(({ helpIcon, outSegTemplate, setOutSegTemplate
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<span role="button" onClick={onShowClick} style={{ cursor: needToShow ? undefined : 'pointer' }}>
|
||||
{outSegFileNames != null && t('Output name(s):', { count: outSegFileNames.length })} {outSegFileNames != null && <HighlightedText style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{outSegFileNames[currentSegIndexSafe] || outSegFileNames[0]}</HighlightedText>}
|
||||
<span>
|
||||
{outSegFileNames != null && t('Output name(s):', { count: outSegFileNames.length })}
|
||||
{' '}
|
||||
{outSegFileNames != null && <HighlightedText role="button" onClick={onShowClick} style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', cursor: needToShow ? undefined : 'pointer' }}>{outSegFileNames[currentSegIndexSafe] || outSegFileNames[0] || '-'}</HighlightedText>}
|
||||
</span>
|
||||
{helpIcon}
|
||||
</div>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import i18n from 'i18next';
|
||||
import fromPairs from 'lodash/fromPairs';
|
||||
import { Select } from 'evergreen-ui';
|
||||
|
||||
import allOutFormats from '../outFormats';
|
||||
import { withBlur } from '../util';
|
||||
import Select from './Select';
|
||||
|
||||
const commonFormats = ['mov', 'mp4', 'matroska', 'webm', 'mp3', 'ipod'];
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getSegColor } from '../util/colors';
|
||||
import useUserSettings from '../hooks/useUserSettings';
|
||||
|
||||
const SegmentCutpointButton = ({ currentCutSeg, side, Icon, onClick, title, style }) => {
|
||||
const segColor = getSegColor(currentCutSeg);
|
||||
const { darkMode } = useUserSettings();
|
||||
const segColor = getSegColor(currentCutSeg, darkMode);
|
||||
|
||||
const start = side === 'start';
|
||||
const border = `4px solid ${segColor.lighten(0.1).string()}`;
|
||||
|
10
src/components/Select.jsx
Normal file
10
src/components/Select.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import styles from './Select.module.css';
|
||||
|
||||
const Select = memo((props) => (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<select className={styles.select} {...props} />
|
||||
));
|
||||
|
||||
export default Select;
|
18
src/components/Select.module.css
Normal file
18
src/components/Select.module.css
Normal file
@ -0,0 +1,18 @@
|
||||
.select {
|
||||
appearance: none;
|
||||
font: inherit;
|
||||
line-height: 120%;
|
||||
font-size: .8em;
|
||||
background-color: var(--gray3);
|
||||
color: var(--gray12);
|
||||
border-radius: .3em;
|
||||
padding: 0 .3em;
|
||||
outline: .05em solid var(--gray8);
|
||||
border: .05em solid var(--gray7);
|
||||
|
||||
background-image: url("data:image/svg+xml;utf8,<svg fill='white' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>");
|
||||
background-repeat: no-repeat;
|
||||
background-position-x: 100%;
|
||||
background-position-y: 0;
|
||||
background-size: auto 100%;
|
||||
}
|
387
src/components/Settings.jsx
Normal file
387
src/components/Settings.jsx
Normal file
@ -0,0 +1,387 @@
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { FaYinYang, FaKeyboard } from 'react-icons/fa';
|
||||
import { GlobeIcon, CleanIcon, CogIcon, Button, NumericalIcon, KeyIcon, FolderCloseIcon, DocumentIcon, TimeIcon } from 'evergreen-ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import CaptureFormatButton from './CaptureFormatButton';
|
||||
import AutoExportToggler from './AutoExportToggler';
|
||||
import Switch from './Switch';
|
||||
import useUserSettings from '../hooks/useUserSettings';
|
||||
import { askForFfPath } from '../dialogs';
|
||||
import { isMasBuild, isStoreBuild } from '../util';
|
||||
import { langNames } from '../util/constants';
|
||||
import styles from './Settings.module.css';
|
||||
import Select from './Select';
|
||||
|
||||
import { getModifierKeyNames } from '../hooks/useTimelineScroll';
|
||||
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
const Row = (props) => <tr {...props} />;
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
const KeyCell = (props) => <td {...props} />;
|
||||
|
||||
const Header = ({ title }) => (
|
||||
<Row className={styles.header}>
|
||||
<th>{title}</th>
|
||||
<th />
|
||||
</Row>
|
||||
);
|
||||
|
||||
const detailsStyle = { opacity: 0.7, fontSize: '.9em', marginTop: '.3em' };
|
||||
|
||||
const Settings = memo(({
|
||||
onTunerRequested,
|
||||
onKeyboardShortcutsDialogRequested,
|
||||
askForCleanupChoices,
|
||||
toggleStoreProjectInWorkingDir,
|
||||
}) => {
|
||||
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, customFfPath, setCustomFfPath, storeProjectInWorkingDir, enableOverwriteOutput, setEnableOverwriteOutput, mouseWheelZoomModifierKey, setMouseWheelZoomModifierKey, captureFrameMethod, setCaptureFrameMethod, captureFrameQuality, setCaptureFrameQuality, captureFrameFileNameFormat, setCaptureFrameFileNameFormat, enableNativeHevc, setEnableNativeHevc, enableUpdateCheck, setEnableUpdateCheck, allowMultipleInstances, setAllowMultipleInstances } = useUserSettings();
|
||||
|
||||
const onLangChange = useCallback((e) => {
|
||||
const { value } = e.target;
|
||||
const l = value !== '' ? value : undefined;
|
||||
setLanguage(l);
|
||||
}, [setLanguage]);
|
||||
|
||||
const timecodeFormatOptions = useMemo(() => ({
|
||||
frameCount: t('Frame counts'),
|
||||
timecodeWithDecimalFraction: t('Millisecond fractions'),
|
||||
timecodeWithFramesFraction: t('Frame fractions'),
|
||||
}), [t]);
|
||||
|
||||
const onTimecodeFormatClick = useCallback(() => {
|
||||
const keys = Object.keys(timecodeFormatOptions);
|
||||
let index = keys.indexOf(timecodeFormat);
|
||||
if (index === -1 || index >= keys.length - 1) index = 0;
|
||||
else index += 1;
|
||||
setTimecodeFormat(keys[index]);
|
||||
}, [setTimecodeFormat, timecodeFormat, timecodeFormatOptions]);
|
||||
|
||||
const changeCustomFfPath = useCallback(async () => {
|
||||
const newCustomFfPath = await askForFfPath(customFfPath);
|
||||
setCustomFfPath(newCustomFfPath);
|
||||
}, [customFfPath, setCustomFfPath]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ margin: '0 2em' }}>
|
||||
<div>{t('Hover mouse over buttons in the main interface to see which function they have')}</div>
|
||||
</div>
|
||||
|
||||
<table style={{ marginTop: 20 }} className={styles.settings}>
|
||||
<thead>
|
||||
<tr className={styles.header}>
|
||||
<th>{t('Settings')}</th>
|
||||
<th style={{ width: 300 }}>{t('Current setting')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Row>
|
||||
<KeyCell><GlobeIcon style={{ verticalAlign: 'middle', marginRight: '.5em' }} /> App language</KeyCell>
|
||||
<td>
|
||||
<Select value={language || ''} onChange={onLangChange} style={{ fontSize: '1.2em' }}>
|
||||
<option key="" value="">{t('System language')}</option>
|
||||
{Object.keys(langNames).map((lang) => <option key={lang} value={lang}>{langNames[lang]}</option>)}
|
||||
</Select>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Choose cutting mode: Remove or keep selected segments from video when exporting?')}<br />
|
||||
<div style={detailsStyle}>
|
||||
<b>{t('Keep')}</b>: {t('The video inside segments will be kept, while the video outside will be discarded.')}<br />
|
||||
<b>{t('Remove')}</b>: {t('The video inside segments will be discarded, while the video surrounding them will be kept.')}
|
||||
</div>
|
||||
</KeyCell>
|
||||
<td>
|
||||
<Button iconBefore={FaYinYang} appearance={invertCutSegments ? 'default' : 'primary'} intent="success" onClick={() => setInvertCutSegments((v) => !v)}>
|
||||
{invertCutSegments ? t('Remove') : t('Keep')}
|
||||
</Button>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Working directory')}<br />
|
||||
<div style={detailsStyle}>
|
||||
{t('This is where working files and exported files are stored.')}
|
||||
</div>
|
||||
</KeyCell>
|
||||
<td>
|
||||
<Button iconBefore={customOutDir ? FolderCloseIcon : DocumentIcon} onClick={changeOutDir}>
|
||||
{customOutDir ? t('Custom working directory') : t('Same directory as input file')}...
|
||||
</Button>
|
||||
<div>{customOutDir}</div>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Auto save project file?')}<br />
|
||||
</KeyCell>
|
||||
<td>
|
||||
<Switch checked={autoSaveProjectFile} onCheckedChange={setAutoSaveProjectFile} />
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Store project file (.llc) in the working directory or next to loaded media file?')}</KeyCell>
|
||||
<td>
|
||||
<Button iconBefore={storeProjectInWorkingDir ? FolderCloseIcon : DocumentIcon} disabled={!autoSaveProjectFile} onClick={toggleStoreProjectInWorkingDir}>
|
||||
{storeProjectInWorkingDir ? t('Store in working directory') : t('Store next to media file')}
|
||||
</Button>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Header title={t('Keyboard, mouse and input')} />
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Keyboard & mouse shortcuts')}</KeyCell>
|
||||
<td>
|
||||
<Button iconBefore={<FaKeyboard />} onClick={onKeyboardShortcutsDialogRequested}>{t('Keyboard & mouse shortcuts')}</Button>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Mouse wheel zoom modifier key')}</KeyCell>
|
||||
<td>
|
||||
<Select value={mouseWheelZoomModifierKey} onChange={(e) => setMouseWheelZoomModifierKey(e.target.value)}>
|
||||
{Object.entries(getModifierKeyNames()).map(([key, value]) => (
|
||||
<option key={key} value={key}>{value}</option>
|
||||
))}
|
||||
</Select>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Timeline trackpad/wheel sensitivity')}</KeyCell>
|
||||
<td>
|
||||
<Button iconBefore={CogIcon} onClick={() => onTunerRequested('wheelSensitivity')}>{t('Change value')}</Button>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Timeline keyboard seek speed')}</KeyCell>
|
||||
<td>
|
||||
<Button iconBefore={CogIcon} onClick={() => onTunerRequested('keyboardNormalSeekSpeed')}>{t('Change value')}</Button>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Timeline keyboard seek acceleration')}</KeyCell>
|
||||
<td>
|
||||
<Button iconBefore={CogIcon} onClick={() => onTunerRequested('keyboardSeekAccFactor')}>{t('Change value')}</Button>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Invert timeline trackpad/wheel direction?')}</KeyCell>
|
||||
<td>
|
||||
<Switch checked={invertTimelineScroll} onCheckedChange={setInvertTimelineScroll} />
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Header title={t('Options affecting exported files')} />
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Set file modification date/time of output files to:')}</KeyCell>
|
||||
<td>
|
||||
<Button iconBefore={enableTransferTimestamps ? DocumentIcon : TimeIcon} onClick={() => setEnableTransferTimestamps((v) => !v)}>
|
||||
{enableTransferTimestamps ? t('Source file\'s time') : t('Current time')}
|
||||
</Button>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Keyframe cut mode')}<br />
|
||||
<div style={detailsStyle}>
|
||||
<b>{t('Keyframe cut')}</b>: {t('Cut at the nearest keyframe (not accurate time.) Equiv to')} <i>ffmpeg -ss -i ...</i><br />
|
||||
<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 />
|
||||
</div>
|
||||
</KeyCell>
|
||||
<td>
|
||||
<Button iconBefore={keyframeCut ? KeyIcon : undefined} onClick={() => toggleKeyframeCut()}>
|
||||
{keyframeCut ? t('Keyframe cut') : t('Normal cut')}
|
||||
</Button>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Overwrite files when exporting, if a file with the same name as the output file name exists?')}</KeyCell>
|
||||
<td>
|
||||
<Switch checked={enableOverwriteOutput} onCheckedChange={setEnableOverwriteOutput} />
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Cleanup files after export?')}</KeyCell>
|
||||
<td>
|
||||
<Button iconBefore={<CleanIcon />} onClick={askForCleanupChoices}>{t('Change preferences')}</Button>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Header title={t('Snapshots and frame extraction')} />
|
||||
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Snapshot capture format')}
|
||||
</KeyCell>
|
||||
<td>
|
||||
<CaptureFormatButton showIcon />
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Snapshot capture method')}
|
||||
<div style={detailsStyle}>{t('FFmpeg capture method might sometimes capture more correct colors, but the captured snapshot might be off by one or more frames, relative to the preview.')}</div>
|
||||
</KeyCell>
|
||||
<td>
|
||||
<Button onClick={() => setCaptureFrameMethod((existing) => (existing === 'ffmpeg' ? 'videotag' : 'ffmpeg'))}>
|
||||
{captureFrameMethod === 'ffmpeg' ? t('FFmpeg') : t('HTML video tag')}
|
||||
</Button>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Snapshot capture quality')}</KeyCell>
|
||||
<td>
|
||||
<input type="range" min={1} max={1000} style={{ width: 200 }} value={Math.round(captureFrameQuality * 1000)} onChange={(e) => setCaptureFrameQuality(Math.max(Math.min(1, parseInt(e.target.value, 10) / 1000)), 0)} /><br />
|
||||
{Math.round(captureFrameQuality * 100)}%
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('File names of extracted video frames')}</KeyCell>
|
||||
<td>
|
||||
<Button iconBefore={captureFrameFileNameFormat === 'timestamp' ? TimeIcon : NumericalIcon} onClick={() => setCaptureFrameFileNameFormat((existing) => (existing === 'timestamp' ? 'index' : 'timestamp'))}>
|
||||
{captureFrameFileNameFormat === 'timestamp' ? t('Frame timestamp') : t('File number')}
|
||||
</Button>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('In timecode show')}</KeyCell>
|
||||
<td>
|
||||
<Button iconBefore={timecodeFormat === 'frameCount' ? NumericalIcon : TimeIcon} onClick={onTimecodeFormatClick}>
|
||||
{timecodeFormatOptions[timecodeFormat]}
|
||||
</Button>
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Header title={t('Prompts and dialogs')} />
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Show informational notifications')}</KeyCell>
|
||||
<td>
|
||||
<Switch checked={!hideNotifications} onCheckedChange={(v) => setHideNotifications(v ? 'all' : undefined)} />
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Ask about what to do when opening a new file when another file is already already open?')}</KeyCell>
|
||||
<td>
|
||||
<Switch checked={enableAskForFileOpenAction} onCheckedChange={setEnableAskForFileOpenAction} />
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Ask for confirmation when closing app or file?')}</KeyCell>
|
||||
<td>
|
||||
<Switch checked={askBeforeClose} onCheckedChange={setAskBeforeClose} />
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Ask about importing chapters from opened file?')}</KeyCell>
|
||||
<td>
|
||||
<Switch checked={enableAskForImportChapters} onCheckedChange={setEnableAskForImportChapters} />
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Header title={t('Advanced options')} />
|
||||
|
||||
{!isMasBuild && (
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Custom FFmpeg directory (experimental)')}<br />
|
||||
<div style={detailsStyle}>
|
||||
{t('This allows you to specify custom FFmpeg and FFprobe binaries to use. Make sure the "ffmpeg" and "ffprobe" executables exist in the same directory, and then select the directory.')}
|
||||
</div>
|
||||
</KeyCell>
|
||||
<td>
|
||||
<Button iconBefore={CogIcon} onClick={changeCustomFfPath}>
|
||||
{customFfPath ? t('Using external ffmpeg') : t('Using built-in ffmpeg')}
|
||||
</Button>
|
||||
<div>{customFfPath}</div>
|
||||
</td>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
{!isStoreBuild && (
|
||||
<Row>
|
||||
<KeyCell>{t('Check for updates on startup?')}</KeyCell>
|
||||
<td>
|
||||
<Switch checked={enableUpdateCheck} onCheckedChange={setEnableUpdateCheck} />
|
||||
</td>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Allow multiple instances of LosslessCut to run concurrently? (experimental)')}</KeyCell>
|
||||
<td>
|
||||
<Switch checked={allowMultipleInstances} onCheckedChange={setAllowMultipleInstances} />
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Enable HEVC / H265 hardware decoding (you may need to turn this off if you have problems with HEVC files)')}</KeyCell>
|
||||
<td>
|
||||
<Switch checked={enableNativeHevc} onCheckedChange={setEnableNativeHevc} />
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Enable experimental ffmpeg features flag?')}</KeyCell>
|
||||
<td>
|
||||
<Switch checked={ffmpegExperimental} onCheckedChange={setFfmpegExperimental} />
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Auto load timecode from file as an offset in the timeline?')}</KeyCell>
|
||||
<td>
|
||||
<Switch checked={autoLoadTimecode} onCheckedChange={setAutoLoadTimecode} />
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>{t('Try to automatically convert to supported format when opening unsupported file?')}</KeyCell>
|
||||
<td>
|
||||
<Switch checked={enableAutoHtml5ify} onCheckedChange={setEnableAutoHtml5ify} />
|
||||
</td>
|
||||
</Row>
|
||||
|
||||
<Row>
|
||||
<KeyCell>
|
||||
{t('Extract unprocessable tracks to separate files or discard them?')}<br />
|
||||
<div style={detailsStyle}>
|
||||
{t('(data tracks such as GoPro GPS, telemetry etc. are not copied over by default because ffmpeg cannot cut them, thus they will cause the media duration to stay the same after cutting video/audio)')}
|
||||
</div>
|
||||
</KeyCell>
|
||||
<td>
|
||||
<AutoExportToggler />
|
||||
</td>
|
||||
</Row>
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Settings;
|
22
src/components/Settings.module.css
Normal file
22
src/components/Settings.module.css
Normal file
@ -0,0 +1,22 @@
|
||||
.settings td:first-child, .settings th:first-child {
|
||||
padding: 1em 2em 1em 2em;
|
||||
}
|
||||
.settings td:nth-child(2), .settings th:nth-child(2) {
|
||||
padding: 1em 2em 1em 0em;
|
||||
}
|
||||
|
||||
.settings th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.settings tr.header {
|
||||
background-color: var(--blackA3);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .settings tr.header {
|
||||
background-color: var(--whiteA6);
|
||||
}
|
||||
|
||||
.settings {
|
||||
border-collapse: collapse;
|
||||
}
|
@ -2,16 +2,7 @@ import React, { memo } from 'react';
|
||||
import { IoIosCloseCircleOutline } from 'react-icons/io';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
|
||||
const sheetStyle = {
|
||||
padding: '1em 2em',
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
zIndex: 10,
|
||||
overflowY: 'scroll',
|
||||
};
|
||||
import styles from './Sheet.module.css';
|
||||
|
||||
const Sheet = memo(({ visible, onClosePress, style, children }) => (
|
||||
<AnimatePresence>
|
||||
@ -20,11 +11,14 @@ const Sheet = memo(({ visible, onClosePress, style, children }) => (
|
||||
initial={{ scale: 0, opacity: 0.5 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
exit={{ scale: 0, opacity: 0 }}
|
||||
style={{ ...sheetStyle, ...style }}
|
||||
style={style}
|
||||
className={styles.sheet}
|
||||
>
|
||||
<IoIosCloseCircleOutline role="button" onClick={onClosePress} size={30} style={{ position: 'fixed', right: 0, top: 0, padding: 20 }} />
|
||||
|
||||
<div style={{ overflowY: 'scroll', height: '100%' }}>
|
||||
{children}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
16
src/components/Sheet.module.css
Normal file
16
src/components/Sheet.module.css
Normal file
@ -0,0 +1,16 @@
|
||||
.sheet {
|
||||
padding: 1em 2em;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
background: var(--whiteA10);
|
||||
color: var(--gray12);
|
||||
backdrop-filter: blur(30px);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .sheet {
|
||||
background: var(--blackA11);
|
||||
}
|
@ -14,7 +14,7 @@ const SimpleModeButton = memo(({ size = 20, style }) => {
|
||||
<FaBaby
|
||||
title={t('Toggle advanced view')}
|
||||
size={size}
|
||||
style={{ color: simpleMode ? primaryTextColor : 'white', ...style }}
|
||||
style={{ color: simpleMode ? primaryTextColor : 'var(--gray12)', ...style }}
|
||||
onClick={toggleSimpleMode}
|
||||
/>
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { memo, useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { MdSubtitles } from 'react-icons/md';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Select } from 'evergreen-ui';
|
||||
import Select from './Select';
|
||||
|
||||
const SubtitleControl = memo(({ subtitleStreams, activeSubtitleStreamIndex, onActiveSubtitleChange }) => {
|
||||
const [controlVisible, setControlVisible] = useState(false);
|
||||
@ -32,7 +32,6 @@ const SubtitleControl = memo(({ subtitleStreams, activeSubtitleStreamIndex, onAc
|
||||
<>
|
||||
{controlVisible && (
|
||||
<Select
|
||||
height={20}
|
||||
value={activeSubtitleStreamIndex}
|
||||
onChange={onChange}
|
||||
>
|
||||
@ -46,7 +45,7 @@ const SubtitleControl = memo(({ subtitleStreams, activeSubtitleStreamIndex, onAc
|
||||
<MdSubtitles
|
||||
size={30}
|
||||
role="button"
|
||||
style={{ margin: '0 7px' }}
|
||||
style={{ margin: '0 7px', color: 'var(--gray12)', opacity: 0.7 }}
|
||||
onClick={onIconClick}
|
||||
/>
|
||||
</>
|
||||
|
12
src/components/Switch.jsx
Normal file
12
src/components/Switch.jsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import * as RadixSwitch from '@radix-ui/react-switch';
|
||||
|
||||
import classes from './Switch.module.css';
|
||||
|
||||
const Switch = ({ checked, disabled, onCheckedChange }) => (
|
||||
<RadixSwitch.Root disabled={disabled} className={classes.SwitchRoot} checked={checked} onCheckedChange={onCheckedChange}>
|
||||
<RadixSwitch.Thumb className={classes.SwitchThumb} />
|
||||
</RadixSwitch.Root>
|
||||
);
|
||||
|
||||
export default Switch;
|
37
src/components/Switch.module.css
Normal file
37
src/components/Switch.module.css
Normal file
@ -0,0 +1,37 @@
|
||||
.SwitchRoot {
|
||||
all: unset;
|
||||
width: 42px;
|
||||
height: 25px;
|
||||
background-color: var(--gray9);
|
||||
border-radius: 9999px;
|
||||
position: relative;
|
||||
box-shadow: 0 0 0 2px var(--blackA5);
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
.SwitchRoot:focus {
|
||||
box-shadow: 0 0 0 2px var(--gray11);
|
||||
}
|
||||
.SwitchRoot[data-state='checked'] {
|
||||
background-color: var(--cyan9);
|
||||
}
|
||||
.SwitchRoot[data-state='checked']:focus {
|
||||
box-shadow: 0 0 0 2px var(--cyan11);
|
||||
}
|
||||
|
||||
.SwitchThumb {
|
||||
display: block;
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
background-color: white;
|
||||
border-radius: 9999px;
|
||||
box-shadow: 0 2px 2px rgba(0,0,0,0.2);
|
||||
transition: transform 100ms;
|
||||
transform: translateX(2px);
|
||||
will-change: transform;
|
||||
}
|
||||
.SwitchThumb[data-state='checked'] {
|
||||
transform: translateX(19px);
|
||||
}
|
||||
.SwitchRoot:disabled {
|
||||
opacity: .5;
|
||||
}
|
@ -12,7 +12,7 @@ const ToggleExportConfirm = memo(({ size = 23, style }) => {
|
||||
const { exportConfirmEnabled, toggleExportConfirmEnabled } = useUserSettings();
|
||||
|
||||
return (
|
||||
<MdEventNote style={{ color: exportConfirmEnabled ? primaryTextColor : 'rgba(255,255,255,0.7)', ...style }} size={size} title={t('Show export options screen before exporting?')} role="button" onClick={toggleExportConfirmEnabled} />
|
||||
<MdEventNote style={{ color: exportConfirmEnabled ? primaryTextColor : 'var(--gray11)', ...style }} size={size} title={t('Show export options screen before exporting?')} role="button" onClick={toggleExportConfirmEnabled} />
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -11,9 +11,9 @@ const ValueTuner = memo(({ style, title, value, setValue, onFinished, resolution
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ background: 'white', color: 'black', position: 'absolute', bottom: 0, zIndex: 10, padding: 10, margin: 10, borderRadius: 10, ...style }}>
|
||||
<div style={{ background: 'var(--gray1)', color: 'var(--gray12)', position: 'absolute', bottom: 0, zIndex: 10, padding: 10, margin: 10, borderRadius: 10, ...style }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', flexBasis: 400 }}>
|
||||
<div>{title}</div>
|
||||
<div style={{ marginBottom: '.5em' }}>{title}</div>
|
||||
<div style={{ marginLeft: 10, fontWeight: 'bold' }}>{value.toFixed(2)}</div>
|
||||
<div style={{ flexGrow: 1, flexBasis: 10 }} />
|
||||
<Button height={20} onClick={resetToDefault}>{t('Default')}</Button>
|
||||
|
@ -47,7 +47,7 @@ const VolumeControl = memo(({ playbackVolume, setPlaybackVolume, usingDummyVideo
|
||||
title={t('Mute preview? (will not affect output)')}
|
||||
size={30}
|
||||
role="button"
|
||||
style={{ margin: '0 7px' }}
|
||||
style={{ margin: '0 7px', color: 'var(--gray12)', opacity: 0.7 }}
|
||||
onClick={onVolumeIconClick}
|
||||
/>
|
||||
</>
|
||||
|
@ -133,6 +133,8 @@ export default () => {
|
||||
useEffect(() => safeSetConfig({ cleanupChoices }), [cleanupChoices]);
|
||||
const [allowMultipleInstances, setAllowMultipleInstances] = useState(safeGetConfigInitial('allowMultipleInstances'));
|
||||
useEffect(() => safeSetConfig({ allowMultipleInstances }), [allowMultipleInstances]);
|
||||
const [darkMode, setDarkMode] = useState(safeGetConfigInitial('darkMode'));
|
||||
useEffect(() => safeSetConfig({ darkMode }), [darkMode]);
|
||||
|
||||
|
||||
const resetKeyBindings = useCallback(() => {
|
||||
@ -244,5 +246,7 @@ export default () => {
|
||||
setCleanupChoices,
|
||||
allowMultipleInstances,
|
||||
setAllowMultipleInstances,
|
||||
darkMode,
|
||||
setDarkMode,
|
||||
};
|
||||
};
|
||||
|
22
src/main.css
22
src/main.css
@ -1,3 +1,15 @@
|
||||
@import '@radix-ui/colors/red.css';
|
||||
@import '@radix-ui/colors/redDark.css';
|
||||
@import '@radix-ui/colors/green.css';
|
||||
@import '@radix-ui/colors/greenDark.css';
|
||||
@import '@radix-ui/colors/cyan.css';
|
||||
@import '@radix-ui/colors/cyanDark.css';
|
||||
@import '@radix-ui/colors/gray.css';
|
||||
@import '@radix-ui/colors/grayDark.css';
|
||||
@import '@radix-ui/colors/blackA.css';
|
||||
@import '@radix-ui/colors/whiteA.css';
|
||||
|
||||
|
||||
html {
|
||||
font-family: 'Open Sans', 'Noto Sans SemiCondensed', 'Noto Sans', sans-serif;
|
||||
font-size: 16px;
|
||||
@ -24,13 +36,13 @@ kbd {
|
||||
padding: 3px 5px;
|
||||
font-size: 11px;
|
||||
line-height: 10px;
|
||||
color: #555;
|
||||
color: var(--gray11);
|
||||
vertical-align: middle;
|
||||
background-color: #fdfdfd;
|
||||
border: solid 1px #ccc;
|
||||
border-bottom-color: #bbb;
|
||||
background-color: var(--gray4);
|
||||
border: solid 1px var(--gray8);
|
||||
border-bottom-color: var(--gray8);
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 -1px 0 #bbb;
|
||||
box-shadow: inset 0 -1px 0 var(--gray8);
|
||||
}
|
||||
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
|
41
src/theme.js
41
src/theme.js
@ -1,16 +1,47 @@
|
||||
import { defaultTheme } from 'evergreen-ui';
|
||||
|
||||
|
||||
function colorKeyForIntent(intent) {
|
||||
if (intent === 'danger') return 'var(--red12)';
|
||||
if (intent === 'success') return 'var(--green12)';
|
||||
return 'var(--gray12)';
|
||||
}
|
||||
|
||||
function borderColorForIntent(intent, isHover) {
|
||||
if (intent === 'danger') return isHover ? 'var(--red8)' : 'var(--red7)';
|
||||
if (intent === 'success') return isHover ? 'var(--green8)' : 'var(--green7)';
|
||||
return 'var(--gray7)';
|
||||
}
|
||||
|
||||
export default {
|
||||
...defaultTheme,
|
||||
components: {
|
||||
...defaultTheme.components,
|
||||
Select: {
|
||||
...defaultTheme.components.Select,
|
||||
Button: {
|
||||
...defaultTheme.components.Button,
|
||||
appearances: {
|
||||
...defaultTheme.components.Select.appearances,
|
||||
...defaultTheme.components.Button.appearances,
|
||||
default: {
|
||||
...defaultTheme.components.Select.appearances.default,
|
||||
backgroundColor: '#fff', // If not, selects will be invisible on dark background
|
||||
...defaultTheme.components.Button.appearances.default,
|
||||
backgroundColor: 'var(--gray3)',
|
||||
|
||||
// https://github.com/segmentio/evergreen/blob/master/src/themes/default/components/button.js
|
||||
border: (theme, props) => `1px solid ${borderColorForIntent(props.intent)}`,
|
||||
color: (theme, props) => props.color || colorKeyForIntent(props.intent),
|
||||
|
||||
_hover: {
|
||||
backgroundColor: 'var(--gray4)',
|
||||
},
|
||||
_active: {
|
||||
backgroundColor: 'var(--gray5)',
|
||||
},
|
||||
_focus: {
|
||||
backgroundColor: 'var(--gray5)',
|
||||
boxShadow: '0 0 0 1px var(--gray8)',
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -9,7 +9,7 @@ function getColor(n) {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function getSegColor(seg) {
|
||||
export function getSegColor(seg, darkMode) {
|
||||
if (!seg) {
|
||||
return color({
|
||||
h: 0,
|
||||
@ -19,5 +19,7 @@ export function getSegColor(seg) {
|
||||
}
|
||||
const { segColorIndex } = seg;
|
||||
|
||||
return getColor(segColorIndex);
|
||||
const theColor = getColor(segColorIndex);
|
||||
if (!darkMode) return theColor.darken(0.6);
|
||||
return theColor;
|
||||
}
|
||||
|
150
yarn.lock
150
yarn.lock
@ -292,6 +292,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.13.10":
|
||||
version: 7.21.0
|
||||
resolution: "@babel/runtime@npm:7.21.0"
|
||||
dependencies:
|
||||
regenerator-runtime: ^0.13.11
|
||||
checksum: 7b33e25bfa9e0e1b9e8828bb61b2d32bdd46b41b07ba7cb43319ad08efc6fda8eb89445193e67d6541814627df0ca59122c0ea795e412b99c5183a0540d338ab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.20.6":
|
||||
version: 7.20.13
|
||||
resolution: "@babel/runtime@npm:7.20.13"
|
||||
@ -1015,6 +1024,145 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/colors@npm:^0.1.8":
|
||||
version: 0.1.8
|
||||
resolution: "@radix-ui/colors@npm:0.1.8"
|
||||
checksum: 35fb8e48b1333e0a800ee55ce554d29016c10d2cb23aa161c6476c32b8231ec78f0ae57535ad482aa35adf0b64c957d92da88ef7f3a3f4a2f8c06870208c7ed1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/primitive@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/primitive@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
checksum: 72996afaf346ec4f4c73422f14f6cb2d0de994801ba7cbb9a4a67b0050e0cd74625182c349ef8017ccae1406579d4b74a34a225ef2efe61e8e5337decf235deb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-compose-refs@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-compose-refs@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: fb98be2e275a1a758ccac647780ff5b04be8dcf25dcea1592db3b691fecf719c4c0700126da605b2f512dd89caa111352b9fad59528d736b4e0e9a0e134a74a1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-context@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-context@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: 43c6b6f2183398161fe6b109e83fff240a6b7babbb27092b815932342a89d5ca42aa9806bfae5927970eed5ff90feed04c67aa29c6721f84ae826f17fcf34ce0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-primitive@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@radix-ui/react-primitive@npm:1.0.1"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/react-slot": 1.0.1
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: 1cc86b72f926be4a42122e7e456e965de0906f16b0dc244b8448bac05905f208598c984a0dd40026f654b4a71d0235335d48a18e377b07b0ec6c6917576a8080
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-slot@npm:1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@radix-ui/react-slot@npm:1.0.1"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/react-compose-refs": 1.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: a20693f8ce532bd6cbff12ba543dfcf90d451f22923bd60b57dc9e639f6e53348915e182002b33444feb6ab753434e78e2a54085bf7092aadda4418f0423763f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-switch@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@radix-ui/react-switch@npm:1.0.1"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/primitive": 1.0.0
|
||||
"@radix-ui/react-compose-refs": 1.0.0
|
||||
"@radix-ui/react-context": 1.0.0
|
||||
"@radix-ui/react-primitive": 1.0.1
|
||||
"@radix-ui/react-use-controllable-state": 1.0.0
|
||||
"@radix-ui/react-use-previous": 1.0.0
|
||||
"@radix-ui/react-use-size": 1.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: daaa44c90a05e4828211cd8db37a6036151b51a35783a3c8d138630d4d69a4b337ca9e3b719b13a0520f6067c384ee995b4ed9a357018ab2e888f0c3dcdacb75
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-use-callback-ref@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-use-callback-ref@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: a8dda76ba0a26e23dc6ab5003831ad7439f59ba9d696a517643b9ee6a7fb06b18ae7a8f5a3c00c530d5c8104745a466a077b7475b99b4c0f5c15f5fc29474471
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-use-controllable-state@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-use-controllable-state@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/react-use-callback-ref": 1.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: 35f1e714bbe3fc9f5362a133339dd890fb96edb79b63168a99403c65dd5f2b63910e0c690255838029086719e31360fa92544a55bc902cfed4442bb3b55822e2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-use-layout-effect@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-use-layout-effect@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: fcdc8cfa79bd45766ebe3de11039c58abe3fed968cb39c12b2efce5d88013c76fe096ea4cee464d42576d02fe7697779b682b4268459bca3c4e48644f5b4ac5e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-use-previous@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-use-previous@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: b45bbc8a7e7dbe29f4c11922e807666ec98979ec7a2696618ceb2c60ade44f695892216a90e1f5d7a457b458b3c95f0a4ce971d5143a6a80646b6b00fda16fb5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@radix-ui/react-use-size@npm:1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@radix-ui/react-use-size@npm:1.0.0"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.13.10
|
||||
"@radix-ui/react-use-layout-effect": 1.0.0
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17.0 || ^18.0
|
||||
checksum: b319564668512bb5c8c64530e3c12810c4b7c75c19a00d5ef758c246e8d85cd5015df19688e174db1cc44b0584c8d7f22411eb00af5f8ac6c2e789aa5c8e34f5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@segment/react-tiny-virtual-list@npm:^2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "@segment/react-tiny-virtual-list@npm:2.2.1"
|
||||
@ -6030,6 +6178,8 @@ __metadata:
|
||||
dependencies:
|
||||
"@electron/remote": ^2.0.9
|
||||
"@fontsource/open-sans": ^4.5.14
|
||||
"@radix-ui/colors": ^0.1.8
|
||||
"@radix-ui/react-switch": ^1.0.1
|
||||
"@types/sortablejs": ^1.15.0
|
||||
"@vitejs/plugin-react": ^3.1.0
|
||||
color: ^3.1.0
|
||||
|
Loading…
Reference in New Issue
Block a user