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

View File

@ -1338,6 +1338,7 @@ function App() {
if (areWeCutting) notices.push(i18n.t('Cutpoints may be inaccurate.'));
const revealPath = willMerge ? mergedOutFilePath : outFiles[0];
invariant(revealPath != null);
if (!hideAllNotifications) openExportFinishedToast({ filePath: revealPath, warnings, notices });
if (cleanupChoices.cleanupAfterExport) await cleanupFilesWithDialog();
@ -2260,6 +2261,8 @@ function App() {
setLastCommandsVisible(false);
setSettingsVisible(false);
setStreamsSelectorShown(false);
setConcatDialogVisible(false);
setKeyboardShortcutsVisible(false);
return false;
}
@ -2506,300 +2509,304 @@ function App() {
// throw new Error('Test error boundary');
return (
<SegColorsContext.Provider value={segColorsContext}>
<UserSettingsContext.Provider value={userSettingsContext}>
<ThemeProvider value={theme}>
<div className={darkMode ? 'dark-theme' : undefined} style={{ display: 'flex', flexDirection: 'column', height: '100vh', color: 'var(--gray12)', background: 'var(--gray1)', transition: darkModeTransition }}>
<TopMenu
filePath={filePath}
fileFormat={fileFormat}
copyAnyAudioTrack={copyAnyAudioTrack}
toggleStripAudio={toggleStripAudio}
clearOutDir={clearOutDir}
isCustomFormatSelected={isCustomFormatSelected}
renderOutFmt={renderOutFmt}
toggleSettings={toggleSettings}
numStreamsToCopy={numStreamsToCopy}
numStreamsTotal={numStreamsTotal}
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>
)}
<>
<SegColorsContext.Provider value={segColorsContext}>
<UserSettingsContext.Provider value={userSettingsContext}>
<ThemeProvider value={theme}>
<div className={darkMode ? 'dark-theme' : undefined} style={{ display: 'flex', flexDirection: 'column', height: '100vh', color: 'var(--gray12)', background: 'var(--gray1)', transition: darkModeTransition }}>
<TopMenu
filePath={filePath}
fileFormat={fileFormat}
copyAnyAudioTrack={copyAnyAudioTrack}
toggleStripAudio={toggleStripAudio}
clearOutDir={clearOutDir}
isCustomFormatSelected={isCustomFormatSelected}
renderOutFmt={renderOutFmt}
toggleSettings={toggleSettings}
numStreamsToCopy={numStreamsToCopy}
numStreamsTotal={numStreamsTotal}
setStreamsSelectorShown={setStreamsSelectorShown}
selectedSegments={selectedSegmentsOrInverse}
/>
<div style={{ flexGrow: 1, display: 'flex', overflowY: 'hidden' }}>
<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>
{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>
<AnimatePresence>
{showRightBar && isFileOpened && filePath != null && (
<SegmentList
width={rightBarWidth}
currentSegIndex={currentSegIndexSafe}
apparentCutSegments={apparentCutSegments}
inverseCutSegments={inverseCutSegments}
getFrameCount={getFrameCount}
<div className="no-user-select" style={bottomStyle}>
<Timeline
shouldShowKeyframes={shouldShowKeyframes}
waveforms={waveforms}
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
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}
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}
loadSubtitleTrackToSegments={loadSubtitleTrackToSegments}
/>
)}
</AnimatePresence>
</div>
</Sheet>
<div className="no-user-select" style={bottomStyle}>
<Timeline
shouldShowKeyframes={shouldShowKeyframes}
waveforms={waveforms}
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}
<LastCommandsSheet
visible={lastCommandsVisible}
onTogglePress={toggleLastCommands}
ffmpegCommandLog={ffmpegCommandLog}
/>
<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}
loadSubtitleTrackToSegments={loadSubtitleTrackToSegments}
<Sheet visible={settingsVisible} onClosePress={toggleSettings}>
<Settings
onTunerRequested={onTunerRequested}
onKeyboardShortcutsDialogRequested={toggleKeyboardShortcuts}
askForCleanupChoices={askForCleanupChoices}
toggleStoreProjectInWorkingDir={toggleStoreProjectInWorkingDir}
simpleMode={simpleMode}
clearOutDir={clearOutDir}
/>
)}
</Sheet>
</Sheet>
<LastCommandsSheet
visible={lastCommandsVisible}
onTogglePress={toggleLastCommands}
ffmpegCommandLog={ffmpegCommandLog}
/>
<ConcatDialog isShown={batchFiles.length > 0 && concatDialogVisible} onHide={() => setConcatDialogVisible(false)} paths={batchFilePaths} onConcat={userConcatFiles} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} />
<Sheet visible={settingsVisible} onClosePress={toggleSettings}>
<Settings
onTunerRequested={onTunerRequested}
onKeyboardShortcutsDialogRequested={toggleKeyboardShortcuts}
askForCleanupChoices={askForCleanupChoices}
toggleStoreProjectInWorkingDir={toggleStoreProjectInWorkingDir}
simpleMode={simpleMode}
clearOutDir={clearOutDir}
/>
</Sheet>
<KeyboardShortcuts isShown={keyboardShortcutsVisible} onHide={() => setKeyboardShortcutsVisible(false)} keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} />
</div>
</ThemeProvider>
</UserSettingsContext.Provider>
</SegColorsContext.Provider>
<ConcatDialog isShown={batchFiles.length > 0 && concatDialogVisible} onHide={() => setConcatDialogVisible(false)} paths={batchFilePaths} onConcat={userConcatFiles} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} />
<KeyboardShortcuts isShown={keyboardShortcutsVisible} onHide={() => setKeyboardShortcutsVisible(false)} keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} />
</div>
</ThemeProvider>
</UserSettingsContext.Provider>
</SegColorsContext.Provider>
<div id="swal2-container-wrapper" className={darkMode ? 'dark-theme' : undefined} style={{ color: 'var(--gray12)', background: 'var(--gray1)' }} />
</>
);
}

View File

@ -16,6 +16,7 @@ import { getActiveDisposition, attachedPicDisposition } from './util/streams';
import TagEditor from './components/TagEditor';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../ffprobe';
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'];
@ -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
const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copyStream, fileDuration, setEditingStream, onExtractStreamPress, paramsByStreamId, updateStreamParams, formatTimecode, loadSubtitleTrackToSegments }: {
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,
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, onInfoClick: (json: unknown, title: string) => void,
}) => {
const { t } = useTranslation();
@ -283,8 +280,8 @@ const Stream = memo(({ filePath, stream, onToggle, batchSetCopyStreamIds, copySt
);
});
function FileHeading({ path, formatData, chapters, onTrashClick, onEditClick, setCopyAllStreams, onExtractAllStreamsPress }: {
path: string, formatData: FFprobeFormat | undefined, chapters?: FFprobeChapter[] | undefined, onTrashClick?: (() => void) | undefined, onEditClick?: (() => void) | undefined, setCopyAllStreams: (a: boolean) => void, onExtractAllStreamsPress?: () => Promise<void>,
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>, onInfoClick: (json: unknown, title: string) => void,
}) {
const { t } = useTranslation();
@ -360,6 +357,7 @@ function StreamsSelector({
const [editingStream, setEditingStream] = useState<EditingStream>();
const [editingTag, setEditingTag] = useState<string>();
const { t } = useTranslation();
const { darkMode } = useUserSettings();
function getFormatDuration(formatData: FFprobeFormat | undefined) {
if (!formatData || !formatData.duration) return undefined;
@ -394,13 +392,17 @@ function StreamsSelector({
const externalFilesEntries = Object.entries(externalFilesMeta);
const onInfoClick = useCallback((json: unknown, title: string) => {
showJson5Dialog({ title, json, darkMode });
}, [darkMode]);
return (
<>
<p style={{ margin: '.5em 2em .5em 1em' }}>{t('Click to select which tracks to keep when exporting:')}</p>
<div style={fileStyle}>
{/* We only support editing main file metadata for now */}
<FileHeading path={mainFilePath} formatData={mainFileFormatData} chapters={mainFileChapters} onEditClick={() => setEditingFile(mainFilePath)} setCopyAllStreams={(enabled) => setCopyAllStreamsForPath(mainFilePath, enabled)} onExtractAllStreamsPress={onExtractAllStreamsPress} />
<FileHeading onInfoClick={onInfoClick} path={mainFilePath} formatData={mainFileFormatData} chapters={mainFileChapters} onEditClick={() => setEditingFile(mainFilePath)} setCopyAllStreams={(enabled) => setCopyAllStreamsForPath(mainFilePath, enabled)} onExtractAllStreamsPress={onExtractAllStreamsPress} />
<table style={tableStyle}>
<Thead />
@ -420,6 +422,7 @@ function StreamsSelector({
updateStreamParams={updateStreamParams}
formatTimecode={formatTimecode}
loadSubtitleTrackToSegments={loadSubtitleTrackToSegments}
onInfoClick={onInfoClick}
/>
))}
</tbody>
@ -428,7 +431,7 @@ function StreamsSelector({
{externalFilesEntries.map(([path, { streams: externalFileStreams, formatData }]) => (
<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}>
<Thead />
@ -446,6 +449,7 @@ function StreamsSelector({
paramsByStreamId={paramsByStreamId}
updateStreamParams={updateStreamParams}
formatTimecode={formatTimecode}
onInfoClick={onInfoClick}
/>
))}
</tbody>

View File

@ -9,4 +9,5 @@
padding: 0 .5em 0 .3em;
outline: .05em solid var(--gray8);
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 { 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 { FaQuestionCircle, FaExclamationTriangle } from 'react-icons/fa';
import { FaQuestionCircle, FaExclamationTriangle, FaCog } from 'react-icons/fa';
import i18n from 'i18next';
import withReactContent from 'sweetalert2-react-content';
import invariant from 'tiny-invariant';
import Checkbox from './Checkbox';
import Swal from '../swal';
import { ReactSwal } from '../swal';
import { readFileMeta, getSmarterOutFormat } from '../ffmpeg';
import useFileFormatState from '../hooks/useFileFormatState';
import OutputFormatSelect from './OutputFormatSelect';
@ -15,17 +15,23 @@ import useUserSettings from '../hooks/useUserSettings';
import { isMov } from '../util/streams';
import { getOutFileExtension, getSuffixedFileName } from '../util';
import { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../../../ffprobe';
import Sheet from './Sheet';
import TextInput from './TextInput';
import Button from './Button';
const { basename } = window.require('path');
const ReactSwal = withReactContent(Swal);
const containerStyle: CSSProperties = { color: 'black' };
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 }: {
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 (
<>
<Dialog
title={t('Merge/concatenate files')}
shouldCloseOnOverlayClick={false}
isShown={isShown}
onCloseComplete={onHide}
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 }}>
<Sheet visible={isShown} onClosePress={onHide} maxWidth="100%" style={{ padding: '0 2em' }}>
<h2>{t('Merge/concatenate files')}</h2>
<div style={{ marginBottom: '1em' }}>
<div style={{ whiteSpace: 'pre-wrap', fontSize: '.9em', marginBottom: '1em' }}>
{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 style={{ backgroundColor: 'var(--gray1)', borderRadius: '.1em' }}>
{paths.map((path, index) => (
<div key={path} style={rowStyle} title={path}>
<div>
{index + 1}
{'. '}
<span style={{ color: 'rgba(0,0,0,0.7)' }}>{basename(path)}</span>
{!allFilesMetaCache[path] && <FaQuestionCircle color="#996A13" style={{ marginLeft: 10 }} />}
{problemsByFile[path] && <IconButton appearance="minimal" icon={FaExclamationTriangle} onClick={() => onProblemsByFileClick(path)} title={i18n.t('Mismatches detected')} color="#996A13" style={{ marginLeft: 10 }} />}
<span>{basename(path)}</span>
{!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')} style={{ color: 'var(--orange8)', marginLeft: '1em' }} />}
</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) && (
<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 && (
<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')}>
<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>
</Dialog>

View File

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

View File

@ -1,6 +1,6 @@
import { memo, Fragment, useEffect, useMemo, useCallback, useState, ReactNode, SetStateAction, Dispatch, useRef } from 'react';
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 Mousetrap from 'mousetrap';
import groupBy from 'lodash/groupBy';
@ -14,6 +14,7 @@ import SegmentCutpointButton from './SegmentCutpointButton';
import { getModifier } from '../hooks/useTimelineScroll';
import { KeyBinding, KeyboardAction } from '../../../../types';
import { StateSegment } from '../types';
import Sheet from './Sheet';
type Category = string;
@ -22,7 +23,7 @@ type ActionsMap = Record<KeyboardAction, { name: string, category?: Category, be
const renderKeys = (keys: string[]) => keys.map((key, i) => (
<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>
</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
const KeyboardShortcuts = memo(({
@ -626,18 +627,18 @@ const KeyboardShortcuts = memo(({
const extraLinesPerCategory: Record<Category, ReactNode> = {
[zoomOperationsCategory]: [
<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 }} />
<FaMouse style={{ marginRight: 3 }} />
<Text>{t('Mouse scroll/wheel up/down')}</Text>
<FaMouse style={{ marginRight: '.3em' }} />
<span>{t('Mouse scroll/wheel up/down')}</span>
</div>,
<div key="2" style={{ ...rowStyle, alignItems: 'center' }}>
<Text>{t('Pan timeline')}</Text>
<span>{t('Pan timeline')}</span>
<div style={{ flexGrow: 1 }} />
{getModifier(mouseWheelZoomModifierKey).map((v) => <kbd key={v} style={{ marginRight: '.7em' }}>{v}</kbd>)}
<FaMouse style={{ marginRight: 3 }} />
<Text>{t('Mouse scroll/wheel up/down')}</Text>
<FaMouse style={{ marginRight: '.3em' }} />
<span>{t('Mouse scroll/wheel up/down')}</span>
</div>,
],
};
@ -723,14 +724,14 @@ const KeyboardShortcuts = memo(({
return (
<>
<div style={{ color: 'black', marginBottom: '1em' }}>
<div>
<div style={{ marginBottom: '1em' }}>
<div style={{ marginBottom: '1em' }}>
<SearchInput ref={searchInputRef} value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search" width="100%" />
</div>
{categoriesWithActions.map(([category, actionsInCategory]) => (
<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]) => {
const actionName = (actionObj && actionObj.name) || action;
@ -742,8 +743,8 @@ const KeyboardShortcuts = memo(({
<div key={action} style={rowStyle}>
<div>
{beforeContent}
<Text title={action} marginRight={10}>{actionName}</Text>
<div style={{ fontSize: '.8em', opacity: 0.4 }} title={t('API action name: {{action}}', { action })}>{action}</div>
<span title={action} style={{ marginRight: '.5em', opacity: 0.9 }}>{actionName}</span>
<div style={{ fontSize: '.8em', opacity: 0.3 }} title={t('API action name: {{action}}', { action })}>{action}</div>
</div>
<div style={{ flexGrow: 1 }} />
@ -757,7 +758,7 @@ const KeyboardShortcuts = memo(({
</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>
<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();
return (
<Dialog
title={t('Keyboard & mouse shortcuts')}
isShown={isShown}
confirmLabel={t('Done')}
hasCancel={false}
onCloseComplete={onHide}
onConfirm={onHide}
topOffset="3vh"
>
{isShown ? <KeyboardShortcuts keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} /> : <div />}
</Dialog>
<Sheet visible={isShown} onClosePress={onHide} maxWidth="40em" style={{ padding: '0 2em' }}>
<h2>{t('Keyboard & mouse shortcuts')}</h2>
<KeyboardShortcuts keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} />
<Button onClick={onHide}>{t('Done')}</Button>
</Sheet>
);
}

View File

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

View File

@ -7,7 +7,7 @@ import styles from './Sheet.module.css';
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();

View File

@ -1,13 +1,9 @@
import { useState, useCallback } from 'react';
import { Checkbox, RadioGroup, Paragraph } from 'evergreen-ui';
import i18n from 'i18next';
import withReactContent from 'sweetalert2-react-content';
import Swal from '../swal';
import { ReactSwal } from '../swal';
import { Html5ifyMode } from '../../../../types';
const ReactSwal = withReactContent(Swal);
import Checkbox from '../components/Checkbox';
// 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 rememberChoice = !!initialOption;
const Html = () => {
function AskForHtml5ifySpeed() {
const [option, setOption] = useState(selectedOption);
const [remember, setRemember] = useState(rememberChoice);
const onOptionChange = useCallback((e) => {
selectedOption = e.target.value;
selectedOption = e.currentTarget.value;
setOption(selectedOption);
}, []);
const onRememberChange = useCallback((e) => {
rememberChoice = e.target.checked;
const onRememberChange = useCallback((checked) => {
rememberChoice = checked;
setRemember(rememberChoice);
}, []);
return (
<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>
<RadioGroup
options={Object.entries(inputOptions).map(([value, label]) => ({ label, value }))}
value={option}
onChange={onOptionChange}
/>
{showRemember && <Checkbox checked={remember} onChange={onRememberChange} label={i18n.t('Use this for all files until LosslessCut is restarted?')} />}
<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>
{Object.entries(inputOptions).map(([value, label]) => {
const id = `html5ify-${value}`;
return (
<div key={value}>
<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>
);
};
}
const { value: response } = await ReactSwal.fire({
title: i18n.t('Convert to supported format'),
html: <Html />,
html: <AskForHtml5ifySpeed />,
showCancelButton: true,
});

View File

@ -1,23 +1,22 @@
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 { Trans } from 'react-i18next';
import withReactContent from 'sweetalert2-react-content';
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 { SweetAlertOptions } from 'sweetalert2';
import type { SweetAlertOptions } from 'sweetalert2';
import { formatDuration } from '../util/duration';
import Swal, { swalToastOptions, toast } from '../swal';
import Swal, { ReactSwal, swalToastOptions, toast } from '../swal';
import { parseYouTube } from '../edlFormats';
import CopyClipboardButton from '../components/CopyClipboardButton';
import Checkbox from '../components/Checkbox';
import { isWindows, showItemInFolder } from '../util';
import { ParseTimecode, SegmentBase } from '../types';
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 }) {
const { value } = await Swal.fire({
@ -119,12 +118,12 @@ export async function askForFileOpenAction(inputOptions: Record<string, string>)
{Object.entries(inputOptions).map(([key, text]) => (
<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 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>
</div>
@ -390,18 +389,18 @@ const CleanupChoices = ({ cleanupChoicesInitial, onChange: onChangeProp }) => {
<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>
<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 }}>
<Checkbox label={i18n.t('Trash auto-generated files')} checked={trashTmpFiles} onChange={(e) => onChange('trashTmpFiles', e.target.checked)} />
<Checkbox label={i18n.t('Trash original source file')} checked={trashSourceFile} onChange={(e) => onChange('trashSourceFile', e.target.checked)} />
<Checkbox label={i18n.t('Trash project LLC file')} checked={trashProjectFile} onChange={(e) => onChange('trashProjectFile', e.target.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('Trash auto-generated files')} checked={trashTmpFiles} onCheckedChange={(checked) => onChange('trashTmpFiles', 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} onCheckedChange={(checked) => onChange('trashProjectFile', 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 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('Do all of this automatically after exporting a file?')} checked={cleanupAfterExport} onChange={(e) => onChange('cleanupAfterExport', 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} onCheckedChange={(checked) => onChange('cleanupAfterExport', checked)} />
</div>
</div>
);
@ -442,7 +441,7 @@ export async function createRandomSegments(fileDuration: number) {
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[] = [];
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: (
<div style={{ textAlign: 'left' }}>
<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 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>
{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}
</button>
))}
@ -580,9 +579,9 @@ export async function selectSegmentsByExprDialog(inputValidator: (v: string) =>
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 = (
<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)}
</SyntaxHighlighter>
);
@ -598,7 +597,7 @@ export async function openDirToast({ filePath, text, html, ...props }: SweetAler
const swal = text ? toast : ReactSwal;
// @ts-expect-error todo
const { value } = await swal.fire({
const { value } = await swal.fire<string>({
...swalToastOptions,
showConfirmButton: true,
confirmButtonText: i18n.t('Show'),
@ -611,19 +610,31 @@ export async function openDirToast({ filePath, text, html, ...props }: SweetAler
if (value) showItemInFolder(filePath);
}
const UnorderedList = ({ children }) => <ul style={{ paddingLeft: '1em' }}>{children}</ul>;
// @ts-expect-error todo
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 UnorderedList = ({ children }) => (
<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 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 Warnings = ({ warnings }) => warnings.map((msg) => <ListItem key={msg} icon={WarningSignIcon} iconColor="warning">{msg}</ListItem>);
const OutputIncorrectSeeHelpMenu = () => <ListItem icon={HelpIcon}>{i18n.t('If output does not look right, see the Help menu.')}</ListItem>;
const Notices = ({ notices }: { notices: string[] }) => notices.map((msg) => (
<ListItem key={msg} icon={InfoSignIcon} iconColor="var(--blue9)">{msg}</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 html = (
<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>
<OutputIncorrectSeeHelpMenu />
<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 });
}
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 html = (
<UnorderedList>

View File

@ -1,14 +1,14 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { Button, TextInputField, LinkIcon } from 'evergreen-ui';
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 ReactSwal = withReactContent(Swal);
export interface ParameterDialogParameter { value: string, label?: string, hint?: string }
export type ParameterDialogParameters = Record<string, ParameterDialogParameter>;
@ -37,15 +37,28 @@ const ParametersInput = ({ description, parameters: parametersIn, onChange, onSu
}, []);
return (
<div style={{ textAlign: 'left' }}>
<div style={{ textAlign: 'left', padding: '.5em', borderRadius: '.3em' }}>
{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}>
{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} />
))}
{Object.entries(parametersIn).map(([key, parameter], i) => {
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' }} />
</form>

View File

@ -6,8 +6,6 @@ import * as Electron from 'electron';
import Remote from '@electron/remote';
import type path from 'node:path';
import 'sweetalert2/dist/sweetalert2.css';
import '@fontsource/open-sans/300.css';
import '@fontsource/open-sans/300-italic.css';
import '@fontsource/open-sans/400.css';
@ -28,6 +26,7 @@ import ErrorBoundary from './ErrorBoundary';
import './i18n';
import './main.css';
import './swal2.scss';
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/cyan.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/grayDark.css';
@import '@radix-ui/colors/blackA.css';
@ -85,6 +87,17 @@ code.highlighted {
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 */
video.main-player::-webkit-media-controls {
display:none !important;

View File

@ -1,11 +1,9 @@
import withReactContent from 'sweetalert2-react-content';
import i18n from 'i18next';
import { Trans } from 'react-i18next';
import { CSSProperties } from 'react';
import CopyClipboardButton from './components/CopyClipboardButton';
import { isStoreBuild, isMasBuild, isWindowsStoreBuild } from './util';
import Swal from './swal';
import { ReactSwal } from './swal';
const electron = window.require('electron');
@ -16,23 +14,18 @@ const { app } = remote;
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
export function openSendReportDialog(err: unknown | undefined, state?: unknown) {
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>
<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>
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>
</Trans>
);
@ -67,11 +60,11 @@ export function openSendReportDialog(err: unknown | undefined, state?: unknown)
<div style={{ textAlign: 'left', overflow: 'auto', maxHeight: 300, overflowY: 'auto' }}>
{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}
</div>
</div>

View File

@ -1,6 +1,6 @@
import SwalRaw, { SweetAlertOptions } from 'sweetalert2';
import { primaryColor } from './colors';
import SwalRaw from 'sweetalert2/dist/sweetalert2.js';
import type { SweetAlertOptions } from 'sweetalert2';
import withReactContent from 'sweetalert2-react-content';
const { systemPreferences } = window.require('@electron/remote');
@ -8,7 +8,7 @@ const { systemPreferences } = window.require('@electron/remote');
const animationSettings = systemPreferences.getAnimationSettings();
let commonSwalOptions: SweetAlertOptions = {
confirmButtonColor: primaryColor,
target: '#swal2-container-wrapper',
};
if (animationSettings.prefersReducedMotion) {
@ -53,3 +53,5 @@ export const errorToast = (text: string) => toast.fire({
icon: 'error',
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
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":
version: 1.0.0
resolution: "@radix-ui/react-compose-refs@npm:1.0.0"
@ -1720,6 +1756,21 @@ __metadata:
languageName: node
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":
version: 1.0.0
resolution: "@radix-ui/react-context@npm:1.0.0"
@ -1731,6 +1782,42 @@ __metadata:
languageName: node
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":
version: 1.0.1
resolution: "@radix-ui/react-primitive@npm:1.0.1"
@ -1744,6 +1831,26 @@ __metadata:
languageName: node
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":
version: 1.0.1
resolution: "@radix-ui/react-slot@npm:1.0.1"
@ -1756,6 +1863,22 @@ __metadata:
languageName: node
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":
version: 1.0.1
resolution: "@radix-ui/react-switch@npm:1.0.1"
@ -1786,6 +1909,21 @@ __metadata:
languageName: node
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":
version: 1.0.0
resolution: "@radix-ui/react-use-controllable-state@npm:1.0.0"
@ -1798,6 +1936,22 @@ __metadata:
languageName: node
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":
version: 1.0.0
resolution: "@radix-ui/react-use-layout-effect@npm:1.0.0"
@ -1809,6 +1963,21 @@ __metadata:
languageName: node
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":
version: 1.0.0
resolution: "@radix-ui/react-use-previous@npm:1.0.0"
@ -1820,6 +1989,21 @@ __metadata:
languageName: node
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":
version: 1.0.0
resolution: "@radix-ui/react-use-size@npm:1.0.0"
@ -1832,6 +2016,22 @@ __metadata:
languageName: node
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":
version: 4.10.0
resolution: "@rollup/rollup-android-arm-eabi@npm:4.10.0"
@ -2797,6 +2997,16 @@ __metadata:
languageName: node
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":
version: 4.0.0
resolution: "app-builder-bin@npm:4.0.0"
@ -3175,6 +3385,13 @@ __metadata:
languageName: node
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":
version: 4.1.0
resolution: "bl@npm:4.1.0"
@ -3271,6 +3488,15 @@ __metadata:
languageName: node
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":
version: 1.7.0
resolution: "broccoli-node-api@npm:1.7.0"
@ -3619,6 +3845,25 @@ __metadata:
languageName: node
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":
version: 1.1.4
resolution: "chownr@npm:1.1.4"
@ -5822,6 +6067,15 @@ __metadata:
languageName: node
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":
version: 1.2.0
resolution: "finalhandler@npm:1.2.0"
@ -6290,7 +6544,7 @@ __metadata:
languageName: node
linkType: hard
"glob-parent@npm:^5.1.2":
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
version: 5.1.2
resolution: "glob-parent@npm:5.1.2"
dependencies:
@ -6878,6 +7132,13 @@ __metadata:
languageName: node
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":
version: 3.3.0
resolution: "import-fresh@npm:3.3.0"
@ -7062,6 +7323,15 @@ __metadata:
languageName: node
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":
version: 1.1.2
resolution: "is-boolean-object@npm:1.1.2"
@ -7172,7 +7442,7 @@ __metadata:
languageName: node
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
resolution: "is-glob@npm:4.0.3"
dependencies:
@ -7893,6 +8163,7 @@ __metadata:
"@fontsource/open-sans": "npm:^4.5.14"
"@octokit/core": "npm:5"
"@radix-ui/colors": "npm:^0.1.8"
"@radix-ui/react-checkbox": "npm:^1.0.4"
"@radix-ui/react-switch": "npm:^1.0.1"
"@tsconfig/node18": "npm:^18.2.2"
"@tsconfig/strictest": "npm:^2.0.2"
@ -7967,6 +8238,7 @@ __metadata:
react-syntax-highlighter: "npm:^15.4.3"
react-use: "npm:^17.4.0"
rimraf: "npm:^5.0.5"
sass: "npm:^1.77.2"
screenfull: "npm:^6.0.2"
scroll-into-view-if-needed: "npm:^2.2.28"
semver: "npm:^7.6.0"
@ -8675,6 +8947,13 @@ __metadata:
languageName: node
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":
version: 6.1.0
resolution: "normalize-url@npm:6.1.0"
@ -9200,7 +9479,7 @@ __metadata:
languageName: node
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
resolution: "picomatch@npm:2.3.1"
checksum: 60c2595003b05e4535394d1da94850f5372c9427ca4413b71210f437f7b2ca091dbd611c45e8b37d10036fa8eade25c1b8951654f9d3973bfa66a2ff4d3b08bc
@ -9789,6 +10068,15 @@ __metadata:
languageName: node
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":
version: 1.0.5
resolution: "reflect.getprototypeof@npm:1.0.5"
@ -10259,6 +10547,19 @@ __metadata:
languageName: node
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":
version: 1.2.4
resolution: "sax@npm:1.2.4"
@ -10616,6 +10917,13 @@ __metadata:
languageName: node
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":
version: 1.0.2
resolution: "source-map-js@npm:1.0.2"