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

improvements

- implement full screen waveform #260
- make thumbnails not overlap timeline #483
- allow thumbnails at the same time as waveform #260
- fix waveform color in light mode
- fix waveform window overlaps
- make tracks screen dark mode too
- add more borders and dark mode fixes
This commit is contained in:
Mikael Finstad 2023-03-12 16:51:15 +08:00
parent 4b7fb42111
commit 574abcb19a
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
16 changed files with 310 additions and 140 deletions

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 { SideSheet, Position, ThemeProvider } from 'evergreen-ui';
import { 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';
@ -79,6 +79,7 @@ import { fallbackLng } from './i18n';
import { createSegment, getCleanCutSegments, findSegmentsAtCursor, sortSegments, getSegmentTags, convertSegmentsToChapters, hasAnySegmentOverlap, isDurationValid, playOnlyCurrentSegment } from './segments';
import { getOutSegError as getOutSegErrorRaw } from './util/outputNameTemplate';
import { rightBarWidth, leftBarWidth, ffmpegExtractWindow, zoomMax } from './util/constants';
import BigWaveform from './components/BigWaveform';
import isDev from './isDev';
@ -142,7 +143,8 @@ const App = memo(() => {
const { fileFormat, setFileFormat, detectedFileFormat, setDetectedFileFormat, isCustomFormatSelected } = useFileFormatState();
// State per application launch
const [timelineMode, setTimelineMode] = useState();
const [waveformMode, setWaveformMode] = useState();
const [thumbnailsEnabled, setThumbnailsEnabled] = useState(false);
const [keyframesEnabled, setKeyframesEnabled] = useState(true);
const [showRightBar, setShowRightBar] = useState(true);
const [rememberConvertToSupportedFormat, setRememberConvertToSupportedFormat] = useState();
@ -200,13 +202,17 @@ const App = memo(() => {
}
}, [detectedFileFormat, outFormatLocked, setFileFormat, setOutFormatLocked]);
const toggleTimelineMode = useCallback((newMode) => {
if (newMode === timelineMode) {
setTimelineMode();
const toggleWaveformMode = useCallback((newMode) => {
if (waveformMode === 'waveform') {
setWaveformMode('big-waveform');
} else if (waveformMode === 'big-waveform') {
setWaveformMode();
} else {
setTimelineMode(newMode);
setWaveformMode(newMode);
}
}, [timelineMode]);
}, [waveformMode]);
const toggleEnableThumbnails = useCallback(() => setThumbnailsEnabled((v) => !v), []);
const toggleExportConfirmEnabled = useCallback(() => setExportConfirmEnabled((v) => {
const newVal = !v;
@ -589,12 +595,13 @@ const App = memo(() => {
const hasAudio = !!mainAudioStream;
const hasVideo = !!mainVideoStream;
const waveformEnabled = timelineMode === 'waveform' && hasAudio;
const thumbnailsEnabled = timelineMode === 'thumbnails' && hasVideo;
const waveformEnabled = hasAudio && ['waveform', 'big-waveform'].includes(waveformMode);
const bigWaveformEnabled = waveformEnabled && waveformMode === 'big-waveform';
const showThumbnails = thumbnailsEnabled && hasVideo;
const [, cancelRenderThumbnails] = useDebounceOld(() => {
async function renderThumbnails() {
if (!thumbnailsEnabled || thumnailsRenderingPromiseRef.current) return;
if (!showThumbnails || thumnailsRenderingPromiseRef.current) return;
try {
setThumbnails([]);
@ -609,7 +616,7 @@ const App = memo(() => {
}
if (isDurationValid(zoomedDuration)) renderThumbnails();
}, 500, [zoomedDuration, filePath, zoomWindowStartTime, thumbnailsEnabled]);
}, 500, [zoomedDuration, filePath, zoomWindowStartTime, showThumbnails]);
// Cleanup removed thumbnails
useEffect(() => {
@ -628,11 +635,11 @@ const App = memo(() => {
subtitlesByStreamIdRef.current = subtitlesByStreamId;
}, [subtitlesByStreamId]);
const shouldShowKeyframes = keyframesEnabled && !!mainVideoStream && calcShouldShowKeyframes(zoomedDuration);
const shouldShowKeyframes = keyframesEnabled && hasVideo && calcShouldShowKeyframes(zoomedDuration);
const shouldShowWaveform = calcShouldShowWaveform(zoomedDuration);
const { neighbouringKeyFrames, findNearestKeyFrameTime } = useKeyframes({ keyframesEnabled, filePath, commandedTime, mainVideoStream, detectedFps, ffmpegExtractWindow });
const { waveforms } = useWaveform({ filePath, commandedTime, zoomedDuration, waveformEnabled, mainAudioStream, shouldShowWaveform, ffmpegExtractWindow });
const { waveforms } = useWaveform({ darkMode, filePath, relevantTime, waveformEnabled, mainAudioStream, shouldShowWaveform, ffmpegExtractWindow, durationSafe });
const resetState = useCallback(() => {
console.log('State reset');
@ -1420,6 +1427,7 @@ const App = memo(() => {
if (timecode) setStartTimeOffset(timecode);
setDetectedFps(haveVideoStream ? getStreamFps(videoStream) : undefined);
if (!haveVideoStream) setWaveformMode('big-waveform');
setMainFileMeta({ streams: fileMeta.streams, formatData: fileMeta.format, chapters: fileMeta.chapters });
setMainVideoStream(videoStream);
setMainAudioStream(audioStream);
@ -1916,6 +1924,7 @@ const App = memo(() => {
closeExportConfirm();
setLastCommandsVisible(false);
setSettingsVisible(false);
setStreamsSelectorShown(false);
return false;
}
@ -2205,7 +2214,7 @@ const App = memo(() => {
<div style={{ position: 'relative', flexGrow: 1, overflow: 'hidden' }}>
{!isFileOpened && <NoFileLoaded mifiLink={mifiLink} currentCutSeg={currentCutSeg} />}
<div className="no-user-select" style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, visibility: !isFileOpened ? 'hidden' : undefined }} onWheel={onTimelineWheel}>
<div className="no-user-select" style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, visibility: !isFileOpened || !hasVideo || bigWaveformEnabled ? 'hidden' : undefined }} onWheel={onTimelineWheel}>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video
muted={playbackVolume === 0}
@ -2224,6 +2233,8 @@ const App = memo(() => {
{canvasPlayerEnabled && <Canvas rotate={effectiveRotation} filePath={filePath} width={mainVideoStream.width} height={mainVideoStream.height} streamIndex={mainVideoStream.index} playerTime={playerTime} commandedTime={commandedTime} playing={playing} eventId={canvasPlayerEventId} />}
</div>
{bigWaveformEnabled && <BigWaveform waveforms={waveforms} relevantTime={relevantTime} playing={playing} durationSafe={durationSafe} zoom={zoomUnrounded} seekRel={seekRel} />}
{isRotationSet && !hideCanvasPreview && (
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, marginTop: '1em', marginLeft: '1em', color: 'white', display: 'flex', alignItems: 'center' }}>
<MdRotate90DegreesCcw size={26} style={{ marginRight: 5 }} />
@ -2301,7 +2312,7 @@ const App = memo(() => {
waveforms={waveforms}
shouldShowWaveform={shouldShowWaveform}
waveformEnabled={waveformEnabled}
thumbnailsEnabled={thumbnailsEnabled}
showThumbnails={showThumbnails}
neighbouringKeyFrames={neighbouringKeyFrames}
thumbnails={thumbnailsSorted}
playerTime={playerTime}
@ -2358,8 +2369,10 @@ const App = memo(() => {
shortStep={shortStep}
seekClosestKeyframe={seekClosestKeyframe}
togglePlay={togglePlay}
toggleTimelineMode={toggleTimelineMode}
timelineMode={timelineMode}
showThumbnails={showThumbnails}
toggleEnableThumbnails={toggleEnableThumbnails}
toggleWaveformMode={toggleWaveformMode}
waveformMode={waveformMode}
hasAudio={hasAudio}
keyframesEnabled={keyframesEnabled}
toggleKeyframesEnabled={toggleKeyframesEnabled}
@ -2371,13 +2384,7 @@ const App = memo(() => {
/>
</div>
<SideSheet
width={700}
containerProps={{ style: { maxWidth: '100%' } }}
position={Position.LEFT}
isShown={streamsSelectorShown}
onCloseComplete={() => setStreamsSelectorShown(false)}
>
<Sheet visible={streamsSelectorShown} onClosePress={() => setStreamsSelectorShown(false)} style={{ padding: '1em 0' }}>
{mainStreams && (
<StreamsSelector
mainFilePath={filePath}
@ -2405,7 +2412,7 @@ const App = memo(() => {
setDispositionByStreamId={setDispositionByStreamId}
/>
)}
</SideSheet>
</Sheet>
<ExportConfirm filePath={filePath} areWeCutting={areWeCutting} nonFilteredSegmentsOrInverse={nonFilteredSegmentsOrInverse} selectedSegments={selectedSegmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} setStreamsSelectorShown={setStreamsSelectorShown} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} getOutSegError={getOutSegError} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} needSmartCut={needSmartCut} />

View File

@ -135,9 +135,10 @@ const BottomBar = memo(({
seekAbs, currentSegIndexSafe, cutSegments, currentCutSeg, setCutStart, setCutEnd,
setCurrentSegIndex,
jumpTimelineStart, jumpTimelineEnd, jumpCutEnd, jumpCutStart, startTimeOffset, setCutTime, currentApparentCutSeg,
playing, shortStep, togglePlay, toggleLoopSelectedSegments, toggleTimelineMode, hasAudio, timelineMode,
playing, shortStep, togglePlay, toggleLoopSelectedSegments, hasAudio,
keyframesEnabled, toggleKeyframesEnabled, seekClosestKeyframe, detectedFps, isFileOpened, selectedSegments,
darkMode, setDarkMode,
toggleEnableThumbnails, toggleWaveformMode, waveformMode, showThumbnails,
}) => {
const { t } = useTranslation();
@ -194,7 +195,7 @@ const BottomBar = memo(({
const text = seg ? `${newIndex + 1}` : '-';
const wide = text.length > 1;
const segButtonStyle = {
backgroundColor, opacity, padding: `6px ${wide ? 4 : 6}px`, borderRadius: 10, color: 'white', fontSize: wide ? 12 : 14, width: 20, boxSizing: 'border-box', letterSpacing: -1, lineHeight: '10px', fontWeight: 'bold', margin: '0 6px',
backgroundColor, opacity, padding: `6px ${wide ? 4 : 6}px`, borderRadius: 10, color: seg ? 'white' : undefined, fontSize: wide ? 12 : 14, width: 20, boxSizing: 'border-box', letterSpacing: -1, lineHeight: '10px', fontWeight: 'bold', margin: '0 6px',
};
return (
@ -222,20 +223,20 @@ const BottomBar = memo(({
{hasAudio && (
<GiSoundWaves
size={24}
style={{ padding: '0 .1em', color: timelineMode === 'waveform' ? primaryTextColor : undefined }}
style={{ padding: '0 .1em', color: ['big-waveform', 'waveform'].includes(waveformMode) ? primaryTextColor : undefined }}
role="button"
title={t('Show waveform')}
onClick={() => toggleTimelineMode('waveform')}
onClick={() => toggleWaveformMode('waveform')}
/>
)}
{hasVideo && (
<>
<FaImages
size={20}
style={{ padding: '0 .2em', color: timelineMode === 'thumbnails' ? primaryTextColor : undefined }}
style={{ padding: '0 .2em', color: showThumbnails ? primaryTextColor : undefined }}
role="button"
title={t('Show thumbnails')}
onClick={() => toggleTimelineMode('thumbnails')}
onClick={toggleEnableThumbnails}
/>
<FaKey

View File

@ -13,18 +13,18 @@ const NoFileLoaded = memo(({ mifiLink, currentCutSeg }) => {
const { simpleMode } = useUserSettings();
return (
<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 className="no-user-select" style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0, border: '1.5vmin dashed var(--gray3)', color: 'var(--gray12)', margin: '5vmin', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', whiteSpace: 'nowrap' }}>
<div style={{ fontSize: '6vmin', textTransform: 'uppercase', color: 'var(--gray11)' }}>{t('DROP FILE(S)')}</div>
<div style={{ fontSize: '3vmin', color: 'var(--gray11)', marginBottom: '.3em' }}>
<div style={{ fontSize: '2.5vmin', color: 'var(--gray11)', marginBottom: '.3em' }}>
<Trans>See <b>Help</b> menu for help</Trans>
</div>
<div style={{ fontSize: '3vmin', color: 'var(--gray11)' }}>
<div style={{ fontSize: '2.5vmin', 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: 'var(--gray11)' }} role="button">
<div style={{ fontSize: '2.5vmin', color: 'var(--gray11)' }} role="button">
{simpleMode ? (
<Trans><SimpleModeButton style={{ verticalAlign: 'middle' }} size={16} /> to show advanced view</Trans>
) : (

View File

@ -258,7 +258,7 @@ const SegmentList = memo(({
return (
<motion.div
style={{ width, background: controlsBackground, color: 'var(--gray11)', transition: darkModeTransition, display: 'flex', flexDirection: 'column', overflowY: 'hidden' }}
style={{ width, background: controlsBackground, borderLeft: '1px solid var(--gray6)', color: 'var(--gray11)', transition: darkModeTransition, display: 'flex', flexDirection: 'column', overflowY: 'hidden' }}
initial={{ x: width }}
animate={{ x: 0 }}
exit={{ x: width }}

View File

@ -4,7 +4,7 @@ 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, Heading, SortAscIcon, SortDescIcon, Dialog, Button, PlusIcon, Pane, ForkIcon, Alert } from 'evergreen-ui';
import { BookIcon, TextInput, MoreIcon, Position, Popover, Menu, TrashIcon, EditIcon, InfoSignIcon, IconButton, Heading, SortAscIcon, SortDescIcon, Dialog, Button, PlusIcon, ForkIcon, WarningSignIcon } from 'evergreen-ui';
import { useTranslation } from 'react-i18next';
import prettyBytes from 'pretty-bytes';
@ -243,7 +243,7 @@ const Stream = memo(({ dispositionByStreamId, setDispositionByStreamId, filePath
return (
<tr style={{ opacity: copyStream ? undefined : 0.4 }}>
<td style={{ whiteSpace: 'nowrap', display: 'flex', alignItems: 'center' }}>
<IconButton title={`${t('Click to toggle track inclusion when exporting')} (type ${codecTypeHuman})`} appearance="minimal" icon={<Icon color={copyStream ? '#52BD95' : '#D14343'} size={20} />} onClick={onClick} />
<IconButton iconSize={20} color={copyStream ? '#52BD95' : '#D14343'} title={`${t('Click to toggle track inclusion when exporting')} (type ${codecTypeHuman})`} appearance="minimal" icon={Icon} onClick={onClick} />
<div style={{ width: 20, textAlign: 'center' }}>{stream.index + 1}</div>
</td>
<td style={{ maxWidth: '3em', overflow: 'hidden' }} title={stream.codec_name}>{stream.codec_name} {codecTag}</td>
@ -255,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 style={{ width: 100 }} value={effectiveDisposition || unchangedDispositionValue} onChange={onDispositionChange}>
<Select style={{ width: '7em', fontSize: '1.1em' }} value={effectiveDisposition || unchangedDispositionValue} onChange={onDispositionChange}>
<option value="" disabled>{t('Disposition')}</option>
<option value={unchangedDispositionValue}>{t('Unchanged')}</option>
<option value={deleteDispositionValue}>{t('Remove')}</option>
@ -266,9 +266,10 @@ const Stream = memo(({ dispositionByStreamId, setDispositionByStreamId, filePath
</Select>
</td>
<td style={{ display: 'flex' }}>
<td style={{ display: 'flex', justifyContent: 'flex-end' }}>
<IconButton icon={InfoSignIcon} onClick={() => onInfoClick(stream, t('Track {{num}} info', { num: stream.index + 1 }))} appearance="minimal" iconSize={18} />
<IconButton title={t('Extract this track as file')} icon={<FaFileExport size={18} />} onClick={onExtractStreamPress} appearance="minimal" iconSize={18} />
{onExtractStreamPress && <IconButton title={t('Extract this track as file')} icon={FaFileExport} onClick={onExtractStreamPress} appearance="minimal" iconSize={18} />}
<Popover
position={Position.BOTTOM_LEFT}
@ -302,8 +303,8 @@ const FileHeading = ({ path, formatData, chapters, onTrashClick, onEditClick, se
const { t } = useTranslation();
return (
<div style={{ display: 'flex', marginBottom: 15, marginLeft: 5, marginRight: 5, marginTop: 5, alignItems: 'center' }}>
<Heading title={path} style={{ wordBreak: 'break-all', marginRight: 10 }}>{path.replace(/.*\/([^/]+)$/, '$1')}</Heading>
<div style={{ display: 'flex', marginBottom: '.2em', borderBottom: '1px solid var(--gray7)' }}>
<div title={path} style={{ wordBreak: 'break-all', marginRight: '1em', fontWeight: 'bold' }}>{path.replace(/.*\/([^/]+)$/, '$1')}</div>
<div style={{ flexGrow: 1 }} />
@ -311,9 +312,9 @@ const FileHeading = ({ path, formatData, chapters, onTrashClick, onEditClick, se
{chapters && chapters.length > 0 && <IconButton icon={BookIcon} onClick={() => onInfoClick(chapters, t('Chapters'))} appearance="minimal" iconSize={18} />}
{onEditClick && <IconButton icon={EditIcon} onClick={onEditClick} appearance="minimal" iconSize={18} />}
{onTrashClick && <IconButton icon={TrashIcon} onClick={onTrashClick} appearance="minimal" iconSize={18} />}
<IconButton icon={<FaCheckCircle color="#52BD95" size={18} />} onClick={() => setCopyAllStreams(true)} appearance="minimal" />
<IconButton icon={<FaBan color="#D14343" size={18} />} onClick={() => setCopyAllStreams(false)} appearance="minimal" />
{onExtractAllStreamsPress && <IconButton title={t('Export each track as individual files')} icon={<ForkIcon size={16} />} onClick={onExtractAllStreamsPress} appearance="minimal" />}
<IconButton iconSize={18} color="#52BD95" icon={FaCheckCircle} onClick={() => setCopyAllStreams(true)} appearance="minimal" />
<IconButton iconSize={18} color="#D14343" icon={FaBan} onClick={() => setCopyAllStreams(false)} appearance="minimal" />
{onExtractAllStreamsPress && <IconButton iconSize={16} title={t('Export each track as individual files')} icon={ForkIcon} onClick={onExtractAllStreamsPress} appearance="minimal" />}
</div>
);
};
@ -321,7 +322,7 @@ const FileHeading = ({ path, formatData, chapters, onTrashClick, onEditClick, se
const Thead = () => {
const { t } = useTranslation();
return (
<thead style={{ color: 'rgba(0,0,0,0.6)', textAlign: 'left' }}>
<thead style={{ color: 'var(--gray12)', textAlign: 'left' }}>
<tr>
<th>{t('Keep?')}</th>
<th>{t('Codec')}</th>
@ -337,7 +338,7 @@ const Thead = () => {
};
const tableStyle = { fontSize: 14, width: '100%' };
const fileStyle = { marginBottom: 20, padding: 5, minWidth: '100%', overflowX: 'auto' };
const fileStyle = { margin: '1.5em 1em 1.5em 1em', padding: 5, overflowX: 'auto' };
const StreamsSelector = memo(({
mainFilePath, mainFileFormatData, mainFileStreams, mainFileChapters, isCopyingStreamId, toggleCopyStreamId,
@ -383,79 +384,79 @@ const StreamsSelector = memo(({
return (
<>
<div style={{ color: 'black', padding: 10 }}>
<Paragraph marginBottom={10}>{t('Click to select which tracks to keep when exporting:')}</Paragraph>
<p style={{ margin: '.5em 1em' }}>{t('Click to select which tracks to keep when exporting:')}</p>
<div style={fileStyle}>
{/* We only support editing main file metadata for now */}
<FileHeading path={mainFilePath} formatData={mainFileFormatData} chapters={mainFileChapters} onEditClick={() => setEditingFile(mainFilePath)} setCopyAllStreams={(enabled) => setCopyAllStreamsForPath(mainFilePath, enabled)} onExtractAllStreamsPress={onExtractAllStreamsPress} />
<table style={tableStyle}>
<Thead />
<tbody>
{mainFileStreams.map((stream) => (
<Stream
dispositionByStreamId={dispositionByStreamId}
setDispositionByStreamId={setDispositionByStreamId}
key={stream.index}
filePath={mainFilePath}
stream={stream}
copyStream={isCopyingStreamId(mainFilePath, stream.index)}
onToggle={(streamId) => toggleCopyStreamId(mainFilePath, streamId)}
batchSetCopyStreamIds={(filter, enabled) => batchSetCopyStreamIdsForPath(mainFilePath, mainFileStreams, filter, enabled)}
setEditingStream={setEditingStream}
fileDuration={getFormatDuration(mainFileFormatData)}
onExtractStreamPress={() => onExtractStreamPress(stream.index)}
/>
))}
</tbody>
</table>
</div>
{externalFilesEntries.map(([path, { streams, formatData }]) => (
<div key={path} style={fileStyle}>
<FileHeading path={path} formatData={formatData} onTrashClick={() => removeFile(path)} setCopyAllStreams={(enabled) => setCopyAllStreamsForPath(path, enabled)} />
<Pane elevation={1} style={fileStyle}>
{/* We only support editing main file metadata for now */}
<FileHeading path={mainFilePath} formatData={mainFileFormatData} chapters={mainFileChapters} onEditClick={() => setEditingFile(mainFilePath)} setCopyAllStreams={(enabled) => setCopyAllStreamsForPath(mainFilePath, enabled)} onExtractAllStreamsPress={onExtractAllStreamsPress} />
<table style={tableStyle}>
<Thead />
<tbody>
{mainFileStreams.map((stream) => (
{streams.map((stream) => (
<Stream
dispositionByStreamId={dispositionByStreamId}
setDispositionByStreamId={setDispositionByStreamId}
key={stream.index}
filePath={mainFilePath}
filePath={path}
stream={stream}
copyStream={isCopyingStreamId(mainFilePath, stream.index)}
onToggle={(streamId) => toggleCopyStreamId(mainFilePath, streamId)}
batchSetCopyStreamIds={(filter, enabled) => batchSetCopyStreamIdsForPath(mainFilePath, mainFileStreams, filter, enabled)}
copyStream={isCopyingStreamId(path, stream.index)}
onToggle={(streamId) => toggleCopyStreamId(path, streamId)}
batchSetCopyStreamIds={(filter, enabled) => batchSetCopyStreamIdsForPath(path, streams, filter, enabled)}
setEditingStream={setEditingStream}
fileDuration={getFormatDuration(mainFileFormatData)}
onExtractStreamPress={() => onExtractStreamPress(stream.index)}
fileDuration={getFormatDuration(formatData)}
/>
))}
</tbody>
</table>
</Pane>
{externalFilesEntries.map(([path, { streams, formatData }]) => (
<Pane elevation={1} key={path} style={fileStyle}>
<FileHeading path={path} formatData={formatData} onTrashClick={() => removeFile(path)} setCopyAllStreams={(enabled) => setCopyAllStreamsForPath(path, enabled)} />
<table style={tableStyle}>
<Thead />
<tbody>
{streams.map((stream) => (
<Stream
dispositionByStreamId={dispositionByStreamId}
setDispositionByStreamId={setDispositionByStreamId}
key={stream.index}
filePath={path}
stream={stream}
copyStream={isCopyingStreamId(path, stream.index)}
onToggle={(streamId) => toggleCopyStreamId(path, streamId)}
batchSetCopyStreamIds={(filter, enabled) => batchSetCopyStreamIdsForPath(path, streams, filter, enabled)}
setEditingStream={setEditingStream}
fileDuration={getFormatDuration(formatData)}
/>
))}
</tbody>
</table>
</Pane>
))}
</div>
))}
<div style={{ margin: '1em 1em' }}>
{externalFilesEntries.length > 0 && (
<Alert intent="warning" appearance="card" marginBottom={5}>{t('Note: Cutting and including external tracks at the same time does not yet work. If you want to do both, it must be done as separate operations. See github issue #896.')}</Alert>
<div style={{ marginBottom: '1em' }}><WarningSignIcon color="warning" /> {t('Note: Cutting and including external tracks at the same time does not yet work. If you want to do both, it must be done as separate operations. See github issue #896.')}</div>
)}
<Button iconBefore={<FaFileImport size={16} />} onClick={showAddStreamSourceDialog}>
<Button iconBefore={<FaFileImport size={16} />} marginBottom="1em" onClick={showAddStreamSourceDialog}>
{t('Include more tracks from other file')}
</Button>
{nonCopiedExtraStreams.length > 0 && (
<div style={{ margin: '10px 0' }}>
<div style={{ marginBottom: '1em' }}>
<span style={{ marginRight: 10 }}>{t('Discard or extract unprocessable tracks to separate files?')}</span>
<AutoExportToggler />
</div>
)}
{externalFilesEntries.length > 0 && (
<div style={{ margin: '10px 0' }}>
<span style={{ marginRight: 10 }}>{t('When tracks have different lengths, do you want to make the output file as long as the longest or the shortest track?')}</span>
<div style={{ marginBottom: '1em' }}>
<div style={{ marginBottom: '.5em' }}>{t('When tracks have different lengths, do you want to make the output file as long as the longest or the shortest track?')}</div>
<Button iconBefore={shortestFlag ? SortDescIcon : SortAscIcon} onClick={() => setShortestFlag((value) => !value)}>
{shortestFlag ? t('Shortest') : t('Longest')}
</Button>

View File

@ -34,8 +34,8 @@ const Waveform = memo(({ waveform, calculateTimelinePercent, durationSafe }) =>
);
});
const Waveforms = memo(({ calculateTimelinePercent, durationSafe, waveforms, zoom, timelineHeight }) => (
<div style={{ height: timelineHeight, width: `${zoom * 100}%`, position: 'relative' }}>
const Waveforms = memo(({ calculateTimelinePercent, durationSafe, waveforms, zoom, height }) => (
<div style={{ height, width: `${zoom * 100}%`, position: 'relative' }}>
{waveforms.map((waveform) => (
<Waveform key={`${waveform.from}-${waveform.to}`} waveform={waveform} calculateTimelinePercent={calculateTimelinePercent} durationSafe={durationSafe} />
))}
@ -58,12 +58,14 @@ const Timeline = memo(({
durationSafe, startTimeOffset, playerTime, commandedTime, relevantTime,
zoom, neighbouringKeyFrames, seekAbs, apparentCutSegments,
setCurrentSegIndex, currentSegIndexSafe, inverseCutSegments, formatTimecode,
waveforms, shouldShowWaveform, shouldShowKeyframes, timelineHeight = 36, thumbnails,
onZoomWindowStartTimeChange, waveformEnabled, thumbnailsEnabled,
waveforms, shouldShowWaveform, shouldShowKeyframes, thumbnails,
onZoomWindowStartTimeChange, waveformEnabled, showThumbnails,
playing, isFileOpened, onWheel, commandedTimeRef, goToTimecode, isSegmentSelected,
}) => {
const { t } = useTranslation();
const timelineHeight = 36;
const { invertCutSegments, darkMode } = useUserSettings();
const timelineScrollerRef = useRef();
@ -239,11 +241,17 @@ const Timeline = memo(({
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/mouse-events-have-key-events
<div
style={{ position: 'relative' }}
style={{ position: 'relative', borderTop: '1px solid var(--gray6)', borderBottom: '1px solid var(--gray6)' }}
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseOut={onMouseOut}
>
{(waveformEnabled && !shouldShowWaveform) && (
<div style={{ 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>
)}
<div
style={{ overflowX: 'scroll' }}
className="hide-scrollbar"
@ -257,19 +265,19 @@ const Timeline = memo(({
durationSafe={durationSafe}
waveforms={waveforms}
zoom={zoom}
timelineHeight={timelineHeight}
height={40}
/>
)}
{thumbnailsEnabled && (
<div style={{ height: timelineHeight, width: `${zoom * 100}%`, position: 'relative' }}>
{showThumbnails && (
<div style={{ height: 60, width: `${zoom * 100}%`, position: 'relative', marginBottom: 3 }}>
{thumbnails.map((thumbnail, i) => {
const leftPercent = (thumbnail.time / durationSafe) * 100;
const nextThumbnail = thumbnails[i + 1];
const nextThumbTime = nextThumbnail ? nextThumbnail.time : durationSafe;
const maxWidthPercent = ((nextThumbTime - thumbnail.time) / durationSafe) * 100 * 0.9;
return (
<img key={thumbnail.url} src={thumbnail.url} alt="" style={{ position: 'absolute', left: `${leftPercent}%`, height: timelineHeight * 1.5, zIndex: 1, maxWidth: `${maxWidthPercent}%`, objectFit: 'cover', border: '1px solid rgba(255, 255, 255, 0.5)', borderBottomRightRadius: 15, borderTopLeftRadius: 15, borderTopRightRadius: 15, pointerEvents: 'none' }} />
<img key={thumbnail.url} src={thumbnail.url} alt="" style={{ position: 'absolute', left: `${leftPercent}%`, height: '100%', boxSizing: 'border-box', zIndex: 1, maxWidth: `${maxWidthPercent}%`, objectFit: 'cover', border: '1px solid rgba(255, 255, 255, 0.5)', borderBottomRightRadius: 15, borderTopLeftRadius: 15, borderTopRightRadius: 15, pointerEvents: 'none' }} />
);
})}
</div>
@ -327,12 +335,6 @@ const Timeline = memo(({
</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: 'var(--gray11)' }}>
{t('Zoom in more to view waveform')}
</div>
)}
<div style={{ position: 'absolute', height: timelineHeight, left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none', zIndex: 2 }}>
<div style={{ background: 'rgba(0,0,0,0.4)', borderRadius: 3, padding: '2px 4px', color: 'rgba(255, 255, 255, 0.8)' }}>
{formatTimecode({ seconds: displayTime })}{isZoomed ? ` ${displayTimePercent}` : ''}

View File

@ -2,7 +2,8 @@ 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 waveformColorLight = '#000000'; // Must be hex because used by ffmpeg
export const waveformColorDark = '#ffffff'; // Must be hex because used by ffmpeg
export const controlsBackground = 'var(--gray4)';
export const timelineBackground = 'var(--gray2)';
export const darkModeTransition = 'background .5s';

View File

@ -46,7 +46,7 @@ const BatchFilesList = memo(({ selectedBatchFiles, filePath, width, batchFiles,
return (
<motion.div
className="no-user-select"
style={{ width, background: controlsBackground, color: 'var(--gray12)', transition: darkModeTransition, display: 'flex', flexDirection: 'column', overflowY: 'hidden', overflowX: 'hidden', resize: 'horizontal' }}
style={{ width, background: controlsBackground, color: 'var(--gray12)', borderRight: '1px solid var(--gray6)', transition: darkModeTransition, display: 'flex', flexDirection: 'column', overflowY: 'hidden', overflowX: 'hidden', resize: 'horizontal' }}
initial={{ x: -width }}
animate={{ x: 0 }}
exit={{ x: -width }}

View File

@ -0,0 +1,116 @@
import React, { Fragment, memo, useEffect, useState, useCallback, useRef } from 'react';
import { ffmpegExtractWindow } from '../util/constants';
const BigWaveform = memo(({ waveforms, relevantTime, playing, durationSafe, zoom, seekRel }) => {
const windowSize = ffmpegExtractWindow * 2;
const windowStart = Math.max(0, relevantTime - windowSize);
const windowEnd = relevantTime + windowSize;
const filtered = waveforms.filter((waveform) => waveform.from >= windowStart && waveform.to <= windowEnd);
const scaleFactor = zoom;
const [smoothTime, setSmoothTime] = useState(relevantTime);
const mouseDownRef = useRef();
const containerRef = useRef();
const getRect = useCallback(() => containerRef.current.getBoundingClientRect(), []);
const handleMouseDown = useCallback((e) => {
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left;
mouseDownRef.current = { relevantTime, x };
e.preventDefault();
}, [relevantTime]);
const scaleToTime = useCallback((v) => (((v) / getRect().width) * windowSize) / zoom, [getRect, windowSize, zoom]);
const handleMouseMove = useCallback((e) => {
if (mouseDownRef.current == null) return;
seekRel(-scaleToTime(e.movementX));
e.preventDefault();
}, [scaleToTime, seekRel]);
const handleWheel = useCallback((e) => {
seekRel(scaleToTime(e.deltaX));
}, [scaleToTime, seekRel]);
const handleMouseUp = useCallback((e) => {
if (!mouseDownRef.current) return;
mouseDownRef.current = undefined;
e.preventDefault();
}, []);
useEffect(() => {
let time = relevantTime;
setSmoothTime(time);
const startTime = new Date().getTime();
if (playing) {
let raf;
// eslint-disable-next-line no-inner-declarations
function render() {
raf = window.requestAnimationFrame(() => {
time = new Date().getTime() / 1000;
setSmoothTime(relevantTime + (new Date().getTime() - startTime) / 1000);
render();
});
}
render();
return () => window.cancelAnimationFrame(raf);
}
return undefined;
}, [relevantTime, playing]);
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
ref={containerRef}
style={{ height: '100%', width: '100%', position: 'relative', cursor: 'grab' }}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
onMouseMove={handleMouseMove}
onWheel={handleWheel}
>
{filtered.map((waveform) => {
const left = 0.5 + ((waveform.from - smoothTime) / windowSize) * scaleFactor;
const width = ((waveform.to - waveform.from) / windowSize) * scaleFactor;
const leftPercent = `${left * 100}%`;
const widthPercent = `${width * 100}%`;
return (
<Fragment key={`${waveform.from}-${waveform.to}`}>
<img
src={waveform.url}
draggable={false}
alt=""
style={{
pointerEvents: 'none',
backgroundColor: 'var(--gray3)',
position: 'absolute',
height: '100%',
width: widthPercent,
left: leftPercent,
borderLeft: waveform.from === 0 ? '1px solid var(--gray11)' : undefined,
borderRight: waveform.to >= durationSafe ? '1px solid var(--gray11)' : undefined,
}}
/>
<div style={{ pointerEvents: 'none', position: 'absolute', width: widthPercent, backgroundColor: 'var(--gray12)', height: 1, top: '50%', left: leftPercent }} />
</Fragment>
);
})}
<div style={{ pointerEvents: 'none', position: 'absolute', height: '100%', backgroundColor: 'var(--red11)', width: 1, left: '50%', top: 0 }} />
</div>
);
});
export default BigWaveform;

View File

@ -2,7 +2,7 @@ import React, { memo, useState, useEffect, useCallback } from 'react';
import { useDebounce } from 'use-debounce';
import i18n from 'i18next';
import { useTranslation } from 'react-i18next';
import { Text, Button, Alert, IconButton, TickIcon, ResetIcon, Heading } from 'evergreen-ui';
import { WarningSignIcon, ErrorIcon, Button, Alert, IconButton, TickIcon, ResetIcon } from 'evergreen-ui';
import withReactContent from 'sweetalert2-react-content';
import Swal from '../swal';
@ -93,7 +93,7 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate
{needToShow && (
<>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 5, marginTop: 5 }}>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 5, marginTop: 10 }}>
<input type="text" style={inputStyle} onChange={onTextChange} value={text} autoComplete="off" autoCapitalize="off" autoCorrect="off" />
{outSegFileNames && <Button height={20} onClick={onAllSegmentsPreviewPress} marginLeft={5}>{t('Preview')}</Button>}
@ -102,8 +102,8 @@ const OutSegTemplateEditor = memo(({ outSegTemplate, setOutSegTemplate, generate
<IconButton title={t('Close')} icon={TickIcon} height={20} onClick={onHideClick} marginLeft={5} intent="success" />
</div>
<div style={{ maxWidth: 600 }}>
{error != null && <Alert intent="danger" appearance="card"><Heading color="danger">{i18n.t('There is an error in the file name template:')}</Heading><Text>{error}</Text></Alert>}
{isMissingExtension && <Alert intent="warning" appearance="card">{i18n.t('The file name template is missing {{ext}} and will result in a file without the suggested extension. This may result in an unplayable output file.', { ext: extVar })}</Alert>}
{error != null && <div style={{ marginBottom: '1em' }}><ErrorIcon color="var(--red9)" /> {i18n.t('There is an error in the file name template:')} {error}</div>}
{isMissingExtension && <div style={{ marginBottom: '1em' }}><WarningSignIcon color="var(--amber9)" /> {i18n.t('The file name template is missing {{ext}} and will result in a file without the suggested extension. This may result in an unplayable output file.', { ext: extVar })}</div>}
<div style={{ fontSize: '.8em', color: 'var(--gray11)' }}>
{`${i18n.t('Variables')}`}{': '}
{['FILENAME', 'CUT_FROM', 'CUT_TO', 'SEG_NUM', 'SEG_LABEL', 'SEG_SUFFIX', 'EXT', 'SEG_TAGS.XX'].map((variable) => <span key={variable} role="button" style={{ cursor: 'pointer', marginRight: '.2em' }} onClick={() => setText((oldText) => `${oldText}\${${variable}}`)}>{variable}</span>)}

View File

@ -10,9 +10,13 @@
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-image: url("data:image/svg+xml;utf8,<svg fill='rgba(0,0,0,0.6)' 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%;
}
:global(.dark-theme) .select {
background-image: url("data:image/svg+xml;utf8,<svg fill='rgba(255,255,255,0.6)' 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>");
}

View File

@ -14,7 +14,7 @@ const Sheet = memo(({ visible, onClosePress, style, children }) => (
style={style}
className={styles.sheet}
>
<IoIosCloseCircleOutline role="button" onClick={onClosePress} size={30} style={{ position: 'fixed', right: 0, top: 0, padding: 20 }} />
<IoIosCloseCircleOutline role="button" onClick={onClosePress} size={30} style={{ position: 'fixed', right: 0, top: 0, padding: 20, zIndex: 1, cursor: 'pointer' }} />
<div style={{ overflowY: 'scroll', height: '100%' }}>
{children}

View File

@ -596,14 +596,12 @@ export async function renderThumbnails({ filePath, from, duration, onThumbnail }
}
export async function renderWaveformPng({ filePath, aroundTime, window, color }) {
const { from, to } = getIntervalAroundTime(aroundTime, window);
export async function renderWaveformPng({ filePath, start, duration, color }) {
const args1 = [
'-hide_banner',
'-i', filePath,
'-ss', from,
'-t', to - from,
'-ss', start,
'-t', duration,
'-c', 'copy',
'-vn',
'-map', 'a:0',
@ -614,7 +612,7 @@ export async function renderWaveformPng({ filePath, aroundTime, window, color })
const args2 = [
'-hide_banner',
'-i', '-',
'-filter_complex', `aformat=channel_layouts=mono,showwavespic=s=640x120:scale=sqrt:colors=${color}`,
'-filter_complex', `showwavespic=s=2000x300:scale=lin:filter=peak:split_channels=1:colors=${color}`,
'-frames:v', '1',
'-vcodec', 'png',
'-f', 'image2',
@ -622,18 +620,19 @@ export async function renderWaveformPng({ filePath, aroundTime, window, color })
];
console.log(getFfCommandLine('ffmpeg1', args1));
console.log(getFfCommandLine('ffmpeg2', args2));
console.log('|', getFfCommandLine('ffmpeg2', args2));
let ps1;
let ps2;
try {
ps1 = runFfmpeg(args1, { encoding: null, buffer: false });
ps2 = runFfmpeg(args2, { encoding: null });
ps1 = runFfmpeg(args1, { encoding: null, buffer: false }, { logCli: false });
ps2 = runFfmpeg(args2, { encoding: null }, { logCli: false });
ps1.stdout.pipe(ps2.stdin);
const timer = setTimeout(() => {
ps1.kill();
ps2.kill();
console.warn('ffmpeg timed out');
}, 10000);
let stdout;
@ -647,9 +646,9 @@ export async function renderWaveformPng({ filePath, aroundTime, window, color })
return {
url: URL.createObjectURL(blob),
from,
aroundTime,
to,
from: start,
to: start + duration,
duration,
createdAt: new Date(),
};
} catch (err) {

View File

@ -1,27 +1,39 @@
import { useState, useRef, useEffect } from 'react';
import sortBy from 'lodash/sortBy';
import useDebounceOld from 'react-use/lib/useDebounce'; // Want to phase out this
import { waveformColor } from '../colors';
import useThrottle from 'react-use/lib/useThrottle';
import { waveformColorDark, waveformColorLight } from '../colors';
import { renderWaveformPng } from '../ffmpeg';
const maxWaveforms = 100;
// const maxWaveforms = 3; // testing
export default ({ filePath, commandedTime, zoomedDuration, waveformEnabled, mainAudioStream, shouldShowWaveform, ffmpegExtractWindow }) => {
export default ({ darkMode, filePath, relevantTime, durationSafe, waveformEnabled, mainAudioStream, shouldShowWaveform, ffmpegExtractWindow }) => {
const creatingWaveformPromise = useRef();
const [waveforms, setWaveforms] = useState([]);
const waveformsRef = useRef();
useDebounceOld(() => {
useEffect(() => {
waveformsRef.current = waveforms;
}, [waveforms]);
const waveformColor = darkMode ? waveformColorDark : waveformColorLight;
const timeThrottled = useThrottle(relevantTime, 1000);
useEffect(() => {
let aborted = false;
(async () => {
const alreadyHaveWaveformAtCommandedTime = waveforms.some((waveform) => waveform.from <= commandedTime && waveform.to >= commandedTime);
const shouldRun = filePath && mainAudioStream && commandedTime != null && shouldShowWaveform && waveformEnabled && !alreadyHaveWaveformAtCommandedTime && !creatingWaveformPromise.current;
const waveformStartTime = Math.floor(timeThrottled / ffmpegExtractWindow) * ffmpegExtractWindow;
const alreadyHaveWaveformAtTime = (waveformsRef.current || []).some((waveform) => waveform.from === waveformStartTime);
const shouldRun = filePath && mainAudioStream && timeThrottled != null && shouldShowWaveform && waveformEnabled && !alreadyHaveWaveformAtTime && !creatingWaveformPromise.current;
if (!shouldRun) return;
try {
const promise = renderWaveformPng({ filePath, aroundTime: commandedTime, window: ffmpegExtractWindow, color: waveformColor });
const safeExtractDuration = Math.min(waveformStartTime + ffmpegExtractWindow, durationSafe) - waveformStartTime;
const promise = renderWaveformPng({ filePath, start: waveformStartTime, duration: safeExtractDuration, color: waveformColor });
creatingWaveformPromise.current = promise;
const newWaveform = await promise;
if (aborted) return;
@ -43,7 +55,7 @@ export default ({ filePath, commandedTime, zoomedDuration, waveformEnabled, main
return () => {
aborted = true;
};
}, 500, [filePath, commandedTime, zoomedDuration, waveformEnabled, mainAudioStream, shouldShowWaveform, waveforms, ffmpegExtractWindow]);
}, [filePath, timeThrottled, waveformEnabled, mainAudioStream, shouldShowWaveform, ffmpegExtractWindow, durationSafe, waveformColor, setWaveforms]);
const lastWaveformsRef = useRef([]);
useEffect(() => {
@ -54,8 +66,8 @@ export default ({ filePath, commandedTime, zoomedDuration, waveformEnabled, main
lastWaveformsRef.current = waveforms;
}, [waveforms]);
useEffect(() => setWaveforms([]), [filePath]);
useEffect(() => () => setWaveforms([]), []);
useEffect(() => setWaveforms([]), [filePath, setWaveforms]);
useEffect(() => () => setWaveforms([]), [setWaveforms]);
return { waveforms };
};

View File

@ -4,6 +4,8 @@ https://www.radix-ui.com/docs/colors/palette-composition/understanding-the-scale
*/
@import '@radix-ui/colors/red.css';
@import '@radix-ui/colors/redDark.css';
@import '@radix-ui/colors/amber.css';
@import '@radix-ui/colors/amberDark.css';
@import '@radix-ui/colors/green.css';
@import '@radix-ui/colors/greenDark.css';
@import '@radix-ui/colors/cyan.css';

View File

@ -10,11 +10,20 @@ function colorKeyForIntent(intent) {
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)';
return 'var(--gray8)';
}
export default {
...defaultTheme,
colors: {
...defaultTheme.colors,
icon: {
default: 'var(--gray12)',
muted: 'var(--gray11)',
disabled: 'var(--gray8)',
selected: 'var(--gray12)',
},
},
components: {
...defaultTheme.components,
Button: {
@ -43,6 +52,22 @@ export default {
opacity: 0.5,
},
},
minimal: {
...defaultTheme.components.Button.appearances.minimal,
// https://github.com/segmentio/evergreen/blob/master/src/themes/default/components/button.js
color: (theme, props) => props.color || colorKeyForIntent(props.intent),
_hover: {
backgroundColor: 'var(--gray4)',
},
_active: {
backgroundColor: 'var(--gray5)',
},
disabled: {
opacity: 0.5,
},
},
},
},
},