1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-22 02:12:30 +01:00

improve dark mode #1969

This commit is contained in:
Mikael Finstad 2024-05-27 21:10:27 +02:00
parent 0f3e2eb100
commit 122d79c300
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
20 changed files with 913 additions and 450 deletions

View File

@ -41,6 +41,7 @@
"devDependencies": { "devDependencies": {
"@fontsource/open-sans": "^4.5.14", "@fontsource/open-sans": "^4.5.14",
"@radix-ui/colors": "^0.1.8", "@radix-ui/colors": "^0.1.8",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-switch": "^1.0.1",
"@tsconfig/node18": "^18.2.2", "@tsconfig/node18": "^18.2.2",
"@tsconfig/strictest": "^2.0.2", "@tsconfig/strictest": "^2.0.2",
@ -101,6 +102,7 @@
"react-syntax-highlighter": "^15.4.3", "react-syntax-highlighter": "^15.4.3",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
"sass": "^1.77.2",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"scroll-into-view-if-needed": "^2.2.28", "scroll-into-view-if-needed": "^2.2.28",
"sharp": "^0.32.6", "sharp": "^0.32.6",

View File

@ -1338,6 +1338,7 @@ function App() {
if (areWeCutting) notices.push(i18n.t('Cutpoints may be inaccurate.')); if (areWeCutting) notices.push(i18n.t('Cutpoints may be inaccurate.'));
const revealPath = willMerge ? mergedOutFilePath : outFiles[0]; const revealPath = willMerge ? mergedOutFilePath : outFiles[0];
invariant(revealPath != null);
if (!hideAllNotifications) openExportFinishedToast({ filePath: revealPath, warnings, notices }); if (!hideAllNotifications) openExportFinishedToast({ filePath: revealPath, warnings, notices });
if (cleanupChoices.cleanupAfterExport) await cleanupFilesWithDialog(); if (cleanupChoices.cleanupAfterExport) await cleanupFilesWithDialog();
@ -2260,6 +2261,8 @@ function App() {
setLastCommandsVisible(false); setLastCommandsVisible(false);
setSettingsVisible(false); setSettingsVisible(false);
setStreamsSelectorShown(false); setStreamsSelectorShown(false);
setConcatDialogVisible(false);
setKeyboardShortcutsVisible(false);
return false; return false;
} }
@ -2506,300 +2509,304 @@ function App() {
// throw new Error('Test error boundary'); // throw new Error('Test error boundary');
return ( return (
<SegColorsContext.Provider value={segColorsContext}> <>
<UserSettingsContext.Provider value={userSettingsContext}> <SegColorsContext.Provider value={segColorsContext}>
<ThemeProvider value={theme}> <UserSettingsContext.Provider value={userSettingsContext}>
<div className={darkMode ? 'dark-theme' : undefined} style={{ display: 'flex', flexDirection: 'column', height: '100vh', color: 'var(--gray12)', background: 'var(--gray1)', transition: darkModeTransition }}> <ThemeProvider value={theme}>
<TopMenu <div className={darkMode ? 'dark-theme' : undefined} style={{ display: 'flex', flexDirection: 'column', height: '100vh', color: 'var(--gray12)', background: 'var(--gray1)', transition: darkModeTransition }}>
filePath={filePath} <TopMenu
fileFormat={fileFormat} filePath={filePath}
copyAnyAudioTrack={copyAnyAudioTrack} fileFormat={fileFormat}
toggleStripAudio={toggleStripAudio} copyAnyAudioTrack={copyAnyAudioTrack}
clearOutDir={clearOutDir} toggleStripAudio={toggleStripAudio}
isCustomFormatSelected={isCustomFormatSelected} clearOutDir={clearOutDir}
renderOutFmt={renderOutFmt} isCustomFormatSelected={isCustomFormatSelected}
toggleSettings={toggleSettings} renderOutFmt={renderOutFmt}
numStreamsToCopy={numStreamsToCopy} toggleSettings={toggleSettings}
numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy}
setStreamsSelectorShown={setStreamsSelectorShown} numStreamsTotal={numStreamsTotal}
selectedSegments={selectedSegmentsOrInverse} setStreamsSelectorShown={setStreamsSelectorShown}
/> selectedSegments={selectedSegmentsOrInverse}
/>
<div style={{ flexGrow: 1, display: 'flex', overflowY: 'hidden' }}>
<AnimatePresence>
{showLeftBar && (
<BatchFilesList
selectedBatchFiles={selectedBatchFiles}
filePath={filePath}
width={leftBarWidth}
batchFiles={batchFiles}
setBatchFiles={setBatchFiles}
onBatchFileSelect={onBatchFileSelect}
batchListRemoveFile={batchListRemoveFile}
closeBatch={closeBatch}
onMergeFilesClick={concatBatch}
onBatchConvertToSupportedFormatClick={convertFormatBatch}
/>
)}
</AnimatePresence>
{/* Middle part (also shown in fullscreen): */}
<div style={{ position: 'relative', flexGrow: 1, overflow: 'hidden' }} ref={videoContainerRef}>
{!isFileOpened && <NoFileLoaded mifiLink={mifiLink} currentCutSeg={currentCutSeg} onClick={openFilesDialog} darkMode={darkMode} />}
<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
className="main-player"
tabIndex={-1}
muted={playbackVolume === 0 || compatPlayerEnabled}
ref={videoRef}
style={videoStyle}
src={fileUri}
onPlay={onSartPlaying}
onPause={onStopPlaying}
onAbort={onVideoAbort}
onDurationChange={onDurationChange}
onTimeUpdate={onTimeUpdate}
onError={onVideoError}
onClick={onVideoClick}
onDoubleClick={toggleFullscreenVideo}
onFocusCapture={onVideoFocus}
>
{renderSubtitles()}
</video>
{filePath != null && compatPlayerEnabled && <MediaSourcePlayer rotate={effectiveRotation} filePath={filePath} videoStream={activeVideoStream} audioStream={activeAudioStream} playerTime={playerTime ?? 0} commandedTime={commandedTime} playing={playing} eventId={compatPlayerEventId} masterVideoRef={videoRef} mediaSourceQuality={mediaSourceQuality} playbackVolume={playbackVolume} />}
</div>
{bigWaveformEnabled && <BigWaveform waveforms={waveforms} relevantTime={relevantTime} playing={playing} durationSafe={durationSafe} zoom={zoomUnrounded} seekRel={seekRel} />}
{compatPlayerEnabled && (
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, marginTop: '1em', marginLeft: '1em', color: 'white', opacity: 0.7, display: 'flex', alignItems: 'center', pointerEvents: 'none' }}>
{isRotationSet ? (
<>
<MdRotate90DegreesCcw size={26} style={{ marginRight: 5 }} />
{t('Rotation preview')}
</>
) : (
<>
{t('FFmpeg-assisted playback')}
</>
)}
{!compatPlayerRequired && <FaWindowClose role="button" style={{ cursor: 'pointer', pointerEvents: 'initial', verticalAlign: 'middle', padding: 10 }} onClick={() => setHideMediaSourcePlayer(true)} />}
</div>
)}
{isFileOpened && (
<div className="no-user-select" style={{ position: 'absolute', right: 0, bottom: 0, marginBottom: 10, display: 'flex', alignItems: 'center' }}>
<VolumeControl playbackVolume={playbackVolume} setPlaybackVolume={setPlaybackVolume} />
{shouldShowPlaybackStreamSelector && <PlaybackStreamSelector subtitleStreams={subtitleStreams} videoStreams={videoStreams} audioStreams={audioStreams} activeSubtitleStreamIndex={activeSubtitleStreamIndex} activeVideoStreamIndex={activeVideoStreamIndex} activeAudioStreamIndex={activeAudioStreamIndex} onActiveSubtitleChange={onActiveSubtitleChange} onActiveVideoStreamChange={onActiveVideoStreamChange} onActiveAudioStreamChange={onActiveAudioStreamChange} />}
{compatPlayerEnabled && <div style={{ color: 'white', opacity: 0.7, padding: '.5em' }} role="button" onClick={() => incrementMediaSourceQuality()} title={t('Select playback quality')}>{mediaSourceQualities[mediaSourceQuality]}</div>}
{!showRightBar && (
<FaAngleLeft
title={t('Show sidebar')}
size={30}
role="button"
style={{ marginRight: 10, color: 'var(--gray12)', opacity: 0.7 }}
onClick={toggleSegmentsList}
/>
)}
</div>
)}
<div style={{ flexGrow: 1, display: 'flex', overflowY: 'hidden' }}>
<AnimatePresence> <AnimatePresence>
{working && <Working text={working.text} cutProgress={cutProgress} onAbortClick={handleAbortWorkingClick} />} {showLeftBar && (
<BatchFilesList
selectedBatchFiles={selectedBatchFiles}
filePath={filePath}
width={leftBarWidth}
batchFiles={batchFiles}
setBatchFiles={setBatchFiles}
onBatchFileSelect={onBatchFileSelect}
batchListRemoveFile={batchListRemoveFile}
closeBatch={closeBatch}
onMergeFilesClick={concatBatch}
onBatchConvertToSupportedFormatClick={convertFormatBatch}
/>
)}
</AnimatePresence> </AnimatePresence>
{tunerVisible && <ValueTuners type={tunerVisible} onFinished={() => setTunerVisible(undefined)} />} {/* Middle part (also shown in fullscreen): */}
<div style={{ position: 'relative', flexGrow: 1, overflow: 'hidden' }} ref={videoContainerRef}>
{!isFileOpened && <NoFileLoaded mifiLink={mifiLink} currentCutSeg={currentCutSeg} onClick={openFilesDialog} darkMode={darkMode} />}
<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
className="main-player"
tabIndex={-1}
muted={playbackVolume === 0 || compatPlayerEnabled}
ref={videoRef}
style={videoStyle}
src={fileUri}
onPlay={onSartPlaying}
onPause={onStopPlaying}
onAbort={onVideoAbort}
onDurationChange={onDurationChange}
onTimeUpdate={onTimeUpdate}
onError={onVideoError}
onClick={onVideoClick}
onDoubleClick={toggleFullscreenVideo}
onFocusCapture={onVideoFocus}
>
{renderSubtitles()}
</video>
{filePath != null && compatPlayerEnabled && <MediaSourcePlayer rotate={effectiveRotation} filePath={filePath} videoStream={activeVideoStream} audioStream={activeAudioStream} playerTime={playerTime ?? 0} commandedTime={commandedTime} playing={playing} eventId={compatPlayerEventId} masterVideoRef={videoRef} mediaSourceQuality={mediaSourceQuality} playbackVolume={playbackVolume} />}
</div>
{bigWaveformEnabled && <BigWaveform waveforms={waveforms} relevantTime={relevantTime} playing={playing} durationSafe={durationSafe} zoom={zoomUnrounded} seekRel={seekRel} />}
{compatPlayerEnabled && (
<div style={{ position: 'absolute', top: 0, right: 0, left: 0, marginTop: '1em', marginLeft: '1em', color: 'white', opacity: 0.7, display: 'flex', alignItems: 'center', pointerEvents: 'none' }}>
{isRotationSet ? (
<>
<MdRotate90DegreesCcw size={26} style={{ marginRight: 5 }} />
{t('Rotation preview')}
</>
) : (
<>
{t('FFmpeg-assisted playback')}
</>
)}
{!compatPlayerRequired && <FaWindowClose role="button" style={{ cursor: 'pointer', pointerEvents: 'initial', verticalAlign: 'middle', padding: 10 }} onClick={() => setHideMediaSourcePlayer(true)} />}
</div>
)}
{isFileOpened && (
<div className="no-user-select" style={{ position: 'absolute', right: 0, bottom: 0, marginBottom: 10, display: 'flex', alignItems: 'center' }}>
<VolumeControl playbackVolume={playbackVolume} setPlaybackVolume={setPlaybackVolume} />
{shouldShowPlaybackStreamSelector && <PlaybackStreamSelector subtitleStreams={subtitleStreams} videoStreams={videoStreams} audioStreams={audioStreams} activeSubtitleStreamIndex={activeSubtitleStreamIndex} activeVideoStreamIndex={activeVideoStreamIndex} activeAudioStreamIndex={activeAudioStreamIndex} onActiveSubtitleChange={onActiveSubtitleChange} onActiveVideoStreamChange={onActiveVideoStreamChange} onActiveAudioStreamChange={onActiveAudioStreamChange} />}
{compatPlayerEnabled && <div style={{ color: 'white', opacity: 0.7, padding: '.5em' }} role="button" onClick={() => incrementMediaSourceQuality()} title={t('Select playback quality')}>{mediaSourceQualities[mediaSourceQuality]}</div>}
{!showRightBar && (
<FaAngleLeft
title={t('Show sidebar')}
size={30}
role="button"
style={{ marginRight: 10, color: 'var(--gray12)', opacity: 0.7 }}
onClick={toggleSegmentsList}
/>
)}
</div>
)}
<AnimatePresence>
{working && <Working text={working.text} cutProgress={cutProgress} onAbortClick={handleAbortWorkingClick} />}
</AnimatePresence>
{tunerVisible && <ValueTuners type={tunerVisible} onFinished={() => setTunerVisible(undefined)} />}
</div>
<AnimatePresence>
{showRightBar && isFileOpened && filePath != null && (
<SegmentList
width={rightBarWidth}
currentSegIndex={currentSegIndexSafe}
apparentCutSegments={apparentCutSegments}
inverseCutSegments={inverseCutSegments}
getFrameCount={getFrameCount}
formatTimecode={formatTimecode}
onSegClick={setCurrentSegIndex}
updateSegOrder={updateSegOrder}
updateSegOrders={updateSegOrders}
onLabelSegment={onLabelSegment}
currentCutSeg={currentCutSeg}
segmentAtCursor={segmentAtCursor}
addSegment={addSegment}
onDuplicateSegmentClick={duplicateSegment}
removeCutSegment={removeCutSegment}
onRemoveSelected={removeSelectedSegments}
toggleSegmentsList={toggleSegmentsList}
splitCurrentSegment={splitCurrentSegment}
isSegmentSelected={isSegmentSelected}
selectedSegments={selectedSegmentsOrInverse}
onSelectSingleSegment={selectOnlySegment}
onToggleSegmentSelected={toggleSegmentSelected}
onDeselectAllSegments={deselectAllSegments}
onSelectAllSegments={selectAllSegments}
onInvertSelectedSegments={invertSelectedSegments}
onExtractSegmentFramesAsImages={extractSegmentFramesAsImages}
jumpSegStart={jumpSegStart}
jumpSegEnd={jumpSegEnd}
onSelectSegmentsByLabel={onSelectSegmentsByLabel}
onSelectSegmentsByExpr={onSelectSegmentsByExpr}
onLabelSelectedSegments={onLabelSelectedSegments}
updateSegAtIndex={updateSegAtIndex}
editingSegmentTags={editingSegmentTags}
editingSegmentTagsSegmentIndex={editingSegmentTagsSegmentIndex}
setEditingSegmentTags={setEditingSegmentTags}
setEditingSegmentTagsSegmentIndex={setEditingSegmentTagsSegmentIndex}
onEditSegmentTags={onEditSegmentTags}
/>
)}
</AnimatePresence>
</div> </div>
<AnimatePresence> <div className="no-user-select" style={bottomStyle}>
{showRightBar && isFileOpened && filePath != null && ( <Timeline
<SegmentList shouldShowKeyframes={shouldShowKeyframes}
width={rightBarWidth} waveforms={waveforms}
currentSegIndex={currentSegIndexSafe} shouldShowWaveform={shouldShowWaveform}
apparentCutSegments={apparentCutSegments} waveformEnabled={waveformEnabled}
inverseCutSegments={inverseCutSegments} showThumbnails={showThumbnails}
getFrameCount={getFrameCount} neighbouringKeyFrames={neighbouringKeyFrames}
thumbnails={thumbnailsSorted}
playerTime={playerTime}
commandedTime={commandedTime}
relevantTime={relevantTime}
commandedTimeRef={commandedTimeRef}
startTimeOffset={startTimeOffset}
zoom={zoom}
seekAbs={userSeekAbs}
durationSafe={durationSafe}
apparentCutSegments={apparentCutSegments}
setCurrentSegIndex={setCurrentSegIndex}
currentSegIndexSafe={currentSegIndexSafe}
inverseCutSegments={inverseCutSegments}
formatTimecode={formatTimecode}
formatTimeAndFrames={formatTimeAndFrames}
onZoomWindowStartTimeChange={setZoomWindowStartTime}
playing={playing}
isFileOpened={isFileOpened}
onWheel={onTimelineWheel}
goToTimecode={goToTimecode}
isSegmentSelected={isSegmentSelected}
/>
<BottomBar
zoom={zoom}
setZoom={setZoom}
timelineToggleComfortZoom={timelineToggleComfortZoom}
hasVideo={hasVideo}
isRotationSet={isRotationSet}
rotation={rotation}
areWeCutting={areWeCutting}
increaseRotation={increaseRotation}
cleanupFilesDialog={cleanupFilesDialog}
captureSnapshot={captureSnapshot}
onExportPress={onExportPress}
segmentsToExport={segmentsToExport}
seekAbs={userSeekAbs}
currentSegIndexSafe={currentSegIndexSafe}
cutSegments={cutSegments}
currentCutSeg={currentCutSeg}
selectedSegments={selectedSegments}
setCutStart={setCutStart}
setCutEnd={setCutEnd}
setCurrentSegIndex={setCurrentSegIndex}
jumpCutEnd={jumpCutEnd}
jumpCutStart={jumpCutStart}
jumpTimelineStart={jumpTimelineStart}
jumpTimelineEnd={jumpTimelineEnd}
startTimeOffset={startTimeOffset}
setCutTime={setCutTime}
currentApparentCutSeg={currentApparentCutSeg}
playing={playing}
shortStep={shortStep}
seekClosestKeyframe={seekClosestKeyframe}
togglePlay={togglePlay}
showThumbnails={showThumbnails}
toggleShowThumbnails={toggleShowThumbnails}
toggleWaveformMode={toggleWaveformMode}
waveformMode={waveformMode}
hasAudio={hasAudio}
keyframesEnabled={keyframesEnabled}
toggleShowKeyframes={toggleShowKeyframes}
detectedFps={detectedFps}
toggleLoopSelectedSegments={toggleLoopSelectedSegments}
isFileOpened={isFileOpened}
darkMode={darkMode}
setDarkMode={setDarkMode}
outputPlaybackRate={outputPlaybackRate}
setOutputPlaybackRate={setOutputPlaybackRate}
formatTimecode={formatTimecode}
parseTimecode={parseTimecode}
/>
</div>
<ExportConfirm areWeCutting={areWeCutting} nonFilteredSegmentsOrInverse={nonFilteredSegmentsOrInverse} selectedSegments={selectedSegmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} onShowStreamsSelectorClick={handleShowStreamsSelectorClick} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} needSmartCut={needSmartCut} mergedOutFileName={mergedOutFileName} setMergedOutFileName={setMergedOutFileName} smartCutBitrate={smartCutBitrate} setSmartCutBitrate={setSmartCutBitrate} />
<Sheet visible={streamsSelectorShown} onClosePress={() => setStreamsSelectorShown(false)} maxWidth={1000}>
{mainStreams && filePath != null && (
<StreamsSelector
mainFilePath={filePath}
mainFileFormatData={mainFileFormatData}
mainFileChapters={mainFileChapters}
allFilesMeta={allFilesMeta}
externalFilesMeta={externalFilesMeta}
setExternalFilesMeta={setExternalFilesMeta}
showAddStreamSourceDialog={showIncludeExternalStreamsDialog}
mainFileStreams={mainStreams}
isCopyingStreamId={isCopyingStreamId}
toggleCopyStreamId={toggleCopyStreamId}
setCopyStreamIdsForPath={setCopyStreamIdsForPath}
onExtractAllStreamsPress={extractAllStreams}
onExtractStreamPress={extractSingleStream}
shortestFlag={shortestFlag}
setShortestFlag={setShortestFlag}
nonCopiedExtraStreams={nonCopiedExtraStreams}
customTagsByFile={customTagsByFile}
setCustomTagsByFile={setCustomTagsByFile}
paramsByStreamId={paramsByStreamId}
updateStreamParams={updateStreamParams}
formatTimecode={formatTimecode} formatTimecode={formatTimecode}
onSegClick={setCurrentSegIndex} loadSubtitleTrackToSegments={loadSubtitleTrackToSegments}
updateSegOrder={updateSegOrder}
updateSegOrders={updateSegOrders}
onLabelSegment={onLabelSegment}
currentCutSeg={currentCutSeg}
segmentAtCursor={segmentAtCursor}
addSegment={addSegment}
onDuplicateSegmentClick={duplicateSegment}
removeCutSegment={removeCutSegment}
onRemoveSelected={removeSelectedSegments}
toggleSegmentsList={toggleSegmentsList}
splitCurrentSegment={splitCurrentSegment}
isSegmentSelected={isSegmentSelected}
selectedSegments={selectedSegmentsOrInverse}
onSelectSingleSegment={selectOnlySegment}
onToggleSegmentSelected={toggleSegmentSelected}
onDeselectAllSegments={deselectAllSegments}
onSelectAllSegments={selectAllSegments}
onInvertSelectedSegments={invertSelectedSegments}
onExtractSegmentFramesAsImages={extractSegmentFramesAsImages}
jumpSegStart={jumpSegStart}
jumpSegEnd={jumpSegEnd}
onSelectSegmentsByLabel={onSelectSegmentsByLabel}
onSelectSegmentsByExpr={onSelectSegmentsByExpr}
onLabelSelectedSegments={onLabelSelectedSegments}
updateSegAtIndex={updateSegAtIndex}
editingSegmentTags={editingSegmentTags}
editingSegmentTagsSegmentIndex={editingSegmentTagsSegmentIndex}
setEditingSegmentTags={setEditingSegmentTags}
setEditingSegmentTagsSegmentIndex={setEditingSegmentTagsSegmentIndex}
onEditSegmentTags={onEditSegmentTags}
/> />
)} )}
</AnimatePresence> </Sheet>
</div>
<div className="no-user-select" style={bottomStyle}> <LastCommandsSheet
<Timeline visible={lastCommandsVisible}
shouldShowKeyframes={shouldShowKeyframes} onTogglePress={toggleLastCommands}
waveforms={waveforms} ffmpegCommandLog={ffmpegCommandLog}
shouldShowWaveform={shouldShowWaveform}
waveformEnabled={waveformEnabled}
showThumbnails={showThumbnails}
neighbouringKeyFrames={neighbouringKeyFrames}
thumbnails={thumbnailsSorted}
playerTime={playerTime}
commandedTime={commandedTime}
relevantTime={relevantTime}
commandedTimeRef={commandedTimeRef}
startTimeOffset={startTimeOffset}
zoom={zoom}
seekAbs={userSeekAbs}
durationSafe={durationSafe}
apparentCutSegments={apparentCutSegments}
setCurrentSegIndex={setCurrentSegIndex}
currentSegIndexSafe={currentSegIndexSafe}
inverseCutSegments={inverseCutSegments}
formatTimecode={formatTimecode}
formatTimeAndFrames={formatTimeAndFrames}
onZoomWindowStartTimeChange={setZoomWindowStartTime}
playing={playing}
isFileOpened={isFileOpened}
onWheel={onTimelineWheel}
goToTimecode={goToTimecode}
isSegmentSelected={isSegmentSelected}
/> />
<BottomBar <Sheet visible={settingsVisible} onClosePress={toggleSettings}>
zoom={zoom} <Settings
setZoom={setZoom} onTunerRequested={onTunerRequested}
timelineToggleComfortZoom={timelineToggleComfortZoom} onKeyboardShortcutsDialogRequested={toggleKeyboardShortcuts}
hasVideo={hasVideo} askForCleanupChoices={askForCleanupChoices}
isRotationSet={isRotationSet} toggleStoreProjectInWorkingDir={toggleStoreProjectInWorkingDir}
rotation={rotation} simpleMode={simpleMode}
areWeCutting={areWeCutting} clearOutDir={clearOutDir}
increaseRotation={increaseRotation}
cleanupFilesDialog={cleanupFilesDialog}
captureSnapshot={captureSnapshot}
onExportPress={onExportPress}
segmentsToExport={segmentsToExport}
seekAbs={userSeekAbs}
currentSegIndexSafe={currentSegIndexSafe}
cutSegments={cutSegments}
currentCutSeg={currentCutSeg}
selectedSegments={selectedSegments}
setCutStart={setCutStart}
setCutEnd={setCutEnd}
setCurrentSegIndex={setCurrentSegIndex}
jumpCutEnd={jumpCutEnd}
jumpCutStart={jumpCutStart}
jumpTimelineStart={jumpTimelineStart}
jumpTimelineEnd={jumpTimelineEnd}
startTimeOffset={startTimeOffset}
setCutTime={setCutTime}
currentApparentCutSeg={currentApparentCutSeg}
playing={playing}
shortStep={shortStep}
seekClosestKeyframe={seekClosestKeyframe}
togglePlay={togglePlay}
showThumbnails={showThumbnails}
toggleShowThumbnails={toggleShowThumbnails}
toggleWaveformMode={toggleWaveformMode}
waveformMode={waveformMode}
hasAudio={hasAudio}
keyframesEnabled={keyframesEnabled}
toggleShowKeyframes={toggleShowKeyframes}
detectedFps={detectedFps}
toggleLoopSelectedSegments={toggleLoopSelectedSegments}
isFileOpened={isFileOpened}
darkMode={darkMode}
setDarkMode={setDarkMode}
outputPlaybackRate={outputPlaybackRate}
setOutputPlaybackRate={setOutputPlaybackRate}
formatTimecode={formatTimecode}
parseTimecode={parseTimecode}
/>
</div>
<ExportConfirm areWeCutting={areWeCutting} nonFilteredSegmentsOrInverse={nonFilteredSegmentsOrInverse} selectedSegments={selectedSegmentsOrInverse} segmentsToExport={segmentsToExport} willMerge={willMerge} visible={exportConfirmVisible} onClosePress={closeExportConfirm} onExportConfirm={onExportConfirm} renderOutFmt={renderOutFmt} outputDir={outputDir} numStreamsTotal={numStreamsTotal} numStreamsToCopy={numStreamsToCopy} onShowStreamsSelectorClick={handleShowStreamsSelectorClick} outFormat={fileFormat} setOutSegTemplate={setOutSegTemplate} outSegTemplate={outSegTemplateOrDefault} generateOutSegFileNames={generateOutSegFileNames} currentSegIndexSafe={currentSegIndexSafe} mainCopiedThumbnailStreams={mainCopiedThumbnailStreams} needSmartCut={needSmartCut} mergedOutFileName={mergedOutFileName} setMergedOutFileName={setMergedOutFileName} smartCutBitrate={smartCutBitrate} setSmartCutBitrate={setSmartCutBitrate} />
<Sheet visible={streamsSelectorShown} onClosePress={() => setStreamsSelectorShown(false)} maxWidth={1000}>
{mainStreams && filePath != null && (
<StreamsSelector
mainFilePath={filePath}
mainFileFormatData={mainFileFormatData}
mainFileChapters={mainFileChapters}
allFilesMeta={allFilesMeta}
externalFilesMeta={externalFilesMeta}
setExternalFilesMeta={setExternalFilesMeta}
showAddStreamSourceDialog={showIncludeExternalStreamsDialog}
mainFileStreams={mainStreams}
isCopyingStreamId={isCopyingStreamId}
toggleCopyStreamId={toggleCopyStreamId}
setCopyStreamIdsForPath={setCopyStreamIdsForPath}
onExtractAllStreamsPress={extractAllStreams}
onExtractStreamPress={extractSingleStream}
shortestFlag={shortestFlag}
setShortestFlag={setShortestFlag}
nonCopiedExtraStreams={nonCopiedExtraStreams}
customTagsByFile={customTagsByFile}
setCustomTagsByFile={setCustomTagsByFile}
paramsByStreamId={paramsByStreamId}
updateStreamParams={updateStreamParams}
formatTimecode={formatTimecode}
loadSubtitleTrackToSegments={loadSubtitleTrackToSegments}
/> />
)} </Sheet>
</Sheet>
<LastCommandsSheet <ConcatDialog isShown={batchFiles.length > 0 && concatDialogVisible} onHide={() => setConcatDialogVisible(false)} paths={batchFilePaths} onConcat={userConcatFiles} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} />
visible={lastCommandsVisible}
onTogglePress={toggleLastCommands}
ffmpegCommandLog={ffmpegCommandLog}
/>
<Sheet visible={settingsVisible} onClosePress={toggleSettings}> <KeyboardShortcuts isShown={keyboardShortcutsVisible} onHide={() => setKeyboardShortcutsVisible(false)} keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} />
<Settings </div>
onTunerRequested={onTunerRequested} </ThemeProvider>
onKeyboardShortcutsDialogRequested={toggleKeyboardShortcuts} </UserSettingsContext.Provider>
askForCleanupChoices={askForCleanupChoices} </SegColorsContext.Provider>
toggleStoreProjectInWorkingDir={toggleStoreProjectInWorkingDir}
simpleMode={simpleMode}
clearOutDir={clearOutDir}
/>
</Sheet>
<ConcatDialog isShown={batchFiles.length > 0 && concatDialogVisible} onHide={() => setConcatDialogVisible(false)} paths={batchFilePaths} onConcat={userConcatFiles} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} /> <div id="swal2-container-wrapper" className={darkMode ? 'dark-theme' : undefined} style={{ color: 'var(--gray12)', background: 'var(--gray1)' }} />
</>
<KeyboardShortcuts isShown={keyboardShortcutsVisible} onHide={() => setKeyboardShortcutsVisible(false)} keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} />
</div>
</ThemeProvider>
</UserSettingsContext.Provider>
</SegColorsContext.Provider>
); );
} }

View File

@ -16,6 +16,7 @@ import { getActiveDisposition, attachedPicDisposition } from './util/streams';
import TagEditor from './components/TagEditor'; import TagEditor from './components/TagEditor';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe'; import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
import { CustomTagsByFile, FilesMeta, FormatTimecode, ParamsByStreamId, StreamParams } from './types'; import { CustomTagsByFile, FilesMeta, FormatTimecode, ParamsByStreamId, StreamParams } from './types';
import useUserSettings from './hooks/useUserSettings';
const dispositionOptions = ['default', 'dub', 'original', 'comment', 'lyrics', 'karaoke', 'forced', 'hearing_impaired', 'visual_impaired', 'clean_effects', 'attached_pic', 'captions', 'descriptions', 'dependent', 'metadata']; const dispositionOptions = ['default', 'dub', 'original', 'comment', 'lyrics', 'karaoke', 'forced', 'hearing_impaired', 'visual_impaired', 'clean_effects', 'attached_pic', 'captions', 'descriptions', 'dependent', 'metadata'];
@ -147,13 +148,9 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat
); );
}); });
function onInfoClick(json: unknown, title: string) {
showJson5Dialog({ title, json });
}
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copyStream, fileDuration, setEditingStream, onExtractStreamPress, paramsByStreamId, updateStreamParams, formatTimecode, loadSubtitleTrackToSegments }: { const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copyStream, fileDuration, setEditingStream, onExtractStreamPress, paramsByStreamId, updateStreamParams, formatTimecode, loadSubtitleTrackToSegments, onInfoClick }: {
filePath: string, stream: FFprobeStream, onToggle: (a: number) => void, batchSetCopyStreamIds: (filter: (a: FFprobeStream) => boolean, enabled: boolean) => void, copyStream: boolean, fileDuration: number | undefined, setEditingStream: (a: EditingStream) => void, onExtractStreamPress?: () => void, paramsByStreamId: ParamsByStreamId, updateStreamParams: UpdateStreamParams, formatTimecode: FormatTimecode, loadSubtitleTrackToSegments?: (index: number) => void, filePath: string, stream: FFprobeStream, onToggle: (a: number) => void, batchSetCopyStreamIds: (filter: (a: FFprobeStream) => boolean, enabled: boolean) => void, copyStream: boolean, fileDuration: number | undefined, setEditingStream: (a: EditingStream) => void, onExtractStreamPress?: () => void, paramsByStreamId: ParamsByStreamId, updateStreamParams: UpdateStreamParams, formatTimecode: FormatTimecode, loadSubtitleTrackToSegments?: (index: number) => void, onInfoClick: (json: unknown, title: string) => void,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -283,8 +280,8 @@ const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copySt
); );
}); });
function FileHeading({ path, formatData, chapters, onTrashClick, onEditClick, setCopyAllStreams, onExtractAllStreamsPress }: { function FileHeading({ path, formatData, chapters, onTrashClick, onEditClick, setCopyAllStreams, onExtractAllStreamsPress, onInfoClick }: {
path: string, formatData: FFprobeFormat | undefined, chapters?: FFprobeChapter[] | undefined, onTrashClick?: (() => void) | undefined, onEditClick?: (() => void) | undefined, setCopyAllStreams: (a: boolean) => void, onExtractAllStreamsPress?: () => Promise<void>, path: string, formatData: FFprobeFormat | undefined, chapters?: FFprobeChapter[] | undefined, onTrashClick?: (() => void) | undefined, onEditClick?: (() => void) | undefined, setCopyAllStreams: (a: boolean) => void, onExtractAllStreamsPress?: () => Promise<void>, onInfoClick: (json: unknown, title: string) => void,
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -360,6 +357,7 @@ function StreamsSelector({
const [editingStream, setEditingStream] = useState<EditingStream>(); const [editingStream, setEditingStream] = useState<EditingStream>();
const [editingTag, setEditingTag] = useState<string>(); const [editingTag, setEditingTag] = useState<string>();
const { t } = useTranslation(); const { t } = useTranslation();
const { darkMode } = useUserSettings();
function getFormatDuration(formatData: FFprobeFormat | undefined) { function getFormatDuration(formatData: FFprobeFormat | undefined) {
if (!formatData || !formatData.duration) return undefined; if (!formatData || !formatData.duration) return undefined;
@ -394,13 +392,17 @@ function StreamsSelector({
const externalFilesEntries = Object.entries(externalFilesMeta); const externalFilesEntries = Object.entries(externalFilesMeta);
const onInfoClick = useCallback((json: unknown, title: string) => {
showJson5Dialog({ title, json, darkMode });
}, [darkMode]);
return ( return (
<> <>
<p style={{ margin: '.5em 2em .5em 1em' }}>{t('Click to select which tracks to keep when exporting:')}</p> <p style={{ margin: '.5em 2em .5em 1em' }}>{t('Click to select which tracks to keep when exporting:')}</p>
<div style={fileStyle}> <div style={fileStyle}>
{/* We only support editing main file metadata for now */} {/* 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} /> <FileHeading onInfoClick={onInfoClick} path={mainFilePath} formatData={mainFileFormatData} chapters={mainFileChapters} onEditClick={() => setEditingFile(mainFilePath)} setCopyAllStreams={(enabled) => setCopyAllStreamsForPath(mainFilePath, enabled)} onExtractAllStreamsPress={onExtractAllStreamsPress} />
<table style={tableStyle}> <table style={tableStyle}>
<Thead /> <Thead />
@ -420,6 +422,7 @@ function StreamsSelector({
updateStreamParams={updateStreamParams} updateStreamParams={updateStreamParams}
formatTimecode={formatTimecode} formatTimecode={formatTimecode}
loadSubtitleTrackToSegments={loadSubtitleTrackToSegments} loadSubtitleTrackToSegments={loadSubtitleTrackToSegments}
onInfoClick={onInfoClick}
/> />
))} ))}
</tbody> </tbody>
@ -428,7 +431,7 @@ function StreamsSelector({
{externalFilesEntries.map(([path, { streams: externalFileStreams, formatData }]) => ( {externalFilesEntries.map(([path, { streams: externalFileStreams, formatData }]) => (
<div key={path} style={fileStyle}> <div key={path} style={fileStyle}>
<FileHeading path={path} formatData={formatData} onTrashClick={() => removeFile(path)} setCopyAllStreams={(enabled) => setCopyAllStreamsForPath(path, enabled)} /> <FileHeading path={path} formatData={formatData} onTrashClick={() => removeFile(path)} setCopyAllStreams={(enabled) => setCopyAllStreamsForPath(path, enabled)} onInfoClick={onInfoClick} />
<table style={tableStyle}> <table style={tableStyle}>
<Thead /> <Thead />
@ -446,6 +449,7 @@ function StreamsSelector({
paramsByStreamId={paramsByStreamId} paramsByStreamId={paramsByStreamId}
updateStreamParams={updateStreamParams} updateStreamParams={updateStreamParams}
formatTimecode={formatTimecode} formatTimecode={formatTimecode}
onInfoClick={onInfoClick}
/> />
))} ))}
</tbody> </tbody>

View File

@ -9,4 +9,5 @@
padding: 0 .5em 0 .3em; padding: 0 .5em 0 .3em;
outline: .05em solid var(--gray8); outline: .05em solid var(--gray8);
border: .05em solid var(--gray7); border: .05em solid var(--gray7);
cursor: pointer;
} }

View File

@ -0,0 +1,33 @@
.CheckboxRoot {
all: unset
}
.CheckboxRoot {
background-color: var(--gray8);
width: 1em;
height: 1em;
border-radius: .2em;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px var(--gray1);
}
.CheckboxRoot:hover {
background-color: var(--gray9);
}
.CheckboxRoot:focus {
box-shadow: 0 0 0 2px var(--gray1);
}
.CheckboxIndicator {
color: var(--gray12);
}
.CheckboxRoot[data-disabled]{
opacity: .5;
}
.Label {
padding-left: .5em;
line-height: 1.2;
}

View File

@ -0,0 +1,25 @@
import { useId } from 'react';
import { Root, Indicator, CheckboxProps } from '@radix-ui/react-checkbox';
import { FaCheck } from 'react-icons/fa';
import classes from './Checkbox.module.css';
export default function Checkbox({ label, disabled, style, ...props }: CheckboxProps & { label?: string | undefined }) {
const id = useId();
return (
<div style={{ display: 'flex', alignItems: 'center', ...style }}>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<Root className={classes['CheckboxRoot']} disabled={disabled} {...props} id={id}>
<Indicator className={classes['CheckboxIndicator']}>
<FaCheck style={{ fontSize: '.7em' }} />
</Indicator>
</Root>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label className={classes['Label']} htmlFor={id} style={{ opacity: disabled ? 0.5 : undefined }}>
{label}
</label>
</div>
);
}

View File

@ -1,13 +1,13 @@
import { memo, useState, useCallback, useEffect, useMemo, CSSProperties } from 'react'; import { memo, useState, useCallback, useEffect, useMemo, CSSProperties } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TextInput, IconButton, Alert, Checkbox, Dialog, Button, Paragraph, CogIcon } from 'evergreen-ui'; import { IconButton, Checkbox as EvergreenCheckbox, Dialog, Paragraph } from 'evergreen-ui';
import { AiOutlineMergeCells } from 'react-icons/ai'; import { AiOutlineMergeCells } from 'react-icons/ai';
import { FaQuestionCircle, FaExclamationTriangle } from 'react-icons/fa'; import { FaQuestionCircle, FaExclamationTriangle, FaCog } from 'react-icons/fa';
import i18n from 'i18next'; import i18n from 'i18next';
import withReactContent from 'sweetalert2-react-content';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
import Checkbox from './Checkbox';
import Swal from '../swal'; import { ReactSwal } from '../swal';
import { readFileMeta, getSmarterOutFormat } from '../ffmpeg'; import { readFileMeta, getSmarterOutFormat } from '../ffmpeg';
import useFileFormatState from '../hooks/useFileFormatState'; import useFileFormatState from '../hooks/useFileFormatState';
import OutputFormatSelect from './OutputFormatSelect'; import OutputFormatSelect from './OutputFormatSelect';
@ -15,17 +15,23 @@ import useUserSettings from '../hooks/useUserSettings';
import { isMov } from '../util/streams'; import { isMov } from '../util/streams';
import { getOutFileExtension, getSuffixedFileName } from '../util'; import { getOutFileExtension, getSuffixedFileName } from '../util';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../../ffprobe'; import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../../ffprobe';
import Sheet from './Sheet';
import TextInput from './TextInput';
import Button from './Button';
const { basename } = window.require('path'); const { basename } = window.require('path');
const ReactSwal = withReactContent(Swal);
const containerStyle: CSSProperties = { color: 'black' };
const rowStyle: CSSProperties = { const rowStyle: CSSProperties = {
color: 'black', fontSize: 14, margin: '4px 0px', overflowY: 'auto', whiteSpace: 'nowrap', fontSize: '1em', margin: '4px 0px', overflowY: 'auto', whiteSpace: 'nowrap',
}; };
function Alert({ text }: { text: string }) {
return (
<div style={{ marginBottom: '1em' }}><FaExclamationTriangle style={{ color: 'var(--orange8)', fontSize: '1.3em', verticalAlign: 'middle', marginRight: '.2em' }} /> {text}</div>
);
}
function ConcatDialog({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles }: { function ConcatDialog({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles }: {
isShown: boolean, onHide: () => void, paths: string[], onConcat: (a: { paths: string[], includeAllStreams: boolean, streams: FFprobeStream[], outFileName: string, fileFormat: string, clearBatchFilesAfterConcat: boolean }) => Promise<void>, alwaysConcatMultipleFiles: boolean, setAlwaysConcatMultipleFiles: (a: boolean) => void, isShown: boolean, onHide: () => void, paths: string[], onConcat: (a: { paths: string[], includeAllStreams: boolean, streams: FFprobeStream[], outFileName: string, fileFormat: string, clearBatchFilesAfterConcat: boolean }) => Promise<void>, alwaysConcatMultipleFiles: boolean, setAlwaysConcatMultipleFiles: (a: boolean) => void,
}) { }) {
@ -167,72 +173,65 @@ function ConcatDialog({ isShown, onHide, paths, onConcat, alwaysConcatMultipleFi
return ( return (
<> <>
<Dialog <Sheet visible={isShown} onClosePress={onHide} maxWidth="100%" style={{ padding: '0 2em' }}>
title={t('Merge/concatenate files')} <h2>{t('Merge/concatenate files')}</h2>
shouldCloseOnOverlayClick={false}
isShown={isShown} <div style={{ marginBottom: '1em' }}>
onCloseComplete={onHide} <div style={{ whiteSpace: 'pre-wrap', fontSize: '.9em', marginBottom: '1em' }}>
topOffset="3vh"
width="90vw"
footer={(
<>
<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={CogIcon} onClick={() => setSettingsVisible(true)}>{t('Options')}</Button>
{fileFormat && detectedFileFormat ? (
<OutputFormatSelect style={{ height: 30, maxWidth: 180 }} detectedFileFormat={detectedFileFormat} fileFormat={fileFormat} onOutputFormatUserChange={onOutputFormatUserChange} />
) : (
<Button disabled isLoading>{t('Loading')}</Button>
)}
<Button iconBefore={<AiOutlineMergeCells />} isLoading={detectedFileFormat == null} disabled={!isOutFileNameValid} appearance="primary" onClick={onConcatClick}>{t('Merge!')}</Button>
</div>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'flex-end' }}>
<Paragraph marginRight=".5em">{t('Output file name')}:</Paragraph>
<TextInput value={outFileName || ''} onChange={(e) => setOutFileName(e.target.value)} />
</div>
</>
)}
>
<div style={containerStyle}>
<div style={{ whiteSpace: 'pre-wrap', fontSize: 14, marginBottom: 10 }}>
{t('This dialog can be used to concatenate files in series, e.g. one after the other:\n[file1][file2][file3]\nIt can NOT be used for merging tracks in parallell (like adding an audio track to a video).\nMake sure all files are of the exact same codecs & codec parameters (fps, resolution etc).')} {t('This dialog can be used to concatenate files in series, e.g. one after the other:\n[file1][file2][file3]\nIt can NOT be used for merging tracks in parallell (like adding an audio track to a video).\nMake sure all files are of the exact same codecs & codec parameters (fps, resolution etc).')}
</div> </div>
<div> <div style={{ backgroundColor: 'var(--gray1)', borderRadius: '.1em' }}>
{paths.map((path, index) => ( {paths.map((path, index) => (
<div key={path} style={rowStyle} title={path}> <div key={path} style={rowStyle} title={path}>
<div> <div>
{index + 1} {index + 1}
{'. '} {'. '}
<span style={{ color: 'rgba(0,0,0,0.7)' }}>{basename(path)}</span> <span>{basename(path)}</span>
{!allFilesMetaCache[path] && <FaQuestionCircle color="#996A13" style={{ marginLeft: 10 }} />} {!allFilesMetaCache[path] && <FaQuestionCircle style={{ color: 'var(--orange8)', verticalAlign: 'middle', marginLeft: '1em' }} />}
{problemsByFile[path] && <IconButton appearance="minimal" icon={FaExclamationTriangle} onClick={() => onProblemsByFileClick(path)} title={i18n.t('Mismatches detected')} color="#996A13" style={{ marginLeft: 10 }} />} {problemsByFile[path] && <IconButton appearance="minimal" icon={FaExclamationTriangle} onClick={() => onProblemsByFileClick(path)} title={i18n.t('Mismatches detected')} style={{ color: 'var(--orange8)', marginLeft: '1em' }} />}
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', marginBottom: '.5em', gap: '.5em' }}>
<Checkbox checked={enableReadFileMeta} onCheckedChange={(checked) => setEnableReadFileMeta(!!checked)} label={t('Check compatibility')} />
<Button onClick={() => setSettingsVisible(true)} style={{ height: '1.7em' }}><FaCog style={{ fontSize: '1em', verticalAlign: 'middle' }} /> {t('Options')}</Button>
{fileFormat && detectedFileFormat && (
<OutputFormatSelect style={{ height: '1.7em', maxWidth: '20em' }} detectedFileFormat={detectedFileFormat} fileFormat={fileFormat} onOutputFormatUserChange={onOutputFormatUserChange} />
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'flex-end', marginBottom: '1em' }}>
<div style={{ marginRight: '.5em' }}>{t('Output file name')}:</div>
<TextInput value={outFileName || ''} onChange={(e) => setOutFileName(e.target.value)} />
<Button disabled={detectedFileFormat == null || !isOutFileNameValid} onClick={onConcatClick} style={{ fontSize: '1.3em', padding: '0 .3em', marginLeft: '1em' }}><AiOutlineMergeCells style={{ fontSize: '1.4em', verticalAlign: 'middle' }} /> {t('Merge!')}</Button>
</div>
{enableReadFileMeta && (!allFilesMeta || Object.values(problemsByFile).length > 0) && ( {enableReadFileMeta && (!allFilesMeta || Object.values(problemsByFile).length > 0) && (
<Alert intent="warning">{t('A mismatch was detected in at least one file. You may proceed, but the resulting file might not be playable.')}</Alert> <Alert text={t('A mismatch was detected in at least one file. You may proceed, but the resulting file might not be playable.')} />
)} )}
{!enableReadFileMeta && ( {!enableReadFileMeta && (
<Alert intent="warning">{t('File compatibility check is not enabled, so the merge operation might not produce a valid output. Enable "Check compatibility" below to check file compatibility before merging.')}</Alert> <Alert text={t('File compatibility check is not enabled, so the merge operation might not produce a valid output. Enable "Check compatibility" below to check file compatibility before merging.')} />
)} )}
</Dialog> </Sheet>
<Dialog isShown={settingsVisible} onCloseComplete={() => setSettingsVisible(false)} title={t('Merge options')} hasCancel={false} confirmLabel={t('Close')}> <Dialog isShown={settingsVisible} onCloseComplete={() => setSettingsVisible(false)} title={t('Merge options')} hasCancel={false} confirmLabel={t('Close')}>
<Checkbox checked={includeAllStreams} onChange={(e) => setIncludeAllStreams(e.target.checked)} label={`${t('Include all tracks?')} ${t('If this is checked, all audio/video/subtitle/data tracks will be included. This may not always work for all file types. If not checked, only default streams will be included.')}`} /> <EvergreenCheckbox checked={includeAllStreams} onChange={(e) => setIncludeAllStreams(e.target.checked)} label={`${t('Include all tracks?')} ${t('If this is checked, all audio/video/subtitle/data tracks will be included. This may not always work for all file types. If not checked, only default streams will be included.')}`} />
<Checkbox checked={preserveMetadataOnMerge} onChange={(e) => setPreserveMetadataOnMerge(e.target.checked)} label={t('Preserve original metadata when merging? (slow)')} /> <EvergreenCheckbox checked={preserveMetadataOnMerge} onChange={(e) => setPreserveMetadataOnMerge(e.target.checked)} label={t('Preserve original metadata when merging? (slow)')} />
{fileFormat != null && isMov(fileFormat) && <Checkbox checked={preserveMovData} onChange={(e) => setPreserveMovData(e.target.checked)} label={t('Preserve all MP4/MOV metadata?')} />} {fileFormat != null && isMov(fileFormat) && <EvergreenCheckbox checked={preserveMovData} onChange={(e) => setPreserveMovData(e.target.checked)} label={t('Preserve all MP4/MOV metadata?')} />}
<Checkbox checked={segmentsToChapters} onChange={(e) => setSegmentsToChapters(e.target.checked)} label={t('Create chapters from merged segments? (slow)')} /> <EvergreenCheckbox checked={segmentsToChapters} onChange={(e) => setSegmentsToChapters(e.target.checked)} label={t('Create chapters from merged segments? (slow)')} />
<Checkbox checked={alwaysConcatMultipleFiles} onChange={(e) => setAlwaysConcatMultipleFiles(e.target.checked)} label={t('Always open this dialog when opening multiple files')} /> <EvergreenCheckbox checked={alwaysConcatMultipleFiles} onChange={(e) => setAlwaysConcatMultipleFiles(e.target.checked)} label={t('Always open this dialog when opening multiple files')} />
<Checkbox checked={clearBatchFilesAfterConcat} onChange={(e) => setClearBatchFilesAfterConcat(e.target.checked)} label={t('Clear batch file list after merge')} /> <EvergreenCheckbox checked={clearBatchFilesAfterConcat} onChange={(e) => setClearBatchFilesAfterConcat(e.target.checked)} label={t('Clear batch file list after merge')} />
<Paragraph>{t('Note that also other settings from the normal export dialog apply to this merge function. For more information about all options, see the export dialog.')}</Paragraph> <Paragraph>{t('Note that also other settings from the normal export dialog apply to this merge function. For more information about all options, see the export dialog.')}</Paragraph>
</Dialog> </Dialog>

View File

@ -5,7 +5,7 @@ import { FaRegCheckCircle } from 'react-icons/fa';
import i18n from 'i18next'; import i18n from 'i18next';
import { useTranslation, Trans } from 'react-i18next'; import { useTranslation, Trans } from 'react-i18next';
import { IoIosHelpCircle } from 'react-icons/io'; import { IoIosHelpCircle } from 'react-icons/io';
import { SweetAlertIcon } from 'sweetalert2'; import type { SweetAlertIcon } from 'sweetalert2';
import ExportButton from './ExportButton'; import ExportButton from './ExportButton';
import ExportModeButton from './ExportModeButton'; import ExportModeButton from './ExportModeButton';

View File

@ -1,6 +1,6 @@
import { memo, Fragment, useEffect, useMemo, useCallback, useState, ReactNode, SetStateAction, Dispatch, useRef } from 'react'; import { memo, Fragment, useEffect, useMemo, useCallback, useState, ReactNode, SetStateAction, Dispatch, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SearchInput, PlusIcon, InlineAlert, UndoIcon, Paragraph, TakeActionIcon, IconButton, Button, DeleteIcon, AddIcon, Heading, Text, Dialog } from 'evergreen-ui'; import { SearchInput, PlusIcon, InlineAlert, UndoIcon, Paragraph, TakeActionIcon, IconButton, Button, DeleteIcon, AddIcon, Dialog } from 'evergreen-ui';
import { FaMouse, FaPlus, FaStepForward, FaStepBackward } from 'react-icons/fa'; import { FaMouse, FaPlus, FaStepForward, FaStepBackward } from 'react-icons/fa';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
@ -14,6 +14,7 @@ import SegmentCutpointButton from './SegmentCutpointButton';
import { getModifier } from '../hooks/useTimelineScroll'; import { getModifier } from '../hooks/useTimelineScroll';
import { KeyBinding, KeyboardAction } from '../../../../types'; import { KeyBinding, KeyboardAction } from '../../../../types';
import { StateSegment } from '../types'; import { StateSegment } from '../types';
import Sheet from './Sheet';
type Category = string; type Category = string;
@ -22,7 +23,7 @@ type ActionsMap = Record<KeyboardAction, { name: string, category?: Category, be
const renderKeys = (keys: string[]) => keys.map((key, i) => ( const renderKeys = (keys: string[]) => keys.map((key, i) => (
<Fragment key={key}> <Fragment key={key}>
{i > 0 && <FaPlus size={8} style={{ marginLeft: 4, marginRight: 4, color: 'rgba(0,0,0,0.5)' }} />} {i > 0 && <FaPlus style={{ fontSize: '.4em', opacity: 0.8, marginLeft: '.4em', marginRight: '.4em' }} />}
<kbd>{key.toUpperCase()}</kbd> <kbd>{key.toUpperCase()}</kbd>
</Fragment> </Fragment>
)); ));
@ -116,7 +117,7 @@ const CreateBinding = memo(({
); );
}); });
const rowStyle = { display: 'flex', alignItems: 'center', margin: '.2em 0', borderBottom: '1px solid rgba(0,0,0,0.1)', paddingBottom: '.5em' }; const rowStyle = { display: 'flex', alignItems: 'center', borderBottom: '1px solid rgba(0,0,0,0.1)', paddingBottom: '.2em' };
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
const KeyboardShortcuts = memo(({ const KeyboardShortcuts = memo(({
@ -626,18 +627,18 @@ const KeyboardShortcuts = memo(({
const extraLinesPerCategory: Record<Category, ReactNode> = { const extraLinesPerCategory: Record<Category, ReactNode> = {
[zoomOperationsCategory]: [ [zoomOperationsCategory]: [
<div key="1" style={{ ...rowStyle, alignItems: 'center' }}> <div key="1" style={{ ...rowStyle, alignItems: 'center' }}>
<Text>{t('Zoom in/out timeline')}</Text> <span>{t('Zoom in/out timeline')}</span>
<div style={{ flexGrow: 1 }} /> <div style={{ flexGrow: 1 }} />
<FaMouse style={{ marginRight: 3 }} /> <FaMouse style={{ marginRight: '.3em' }} />
<Text>{t('Mouse scroll/wheel up/down')}</Text> <span>{t('Mouse scroll/wheel up/down')}</span>
</div>, </div>,
<div key="2" style={{ ...rowStyle, alignItems: 'center' }}> <div key="2" style={{ ...rowStyle, alignItems: 'center' }}>
<Text>{t('Pan timeline')}</Text> <span>{t('Pan timeline')}</span>
<div style={{ flexGrow: 1 }} /> <div style={{ flexGrow: 1 }} />
{getModifier(mouseWheelZoomModifierKey).map((v) => <kbd key={v} style={{ marginRight: '.7em' }}>{v}</kbd>)} {getModifier(mouseWheelZoomModifierKey).map((v) => <kbd key={v} style={{ marginRight: '.7em' }}>{v}</kbd>)}
<FaMouse style={{ marginRight: 3 }} /> <FaMouse style={{ marginRight: '.3em' }} />
<Text>{t('Mouse scroll/wheel up/down')}</Text> <span>{t('Mouse scroll/wheel up/down')}</span>
</div>, </div>,
], ],
}; };
@ -723,14 +724,14 @@ const KeyboardShortcuts = memo(({
return ( return (
<> <>
<div style={{ color: 'black', marginBottom: '1em' }}> <div style={{ marginBottom: '1em' }}>
<div> <div style={{ marginBottom: '1em' }}>
<SearchInput ref={searchInputRef} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search" width="100%" /> <SearchInput ref={searchInputRef} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search" width="100%" />
</div> </div>
{categoriesWithActions.map(([category, actionsInCategory]) => ( {categoriesWithActions.map(([category, actionsInCategory]) => (
<div key={category}> <div key={category}>
{category !== 'undefined' && <Heading marginTop={30} marginBottom={14}>{category}</Heading>} {category !== 'undefined' && <div style={{ marginTop: '2em', marginBottom: '.7em', fontSize: '1.4em' }}>{category}</div>}
{actionsInCategory.map(([action, actionObj]) => { {actionsInCategory.map(([action, actionObj]) => {
const actionName = (actionObj && actionObj.name) || action; const actionName = (actionObj && actionObj.name) || action;
@ -742,8 +743,8 @@ const KeyboardShortcuts = memo(({
<div key={action} style={rowStyle}> <div key={action} style={rowStyle}>
<div> <div>
{beforeContent} {beforeContent}
<Text title={action} marginRight={10}>{actionName}</Text> <span title={action} style={{ marginRight: '.5em', opacity: 0.9 }}>{actionName}</span>
<div style={{ fontSize: '.8em', opacity: 0.4 }} title={t('API action name: {{action}}', { action })}>{action}</div> <div style={{ fontSize: '.8em', opacity: 0.3 }} title={t('API action name: {{action}}', { action })}>{action}</div>
</div> </div>
<div style={{ flexGrow: 1 }} /> <div style={{ flexGrow: 1 }} />
@ -757,7 +758,7 @@ const KeyboardShortcuts = memo(({
</div> </div>
))} ))}
{bindingsForThisAction.length === 0 && <Text color="muted">{t('No binding')}</Text>} {bindingsForThisAction.length === 0 && <span style={{ opacity: 0.8, fontSize: '.8em' }}>{t('No binding')}</span>}
</div> </div>
<IconButton title={t('Bind new key to action')} appearance="minimal" intent="success" icon={AddIcon} onClick={() => onAddBindingClick(action)} /> <IconButton title={t('Bind new key to action')} appearance="minimal" intent="success" icon={AddIcon} onClick={() => onAddBindingClick(action)} />
@ -785,17 +786,13 @@ function KeyboardShortcutsDialog({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Dialog <Sheet visible={isShown} onClosePress={onHide} maxWidth="40em" style={{ padding: '0 2em' }}>
title={t('Keyboard & mouse shortcuts')} <h2>{t('Keyboard & mouse shortcuts')}</h2>
isShown={isShown}
confirmLabel={t('Done')} <KeyboardShortcuts keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} />
hasCancel={false} <Button onClick={onHide}>{t('Done')}</Button>
onCloseComplete={onHide}
onConfirm={onHide} </Sheet>
topOffset="3vh"
>
{isShown ? <KeyboardShortcuts keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} /> : <div />}
</Dialog>
); );
} }

View File

@ -3,11 +3,10 @@ import { useDebounce } from 'use-debounce';
import i18n from 'i18next'; import i18n from 'i18next';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { WarningSignIcon, ErrorIcon, Button, IconButton, TickIcon, ResetIcon } from 'evergreen-ui'; import { WarningSignIcon, ErrorIcon, Button, IconButton, TickIcon, ResetIcon } from 'evergreen-ui';
import withReactContent from 'sweetalert2-react-content';
import { IoIosHelpCircle } from 'react-icons/io'; import { IoIosHelpCircle } from 'react-icons/io';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import Swal from '../swal'; import { ReactSwal } from '../swal';
import HighlightedText from './HighlightedText'; import HighlightedText from './HighlightedText';
import { defaultOutSegTemplate, segNumVariable, segSuffixVariable, GenerateOutSegFileNames } from '../util/outputNameTemplate'; import { defaultOutSegTemplate, segNumVariable, segSuffixVariable, GenerateOutSegFileNames } from '../util/outputNameTemplate';
import useUserSettings from '../hooks/useUserSettings'; import useUserSettings from '../hooks/useUserSettings';
@ -15,8 +14,6 @@ import Switch from './Switch';
import Select from './Select'; import Select from './Select';
import TextInput from './TextInput'; import TextInput from './TextInput';
const ReactSwal = withReactContent(Swal);
const electron = window.require('electron'); const electron = window.require('electron');
const formatVariable = (variable) => `\${${variable}}`; const formatVariable = (variable) => `\${${variable}}`;

View File

@ -7,7 +7,7 @@ import styles from './Sheet.module.css';
function Sheet({ visible, onClosePress, children, maxWidth = 800, style }: { function Sheet({ visible, onClosePress, children, maxWidth = 800, style }: {
visible: boolean, onClosePress: () => void, children: ReactNode, maxWidth?: number, style?: CSSProperties visible: boolean, onClosePress: () => void, children: ReactNode, maxWidth?: number | string, style?: CSSProperties
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -1,13 +1,9 @@
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { Checkbox, RadioGroup, Paragraph } from 'evergreen-ui';
import i18n from 'i18next'; import i18n from 'i18next';
import withReactContent from 'sweetalert2-react-content';
import Swal from '../swal'; import { ReactSwal } from '../swal';
import { Html5ifyMode } from '../../../../types'; import { Html5ifyMode } from '../../../../types';
import Checkbox from '../components/Checkbox';
const ReactSwal = withReactContent(Swal);
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
@ -31,33 +27,50 @@ export async function askForHtml5ifySpeed({ allowedOptions, showRemember, initia
let selectedOption: Html5ifyMode = initialOption != null && inputOptions[initialOption] ? initialOption : Object.keys(inputOptions)[0]! as Html5ifyMode; let selectedOption: Html5ifyMode = initialOption != null && inputOptions[initialOption] ? initialOption : Object.keys(inputOptions)[0]! as Html5ifyMode;
let rememberChoice = !!initialOption; let rememberChoice = !!initialOption;
const Html = () => { function AskForHtml5ifySpeed() {
const [option, setOption] = useState(selectedOption); const [option, setOption] = useState(selectedOption);
const [remember, setRemember] = useState(rememberChoice); const [remember, setRemember] = useState(rememberChoice);
const onOptionChange = useCallback((e) => { const onOptionChange = useCallback((e) => {
selectedOption = e.target.value; selectedOption = e.currentTarget.value;
setOption(selectedOption); setOption(selectedOption);
}, []); }, []);
const onRememberChange = useCallback((e) => {
rememberChoice = e.target.checked; const onRememberChange = useCallback((checked) => {
rememberChoice = checked;
setRemember(rememberChoice); setRemember(rememberChoice);
}, []); }, []);
return ( return (
<div style={{ textAlign: 'left' }}> <div style={{ textAlign: 'left' }}>
<Paragraph>{i18n.t('These options will let you convert files to a format that is supported by the player. You can try different options and see which works with your file. Note that the conversion is for preview only. When you run an export, the output will still be lossless with full quality')}</Paragraph> <p>{i18n.t('These options will let you convert files to a format that is supported by the player. You can try different options and see which works with your file. Note that the conversion is for preview only. When you run an export, the output will still be lossless with full quality')}</p>
<RadioGroup
options={Object.entries(inputOptions).map(([value, label]) => ({ label, value }))} {Object.entries(inputOptions).map(([value, label]) => {
value={option} const id = `html5ify-${value}`;
onChange={onOptionChange} return (
/> <div key={value}>
{showRemember && <Checkbox checked={remember} onChange={onRememberChange} label={i18n.t('Use this for all files until LosslessCut is restarted?')} />} <input
id={id}
type="radio"
name="html5ify-speed"
value={value}
checked={option === value}
onChange={onOptionChange}
/>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label htmlFor={id} style={{ marginLeft: '.5em' }}>{label}</label>
</div>
);
})}
{showRemember && <Checkbox checked={remember} onCheckedChange={onRememberChange} label={i18n.t('Use this for all files until LosslessCut is restarted?')} style={{ marginTop: '.5em' }} />}
</div> </div>
); );
}; }
const { value: response } = await ReactSwal.fire({ const { value: response } = await ReactSwal.fire({
title: i18n.t('Convert to supported format'), title: i18n.t('Convert to supported format'),
html: <Html />, html: <AskForHtml5ifySpeed />,
showCancelButton: true, showCancelButton: true,
}); });

View File

@ -1,23 +1,22 @@
import { CSSProperties, ReactNode, useState } from 'react'; import { CSSProperties, ReactNode, useState } from 'react';
import { ArrowRightIcon, HelpIcon, TickCircleIcon, WarningSignIcon, InfoSignIcon, Checkbox, IconComponent } from 'evergreen-ui'; import { ArrowRightIcon, HelpIcon, TickCircleIcon, WarningSignIcon, InfoSignIcon, IconComponent } from 'evergreen-ui';
import i18n from 'i18next'; import i18n from 'i18next';
import { Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import withReactContent from 'sweetalert2-react-content';
import SyntaxHighlighter from 'react-syntax-highlighter'; import SyntaxHighlighter from 'react-syntax-highlighter';
import { tomorrow as syntaxStyle } from 'react-syntax-highlighter/dist/esm/styles/hljs'; import { tomorrow as lightSyntaxStyle, tomorrowNight as darkSyntaxStyle } from 'react-syntax-highlighter/dist/esm/styles/hljs';
import JSON5 from 'json5'; import JSON5 from 'json5';
import { SweetAlertOptions } from 'sweetalert2'; import type { SweetAlertOptions } from 'sweetalert2';
import { formatDuration } from '../util/duration'; import { formatDuration } from '../util/duration';
import Swal, { swalToastOptions, toast } from '../swal'; import Swal, { ReactSwal, swalToastOptions, toast } from '../swal';
import { parseYouTube } from '../edlFormats'; import { parseYouTube } from '../edlFormats';
import CopyClipboardButton from '../components/CopyClipboardButton'; import CopyClipboardButton from '../components/CopyClipboardButton';
import Checkbox from '../components/Checkbox';
import { isWindows, showItemInFolder } from '../util'; import { isWindows, showItemInFolder } from '../util';
import { ParseTimecode, SegmentBase } from '../types'; import { ParseTimecode, SegmentBase } from '../types';
const { dialog, shell } = window.require('@electron/remote'); const { dialog, shell } = window.require('@electron/remote');
const ReactSwal = withReactContent(Swal);
export async function promptTimeOffset({ initialValue, title, text, inputPlaceholder, parseTimecode }: { initialValue?: string | undefined, title: string, text?: string | undefined, inputPlaceholder: string, parseTimecode: ParseTimecode }) { export async function promptTimeOffset({ initialValue, title, text, inputPlaceholder, parseTimecode }: { initialValue?: string | undefined, title: string, text?: string | undefined, inputPlaceholder: string, parseTimecode: ParseTimecode }) {
const { value } = await Swal.fire({ const { value } = await Swal.fire({
@ -119,12 +118,12 @@ export async function askForFileOpenAction(inputOptions: Record<string, string>)
{Object.entries(inputOptions).map(([key, text]) => ( {Object.entries(inputOptions).map(([key, text]) => (
<button type="button" key={key} onClick={() => onClick(key)} className="button-unstyled" style={{ display: 'block', marginBottom: '.5em' }}> <button type="button" key={key} onClick={() => onClick(key)} className="button-unstyled" style={{ display: 'block', marginBottom: '.5em' }}>
<ArrowRightIcon color="rgba(0,0,0,0.5)" verticalAlign="middle" /> {text} <ArrowRightIcon style={{ color: 'var(--gray10)' }} verticalAlign="middle" /> {text}
</button> </button>
))} ))}
<button type="button" onClick={() => onClick()} className="button-unstyled" style={{ display: 'block', marginTop: '.5em' }}> <button type="button" onClick={() => onClick()} className="button-unstyled" style={{ display: 'block', marginTop: '.5em' }}>
<ArrowRightIcon color="rgba(150,0,0,1)" /> {i18n.t('Cancel')} <ArrowRightIcon style={{ color: 'var(--red11)' }} /> {i18n.t('Cancel')}
</button> </button>
</div> </div>
@ -390,18 +389,18 @@ const CleanupChoices = ({ cleanupChoicesInitial, onChange: onChangeProp }) => {
<div style={{ textAlign: 'left' }}> <div style={{ textAlign: 'left' }}>
<p>{i18n.t('What do you want to do after exporting a file or when pressing the "delete source file" button?')}</p> <p>{i18n.t('What do you want to do after exporting a file or when pressing the "delete source file" button?')}</p>
<Checkbox label={i18n.t('Close currently opened file')} checked={closeFile} disabled={trashSourceFile || trashTmpFiles} onChange={(e) => onChange('closeFile', e.target.checked)} /> <Checkbox label={i18n.t('Close currently opened file')} checked={closeFile} disabled={trashSourceFile || trashTmpFiles} onCheckedChange={(checked) => onChange('closeFile', checked)} />
<div style={{ marginTop: 25 }}> <div style={{ marginTop: 25 }}>
<Checkbox label={i18n.t('Trash auto-generated files')} checked={trashTmpFiles} onChange={(e) => onChange('trashTmpFiles', e.target.checked)} /> <Checkbox label={i18n.t('Trash auto-generated files')} checked={trashTmpFiles} onCheckedChange={(checked) => onChange('trashTmpFiles', checked)} />
<Checkbox label={i18n.t('Trash original source file')} checked={trashSourceFile} onChange={(e) => onChange('trashSourceFile', e.target.checked)} /> <Checkbox label={i18n.t('Trash original source file')} checked={trashSourceFile} onCheckedChange={(checked) => onChange('trashSourceFile', checked)} />
<Checkbox label={i18n.t('Trash project LLC file')} checked={trashProjectFile} onChange={(e) => onChange('trashProjectFile', e.target.checked)} /> <Checkbox label={i18n.t('Trash project LLC file')} checked={trashProjectFile} onCheckedChange={(checked) => onChange('trashProjectFile', checked)} />
<Checkbox label={i18n.t('Permanently delete the files if trash fails?')} disabled={!(trashTmpFiles || trashProjectFile || trashSourceFile)} checked={deleteIfTrashFails} onChange={(e) => onChange('deleteIfTrashFails', e.target.checked)} /> <Checkbox label={i18n.t('Permanently delete the files if trash fails?')} disabled={!(trashTmpFiles || trashProjectFile || trashSourceFile)} checked={deleteIfTrashFails} onCheckedChange={(checked) => onChange('deleteIfTrashFails', checked)} />
</div> </div>
<div style={{ marginTop: 25 }}> <div style={{ marginTop: 25 }}>
<Checkbox label={i18n.t('Show this dialog every time?')} checked={askForCleanup} onChange={(e) => onChange('askForCleanup', e.target.checked)} /> <Checkbox label={i18n.t('Show this dialog every time?')} checked={askForCleanup} onCheckedChange={(checked) => onChange('askForCleanup', checked)} />
<Checkbox label={i18n.t('Do all of this automatically after exporting a file?')} checked={cleanupAfterExport} onChange={(e) => onChange('cleanupAfterExport', e.target.checked)} /> <Checkbox label={i18n.t('Do all of this automatically after exporting a file?')} checked={cleanupAfterExport} onCheckedChange={(checked) => onChange('cleanupAfterExport', checked)} />
</div> </div>
</div> </div>
); );
@ -442,7 +441,7 @@ export async function createRandomSegments(fileDuration: number) {
const { durationMin, durationMax, gapMin, gapMax } = response; const { durationMin, durationMax, gapMin, gapMax } = response;
const randomInRange = (min, max) => min + Math.random() * (max - min); const randomInRange = (min: number, max: number) => min + Math.random() * (max - min);
const edl: SegmentBase[] = []; const edl: SegmentBase[] = [];
for (let start = randomInRange(gapMin, gapMax); start < fileDuration && edl.length < maxSegments; start += randomInRange(gapMin, gapMax)) { for (let start = randomInRange(gapMin, gapMax); start < fileDuration && edl.length < maxSegments; start += randomInRange(gapMin, gapMax)) {
@ -560,7 +559,7 @@ export async function selectSegmentsByExprDialog(inputValidator: (v: string) =>
html: ( html: (
<div style={{ textAlign: 'left' }}> <div style={{ textAlign: 'left' }}>
<div style={{ marginBottom: '1em' }}> <div style={{ marginBottom: '1em' }}>
<Trans>Enter a JavaScript expression which will be evaluated for each segment. Segments for which the expression evaluates to &quot;true&quot; will be selected. <button type="button" className="button-unstyled" style={{ fontWeight: 'bold' }} onClick={() => shell.openExternal('https://github.com/mifi/lossless-cut/blob/master/expressions.md')}>View available syntax.</button></Trans> <Trans>Enter a JavaScript expression which will be evaluated for each segment. Segments for which the expression evaluates to &quot;true&quot; will be selected. <button type="button" className="link-button" onClick={() => shell.openExternal('https://github.com/mifi/lossless-cut/blob/master/expressions.md')}>View available syntax.</button></Trans>
</div> </div>
<div style={{ marginBottom: '1em' }}><b>{i18n.t('Variables')}:</b> segment.label, segment.start, segment.end, segment.duration, segment.tags.*</div> <div style={{ marginBottom: '1em' }}><b>{i18n.t('Variables')}:</b> segment.label, segment.start, segment.end, segment.duration, segment.tags.*</div>
@ -568,7 +567,7 @@ export async function selectSegmentsByExprDialog(inputValidator: (v: string) =>
<div><b>{i18n.t('Examples')}:</b></div> <div><b>{i18n.t('Examples')}:</b></div>
{Object.entries(examples).map(([key, { name }]) => ( {Object.entries(examples).map(([key, { name }]) => (
<button key={key} type="button" onClick={() => addExample(key)} className="button-unstyled" style={{ display: 'block', marginBottom: '.1em' }}> <button key={key} type="button" onClick={() => addExample(key)} className="link-button" style={{ display: 'block', marginBottom: '.1em' }}>
{name} {name}
</button> </button>
))} ))}
@ -580,9 +579,9 @@ export async function selectSegmentsByExprDialog(inputValidator: (v: string) =>
return value; return value;
} }
export function showJson5Dialog({ title, json }: { title: string, json: unknown }) { export function showJson5Dialog({ title, json, darkMode }: { title: string, json: unknown, darkMode: boolean }) {
const html = ( const html = (
<SyntaxHighlighter language="javascript" style={syntaxStyle} customStyle={{ textAlign: 'left', maxHeight: 300, overflowY: 'auto', fontSize: 14 }}> <SyntaxHighlighter language="javascript" style={darkMode ? darkSyntaxStyle : lightSyntaxStyle} customStyle={{ textAlign: 'left', maxHeight: 300, overflowY: 'auto', fontSize: 14 }}>
{JSON5.stringify(json, null, 2)} {JSON5.stringify(json, null, 2)}
</SyntaxHighlighter> </SyntaxHighlighter>
); );
@ -598,7 +597,7 @@ export async function openDirToast({ filePath, text, html, ...props }: SweetAler
const swal = text ? toast : ReactSwal; const swal = text ? toast : ReactSwal;
// @ts-expect-error todo // @ts-expect-error todo
const { value } = await swal.fire({ const { value } = await swal.fire<string>({
...swalToastOptions, ...swalToastOptions,
showConfirmButton: true, showConfirmButton: true,
confirmButtonText: i18n.t('Show'), confirmButtonText: i18n.t('Show'),
@ -611,19 +610,31 @@ export async function openDirToast({ filePath, text, html, ...props }: SweetAler
if (value) showItemInFolder(filePath); if (value) showItemInFolder(filePath);
} }
const UnorderedList = ({ children }) => <ul style={{ paddingLeft: '1em' }}>{children}</ul>; const UnorderedList = ({ children }) => (
// @ts-expect-error todo <ul style={{ paddingLeft: '1em' }}>{children}</ul>
const ListItem = ({ icon: Icon, iconColor, children, style }: { icon: IconComponent, iconColor?: string, children: ReactNode, style?: CSSProperties }) => <li style={{ listStyle: 'none', ...style }}>{Icon && <Icon color={iconColor} size={14} marginRight=".3em" />} {children}</li>; );
const ListItem = ({ icon: Icon, iconColor, children, style }: { icon: IconComponent, iconColor?: string, children: ReactNode, style?: CSSProperties }) => (
<li style={{ listStyle: 'none', ...style }}>
{Icon && <Icon style={{ color: iconColor }} size={14} marginRight=".4em" />}
{children}
</li>
);
const Notices = ({ notices }) => notices.map((msg) => <ListItem key={msg} icon={InfoSignIcon} iconColor="info">{msg}</ListItem>); const Notices = ({ notices }: { notices: string[] }) => notices.map((msg) => (
const Warnings = ({ warnings }) => warnings.map((msg) => <ListItem key={msg} icon={WarningSignIcon} iconColor="warning">{msg}</ListItem>); <ListItem key={msg} icon={InfoSignIcon} iconColor="var(--blue9)">{msg}</ListItem>
const OutputIncorrectSeeHelpMenu = () => <ListItem icon={HelpIcon}>{i18n.t('If output does not look right, see the Help menu.')}</ListItem>; ));
const Warnings = ({ warnings }: { warnings: string[] }) => warnings.map((msg) => (
<ListItem key={msg} icon={WarningSignIcon} iconColor="var(--orange8)">{msg}</ListItem>
));
const OutputIncorrectSeeHelpMenu = () => (
<ListItem icon={HelpIcon}>{i18n.t('If output does not look right, see the Help menu.')}</ListItem>
);
export async function openExportFinishedToast({ filePath, warnings, notices }) { export async function openExportFinishedToast({ filePath, warnings, notices }: { filePath: string, warnings: string[], notices: string[] }) {
const hasWarnings = warnings.length > 0; const hasWarnings = warnings.length > 0;
const html = ( const html = (
<UnorderedList> <UnorderedList>
<ListItem icon={TickCircleIcon} iconColor={hasWarnings ? 'warning' : 'success'} style={{ fontWeight: 'bold' }}>{hasWarnings ? i18n.t('Export finished with warning(s)', { count: warnings.length }) : i18n.t('Export is done!')}</ListItem> <ListItem icon={TickCircleIcon} iconColor={hasWarnings ? 'var(--orange8)' : 'var(--green11)'} style={{ fontWeight: 'bold' }}>{hasWarnings ? i18n.t('Export finished with warning(s)', { count: warnings.length }) : i18n.t('Export is done!')}</ListItem>
<ListItem icon={InfoSignIcon}>{i18n.t('Please test the output file in your desired player/editor before you delete the source file.')}</ListItem> <ListItem icon={InfoSignIcon}>{i18n.t('Please test the output file in your desired player/editor before you delete the source file.')}</ListItem>
<OutputIncorrectSeeHelpMenu /> <OutputIncorrectSeeHelpMenu />
<Notices notices={notices} /> <Notices notices={notices} />
@ -634,7 +645,7 @@ export async function openExportFinishedToast({ filePath, warnings, notices }) {
await openDirToast({ filePath, html, width: 800, position: 'center', timer: hasWarnings ? undefined : 30000 }); await openDirToast({ filePath, html, width: 800, position: 'center', timer: hasWarnings ? undefined : 30000 });
} }
export async function openConcatFinishedToast({ filePath, warnings, notices }) { export async function openConcatFinishedToast({ filePath, warnings, notices }: { filePath: string, warnings: string[], notices: string[] }) {
const hasWarnings = warnings.length > 0; const hasWarnings = warnings.length > 0;
const html = ( const html = (
<UnorderedList> <UnorderedList>

View File

@ -1,14 +1,14 @@
import { useState, useCallback, useRef, useEffect } from 'react'; import { useState, useCallback, useRef, useEffect } from 'react';
import { Button, TextInputField, LinkIcon } from 'evergreen-ui';
import i18n from 'i18next'; import i18n from 'i18next';
import withReactContent from 'sweetalert2-react-content'; import { FaLink } from 'react-icons/fa';
import Swal from '../swal'; import Swal, { ReactSwal } from '../swal';
import Button from '../components/Button';
import TextInput from '../components/TextInput';
const { shell } = window.require('electron'); const { shell } = window.require('electron');
const ReactSwal = withReactContent(Swal);
export interface ParameterDialogParameter { value: string, label?: string, hint?: string } export interface ParameterDialogParameter { value: string, label?: string, hint?: string }
export type ParameterDialogParameters = Record<string, ParameterDialogParameter>; export type ParameterDialogParameters = Record<string, ParameterDialogParameter>;
@ -37,15 +37,28 @@ const ParametersInput = ({ description, parameters: parametersIn, onChange, onSu
}, []); }, []);
return ( return (
<div style={{ textAlign: 'left' }}> <div style={{ textAlign: 'left', padding: '.5em', borderRadius: '.3em' }}>
{description && <p>{description}</p>} {description && <p>{description}</p>}
{docUrl && <p><Button iconBefore={LinkIcon} onClick={() => shell.openExternal(docUrl)}>Read more</Button></p>} {docUrl && <p><Button onClick={() => shell.openExternal(docUrl)}><FaLink style={{ fontSize: '.8em' }} /> Read more</Button></p>}
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
{Object.entries(parametersIn).map(([key, parameter], i) => ( {Object.entries(parametersIn).map(([key, parameter], i) => {
<TextInputField ref={i === 0 ? firstInputRef : undefined} key={key} label={parameter.label || key} value={getParameter(key)} onChange={(e) => handleChange(key, e.target.value)} hint={parameter.hint} /> const id = `parameter-${key}`;
))} return (
<div key={key} style={{ marginBottom: '.5em' }}>
<label htmlFor={id} style={{ display: 'block', fontFamily: 'monospace', marginBottom: '.3em' }}>{parameter.label || key}</label>
<TextInput
id={id}
ref={i === 0 ? firstInputRef : undefined}
value={getParameter(key)}
onChange={(e) => handleChange(key, e.target.value)}
style={{ marginBottom: '.2em' }}
/>
{parameter.hint && <div style={{ opacity: 0.6, fontSize: '0.8em' }}>{parameter.hint}</div>}
</div>
);
})}
<input type="submit" value="submit" style={{ display: 'none' }} /> <input type="submit" value="submit" style={{ display: 'none' }} />
</form> </form>

View File

@ -6,8 +6,6 @@ import * as Electron from 'electron';
import Remote from '@electron/remote'; import Remote from '@electron/remote';
import type path from 'node:path'; import type path from 'node:path';
import 'sweetalert2/dist/sweetalert2.css';
import '@fontsource/open-sans/300.css'; import '@fontsource/open-sans/300.css';
import '@fontsource/open-sans/300-italic.css'; import '@fontsource/open-sans/300-italic.css';
import '@fontsource/open-sans/400.css'; import '@fontsource/open-sans/400.css';
@ -28,6 +26,7 @@ import ErrorBoundary from './ErrorBoundary';
import './i18n'; import './i18n';
import './main.css'; import './main.css';
import './swal2.scss';
type TypedRemote = Omit<typeof Remote, 'require'> & { type TypedRemote = Omit<typeof Remote, 'require'> & {

View File

@ -11,6 +11,8 @@ https://www.radix-ui.com/docs/colors/palette-composition/understanding-the-scale
@import '@radix-ui/colors/greenDark.css'; @import '@radix-ui/colors/greenDark.css';
@import '@radix-ui/colors/cyan.css'; @import '@radix-ui/colors/cyan.css';
@import '@radix-ui/colors/cyanDark.css'; @import '@radix-ui/colors/cyanDark.css';
@import '@radix-ui/colors/blue.css';
@import '@radix-ui/colors/blueDark.css';
@import '@radix-ui/colors/gray.css'; @import '@radix-ui/colors/gray.css';
@import '@radix-ui/colors/grayDark.css'; @import '@radix-ui/colors/grayDark.css';
@import '@radix-ui/colors/blackA.css'; @import '@radix-ui/colors/blackA.css';
@ -85,6 +87,17 @@ code.highlighted {
outline: revert; outline: revert;
} }
.link-button {
all: unset;
cursor: pointer;
text-decoration: underline;
text-underline-offset: .15em;
text-decoration-thickness: .05em;
}
.link-button:focus {
outline: revert;
}
/* https://stackoverflow.com/questions/18270894/html5-video-does-not-hide-controls-in-fullscreen-mode-in-chrome */ /* https://stackoverflow.com/questions/18270894/html5-video-does-not-hide-controls-in-fullscreen-mode-in-chrome */
video.main-player::-webkit-media-controls { video.main-player::-webkit-media-controls {
display:none !important; display:none !important;

View File

@ -1,11 +1,9 @@
import withReactContent from 'sweetalert2-react-content';
import i18n from 'i18next'; import i18n from 'i18next';
import { Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import { CSSProperties } from 'react';
import CopyClipboardButton from './components/CopyClipboardButton'; import CopyClipboardButton from './components/CopyClipboardButton';
import { isStoreBuild, isMasBuild, isWindowsStoreBuild } from './util'; import { isStoreBuild, isMasBuild, isWindowsStoreBuild } from './util';
import Swal from './swal'; import { ReactSwal } from './swal';
const electron = window.require('electron'); const electron = window.require('electron');
@ -16,23 +14,18 @@ const { app } = remote;
const { platform } = remote.require('./index.js'); const { platform } = remote.require('./index.js');
const ReactSwal = withReactContent(Swal);
const linkStyle: CSSProperties = { fontWeight: 'bold', cursor: 'pointer' };
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export function openSendReportDialog(err: unknown | undefined, state?: unknown) { export function openSendReportDialog(err: unknown | undefined, state?: unknown) {
const reportInstructions = isStoreBuild const reportInstructions = isStoreBuild
? ( ? (
<p><Trans>Please send an email to <span style={linkStyle} role="button" onClick={() => electron.shell.openExternal('mailto:losslesscut@mifi.no')}>losslesscut@mifi.no</span> where you describe what you were doing.</Trans></p> <p><Trans>Please send an email to <span className="link-button" role="button" onClick={() => electron.shell.openExternal('mailto:losslesscut@mifi.no')}>losslesscut@mifi.no</span> where you describe what you were doing.</Trans></p>
) : ( ) : (
<Trans> <Trans>
<p> <p>
If you&apos;re having a problem or question about LosslessCut, please first check the links in the <b>Help</b> menu. If you cannot find any resolution, you may ask a question in <span style={linkStyle} role="button" onClick={() => electron.shell.openExternal('https://github.com/mifi/lossless-cut/discussions')}>GitHub discussions</span> or on <span style={linkStyle} role="button" onClick={() => electron.shell.openExternal('https://github.com/mifi/lossless-cut')}>Discord.</span> If you&apos;re having a problem or question about LosslessCut, please first check the links in the <b>Help</b> menu. If you cannot find any resolution, you may ask a question in <span className="link-button" role="button" onClick={() => electron.shell.openExternal('https://github.com/mifi/lossless-cut/discussions')}>GitHub discussions</span> or on <span className="link-button" role="button" onClick={() => electron.shell.openExternal('https://github.com/mifi/lossless-cut')}>Discord.</span>
</p> </p>
<p> <p>
If you believe that you found a bug in LosslessCut, you may <span style={linkStyle} role="button" onClick={() => electron.shell.openExternal('https://github.com/mifi/lossless-cut/issues')}>report a bug</span>. If you believe that you found a bug in LosslessCut, you may <span className="link-button" role="button" onClick={() => electron.shell.openExternal('https://github.com/mifi/lossless-cut/issues')}>report a bug</span>.
</p> </p>
</Trans> </Trans>
); );
@ -67,11 +60,11 @@ export function openSendReportDialog(err: unknown | undefined, state?: unknown)
<div style={{ textAlign: 'left', overflow: 'auto', maxHeight: 300, overflowY: 'auto' }}> <div style={{ textAlign: 'left', overflow: 'auto', maxHeight: 300, overflowY: 'auto' }}>
{reportInstructions} {reportInstructions}
<p><Trans>Include the following text:</Trans> <CopyClipboardButton text={text} /></p> <p style={{ marginBottom: 0 }}><Trans>Include the following text:</Trans> <CopyClipboardButton text={text} /></p>
{!isStoreBuild && <p style={{ fontSize: '.8em', color: 'rgba(0,0,0,0.5)' }}><Trans>You might want to redact any sensitive information like paths.</Trans></p>} {!isStoreBuild && <p style={{ marginTop: '.2em', fontSize: '.8em', opacity: 0.7 }}><Trans>You might want to redact any sensitive information like paths.</Trans></p>}
<div style={{ fontWeight: 600, fontSize: 12, whiteSpace: 'pre-wrap', color: '#900' }} contentEditable suppressContentEditableWarning> <div style={{ fontWeight: 600, fontSize: '.75em', fontFamily: 'monospace', whiteSpace: 'pre-wrap', color: 'var(--gray11)', backgroundColor: 'var(--gray3)', padding: '.3em' }} contentEditable suppressContentEditableWarning>
{text} {text}
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
import SwalRaw, { SweetAlertOptions } from 'sweetalert2'; import SwalRaw from 'sweetalert2/dist/sweetalert2.js';
import type { SweetAlertOptions } from 'sweetalert2';
import { primaryColor } from './colors'; import withReactContent from 'sweetalert2-react-content';
const { systemPreferences } = window.require('@electron/remote'); const { systemPreferences } = window.require('@electron/remote');
@ -8,7 +8,7 @@ const { systemPreferences } = window.require('@electron/remote');
const animationSettings = systemPreferences.getAnimationSettings(); const animationSettings = systemPreferences.getAnimationSettings();
let commonSwalOptions: SweetAlertOptions = { let commonSwalOptions: SweetAlertOptions = {
confirmButtonColor: primaryColor, target: '#swal2-container-wrapper',
}; };
if (animationSettings.prefersReducedMotion) { if (animationSettings.prefersReducedMotion) {
@ -53,3 +53,5 @@ export const errorToast = (text: string) => toast.fire({
icon: 'error', icon: 'error',
text, text,
}); });
export const ReactSwal = withReactContent(Swal);

View File

@ -0,0 +1,46 @@
@import 'sweetalert2/src/variables';
// see colors.ts primaryColor
$swal2-confirm-button-background-color: var(--cyan9);
$myswal-background: var(--gray1);
$myswal-foreground: var(--gray12);
$swal2-outline-color: lighten($swal2-outline-color, 10%);
$swal2-background: $myswal-background;
$swal2-html-container-color: $myswal-foreground;
$swal2-title-color: $myswal-foreground;
$swal2-backdrop: rgba(0, 0, 0, .75);
$swal2-close-button-color: var(--gray11);
// FOOTER
$swal2-footer-border-color: var(--gray2);
$swal2-footer-color: $myswal-background;
// TIMER POGRESS BAR
$swal2-timer-progress-bar-background: var(--gray8);
// INPUT
$swal2-input-color: $myswal-foreground;
$swal2-input-background: var(--gray3);
// VALIDATION MESSAGE
$swal2-validation-message-background: var(--gray3);
$swal2-validation-message-color: $myswal-foreground;
// QUEUE
$swal2-progress-step-background: var(--gray5);
// COMMON VARIABLES FOR CONFIRM AND CANCEL BUTTONS
$swal2-button-focus-box-shadow: 0 0 0 1px $swal2-background, 0 0 0 3px $swal2-outline-color;
// TOAST
$swal2-toast-background: $myswal-background;
$swal2-toast-button-focus-box-shadow: 0 0 0 1px $swal2-background, 0 0 0 3px $swal2-outline-color;
.swal2-textarea::placeholder {
color: var(--gray8);
}
@import 'sweetalert2/src/sweetalert2.scss';

314
yarn.lock
View File

@ -1709,6 +1709,42 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/primitive@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/primitive@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
checksum: 2b93e161d3fdabe9a64919def7fa3ceaecf2848341e9211520c401181c9eaebb8451c630b066fad2256e5c639c95edc41de0ba59c40eff37e799918d019822d1
languageName: node
linkType: hard
"@radix-ui/react-checkbox@npm:^1.0.4":
version: 1.0.4
resolution: "@radix-ui/react-checkbox@npm:1.0.4"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/primitive": "npm:1.0.1"
"@radix-ui/react-compose-refs": "npm:1.0.1"
"@radix-ui/react-context": "npm:1.0.1"
"@radix-ui/react-presence": "npm:1.0.1"
"@radix-ui/react-primitive": "npm:1.0.3"
"@radix-ui/react-use-controllable-state": "npm:1.0.1"
"@radix-ui/react-use-previous": "npm:1.0.1"
"@radix-ui/react-use-size": "npm:1.0.1"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: e3f2f169c017349e3e7844911f116641e44a50d9cc3ba9e270a6bc9d2118641ac515c67fe2a611dad98eefb29ae1e2e6a47a81abd44570faaabe7056ec3f02b1
languageName: node
linkType: hard
"@radix-ui/react-compose-refs@npm:1.0.0": "@radix-ui/react-compose-refs@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-compose-refs@npm:1.0.0" resolution: "@radix-ui/react-compose-refs@npm:1.0.0"
@ -1720,6 +1756,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-compose-refs@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-compose-refs@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 2b9a613b6db5bff8865588b6bf4065f73021b3d16c0a90b2d4c23deceeb63612f1f15de188227ebdc5f88222cab031be617a9dd025874c0487b303be3e5cc2a8
languageName: node
linkType: hard
"@radix-ui/react-context@npm:1.0.0": "@radix-ui/react-context@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-context@npm:1.0.0" resolution: "@radix-ui/react-context@npm:1.0.0"
@ -1731,6 +1782,42 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-context@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-context@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: a02187a3bae3a0f1be5fab5ad19c1ef06ceff1028d957e4d9994f0186f594a9c3d93ee34bacb86d1fa8eb274493362944398e1c17054d12cb3b75384f9ae564b
languageName: node
linkType: hard
"@radix-ui/react-presence@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-presence@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-compose-refs": "npm:1.0.1"
"@radix-ui/react-use-layout-effect": "npm:1.0.1"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 406f0b5a54ea4e7881e15bddc3863234bb14bf3abd4a6e56ea57c6df6f9265a9ad5cfa158e3a98614f0dcbbb7c5f537e1f7158346e57cc3f29b522d62cf28823
languageName: node
linkType: hard
"@radix-ui/react-primitive@npm:1.0.1": "@radix-ui/react-primitive@npm:1.0.1":
version: 1.0.1 version: 1.0.1
resolution: "@radix-ui/react-primitive@npm:1.0.1" resolution: "@radix-ui/react-primitive@npm:1.0.1"
@ -1744,6 +1831,26 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-primitive@npm:1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-primitive@npm:1.0.3"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-slot": "npm:1.0.2"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: bedb934ac07c710dc5550a7bfc7065d47e099d958cde1d37e4b1947ae5451f1b7e6f8ff5965e242578bf2c619065e6038c3a3aa779e5eafa7da3e3dbc685799f
languageName: node
linkType: hard
"@radix-ui/react-slot@npm:1.0.1": "@radix-ui/react-slot@npm:1.0.1":
version: 1.0.1 version: 1.0.1
resolution: "@radix-ui/react-slot@npm:1.0.1" resolution: "@radix-ui/react-slot@npm:1.0.1"
@ -1756,6 +1863,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-slot@npm:1.0.2":
version: 1.0.2
resolution: "@radix-ui/react-slot@npm:1.0.2"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-compose-refs": "npm:1.0.1"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 734866561e991438fbcf22af06e56b272ed6ee8f7b536489ee3bf2f736f8b53bf6bc14ebde94834aa0aceda854d018a0ce20bb171defffbaed1f566006cbb887
languageName: node
linkType: hard
"@radix-ui/react-switch@npm:^1.0.1": "@radix-ui/react-switch@npm:^1.0.1":
version: 1.0.1 version: 1.0.1
resolution: "@radix-ui/react-switch@npm:1.0.1" resolution: "@radix-ui/react-switch@npm:1.0.1"
@ -1786,6 +1909,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-use-callback-ref@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-callback-ref@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: b9fd39911c3644bbda14a84e4fca080682bef84212b8d8931fcaa2d2814465de242c4cfd8d7afb3020646bead9c5e539d478cea0a7031bee8a8a3bb164f3bc4c
languageName: node
linkType: hard
"@radix-ui/react-use-controllable-state@npm:1.0.0": "@radix-ui/react-use-controllable-state@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-use-controllable-state@npm:1.0.0" resolution: "@radix-ui/react-use-controllable-state@npm:1.0.0"
@ -1798,6 +1936,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-use-controllable-state@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-controllable-state@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-use-callback-ref": "npm:1.0.1"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: dee2be1937d293c3a492cb6d279fc11495a8f19dc595cdbfe24b434e917302f9ac91db24e8cc5af9a065f3f209c3423115b5442e65a5be9fd1e9091338972be9
languageName: node
linkType: hard
"@radix-ui/react-use-layout-effect@npm:1.0.0": "@radix-ui/react-use-layout-effect@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-use-layout-effect@npm:1.0.0" resolution: "@radix-ui/react-use-layout-effect@npm:1.0.0"
@ -1809,6 +1963,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-use-layout-effect@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-layout-effect@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: bed9c7e8de243a5ec3b93bb6a5860950b0dba359b6680c84d57c7a655e123dec9b5891c5dfe81ab970652e7779fe2ad102a23177c7896dde95f7340817d47ae5
languageName: node
linkType: hard
"@radix-ui/react-use-previous@npm:1.0.0": "@radix-ui/react-use-previous@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-use-previous@npm:1.0.0" resolution: "@radix-ui/react-use-previous@npm:1.0.0"
@ -1820,6 +1989,21 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-use-previous@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-previous@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 66b4312e857c58b75f3bf62a2048ef090b79a159e9da06c19a468c93e62336969c33dbef60ff16969f00b20386cc25d138f6a353f1658b35baac0a6eff4761b9
languageName: node
linkType: hard
"@radix-ui/react-use-size@npm:1.0.0": "@radix-ui/react-use-size@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-use-size@npm:1.0.0" resolution: "@radix-ui/react-use-size@npm:1.0.0"
@ -1832,6 +2016,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-use-size@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-size@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-use-layout-effect": "npm:1.0.1"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 6cc150ad1e9fa85019c225c5a5d50a0af6cdc4653dad0c21b4b40cd2121f36ee076db326c43e6bc91a69766ccff5a84e917d27970176b592577deea3c85a3e26
languageName: node
linkType: hard
"@rollup/rollup-android-arm-eabi@npm:4.10.0": "@rollup/rollup-android-arm-eabi@npm:4.10.0":
version: 4.10.0 version: 4.10.0
resolution: "@rollup/rollup-android-arm-eabi@npm:4.10.0" resolution: "@rollup/rollup-android-arm-eabi@npm:4.10.0"
@ -2797,6 +2997,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"anymatch@npm:~3.1.2":
version: 3.1.3
resolution: "anymatch@npm:3.1.3"
dependencies:
normalize-path: "npm:^3.0.0"
picomatch: "npm:^2.0.4"
checksum: 3e044fd6d1d26545f235a9fe4d7a534e2029d8e59fa7fd9f2a6eb21230f6b5380ea1eaf55136e60cbf8e613544b3b766e7a6fa2102e2a3a117505466e3025dc2
languageName: node
linkType: hard
"app-builder-bin@npm:4.0.0": "app-builder-bin@npm:4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "app-builder-bin@npm:4.0.0" resolution: "app-builder-bin@npm:4.0.0"
@ -3175,6 +3385,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"binary-extensions@npm:^2.0.0":
version: 2.3.0
resolution: "binary-extensions@npm:2.3.0"
checksum: bcad01494e8a9283abf18c1b967af65ee79b0c6a9e6fcfafebfe91dbe6e0fc7272bafb73389e198b310516ae04f7ad17d79aacf6cb4c0d5d5202a7e2e52c7d98
languageName: node
linkType: hard
"bl@npm:^4.0.3": "bl@npm:^4.0.3":
version: 4.1.0 version: 4.1.0
resolution: "bl@npm:4.1.0" resolution: "bl@npm:4.1.0"
@ -3271,6 +3488,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"braces@npm:~3.0.2":
version: 3.0.3
resolution: "braces@npm:3.0.3"
dependencies:
fill-range: "npm:^7.1.1"
checksum: fad11a0d4697a27162840b02b1fad249c1683cbc510cd5bf1a471f2f8085c046d41094308c577a50a03a579dd99d5a6b3724c4b5e8b14df2c4443844cfcda2c6
languageName: node
linkType: hard
"broccoli-node-api@npm:^1.7.0": "broccoli-node-api@npm:^1.7.0":
version: 1.7.0 version: 1.7.0
resolution: "broccoli-node-api@npm:1.7.0" resolution: "broccoli-node-api@npm:1.7.0"
@ -3619,6 +3845,25 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"chokidar@npm:>=3.0.0 <4.0.0":
version: 3.6.0
resolution: "chokidar@npm:3.6.0"
dependencies:
anymatch: "npm:~3.1.2"
braces: "npm:~3.0.2"
fsevents: "npm:~2.3.2"
glob-parent: "npm:~5.1.2"
is-binary-path: "npm:~2.1.0"
is-glob: "npm:~4.0.1"
normalize-path: "npm:~3.0.0"
readdirp: "npm:~3.6.0"
dependenciesMeta:
fsevents:
optional: true
checksum: c327fb07704443f8d15f7b4a7ce93b2f0bc0e6cea07ec28a7570aa22cd51fcf0379df589403976ea956c369f25aa82d84561947e227cd925902e1751371658df
languageName: node
linkType: hard
"chownr@npm:^1.1.1": "chownr@npm:^1.1.1":
version: 1.1.4 version: 1.1.4
resolution: "chownr@npm:1.1.4" resolution: "chownr@npm:1.1.4"
@ -5822,6 +6067,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fill-range@npm:^7.1.1":
version: 7.1.1
resolution: "fill-range@npm:7.1.1"
dependencies:
to-regex-range: "npm:^5.0.1"
checksum: a7095cb39e5bc32fada2aa7c7249d3f6b01bd1ce461a61b0adabacccabd9198500c6fb1f68a7c851a657e273fce2233ba869638897f3d7ed2e87a2d89b4436ea
languageName: node
linkType: hard
"finalhandler@npm:1.2.0": "finalhandler@npm:1.2.0":
version: 1.2.0 version: 1.2.0
resolution: "finalhandler@npm:1.2.0" resolution: "finalhandler@npm:1.2.0"
@ -6290,7 +6544,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"glob-parent@npm:^5.1.2": "glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
version: 5.1.2 version: 5.1.2
resolution: "glob-parent@npm:5.1.2" resolution: "glob-parent@npm:5.1.2"
dependencies: dependencies:
@ -6878,6 +7132,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"immutable@npm:^4.0.0":
version: 4.3.6
resolution: "immutable@npm:4.3.6"
checksum: 59fedb67f26e265035616b27e33ef90b53b434cf76fb09212ec2d6ae32ee8d2fe2641e6dc32dbc78498c521fbf5f72c6740d39affba63a0a36a3884272371857
languageName: node
linkType: hard
"import-fresh@npm:^3.2.1": "import-fresh@npm:^3.2.1":
version: 3.3.0 version: 3.3.0
resolution: "import-fresh@npm:3.3.0" resolution: "import-fresh@npm:3.3.0"
@ -7062,6 +7323,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-binary-path@npm:~2.1.0":
version: 2.1.0
resolution: "is-binary-path@npm:2.1.0"
dependencies:
binary-extensions: "npm:^2.0.0"
checksum: 078e51b4f956c2c5fd2b26bb2672c3ccf7e1faff38e0ebdba45612265f4e3d9fc3127a1fa8370bbf09eab61339203c3d3b7af5662cbf8be4030f8fac37745b0e
languageName: node
linkType: hard
"is-boolean-object@npm:^1.1.0": "is-boolean-object@npm:^1.1.0":
version: 1.1.2 version: 1.1.2
resolution: "is-boolean-object@npm:1.1.2" resolution: "is-boolean-object@npm:1.1.2"
@ -7172,7 +7442,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3": "is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1":
version: 4.0.3 version: 4.0.3
resolution: "is-glob@npm:4.0.3" resolution: "is-glob@npm:4.0.3"
dependencies: dependencies:
@ -7893,6 +8163,7 @@ __metadata:
"@fontsource/open-sans": "npm:^4.5.14" "@fontsource/open-sans": "npm:^4.5.14"
"@octokit/core": "npm:5" "@octokit/core": "npm:5"
"@radix-ui/colors": "npm:^0.1.8" "@radix-ui/colors": "npm:^0.1.8"
"@radix-ui/react-checkbox": "npm:^1.0.4"
"@radix-ui/react-switch": "npm:^1.0.1" "@radix-ui/react-switch": "npm:^1.0.1"
"@tsconfig/node18": "npm:^18.2.2" "@tsconfig/node18": "npm:^18.2.2"
"@tsconfig/strictest": "npm:^2.0.2" "@tsconfig/strictest": "npm:^2.0.2"
@ -7967,6 +8238,7 @@ __metadata:
react-syntax-highlighter: "npm:^15.4.3" react-syntax-highlighter: "npm:^15.4.3"
react-use: "npm:^17.4.0" react-use: "npm:^17.4.0"
rimraf: "npm:^5.0.5" rimraf: "npm:^5.0.5"
sass: "npm:^1.77.2"
screenfull: "npm:^6.0.2" screenfull: "npm:^6.0.2"
scroll-into-view-if-needed: "npm:^2.2.28" scroll-into-view-if-needed: "npm:^2.2.28"
semver: "npm:^7.6.0" semver: "npm:^7.6.0"
@ -8675,6 +8947,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0":
version: 3.0.0
resolution: "normalize-path@npm:3.0.0"
checksum: 88eeb4da891e10b1318c4b2476b6e2ecbeb5ff97d946815ffea7794c31a89017c70d7f34b3c2ebf23ef4e9fc9fb99f7dffe36da22011b5b5c6ffa34f4873ec20
languageName: node
linkType: hard
"normalize-url@npm:^6.0.1": "normalize-url@npm:^6.0.1":
version: 6.1.0 version: 6.1.0
resolution: "normalize-url@npm:6.1.0" resolution: "normalize-url@npm:6.1.0"
@ -9200,7 +9479,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"picomatch@npm:^2.2.3": "picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3":
version: 2.3.1 version: 2.3.1
resolution: "picomatch@npm:2.3.1" resolution: "picomatch@npm:2.3.1"
checksum: 60c2595003b05e4535394d1da94850f5372c9427ca4413b71210f437f7b2ca091dbd611c45e8b37d10036fa8eade25c1b8951654f9d3973bfa66a2ff4d3b08bc checksum: 60c2595003b05e4535394d1da94850f5372c9427ca4413b71210f437f7b2ca091dbd611c45e8b37d10036fa8eade25c1b8951654f9d3973bfa66a2ff4d3b08bc
@ -9789,6 +10068,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"readdirp@npm:~3.6.0":
version: 3.6.0
resolution: "readdirp@npm:3.6.0"
dependencies:
picomatch: "npm:^2.2.1"
checksum: 196b30ef6ccf9b6e18c4e1724b7334f72a093d011a99f3b5920470f0b3406a51770867b3e1ae9711f227ef7a7065982f6ee2ce316746b2cb42c88efe44297fe7
languageName: node
linkType: hard
"reflect.getprototypeof@npm:^1.0.4": "reflect.getprototypeof@npm:^1.0.4":
version: 1.0.5 version: 1.0.5
resolution: "reflect.getprototypeof@npm:1.0.5" resolution: "reflect.getprototypeof@npm:1.0.5"
@ -10259,6 +10547,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"sass@npm:^1.77.2":
version: 1.77.2
resolution: "sass@npm:1.77.2"
dependencies:
chokidar: "npm:>=3.0.0 <4.0.0"
immutable: "npm:^4.0.0"
source-map-js: "npm:>=0.6.2 <2.0.0"
bin:
sass: sass.js
checksum: 4df71f1a01cd59613e7a25bfcec96ddf06e3546c238ba3238b96c6ac0dcf34b9ce238b4de7b39656f6cb0a5e7acccde19f53b521ae4abcdcbe600e0de9c97644
languageName: node
linkType: hard
"sax@npm:^1.2.4": "sax@npm:^1.2.4":
version: 1.2.4 version: 1.2.4
resolution: "sax@npm:1.2.4" resolution: "sax@npm:1.2.4"
@ -10616,6 +10917,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"source-map-js@npm:>=0.6.2 <2.0.0":
version: 1.2.0
resolution: "source-map-js@npm:1.2.0"
checksum: 74f331cfd2d121c50790c8dd6d3c9de6be21926de80583b23b37029b0f37aefc3e019fa91f9a10a5e120c08135297e1ecf312d561459c45908cb1e0e365f49e5
languageName: node
linkType: hard
"source-map-js@npm:^1.0.2": "source-map-js@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "source-map-js@npm:1.0.2" resolution: "source-map-js@npm:1.0.2"