1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-25 03:33:14 +01:00

implement dark/ight mode

still not 100%, but
closes #733
This commit is contained in:
Mikael Finstad 2023-03-10 12:12:48 +08:00
parent 759c079747
commit 9b027bc762
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
44 changed files with 906 additions and 587 deletions

View File

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

View File

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

View File

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

View File

@ -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>
<Settings
onTunerRequested={onTunerRequested}
onKeyboardShortcutsDialogRequested={toggleKeyboardShortcuts}
askForCleanupChoices={askForCleanupChoices}
toggleStoreProjectInWorkingDir={toggleStoreProjectInWorkingDir}
/>
</Table.Body>
</Table>
<Sheet visible={settingsVisible} onClosePress={toggleSettings} style={{ padding: '1em 0' }}>
<Settings
onTunerRequested={onTunerRequested}
onKeyboardShortcutsDialogRequested={toggleKeyboardShortcuts}
askForCleanupChoices={askForCleanupChoices}
toggleStoreProjectInWorkingDir={toggleStoreProjectInWorkingDir}
/>
</Sheet>
<ConcatDialog isShown={batchFiles.length > 0 && concatDialogVisible} onHide={() => setConcatDialogVisible(false)} paths={batchFilePaths} onConcat={userConcatFiles} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} />

View File

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

View File

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

View File

@ -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 ? (

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 && (
<>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
&quot;avoid_negative_ts&quot;
<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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@ -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 }} />
{children}
<div style={{ overflowY: 'scroll', height: '100%' }}>
{children}
</div>
</motion.div>
)}
</AnimatePresence>

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View File

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