diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ca374b5e..9766abfb 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,6 +1,6 @@
# Contributing
-## [Translations](docs/translation.md)
+## [Translations / i18n](docs/translation.md)
## Development environment setup
@@ -28,12 +28,30 @@ yarn download-ffmpeg-win32-arm64
For Windows, you may have to install [7z](https://www.7-zip.org/download.html), and then put the 7z folder in your `PATH`.
-### Running
+### Run app in development mode
```bash
yarn dev
```
+### Run all code checks and tests
+
+Run linting, code style, types, tests etc with the following command:
+
+```bash
+yarn check
+```
+
+Most of these checks are automatically run in GitHub Actions.
+
+### Other scripts
+
+See [package.json](./package.json) "scripts" section.
+
+### Contributing code
+
+To contribute code, use [pull requests](https://github.com/mifi/lossless-cut/pulls). If you would like to contribute a lot of code, please first create an issue to check the viability of your change.
+
## `mas-dev` (Mac App Store) local build
This will sign using the development provisioning profile:
@@ -109,14 +127,6 @@ Before releasing, consider [Maintainence chores](#maintainence-chores) first.
For per-platform build/signing setup, see [this article](https://mifi.no/blog/automated-electron-build-with-release-to-mac-app-store-microsoft-store-snapcraft/).
-## Weblate
-
-`yarn scan-i18n` to get the newest English strings and push so Weblate gets them.
-
-Find the [latest PR](https://github.com/mifi/lossless-cut/pulls) from Weblate and **rebase+merge** it.
-
-**Warning:** Do not squash and merge (see [here why](docs/translation.md#weblate))!
-
## Minimum OS version
See [requirements](docs/requirements.md).
@@ -152,10 +162,13 @@ Links:
- `@electron/remote`
- `package.json` / `yarn.lock`
-### i18n
-```bash
-yarn scan-i18n
-```
+### i18n strings / Weblate
+
+Run `yarn scan-i18n` to get the newest English strings and push so Weblate gets them.
+
+Find the [latest PR](https://github.com/mifi/lossless-cut/pulls) from Weblate and **rebase+merge** it.
+
+**Warning:** Do not squash and merge (see [here why](docs/translation.md#weblate))!
### Regenerate licenses file
diff --git a/README.md b/README.md
index d9736ccb..c2d0ca30 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-
+
LosslessCut
The swiss army knife of lossless video/audio editing
@@ -130,6 +130,7 @@ LosslessCut uses the Chromium browser's HTML5 video player, and not all formats/
## Documentation
+- Official website: [LosslessCut.app](https://losslesscut.app)
- **[Getting started, FAQ and usage documentation](docs/index.md)**
- [Troubleshooting and known issues and limitations](docs/troubleshooting.md)
- [Contributing](CONTRIBUTING.md)
diff --git a/docs/index.md b/docs/index.md
index de09c5ba..ed35ce22 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -6,6 +6,8 @@ Please read the documentation before creating an issue. Thank you 🙏
- **Q:** Is there a keyboard shortcut to do X?
- **A:** First check the Keyboard shortcuts dialog. If you cannot find your shortcut there, [see this issue.](https://github.com/mifi/lossless-cut/issues/254)
+- **Q:** How can I import multiple files into my project and edit them together?
+ - **A:** LosslessCut is not a [non-linear editor](https://github.com/mifi/lossless-cut/issues/976) (like Adobe Premiere) and it is not a linear editor (like Avidemux) - it is something in-between. You create segments on the timeline and the segments represent which portions of video will be exported. If you want more advanced workflows, you have to do it in multiple separate operations.
- **Q:** Can LosslessCut be automated using a CLI or API or do external post-processing?
- **A:** While it was never designed for advanced batching/automation, it does have a [basic CLI and a HTTP API](cli.md). More info: [#980](https://github.com/mifi/lossless-cut/issues/980) [#868](https://github.com/mifi/lossless-cut/issues/868).
- **Q:** I cannot find a particular button or function.
@@ -17,7 +19,7 @@ Please read the documentation before creating an issue. Thank you 🙏
- **Q** What's the difference between the app in the Apple/Microsoft App Store vs. GitHub releases?
- **A** LosslessCut version in the App Stores is often a few versions behind the latest GitHub version, because I want to be sure that the new versions work perfectly before releasing in the App Stores. The GitHub version will contain new, untested features and may contain some bugs (even in existing functionality). I consider the newest GitHub versions to be a public "beta" test. Then, once I'm sure that the new version works well, I will release it in the App Stores as well to give a frictionless as possible experience for customers. They have exactly the same in-app features, except for a few platform limitations: Apple doesn't allow opening VOB files with App Store apps. Apple App Store apps run in a sandbox, and therefore need to prompt for output directory before allowing writing files.
- **Q**: What are all these `.llc` files that get created?
- - **A***: Their purpase is to save your segments so that when you exit/re-open LosslessCut you can continue where you left off. You can turn them off in app settings. See also [#2115](https://github.com/mifi/lossless-cut/issues/2115).
+ - **A***: Their purpose is to save your segments so that when you exit/re-open LosslessCut you can continue where you left off. You can turn them off in app settings. See also [#2115](https://github.com/mifi/lossless-cut/issues/2115).
## Commonly requested features
@@ -53,7 +55,7 @@ If you have a problem with the app or with a file, please see the [🤔 Troubles
- **Drag and drop** a video file into player or use
⌘ /
CTRL +
O .
-
SPACE to play/pause or
← → , . or mouse/trackpad wheel to seek back/forth.
- Set the start/end times of the current segment by first moving the timeline cursor and then pressing
I to set start time and
O to set end time. You can also press hold
SHIFT while dragging a segment with the mouse to move or resize it
--
+ to create a new segment.
+-
+ to create another marker and repeat the previous step to convert it into a segment.
-
B to split the segment at the timeline cursor.
-
BACKSPACE to remove cutpoint/segment.
- If you create segments without an end time, it is a [marker](#markers) instead of a segment.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 62f6660f..24075581 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -14,23 +14,29 @@ If the video exports successfully without any error from LosslessCut, but it doe
## Cutting times are not accurate
-Each segment's *start cut time* normally (but not always) will be "rounded" to the nearest **previous** keyframe. This means that you often have to move the **start cut time** to **few frames after** the desired keyframe.
+Each segment's *start cut time* will normally (but not always) be "rounded" to the nearest **previous** keyframe. This means that you often have to move the **start cut time** to **few frames after** the desired keyframe.
- Lossless cutting is not an exact science. For some files, it just works. For others, you may need to trial and error to get the best cut. See [#330](https://github.com/mifi/lossless-cut/issues/330)
- Your mileage may vary when it comes to *Keyframe cut mode*. Most common video files need *Keyframe cut* enabled, but you may need to try both values. [ffmpeg](https://trac.ffmpeg.org/wiki/Seeking) also has documentation about these two seek/cut modes. In `ffmpeg`, *Keyframe cut* corresponds to `-ss` *before* `-i`.
- Try to change `avoid_negative_ts` (in export options).
- Try also to set the **start**-cutpoint a few frames **before or after** the nearest keyframe (may also solve audio sync issues).
-- You may try to enable the new "Smart cut" mode to allow cutting between keyframes. However it is very experimental and may not work for many files.
+- You may try to enable the experimental "Smart cut" mode to allow cutting between keyframes. However it will not work for many files.
- Currently, the only way to review the exported file (to check the actual cutpoints) is to run the export (possibly with only one segment enabled to speed up) and then manually check the output file. See also [#1887](https://github.com/mifi/lossless-cut/issues/1887)
### Cut starts from wrong keyframe
-For some files, when you place segment start cutpoints at keyframes, and you export, it will instead cut from the keyframe **before** the keyframe that you wanted. This is because with some videos, ffmpeg struggles to find the nearest previous keyframe, see [#1216](https://github.com/mifi/lossless-cut/issues/1216). To workaround this, you can try to shift your segments' **start**-cutpoints forward by a few frames, so that ffmpeg correctly cuts from the *previous* keyframe. You can also enable the Export Option "Shift all start times" by +1, +2, +3 frames or so.
+For some files, when you place segment start cutpoints at keyframes, and you export, it will instead cut from the keyframe **before** the keyframe that you wanted. This is because with some videos, FFmpeg struggles to find the nearest previous keyframe, see [#1216](https://github.com/mifi/lossless-cut/issues/1216). To workaround this, you can try to shift your segments' **start**-cutpoints forward by a few frames, so that ffmpeg correctly cuts from the *previous* keyframe.
- Menu: "Edit" -> "Segments" -> "Shift all segments on timeline"
- Enter `00:00:00.200` (or a larger value if it doesn't help)
- When asked about Start or End timestamps, Select **Start**
-This will effectively shift all start times of segments by 6 frames (`6/30=0.2` for 30fps video).
+This will effectively shift all start times of segments by 6 frames (`6/30=0.2` for 30fps video). Alternatively, to always shift start times, you can enable the Export Option "Shift all start times" by +1, +2, +3 frames or so.
+
+In some videos (like VP9 from `yt-dlp`), some start keyframes are being ignored if LosslessCut output format is set to Matroska. A workaround is to set it to MP4 instead. See [#2804](https://github.com/mifi/lossless-cut/issues/2804).
+
+## Start cut is ignored
+
+Check whether your video has enough keyframes before your cutpoint. You can see keyframes on the timeline as vertical lines. You may have to zoom in to see them. If there are no keyframes, then it is most likely not possible to cut your file at the desired cutpoints losslessly. You can try to enable the Smart Cut mode, alternatively you may try to disable "keyframe cut" mode. See also (#2864)(https://github.com/mifi/lossless-cut/issues/2864).
## Cut file has same length as input
@@ -85,6 +91,10 @@ LosslessCut uses the same video player that is used by Chrome. You can try to op
Some formats or codecs are not natively supported by LosslessCut's built in player, and LosslessCut will automatically use FFmpeg-assisted software decoding to playback in a lower quality. For better playback you may convert these files to a different format from the menu: *File -> Convert to supported format*. Note that this will not affect the output from LosslessCut, it is only used for playback, see [#88](https://github.com/mifi/lossless-cut/issues/88).
+### Auto convert to supported format on certain timestamp
+
+If LosslessCut automatically tries to convert your file into a supported format when you start playing/seeking and reach a certain timestamp, see issue [#2893](https://github.com/mifi/lossless-cut/issues/2893).
+
## Linux specific issues
- If the app crashes on startup or you get an error like `FATAL:setuid_sandbox_host.cc(157)] The SUID sandbox helper binary was found, but is not configured correctly...`, try to run it as `./lossless-cut --no-sandbox`. See [#258](https://github.com/mifi/lossless-cut/issues/258) and [#2614](https://github.com/mifi/lossless-cut/issues/2614)
diff --git a/locales/en/translation.json b/locales/en/translation.json
index 38ea1809..506f62c8 100644
--- a/locales/en/translation.json
+++ b/locales/en/translation.json
@@ -457,6 +457,7 @@
"Manually input current segment's end time": "Manually input current segment's end time",
"Manually input current segment's start time": "Manually input current segment's start time",
"Markers": "Markers",
+ "Markers are segments without an end time and will not be exported. Convert markers to segments by setting their end time.": "Markers are segments without an end time and will not be exported. Convert markers to segments by setting their end time.",
"Markers:": "Markers:",
"Max length": "Max length",
"Maximize": "Maximize",
@@ -517,6 +518,7 @@
"Open": "Open",
"Open file": "Open file",
"Open folder": "Open folder",
+ "Open link in browser": "Open link in browser",
"Open media from URL": "Open media from URL",
"Open next file": "Open next file",
"Open previous file": "Open previous file",
@@ -742,6 +744,7 @@
"The audio track is not supported while previewing. You can convert to a supported format from the menu": "The audio track is not supported while previewing. You can convert to a supported format from the menu",
"The expression must return an object": "The expression must return an object",
"The file name template is missing {{ext}} and will result in a file without the suggested extension. This may result in an unplayable output file.": "The file name template is missing {{ext}} and will result in a file without the suggested extension. This may result in an unplayable output file.",
+ "The language tag (ISO 639-2 code). For example \"eng\" for English. This is used by some players to select the appropriate audio/subtitle track based on the user's language preferences.": "The language tag (ISO 639-2 code). For example \"eng\" for English. This is used by some players to select the appropriate audio/subtitle track based on the user's language preferences.",
"The last executed ffmpeg commands will show up here after you run operations. You can copy them to clipboard and modify them to your needs before running on your command line.": "The last executed ffmpeg commands will show up here after you run operations. You can copy them to clipboard and modify them to your needs before running on your command line.",
"The media file referenced by the project file you tried to open does not exist in the same directory as the project file: {{mediaFileName}}": "The media file referenced by the project file you tried to open does not exist in the same directory as the project file: {{mediaFileName}}",
"The media you tried to open does not exist": "The media you tried to open does not exist",
@@ -821,6 +824,7 @@
"User interface": "User interface",
"Using built-in ffmpeg": "Using built-in ffmpeg",
"Using external ffmpeg": "Using external ffmpeg",
+ "Value must be shorter than total duration ({{totalDuration}})": "Value must be shorter than total duration ({{totalDuration}})",
"Variables": "Variables",
"video": "video",
"Video FPS": "Video FPS",
diff --git a/no.mifi.losslesscut.appdata.xml b/no.mifi.losslesscut.appdata.xml
index 06094cbc..7866e385 100644
--- a/no.mifi.losslesscut.appdata.xml
+++ b/no.mifi.losslesscut.appdata.xml
@@ -18,7 +18,7 @@
A screenshot of the main window of LosslessCut
-
https://mifi.no/losslesscut/
+
https://losslesscut.app/
https://github.com/mifi/lossless-cut/issues
https://paypal.me/mifino/usd
diff --git a/package.json b/package.json
index 2da7fbff..ccd958eb 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-switch": "^1.2.6",
+ "@radix-ui/themes": "^3.3.0",
"@tanstack/react-virtual": "3.13.18",
"@tsconfig/node22": "^22.0.5",
"@tsconfig/strictest": "^2.0.8",
@@ -99,13 +100,13 @@
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-unicorn": "56.0.1",
- "fast-xml-parser": "^5.4.1",
+ "fast-xml-parser": "^5.7.0",
"i18next-cli": "^1.34.1",
"icon-gen": "^5.0.0",
"immer": "^11.1.3",
"ky": "^1.14.2",
"leaflet": "^1.9.4",
- "lodash": "^4.17.23",
+ "lodash": "^4.18.1",
"luxon": "^3.7.2",
"mkdirp": "^3.0.1",
"motion": "^12.24.7",
@@ -134,7 +135,7 @@
"typescript": "~5.9.3",
"use-debounce": "^5.2.1",
"use-trace-update": "^1.3.3",
- "vite": "^7.3.0",
+ "vite": "^7.3.2",
"vitest": "^4.0.16",
"yargs": "^18.0.0"
},
@@ -150,7 +151,7 @@
"express-async-handler": "^1.2.0",
"file-type": "^21.3.2",
"i18next": "^25.7.3",
- "i18next-fs-backend": "^2.6.1",
+ "i18next-fs-backend": "^2.6.4",
"json5": "^2.2.3",
"lodash.debounce": "^4.0.8",
"mime-types": "^3.0.2",
diff --git a/src/common/constants.ts b/src/common/constants.ts
index 3fa2d2db..fc96689f 100644
--- a/src/common/constants.ts
+++ b/src/common/constants.ts
@@ -1,4 +1,4 @@
-export const homepageUrl = 'https://mifi.no/losslesscut/';
+export const homepageUrl = 'https://losslesscut.app/';
export const githubUrl = 'https://github.com/mifi/lossless-cut/';
export const getReleaseUrl = (version: string) => `https://github.com/mifi/lossless-cut/releases/tag/v${version}`;
export const compareReleasesUrl = (fromVersion: string, toVersion: string) => `https://github.com/mifi/lossless-cut/compare/v${fromVersion}...v${toVersion}`;
diff --git a/src/common/util.ts b/src/common/util.ts
index 1d1849b6..5f0db614 100644
--- a/src/common/util.ts
+++ b/src/common/util.ts
@@ -5,3 +5,17 @@ export const parseFfprobeDuration = (durationStr: string | undefined) => (
);
export const getHwaccelArgs = (hwaccel: FfmpegHwAccel) => (hwaccel !== 'none' ? ['-hwaccel', hwaccel] : []);
+
+// Used to be 5, but we recently increased to 6 because https://github.com/mifi/lossless-cut/issues/2838
+// I don't remember why 5 was chosen initially, but if we don't truncate, ffmpeg can sometimes give an error when too many decimal places are used in the time argument, see:
+export const formatFfmpegTime = (time: number) => time.toFixed(6);
+
+export function parseRatio(str: string, char = '/') {
+ const split = str.split(char);
+ if (split.length !== 2) return undefined;
+ const num = parseInt(split[0]!, 10);
+ const den = parseInt(split[1]!, 10);
+ if (Number.isNaN(num) || Number.isNaN(den)) return undefined;
+ if (den <= 0) return undefined;
+ return num / den;
+}
diff --git a/src/main/configStore.ts b/src/main/configStore.ts
index c9f7e6a2..58a495de 100644
--- a/src/main/configStore.ts
+++ b/src/main/configStore.ts
@@ -26,8 +26,8 @@ const defaultKeyBindings: KeyBinding[] = [
{ keys: 'Comma', action: 'seekPreviousFrame' },
{ keys: 'Period', action: 'seekNextFrame' },
{ keys: 'KeyC', action: 'captureSnapshot' },
- { keys: 'ControlLeft+KeyC', action: 'copySegmentsToClipboard' },
- { keys: 'MetaLeft+KeyC', action: 'copySegmentsToClipboard' },
+ { keys: 'ControlLeft+AltLeft+KeyC', action: 'copySegmentsToClipboard' },
+ { keys: 'MetaLeft+AltLeft+KeyC', action: 'copySegmentsToClipboard' },
{ keys: 'ShiftLeft+KeyC', action: 'captureSnapshotToClipboard' },
{ keys: 'KeyI', action: 'setCutStart' },
diff --git a/src/main/ffmpeg.ts b/src/main/ffmpeg.ts
index 3e2e71c9..eeab304f 100644
--- a/src/main/ffmpeg.ts
+++ b/src/main/ffmpeg.ts
@@ -14,7 +14,7 @@ import type { FFprobeFormat } from '../common/ffprobe.js';
import isDev from './isDev.js';
import logger from './logger.js';
import { parseFfmpegProgressLine } from './progress.js';
-import { getHwaccelArgs, parseFfprobeDuration } from '../common/util.js';
+import { formatFfmpegTime, getHwaccelArgs, parseFfprobeDuration } from '../common/util.js';
import { getFfmpegJpegQuality } from './ffmpegUtil.js';
@@ -41,11 +41,13 @@ function escapeCliArg(arg: string) {
return /[^\w-]/.test(arg) ? `'${String(arg).replaceAll("'", '\'"\'"\'')}'` : arg;
}
-export function getFfCommandLine(cmd: string, args: readonly string[]) {
+export type FfCommand = 'ffmpeg' | 'ffprobe';
+
+export function getFfCommandLine(cmd: FfCommand, args: readonly string[]) {
return `${cmd} ${args.map((arg) => escapeCliArg(arg)).join(' ')}`;
}
-function getFfPath(cmd: string) {
+function getFfPath(cmd: FfCommand) {
const exeName = isWindows ? `${cmd}.exe` : cmd;
if (customFfPath) return join(customFfPath, exeName);
@@ -55,10 +57,12 @@ function getFfPath(cmd: string) {
}
// local dev
- const components = ['ffmpeg', `${platform}-${arch}`];
- if (isWindows || isLinux) components.push('lib');
- components.push(exeName);
- return join(...components);
+ return join(
+ 'ffmpeg',
+ `${platform}-${arch}`,
+ ...(isWindows || isLinux ? ['lib'] : []),
+ exeName,
+ );
}
const getFfprobePath = () => getFfPath('ffprobe');
@@ -256,9 +260,9 @@ export async function renderWaveformPng({ filePath, start, duration, resample, c
}
const getInputSeekArgs = ({ filePath, from, to }: { filePath: string, from?: number | undefined, to?: number | undefined }) => [
- ...(from != null ? ['-ss', from.toFixed(5)] : []),
+ ...(from != null ? ['-ss', formatFfmpegTime(from)] : []),
'-i', filePath,
- ...(from != null && to != null ? ['-t', (to - from).toFixed(5)] : []),
+ ...(from != null && to != null ? ['-t', formatFfmpegTime(to - from)] : []),
];
export function mapTimesToSegments(times: number[], includeLast: boolean) {
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index 8a358eba..2cd1a0df 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -13,6 +13,7 @@ import fromPairs from 'lodash/fromPairs';
import sum from 'lodash/sum';
import invariant from 'tiny-invariant';
import type { SweetAlertOptions } from 'sweetalert2';
+import { Theme } from '@radix-ui/themes';
import useTimelineScroll from './hooks/useTimelineScroll';
import useUserSettingsRoot from './hooks/useUserSettingsRoot';
@@ -83,7 +84,7 @@ import {
import getSwal, { errorToast, showPlaybackFailedMessage } from './swal';
import { adjustRate } from './util/rate-calculator';
import { askExtractFramesAsImages } from './dialogs/extractFrames';
-import type { CleanupChoicesType } from './dialogs';
+import type { CleanupChoicesType, OpenFileResponse } from './dialogs';
import { askForOutDir, askForImportChapters, askForFileOpenAction, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, showOpenDialog, showMuxNotSupported, promptDownloadMediaUrl, showOutputNotWritable, deleteFiles, mustDisallowVob, toastError } from './dialogs';
import { openSendReportDialog } from './reporting';
import { sortSegments, convertSegmentsToChaptersWithGaps, hasAnySegmentOverlap, isDurationValid, getPlaybackAction, getSegmentTags, filterNonMarkers, isInitialSegment } from './segments';
@@ -194,14 +195,14 @@ function App() {
const { withErrorHandling, handleError, genericError, setGenericError } = useErrorHandling();
- const { showGenericDialog, genericDialog, closeGenericDialog, confirmDialog, openExportFinishedDialog, openCutFinishedDialog, openConcatFinishedDialog, openCleanupFilesDialog } = useDialog();
+ const { showGenericDialog, genericDialog, closeGenericDialog, confirmDialog, openExportFinishedDialog, openCutFinishedDialog, openConcatFinishedDialog, openCleanupFilesDialog, openShiftSegmentsDialog } = useDialog();
// Note that each action may be multiple key bindings and this will only be the first binding for each action
const keyBindingByAction = useMemo(() => Object.fromEntries(keyBindings.map((binding) => [binding.action, binding])), [keyBindings]);
const { working, setWorking, workingRef, abortWorking } = useLoading();
const { videoRef, videoContainerRef, playbackRate, setPlaybackRate, outputPlaybackRate, setOutputPlaybackRate, commandedTime, seekAbs, playingRef, getRelevantTime, setPlaying, onSeeked, relevantTime, onStartPlaying, setCommandedTime, setOutputPlaybackRateState, commandedTimeRef, onStopPlaying, onVideoAbort, playerTime, setPlayerTime, playbackMode, setPlaybackMode, playbackModeRef, playing, play, pause, seekRel } = useVideo({ filePath });
- const { timecodePlaceholder, formatTimecode, formatTimeAndFrames, parseTimecode, getFrameCount, promptTimecode } = useTimecode({ detectedFps, timecodeFormat, showGenericDialog });
+ const { timecodePlaceholder, formatTimecode, parseTimecode, getFrameCount, promptTimecode } = useTimecode({ detectedFps, timecodeFormat, showGenericDialog });
const { loadSubtitle, subtitlesByStreamId, setSubtitlesByStreamId } = useSubtitles();
const fileDurationNonZero = isDurationValid(fileDuration) ? fileDuration : 1;
@@ -230,6 +231,10 @@ function App() {
const isFileOpened = !!filePath;
+ const [hoveringTime, setHoveringTime] = useState();
+
+ const displayTime = (hoveringTime != null && isFileOpened && !playing ? hoveringTime : relevantTime) + startTimeOffset;
+
const onOutputFormatUserChange = useCallback((newFormat: string) => {
setFileFormat(newFormat);
if (outFormatLocked) {
@@ -345,7 +350,7 @@ function App() {
}, [isFileOpened]);
const {
- cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, labelSegment, splitCurrentSegment, focusSegmentAtCursor, selectSegmentsAtCursor, createNumSegments, createFixedDurationSegments, createFixedByteSizedSegments, createRandomSegments, getSegEstimatedSize, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, clearSegColorCounter, loadCutSegments, setCutTime, setCurrentSegIndex, labelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, selectSegmentsByLabel, selectSegmentsByExpr, selectAllMarkers, mutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, selectedSegments, segmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, findSegmentsAtCursor, maybeCreateFullLengthSegment, currentCutSegOrWholeTimeline, segColorCounter,
+ cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, modifySelectedSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, labelSegment, splitCurrentSegment, focusSegmentAtCursor, selectSegmentsAtCursor, createNumSegments, createFixedDurationSegments, createFixedByteSizedSegments, createRandomSegments, getSegEstimatedSize, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, inverseCutSegments, clearSegments, clearSegColorCounter, loadCutSegments, setCutTime, setCurrentSegIndex, labelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, selectSegmentsByLabel, selectSegmentsByExpr, selectAllMarkers, mutateSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, selectedSegments, segmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, findSegmentsAtCursor, maybeCreateFullLengthSegment, currentCutSegOrWholeTimeline, segColorCounter,
} = useSegments({ filePath, workingRef, setWorking, setProgress, videoStream: activeVideoStream, fileDuration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode, appendFfmpegCommandLog, fileDurationNonZero, mainFileMeta: mainFileMeta?.ffprobeMeta, seekAbs, activeVideoStreamIndex, activeAudioStreamIndexes, handleError, showGenericDialog, simpleMode, ffmpegHwaccel });
const { getEdlFilePath, projectFileSavePath, getProjectFileSavePath } = useSegmentsAutoSave({ autoSaveProjectFile, storeProjectInWorkingDir, filePath, customOutDir, cutSegments });
@@ -864,8 +869,8 @@ function App() {
const generateCutMergedFileNames = useCallback(async (template: string) => {
invariant(fileFormat != null && outputDir != null && filePath != null);
- return generateCutMergedFileNamesRaw({ template, isCustomFormatSelected, fileFormat, sourceFile: { path: filePath }, outputDir, safeOutputFileName, maxLabelLength, exportCount, currentFileExportCount, segLabels: segmentsToExport.map((seg) => seg.name ?? '') });
- }, [currentFileExportCount, exportCount, fileFormat, filePath, isCustomFormatSelected, maxLabelLength, outputDir, safeOutputFileName, segmentsToExport]);
+ return generateCutMergedFileNamesRaw({ template, isCustomFormatSelected, fileFormat, sourceFile: { path: filePath, ...mainFileMeta }, outputDir, safeOutputFileName, maxLabelLength, exportCount, currentFileExportCount, segLabels: segmentsToExport.map((seg) => seg.name ?? '') });
+ }, [currentFileExportCount, exportCount, fileFormat, filePath, isCustomFormatSelected, mainFileMeta, maxLabelLength, outputDir, safeOutputFileName, segmentsToExport]);
const generateMergedFileNames = useCallback(async (params: GenerateMergedOutFileNamesParams) => (
generateMergedFileNamesRaw({ ...params, isCustomFormatSelected, safeOutputFileName, maxLabelLength, exportCount })
@@ -913,11 +918,9 @@ function App() {
invariant(fileName != null);
const outPath = getOutPath({ customOutDir, filePath: firstPath, fileName });
- let chaptersFromSegments: Awaited>;
- if (segmentsToChapters) {
- const chapterNames = paths.map((path) => parsePath(path).name);
- chaptersFromSegments = await createChaptersFromSegments({ segmentPaths: paths, chapterNames });
- }
+ const chaptersFromSegments = segmentsToChapters
+ ? await createChaptersFromSegments({ paths, defaultChapterNames: paths.map((path) => parsePath(path).name), useFileChapters: true })
+ : undefined;
let inputSize: number;
try {
@@ -1269,6 +1272,23 @@ function App() {
}
}, [filePath, workingRef, setWorking, withErrorHandling, getRelevantTime, videoRef, usingPreviewFile, captureFrameMethod, captureFrameFromFfmpeg, customOutDir, captureFormat, captureFrameQuality, captureFrameFromTag, simpleMode, prefersReducedMotion, hideAllNotifications, openExportFinishedDialog]);
+ const shiftAllSegmentTimes = useCallback(async () => {
+ const shift = await openShiftSegmentsDialog({ inputPlaceholder: timecodePlaceholder, parseTimecode });
+ if (shift == null) return;
+
+ const { startShift, endShift } = shift;
+ await modifySelectedSegmentTimes((segment) => {
+ const newSegment = { ...segment };
+ if (startShift != null) {
+ newSegment.start += startShift;
+ }
+ if (endShift != null && newSegment.end != null) {
+ newSegment.end += endShift;
+ }
+ return newSegment;
+ });
+ }, [modifySelectedSegmentTimes, openShiftSegmentsDialog, parseTimecode, timecodePlaceholder]);
+
const captureSnapshotToClipboard = useCallback(async () => {
if (!filePath || workingRef.current) return;
try {
@@ -1527,10 +1547,11 @@ function App() {
const toggleSettings = useCallback(() => setSettingsVisible((val) => !val), []);
const seekClosestKeyframe = useCallback((direction: number) => {
- const time = findNearestKeyFrameTime({ time: getRelevantTime(), direction });
+ const sigma = detectedFps ? (1 / detectedFps) : 0.1; // because we don't want it to find the keyframe we're currently at.
+ const time = findNearestKeyFrameTime({ time: getRelevantTime() + direction * sigma, direction });
if (time == null) return;
seekAbs(time);
- }, [findNearestKeyFrameTime, getRelevantTime, seekAbs]);
+ }, [detectedFps, findNearestKeyFrameTime, getRelevantTime, seekAbs]);
const onTimelineWheel = useTimelineScroll({ wheelSensitivity, mouseWheelZoomModifierKey, mouseWheelFrameSeekModifierKey, mouseWheelKeyframeSeekModifierKey, invertTimelineScroll, zoomRel, seekRel, shortStep, seekClosestKeyframe });
@@ -1838,11 +1859,12 @@ function App() {
if (isFileOpened) inputOptions.mergeWithCurrentFile = i18n.t('Merge/concatenate with current file');
if (batchFiles.length > 0 || newFilePaths.length > 1) inputOptions.addToBatch = i18n.t('Add the file to the batch list');
- const inputOptionsKeys = Object.keys(inputOptions);
+ const inputOptionsKeys = Object.keys(inputOptions) as (keyof typeof inputOptions)[];
- let openFileResponse: string | undefined;
+ let openFileResponse: OpenFileResponse | undefined;
if (inputOptionsKeys.length === 1) [openFileResponse] = inputOptionsKeys;
- if (enableAskForFileOpenAction && inputOptionsKeys.length > 1) openFileResponse = await askForFileOpenAction(inputOptions);
+ if (!enableAskForFileOpenAction && inputOptionsKeys.length > 1) openFileResponse = 'addToBatch';
+ if (enableAskForFileOpenAction && inputOptionsKeys.length > 1) openFileResponse = await askForFileOpenAction(Object.entries(inputOptions) as [OpenFileResponse, string][]);
else if (newFilePaths.length === 1) openFileResponse = 'open';
if (openFileResponse === 'open') {
@@ -1902,8 +1924,8 @@ function App() {
setConcatDialogOpen(true);
}, [batchFiles.length, openFilesDialog]);
- const togglePlaySelectedSegments = useCallback(() => togglePlay({ resetPlaybackRate: true, requestPlaybackMode: 'play-selected-segments' }), [togglePlay]);
- const toggleLoopSelectedSegments = useCallback(() => togglePlay({ resetPlaybackRate: true, requestPlaybackMode: 'loop-selected-segments' }), [togglePlay]);
+ const togglePlaySelectedSegments = useCallback(() => togglePlay({ resetPlaybackRate: false, requestPlaybackMode: 'play-selected-segments' }), [togglePlay]);
+ const toggleLoopSelectedSegments = useCallback(() => togglePlay({ resetPlaybackRate: false, requestPlaybackMode: 'loop-selected-segments' }), [togglePlay]);
const copySegmentsToClipboard = useCallback(async () => {
if (!isFileOpened || selectedSegments.length === 0) return;
@@ -2443,348 +2465,353 @@ function App() {
const rootStyle = useMemo(() => ({ ...baseColorStyle, display: 'flex', flexDirection: 'column', height: '100vh', transition: darkModeTransition }), [baseColorStyle]);
return (
-
-
-
-
-
-
-
-
-
- {showLeftBar && (
-
- )}
-
-
- {/* Middle part (also shown in fullscreen): */}
-
- {!isFileOpened &&
}
-
-
- {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
-
- {renderSubtitles()}
-
-
- {filePath != null && compatPlayerEnabled && }
-
+
+
+
+
+
+
+
- {bigWaveformEnabled &&
}
-
- {compatPlayerEnabled && (
-
- {isRotationSet ? (
- <>
-
- {t('Rotation preview')}
- >
- ) : (
- <>
- {t('FFmpeg-assisted playback')}
- >
- )}
+
+
+ {showLeftBar && (
+
+ )}
+
+
+ {/* Middle part (also shown in fullscreen): */}
+
+ {!isFileOpened &&
}
+
+
+ {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
+
+ {renderSubtitles()}
+
+
+ {filePath != null && compatPlayerEnabled && }
+
-
incrementMediaSourceQuality()} title={t('Select playback quality')}>{mediaSourceQualities[mediaSourceQuality]}
+ {bigWaveformEnabled &&
}
+
+ {compatPlayerEnabled && (
+
+ {isRotationSet ? (
+ <>
+
+ {t('Rotation preview')}
+ >
+ ) : (
+ <>
+ {t('FFmpeg-assisted playback')}
+ >
+ )}
+
+
incrementMediaSourceQuality()} title={t('Select playback quality')}>{mediaSourceQualities[mediaSourceQuality]}
+
+ {!compatPlayerRequired &&
}
+
+ )}
- {!compatPlayerRequired &&
}
-
- )}
+ {isFileOpened && (
+
+
+
+ {shouldShowPlaybackStreamSelector && (
+
+ )}
+
+ {!showRightBar && (
+
+ )}
+
+ )}
+
- {isFileOpened && (
-
-
+
+ {showRightBar && isFileOpened && filePath != null && (
+
+ )}
+
+
- {shouldShowPlaybackStreamSelector && (
-
- )}
+
+
+
+
+
- {!showRightBar && (
-
setTunerVisible(undefined)} />}
+
+ {/* Dialogs */}
+
+
+
+
+
+
+
+ {t('Tracks')}
+ {t('Click to select which tracks to keep when exporting:')}
+
+ {mainStreams && filePath != null && (
+
)}
-
- )}
-
+
+
+
+
+
+
+
+
+
+
+
+ {t('Settings')}
+ {t('Hover mouse over buttons in the main interface to see which function they have')}
+
+
+
+
+
+
+
+ 0 && concatDialogOpen} onHide={() => setConcatDialogOpen(false)} paths={batchFilePaths} mergedFileTemplate={mergedFileTemplateOrDefault} generateMergedFileNames={generateMergedFileNames} onConcat={userConcatFiles} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} fileFormat={fileFormat} setFileFormat={setFileFormat} detectedFileFormat={detectedFileFormat} setDetectedFileFormat={setDetectedFileFormat} onOutputFormatUserChange={onOutputFormatUserChange} />
+
+ setKeyboardShortcutsVisible(false)} keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} getKeyboardAction={getKeyboardAction} />
+
+ {/* This should probably be last, so that it's always on top */}
- {showRightBar && isFileOpened && filePath != null && (
-
- )}
+ {working && }
-
-
-
+ !open && closeGenericDialog()} />
-
-
+
- {tunerVisible != null &&
setTunerVisible(undefined)} />}
-
- {/* Dialogs */}
-
-
-
-
-
-
-
- {t('Tracks')}
- {t('Click to select which tracks to keep when exporting:')}
-
- {mainStreams && filePath != null && (
-
- )}
+ !open && setGenericError(undefined)} />
+
-
-
-
-
-
-
-
-
-
-
-
- {t('Settings')}
- {t('Hover mouse over buttons in the main interface to see which function they have')}
-
-
-
-
-
-
-
-
0 && concatDialogOpen} onHide={() => setConcatDialogOpen(false)} paths={batchFilePaths} mergedFileTemplate={mergedFileTemplateOrDefault} generateMergedFileNames={generateMergedFileNames} onConcat={userConcatFiles} setAlwaysConcatMultipleFiles={setAlwaysConcatMultipleFiles} alwaysConcatMultipleFiles={alwaysConcatMultipleFiles} fileFormat={fileFormat} setFileFormat={setFileFormat} detectedFileFormat={detectedFileFormat} setDetectedFileFormat={setDetectedFileFormat} onOutputFormatUserChange={onOutputFormatUserChange} />
-
- setKeyboardShortcutsVisible(false)} keyBindings={keyBindings} setKeyBindings={setKeyBindings} currentCutSeg={currentCutSeg} resetKeyBindings={resetKeyBindings} getKeyboardAction={getKeyboardAction} />
-
- {/* This should probably be last, so that it's always on top */}
-
- {working && }
-
-
- !open && closeGenericDialog()} />
-
-
-
- !open && setGenericError(undefined)} />
-
-
-
-
-
-
-
+
+
+
+
+
+
);
}
diff --git a/src/renderer/src/BottomBar.tsx b/src/renderer/src/BottomBar.tsx
index 127544ec..2c4463ab 100644
--- a/src/renderer/src/BottomBar.tsx
+++ b/src/renderer/src/BottomBar.tsx
@@ -25,7 +25,7 @@ import { useSegColors } from './contexts';
import { isExactDurationMatch } from './util/duration';
import useUserSettings from './hooks/useUserSettings';
import { askForPlaybackRate, checkAppPath } from './dialogs';
-import type { FormatTimecode, ParseTimecode, PlaybackMode, SegmentColorIndex, SegmentToExport, StateSegment } from './types';
+import type { FormatTimecode, GetFrameCount, ParseTimecode, PlaybackMode, SegmentColorIndex, SegmentToExport, StateSegment } from './types';
import type { WaveformMode } from '../../common/types';
import type { Frame } from './ffmpeg';
@@ -36,6 +36,16 @@ const zoomOptions = Array.from({ length: 13 }).fill(undefined).map((_unused, z)
const leftRightWidth = 100;
+const timeWrapperStyle: CSSProperties = {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ position: 'absolute',
+ inset: 0,
+ marginLeft: '1.5em',
+ pointerEvents: 'none',
+};
+
// eslint-disable-next-line react/display-name
const InvertCutModeButton = memo(({ invertCutSegments, setInvertCutSegments }: { invertCutSegments: boolean, setInvertCutSegments: Dispatch> }) => {
const { t } = useTranslation();
@@ -218,8 +228,9 @@ const CutTimeInput = memo(({ disabled, darkMode, cutTime, setCutTime, startTimeO
marginLeft: isStart ? 0 : 5,
marginRight: isStart ? 5 : 0,
boxSizing: 'border-box',
- fontFamily: 'inherit',
- width: 90,
+ fontFamily: 'monospace',
+ letterSpacing: '-.05em',
+ width: 94,
outline: 'none',
color: error ? dangerColor : (isCutTimeManualSet() ? 'var(--gray-12)' : 'var(--gray-11)'),
}), [border, error, isCutTimeManualSet, isStart]);
@@ -260,7 +271,7 @@ function BottomBar({
toggleShowThumbnails, toggleWaveformMode, waveformMode, showThumbnails,
outputPlaybackRate, setOutputPlaybackRate,
formatTimecode, parseTimecode, playbackRate,
- currentFrame, playbackMode,
+ currentFrame, playbackMode, displayTime, fileDurationNonZero, getFrameCount,
}: {
zoom: number,
setZoom: (fn: (z: number) => number) => void,
@@ -310,10 +321,15 @@ function BottomBar({
playbackRate: number,
currentFrame: Frame | undefined,
playbackMode: PlaybackMode | undefined,
+ displayTime: number,
+ fileDurationNonZero: number,
+ getFrameCount: GetFrameCount,
}) {
const { t } = useTranslation();
const { getSegColor } = useSegColors();
+ const isZoomed = zoom > 1;
+
const playStyle = useMemo(() => ({
paddingLeft: playing ? 0 : '.1em',
color: 'white',
@@ -372,7 +388,7 @@ function BottomBar({
const playbackRateRef = useRef(null);
useEffect(() => {
- playbackRateRef.current?.animate([{ transform: 'scale(1.7)', color: 'var(--gray-12)' }, {}], { duration: 200 });
+ playbackRateRef.current?.animate([{ transform: 'scale(2)', color: 'var(--gray-12)', backgroundColor: playbackRate === 1 ? 'var(--cyan-10)' : (playbackRate < 1 ? 'var(--yellow-8)' : 'var(--orange-10)') }, {}], { duration: 200 });
}, [playbackRate]);
function renderJumpCutpointButton(direction: number) {
@@ -404,9 +420,11 @@ function BottomBar({
const currentCutSegOrDefault = useMemo(() => currentCutSeg ?? { segColorIndex: 0 }, [currentCutSeg]);
+ const displayTimeFrameCount = useMemo(() => getFrameCount(displayTime), [displayTime, getFrameCount]);
+
return (
<>
-
+
{!simpleMode && (
<>
@@ -530,10 +548,7 @@ function BottomBar({
-
+
@@ -555,9 +570,9 @@ function BottomBar({
))}
-
{playbackRate.toFixed(1)}
+
{playbackRate.toFixed(1)}
-
+
{detectedFps != null && (
@@ -568,7 +583,7 @@ function BottomBar({
)}
{isFileOpened && !simpleMode && hasVideo && (
-
+
+
+
+ {formatTimecode({ seconds: displayTime })}
+
+ {displayTimeFrameCount ?? 0}f
+ {isZoomed && {Math.round((displayTime / fileDurationNonZero) * 100)}% }
+
+
+
+
{!simpleMode && isFileOpened && (
+
{/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */}
- { e.stopPropagation(); if ('targetUrl' in mifiLink && typeof mifiLink.targetUrl === 'string') electron.shell.openExternal(mifiLink.targetUrl); }} />
+
{ e.stopPropagation(); if ('targetUrl' in mifiLink && typeof mifiLink.targetUrl === 'string') electron.shell.openExternal(mifiLink.targetUrl); }} />
) : undefined}
diff --git a/src/renderer/src/SegmentList.tsx b/src/renderer/src/SegmentList.tsx
index df7999cf..d5f3e94f 100644
--- a/src/renderer/src/SegmentList.tsx
+++ b/src/renderer/src/SegmentList.tsx
@@ -394,6 +394,8 @@ function SegmentList({
const sortableList = useMemo(() => segmentsOrInverse.map((seg) => ({ id: seg.segId, seg })), [segmentsOrInverse]);
+ const isOnlyMarkers = useMemo(() => segmentsOrInverse.length > 0 && segmentsOrInverse.every((seg) => seg.end == null), [segmentsOrInverse]);
+
function getHeader() {
if (segmentsOrInverse.length === 0) {
if (invertCutSegments) {
@@ -404,7 +406,7 @@ function SegmentList({
return t('No segments to export.');
}
- if (segmentsOrInverse.every((s) => s.end == null)) {
+ if (isOnlyMarkers) {
return t('Markers:');
}
return t('Segments to export:');
@@ -681,6 +683,12 @@ function SegmentList({
+ {isOnlyMarkers && (
+
+ {t('Markers are segments without an end time and will not be exported. Convert markers to segments by setting their end time.')}
+
+ )}
+
{renderFooter()}
>
diff --git a/src/renderer/src/StreamsSelector.tsx b/src/renderer/src/StreamsSelector.tsx
index 5812b51b..2860fb4f 100644
--- a/src/renderer/src/StreamsSelector.tsx
+++ b/src/renderer/src/StreamsSelector.tsx
@@ -13,11 +13,10 @@ import AutoExportToggler from './components/AutoExportToggler';
import Select from './components/Select';
import type { FileStream } from './ffmpeg';
import { getStreamFps } from './ffmpeg';
-import { deleteDispositionValue } from './util';
import { getActiveDisposition, attachedPicDisposition, isGpsStream } from './util/streams';
import TagEditor from './components/TagEditor';
import type { FFprobeChapter, FFprobeFormat, FFprobeStream } from '../../common/ffprobe';
-import type { CustomTagsByFile, FilesMeta, FormatTimecode, ParamsByStreamId, StreamParams } from './types';
+import { contentDispositionOptionsSchema, deleteDispositionValue, dispositionOptions, type ContentDispositionOptions, type CustomTagsByFile, type FilesMeta, type FormatTimecode, type ParamsByStreamId, type StreamParams } from './types';
import Button, { DialogButton } from './components/Button';
import styles from './StreamsSelector.module.css';
import Json5Dialog from './components/Json5Dialog';
@@ -26,9 +25,6 @@ import TextInput from './components/TextInput';
import Switch from './components/Switch';
-const dispositionOptions = ['default', 'dub', 'original', 'comment', 'lyrics', 'karaoke', 'forced', 'hearing_impaired', 'visual_impaired', 'clean_effects', 'attached_pic', 'captions', 'descriptions', 'dependent', 'metadata'];
-const unchangedDispositionValue = 'llc_disposition_unchanged';
-
type UpdateStreamParams = (fileId: string, streamId: number, setter: (a: StreamParams) => void) => void;
interface EditingStream {
@@ -260,7 +256,7 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat
if (params.customTags == null) params.customTags = {};
const tags = params.customTags;
Object.entries(keyValues).forEach(([tag, value]) => {
- tags[tag] = value;
+ tags[tag] = tag === 'language' ? value.toLowerCase() : value;
});
});
}, [editingFile, editingStreamId, updateStreamParams]);
@@ -274,6 +270,13 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat
});
}, [editingFile, editingStreamId, updateStreamParams]);
+ const tagInfo = useMemo(() => ({
+ language: {
+ description: t('The language tag (ISO 639-2 code). For example "eng" for English. This is used by some players to select the appropriate audio/subtitle track based on the user\'s language preferences.'),
+ url: 'https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes',
+ },
+ }), [t]);
+
return (
v === false && setEditingStream(undefined)}>
@@ -286,7 +289,7 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat
Tags
-
+
@@ -301,6 +304,8 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat
);
});
+const unchangedDispositionValue = 'llc_disposition_unchanged';
+
// eslint-disable-next-line react/display-name
const Stream = memo(({ filePath, stream, onToggle, toggleCopyStreamIds, copyStream, fileDuration, setEditingStream, onExtractStreamPress, paramsByStreamId, updateStreamParams, formatTimecode, loadSubtitleTrackToSegments }: {
filePath: string,
@@ -355,9 +360,10 @@ const Stream = memo(({ filePath, stream, onToggle, toggleCopyStreamIds, copyStre
const onClick = () => onToggle && onToggle(stream.index);
const onDispositionChange = useCallback>((e) => {
- let newDisposition: string;
- if (dispositionOptions.includes(e.target.value)) {
- newDisposition = e.target.value;
+ let newDisposition: ContentDispositionOptions | 'llc_disposition_remove' | undefined;
+ const dispositionParsed = contentDispositionOptionsSchema.safeParse(e.target.value);
+ if (dispositionParsed.success) {
+ newDisposition = dispositionParsed.data;
} else if (e.target.value === deleteDispositionValue) {
newDisposition = deleteDispositionValue; // needs a separate value (not a real disposition)
} // else unchanged (undefined)
diff --git a/src/renderer/src/Timeline.module.css b/src/renderer/src/Timeline.module.css
index 1774509f..f0a2b22b 100644
--- a/src/renderer/src/Timeline.module.css
+++ b/src/renderer/src/Timeline.module.css
@@ -1,21 +1,3 @@
-.time-wrapper {
- position: absolute;
- left: 0;
- right: 0;
- bottom: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- pointer-events: none;
-}
-.time {
- background: rgba(0,0,0,0.4);
- border-radius: 3px;
- padding: 2px 4px;
- color: rgba(255, 255, 255, 0.8);
- transition: opacity 0.2s;
-}
-
@keyframes background-animation {
0% {background-position: 0% 50%}
100% {background-position: 100% 50%}
diff --git a/src/renderer/src/Timeline.tsx b/src/renderer/src/Timeline.tsx
index 6f77b1e7..e4e2a8da 100644
--- a/src/renderer/src/Timeline.tsx
+++ b/src/renderer/src/Timeline.tsx
@@ -1,5 +1,5 @@
import type { MutableRefObject, CSSProperties, WheelEventHandler, MouseEventHandler } from 'react';
-import { memo, useRef, useMemo, useCallback, useEffect, useState } from 'react';
+import { memo, useRef, useMemo, useCallback, useEffect } from 'react';
import { motion, useMotionValue, useSpring } from 'motion/react';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
@@ -84,11 +84,8 @@ const CommandedTime = memo(({ commandedTimePercent }: { commandedTimePercent: st
const timelineHeight = 36;
-const timeWrapperStyle: CSSProperties = { height: timelineHeight };
-
function Timeline({
fileDurationNonZero,
- startTimeOffset,
playerTime,
commandedTime,
relevantTime,
@@ -101,7 +98,6 @@ function Timeline({
currentCutSeg,
inverseCutSegments,
formatTimecode,
- formatTimeAndFrames,
waveforms,
overviewWaveform,
shouldShowWaveform,
@@ -114,13 +110,12 @@ function Timeline({
waveformEnabled,
waveformHeight,
showThumbnails,
- playing,
- isFileOpened,
onWheel,
commandedTimeRef,
goToTimecode,
darkMode,
setCutTime,
+ setHoveringTime,
} : {
fileDurationNonZero: number,
startTimeOffset: number,
@@ -136,7 +131,6 @@ function Timeline({
currentCutSeg: StateSegment | undefined,
inverseCutSegments: InverseCutSegment[],
formatTimecode: FormatTimecode,
- formatTimeAndFrames: (a: number) => string,
waveforms: WaveformSlice[],
overviewWaveform: OverviewWaveform | undefined,
shouldShowWaveform: boolean,
@@ -155,7 +149,8 @@ function Timeline({
commandedTimeRef: MutableRefObject,
goToTimecode: () => void,
darkMode: boolean,
- setCutTime: UseSegments['setCutTime'];
+ setCutTime: UseSegments['setCutTime'],
+ setHoveringTime: (time: number | undefined) => void,
}) {
const { t } = useTranslation();
@@ -166,11 +161,6 @@ function Timeline({
const timelineScrollerSkipEventDebounce = useRef<() => void>();
const timelineWrapperRef = useRef(null);
- const [hoveringTime, setHoveringTime] = useState();
-
- const displayTime = (hoveringTime != null && isFileOpened && !playing ? hoveringTime : relevantTime) + startTimeOffset;
- const displayTimePercent = useMemo(() => `${Math.round((displayTime / fileDurationNonZero) * 100)}%`, [displayTime, fileDurationNonZero]);
-
const isZoomed = zoom > 1;
const keyFramesInZoomWindow = useMemo(() => (zoomWindowEndTime == null ? [] : neighbouringKeyFrames.filter((f) => f.time >= zoomWindowStartTime && f.time <= zoomWindowEndTime)), [neighbouringKeyFrames, zoomWindowEndTime, zoomWindowStartTime]);
@@ -292,7 +282,7 @@ function Timeline({
useEffect(() => {
setHoveringTime(undefined);
- }, [relevantTime]);
+ }, [relevantTime, setHoveringTime]);
// for performance
const currentCutSegRef = useRef(currentCutSeg);
@@ -358,29 +348,7 @@ function Timeline({
window.addEventListener('mousemove', onMouseMove);
}, [fileDurationNonZero, getMouseTimelinePos, seekAbs, segmentMouseModifierKey, setCutTime, zoom]);
- const timeRef = useRef(null);
- const timeFadeTimeoutRef = useRef();
-
- const onMouseMove = useCallback>((e) => {
- // need to manually check, because we cannot use css :hover when pointer-events: none
- // and we need pointer-events: none on time because we want to be able to click through it to segments behind (and they are not parent)
- const rect = timeRef.current?.getBoundingClientRect();
- const isInBounds = rect && e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
- const showHide = (show: boolean) => timeRef.current?.style.setProperty('opacity', show ? '0.2' : '1');
- if (isInBounds != null) showHide(isInBounds);
- // console.log('isInBounds', isInBounds);
-
- // https://github.com/mifi/lossless-cut/issues/2592#issuecomment-3476211496
- if (timeFadeTimeoutRef.current) clearTimeout(timeFadeTimeoutRef.current);
- timeFadeTimeoutRef.current = setTimeout(() => showHide(false), 1000);
-
- if (!mouseDownRef.current) { // no button pressed
- setHoveringTime(getMouseTimelinePos(e.nativeEvent));
- }
- e.preventDefault();
- }, [getMouseTimelinePos]);
-
- const onMouseOut = useCallback(() => setHoveringTime(undefined), []);
+ const onMouseOut = useCallback(() => setHoveringTime(undefined), [setHoveringTime]);
const contextMenuTemplate = useMemo(() => [
{ label: t('Seek to timecode'), click: goToTimecode },
@@ -396,9 +364,9 @@ function Timeline({
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/mouse-events-have-key-events
@@ -486,12 +454,6 @@ function Timeline({
)}
-
-
-
- {formatTimeAndFrames(displayTime)}{isZoomed ? ` ${displayTimePercent}` : ''}
-
-
);
}
diff --git a/src/renderer/src/__snapshots__/segments.test.ts.snap b/src/renderer/src/__snapshots__/segments.test.ts.snap
index fec92459..88dc9c7e 100644
--- a/src/renderer/src/__snapshots__/segments.test.ts.snap
+++ b/src/renderer/src/__snapshots__/segments.test.ts.snap
@@ -1,7 +1,53 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[`combineOverlappingSegments 1`] = `
+exports[`combineOverlappingSegments > adjacent 1`] = `
[
+ {
+ "end": 2,
+ "start": 1,
+ },
+ {
+ "end": 3,
+ "start": 2,
+ },
+ {
+ "end": 4,
+ "start": 3,
+ },
+]
+`;
+
+exports[`combineOverlappingSegments > non touching 1`] = `
+[
+ {
+ "end": 2,
+ "start": 1,
+ },
+ {
+ "end": 4,
+ "start": 3,
+ },
+]
+`;
+
+exports[`combineOverlappingSegments > overlapping and non overlapping 1`] = `
+[
+ {
+ "end": 3,
+ "start": 1,
+ },
+ {
+ "end": 5,
+ "start": 4,
+ },
+]
+`;
+
+exports[`combineOverlappingSegments > various 1`] = `
+[
+ {
+ "start": 0,
+ },
{
"end": 87,
"start": 0,
@@ -11,9 +57,16 @@ exports[`combineOverlappingSegments 1`] = `
"start": 229,
},
{
- "end": 1487,
+ "end": 1101,
"start": 838,
},
+ {
+ "end": 1487,
+ "start": 1101,
+ },
+ {
+ "start": 1487,
+ },
{
"end": 1831,
"start": 1561,
diff --git a/src/renderer/src/components/ConcatDialog.tsx b/src/renderer/src/components/ConcatDialog.tsx
index 2be187d0..88c7d62b 100644
--- a/src/renderer/src/components/ConcatDialog.tsx
+++ b/src/renderer/src/components/ConcatDialog.tsx
@@ -2,9 +2,10 @@ import type { CSSProperties, Dispatch, SetStateAction } from 'react';
import { memo, useState, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AiOutlineMergeCells } from 'react-icons/ai';
-import { FaQuestionCircle, FaExclamationTriangle, FaCog, FaCheck, FaNotEqual } from 'react-icons/fa';
+import { FaQuestionCircle, FaExclamationTriangle, FaCog, FaCheck } from 'react-icons/fa';
import invariant from 'tiny-invariant';
import pMap from 'p-map';
+import { Table } from '@radix-ui/themes';
import Checkbox from './Checkbox';
import type { FileFfprobeMeta } from '../ffmpeg';
@@ -23,6 +24,7 @@ import FileNameTemplateEditor from './FileNameTemplateEditor';
import HighlightedText from './HighlightedText';
import type { FileStats } from '../types';
import OutDirSelector from './OutDirSelector';
+import { parseRatio } from '../../../common/util';
const { basename } = window.require('path');
@@ -37,8 +39,17 @@ function Alert({ text }: { text: string }) {
);
}
-type Problem = { index: number, type: 'extraneous' }
- | { index: number, type: 'parameter_mismatch', key: string, values: [string | number | undefined, string | number | undefined] };
+type ProblemValue = string | number | undefined;
+
+type Problem = {
+ index: number,
+} & ({
+ type: 'extraneous',
+} | {
+ type: 'parameter_mismatch',
+ key: string,
+ values: [ProblemValue, ProblemValue],
+});
function ConcatDialog({ isShown, onHide, paths, mergedFileTemplate, generateMergedFileNames, onConcat, alwaysConcatMultipleFiles, setAlwaysConcatMultipleFiles, fileFormat, setFileFormat, detectedFileFormat, setDetectedFileFormat, onOutputFormatUserChange }: {
isShown: boolean,
@@ -128,10 +139,20 @@ function ConcatDialog({ isShown, onHide, paths, mergedFileTemplate, generateMerg
}
// check all these parameters
(['codec_name', 'width', 'height', 'pix_fmt', 'level', 'profile', 'sample_fmt', 'avg_frame_rate', 'r_frame_rate', 'time_base'] as const).forEach((key) => {
- const val = stream[key];
- const referenceVal = referenceStream[key];
- if (val !== referenceVal) {
- addProblem(path, { type: 'parameter_mismatch', index: stream.index, key, values: [String(val), referenceVal] });
+ // special handling: https://github.com/mifi/lossless-cut/discussions/2740
+ if (key === 'avg_frame_rate') {
+ const val = parseRatio(stream[key]);
+ const referenceVal = parseRatio(referenceStream[key]);
+ const sigma = 0.01;
+ if ((val == null && referenceVal != null) || (val != null && referenceVal == null) || (val != null && referenceVal != null && Math.abs(val - referenceVal) >= sigma)) {
+ addProblem(path, { type: 'parameter_mismatch', index: stream.index, key, values: [String(val), referenceVal] });
+ }
+ } else {
+ const val = stream[key];
+ const referenceVal = referenceStream[key];
+ if (val !== referenceVal) {
+ addProblem(path, { type: 'parameter_mismatch', index: stream.index, key, values: [String(val), referenceVal] });
+ }
}
});
});
@@ -236,24 +257,44 @@ function ConcatDialog({ isShown, onHide, paths, mergedFileTemplate, generateMerg
{t('Mismatches detected')}
-
- {(problemsByFile[path] ?? []).map((problem) => (
-
- {t('Track {{num}}', { num: problem.index + 1 })}:
-
- {problem.type === 'extraneous' && t('Extraneous')}
-
- {problem.type === 'parameter_mismatch' && (
- <>
- {problem.key}
- {problem.values[0] ?? t('N/A')}
-
- {problem.values[1] ?? t('N/A')}
- >
- )}
-
- ))}
-
+
+
+
+ {t('Track')}
+ {t('Parameter')}
+ {t('Expected')}
+ {t('Actual')}
+
+
+
+
+ {(problemsByFile[path] ?? []).map((problem) => (
+
+
+ {problem.index + 1}
+
+
+ {problem.type === 'extraneous' && (
+ <>
+
+
+ {t('Extraneous')}
+ >
+ )}
+
+ {problem.type === 'parameter_mismatch' && (
+ <>
+ {problem.key}
+ {problem.values[0] ?? t('N/A')}
+
+ {problem.values[1] ?? t('N/A')}
+
+ >
+ )}
+
+ ))}
+
+
diff --git a/src/renderer/src/components/ExportConfirm.tsx b/src/renderer/src/components/ExportConfirm.tsx
index d03e0add..f348b4ae 100644
--- a/src/renderer/src/components/ExportConfirm.tsx
+++ b/src/renderer/src/components/ExportConfirm.tsx
@@ -170,11 +170,11 @@ function ExportConfirm({
const areWeCuttingProblematicStreams = areWeCutting && mainCopiedThumbnailStreams.length > 0;
const haveSegmentWithProblematicKeyframe = useMemo(() => {
- if (neighbouringKeyFrames.length === 0) return false;
+ if (neighbouringKeyFrames.length === 0) return false; // we don't know
return segmentsToExport.some(({ start, end }) => {
- const previousKeyframeTime = findNearestKeyFrameTime({ time: start, direction: -1 }) ?? 0;
+ const nearestPreviousKeyframeTime = findNearestKeyFrameTime({ time: start, direction: -1 }) ?? 0;
const segmentDuration = end - start;
- const estimatedExportedSegmentDuration = end - previousKeyframeTime;
+ const estimatedExportedSegmentDuration = end - nearestPreviousKeyframeTime;
// if estimated actual output length of segment is more than 1.5 times the intended segment duration, then we consider it problematic and warn the user about it.
return estimatedExportedSegmentDuration > segmentDuration * 1.5;
});
diff --git a/src/renderer/src/components/GenericDialog.tsx b/src/renderer/src/components/GenericDialog.tsx
index 93350708..f9206dd4 100644
--- a/src/renderer/src/components/GenericDialog.tsx
+++ b/src/renderer/src/components/GenericDialog.tsx
@@ -1,8 +1,9 @@
-import type { MouseEventHandler, ReactNode } from 'react';
+import type { FormEventHandler, MouseEventHandler, ReactNode } from 'react';
import React, { useCallback, useContext, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import invariant from 'tiny-invariant';
import { FaCheckCircle, FaInfoCircle } from 'react-icons/fa';
+import { TextField } from '@radix-ui/themes';
import * as Dialog from './Dialog';
import * as AlertDialog from './AlertDialog';
@@ -298,6 +299,83 @@ export function useDialog() {
});
}
+ const openShiftSegmentsDialog = useCallback(({ inputPlaceholder, parseTimecode }: {
+ inputPlaceholder: string,
+ parseTimecode: (s: string) => number | undefined,
+ }) => new Promise<{ startShift?: number, endShift?: number } | undefined>((resolve) => {
+ function ShiftDialog() {
+ const { onOpenChange } = useGenericDialogContext();
+ const [startValue, setStartValue] = useState('');
+ const [endValue, setEndValue] = useState('');
+
+ const parseValue = useCallback((value: string) => {
+ let parseableValue = value.trim();
+ if (!parseableValue) return undefined;
+ let sign = 1;
+ if (parseableValue[0] === '-') {
+ sign = -1;
+ parseableValue = parseableValue.slice(1);
+ }
+
+ const duration = parseTimecode(parseableValue);
+ invariant(duration != null);
+ if (duration === 0) return undefined;
+ return duration * sign;
+ }, []);
+
+ const handleSubmit = useCallback>((e) => {
+ e.preventDefault();
+ try {
+ const start = parseValue(startValue);
+ const end = parseValue(endValue);
+ // require at least one of them to be set
+ invariant(start != null || end != null);
+ const out: { startShift?: number, endShift?: number } = {};
+ if (start != null) out.startShift = start;
+ if (end != null) out.endShift = end;
+ resolve(out);
+ onOpenChange(false);
+ } catch (err) {
+ console.warn(err);
+ }
+ }, [parseValue, startValue, endValue, onOpenChange]);
+
+ return (
+
+ {t('Shift segments')}
+
+ {t('Shift all segments on the timeline by this amount. Negative values will be shifted back, while positive value will be shifted forward in time.')}
+
+
+
+ );
+ }
+
+ showGenericDialog({
+ render: () => ,
+ onClose: () => resolve(undefined),
+ });
+ }), [showGenericDialog, t]);
+
return {
genericDialog,
closeGenericDialog,
@@ -307,6 +385,7 @@ export function useDialog() {
openCutFinishedDialog,
openConcatFinishedDialog,
openCleanupFilesDialog,
+ openShiftSegmentsDialog,
};
}
diff --git a/src/renderer/src/components/HighlightedText.tsx b/src/renderer/src/components/HighlightedText.tsx
index 520925b4..58eb9319 100644
--- a/src/renderer/src/components/HighlightedText.tsx
+++ b/src/renderer/src/components/HighlightedText.tsx
@@ -1,14 +1,15 @@
import type { CSSProperties, HTMLAttributes } from 'react';
-import { memo } from 'react';
+import { memo, forwardRef } from 'react';
import { primaryTextColor } from '../colors';
import styles from './HighlightedText.module.css';
export const highlightedTextStyle: CSSProperties = { textDecorationColor: primaryTextColor };
-function HighlightedText({ children, style, ...props }: HTMLAttributes) {
+// eslint-disable-next-line react/display-name
+const HighlightedText = forwardRef>(({ children, style, ...props }, ref) => (
// eslint-disable-next-line react/jsx-props-no-spreading
- return {children} ;
-}
+ {children}
+));
export default memo(HighlightedText);
diff --git a/src/renderer/src/components/TagEditor.tsx b/src/renderer/src/components/TagEditor.tsx
index 4ab7f4b9..ab007359 100644
--- a/src/renderer/src/components/TagEditor.tsx
+++ b/src/renderer/src/components/TagEditor.tsx
@@ -3,7 +3,8 @@ import { memo, useRef, useState, useMemo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import invariant from 'tiny-invariant';
import { motion } from 'motion/react';
-import { FaCheck, FaEdit, FaPlus, FaTrash, FaUndo } from 'react-icons/fa';
+import { FaCheck, FaEdit, FaInfo, FaPlus, FaTrash, FaUndo } from 'react-icons/fa';
+import { IconButton, Tooltip } from '@radix-ui/themes';
import type { SegmentTags } from '../types';
import { segmentTagsSchema } from '../types';
@@ -15,12 +16,14 @@ import Warning from './Warning';
const { clipboard } = window.require('electron');
+const remote = window.require('@electron/remote');
+const { shell } = remote;
const activeColor = 'var(--gray-12)';
const emptyObject = {};
-function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editingTag, setEditingTag, onTagsChange, onTagReset, addTagTitle }: {
+function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editingTag, setEditingTag, onTagsChange, onTagReset, addTagTitle, tagInfo }: {
existingTags?: SegmentTags,
customTags?: SegmentTags | undefined,
editingTag: string | undefined,
@@ -28,6 +31,7 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi
onTagsChange: (keyValues: Record) => void,
onTagReset: (tag: string) => void,
addTagTitle: string,
+ tagInfo?: Record,
}) {
const { t } = useTranslation();
const ref = useRef(null);
@@ -65,8 +69,7 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi
const saveTag = useCallback(() => {
invariant(editingTag != null);
invariant(editingTagVal != null);
- const editingValTransformed = editingTag === 'language' ? editingTagVal.toLowerCase() : editingTagVal;
- onTagsChange({ [editingTag]: editingValTransformed });
+ onTagsChange({ [editingTag]: editingTagVal });
setEditingTag(undefined);
}, [editingTag, editingTagVal, onTagsChange, setEditingTag]);
@@ -140,7 +143,7 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi
{editingThis ? (
) : (
{effectiveTags[tag] ? String(effectiveTags[tag]) : `<${t('empty')}>`}
@@ -151,6 +154,11 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi
{thisTagNew ? : }
)}
+ {tagInfo?.[tag] && (
+
+ tagInfo[tag]?.url && shell.openExternal(tagInfo[tag].url)}>
+
+ )}
);
diff --git a/src/renderer/src/dialogs/index.tsx b/src/renderer/src/dialogs/index.tsx
index 44c10cef..1217d58a 100644
--- a/src/renderer/src/dialogs/index.tsx
+++ b/src/renderer/src/dialogs/index.tsx
@@ -99,10 +99,12 @@ export async function askForFfPath(defaultPath?: string | undefined) {
return (filePaths && filePaths.length === 1) ? filePaths[0] : undefined;
}
-export async function askForFileOpenAction(inputOptions: Record) {
- let value: string | undefined;
+export type OpenFileResponse = 'open' | 'project' | 'tracks' | 'subtitles' | 'addToBatch' | 'mergeWithCurrentFile';
- function onClick(key?: string) {
+export async function askForFileOpenAction(inputOptions: [OpenFileResponse, string][]) {
+ let value: OpenFileResponse | undefined;
+
+ function onClick(key?: OpenFileResponse) {
value = key;
getSwal().Swal.close();
}
@@ -112,7 +114,7 @@ export async function askForFileOpenAction(inputOptions: Record)
{i18n.t('You opened a new file. What do you want to do?')}
- {Object.entries(inputOptions).map(([key, text]) => (
+ {inputOptions.map(([key, text]) => (
onClick(key)} className="button-unstyled" style={{ display: 'block', marginBottom: '.5em' }}>
{text}
@@ -223,7 +225,12 @@ export async function askForSegmentDuration({ totalDuration, inputPlaceholder, p
const segmentDuration = parseTimecode(v);
if (segmentDuration != null) {
const numSegments = Math.ceil(totalDuration / segmentDuration);
- if (segmentDuration > 0 && segmentDuration < totalDuration && numSegments <= maxSegments) return null;
+ if (segmentDuration > 0 && numSegments <= maxSegments) {
+ if (segmentDuration < totalDuration) {
+ return null;
+ }
+ return i18n.t('Value must be shorter than total duration ({{totalDuration}})', { totalDuration: formatDuration({ seconds: totalDuration, shorten: true }) });
+ }
}
return i18n.t('Please input a valid duration. Example: {{example}}', { example: inputPlaceholder });
},
@@ -286,50 +293,6 @@ async function askForSegmentsStartOrEnd(text: string) {
return value === 'both' ? ['start', 'end'] as const : [value as 'start' | 'end'] as const;
}
-export async function askForShiftSegments({ inputPlaceholder, parseTimecode }: {
- inputPlaceholder: string,
- parseTimecode: ParseTimecode,
-}) {
- function parseValue(value: string) {
- let parseableValue = value;
- let sign = 1;
- if (parseableValue[0] === '-') {
- parseableValue = parseableValue.slice(1);
- sign = -1;
- }
- const duration = parseTimecode(parseableValue);
- if (duration != null && duration > 0) {
- return duration * sign;
- }
- return undefined;
- }
-
- const { value } = await getSwal().Swal.fire
({
- input: 'text',
- showCancelButton: true,
- inputValue: inputPlaceholder,
- text: i18n.t('Shift all segments on the timeline by this amount. Negative values will be shifted back, while positive value will be shifted forward in time.'),
- inputValidator: (v) => {
- const parsed = parseValue(v);
- if (parsed == null) return i18n.t('Please input a valid duration. Example: {{example}}', { example: inputPlaceholder });
- return null;
- },
- });
-
- if (value == null) return undefined;
- const parsed = parseValue(value);
- invariant(parsed != null);
-
- const startOrEnd = await askForSegmentsStartOrEnd(i18n.t('Do you want to shift the start or end timestamp by {{time}}?', { time: formatDuration({ seconds: parsed, shorten: true }) }));
- if (startOrEnd == null) return undefined;
-
- return {
- shiftAmount: parsed,
- shiftKeys: startOrEnd,
- };
-}
-
-
export async function askForAlignSegments() {
const startOrEnd = await askForSegmentsStartOrEnd(i18n.t('Do you want to align the segment start or end timestamps to keyframes?'));
if (startOrEnd == null) return undefined;
diff --git a/src/renderer/src/ffmpeg.ts b/src/renderer/src/ffmpeg.ts
index e44e6521..69d9e880 100644
--- a/src/renderer/src/ffmpeg.ts
+++ b/src/renderer/src/ffmpeg.ts
@@ -14,6 +14,7 @@ import type { FFprobeChapter, FFprobeFormat, FFprobeProbeResult, FFprobeStream }
import { parseSrt, parseSrtToSegments } from './edlFormats';
import { UnsupportedFileError, UserFacingError } from '../errors';
import mainApi from './mainApi';
+import { parseFfprobeDuration } from '../../common/util';
const { ffmpeg } = window.require('@electron/remote').require('./index.js');
@@ -191,9 +192,8 @@ export function getSafeCutTime(frames: Frame[], cutTime: number, nextMode: boole
return frames[index - 1]!.time;
}
-export function findNearestKeyFrameTime({ frames, time, direction, fps }: { frames: Frame[], time: number, direction: number, fps: number | undefined }) {
- const sigma = fps ? (1 / fps) : 0.1;
- const keyframes = frames.filter((f) => f.keyframe && (direction > 0 ? f.time > time + sigma : f.time < time - sigma));
+export function findNearestKeyFrameTime({ frames, time, direction }: { frames: Frame[], time: number, direction: number }) {
+ const keyframes = frames.filter((f) => f.keyframe && (direction > 0 ? f.time >= time : f.time <= time));
if (keyframes.length === 0) return undefined;
const nearestKeyFrame = sortBy(keyframes, (keyframe) => (direction > 0 ? keyframe.time - time : time - keyframe.time))[0];
if (!nearestKeyFrame) return undefined;
@@ -221,22 +221,6 @@ export function tryMapChaptersToEdl(chapters: FFprobeChapter[]) {
}
}
-export async function createChaptersFromSegments({ segmentPaths, chapterNames }: { segmentPaths: string[], chapterNames?: (string | undefined)[] | undefined }) {
- if (!chapterNames) return undefined;
- try {
- const durations = await pMap(segmentPaths, async (segmentPath) => (await getDuration(segmentPath)) ?? 0, { concurrency: 3 });
- let timeAt = 0;
- return durations.map((duration, i) => {
- const ret = { start: timeAt, end: timeAt + duration, name: chapterNames[i] };
- timeAt += duration;
- return ret;
- });
- } catch (err) {
- console.error('Failed to create chapters from segments', err);
- return undefined;
- }
-}
-
/**
* Some of the detected input formats are not the same as the muxer name used for encoding.
* Therefore we have to map between detected input format and encode format
@@ -577,3 +561,49 @@ export async function runFfmpegStartupCheck() {
export const getExperimentalArgs = (ffmpegExperimental: boolean) => (ffmpegExperimental ? ['-strict', 'experimental'] : []);
export const getVideoTimescaleArgs = (videoTimebase: number | undefined) => (videoTimebase != null ? ['-video_track_timescale', String(videoTimebase)] : []);
+
+export async function createChaptersFromSegments({ paths, defaultChapterNames, useFileChapters }: {
+ paths: string[],
+ defaultChapterNames?: (string | undefined)[] | undefined,
+ useFileChapters?: boolean,
+}) {
+ if (defaultChapterNames == null) return undefined;
+
+ try {
+ const filesMeta = await pMap(paths, async (path) => readFileFfprobeMeta(path), { concurrency: 3 });
+
+ let timeAt = 0;
+
+ return filesMeta.flatMap((meta, i) => {
+ const duration = parseFfprobeDuration(meta.format.duration) ?? 0;
+
+ try {
+ // if the file has chapters, use it (but only when not cutting)
+ if (useFileChapters && meta.chapters.length > 0) {
+ return meta.chapters.map((ch) => {
+ const start = parseFloat(ch.start_time);
+ const end = parseFloat(ch.end_time);
+ invariant(!Number.isNaN(start) && !Number.isNaN(end));
+ return {
+ start: timeAt + start,
+ end: timeAt + start,
+ name: ch.tags.title,
+ };
+ });
+ }
+
+ // if no chapters, or if cutting, then create one chapter for the whole file with the default name
+ return [{
+ start: timeAt,
+ end: timeAt + duration,
+ name: defaultChapterNames[i],
+ }];
+ } finally {
+ timeAt += duration;
+ }
+ });
+ } catch (err) {
+ console.error('Failed to create chapters', err);
+ return undefined;
+ }
+}
diff --git a/src/renderer/src/hooks/useFfmpegOperations.ts b/src/renderer/src/hooks/useFfmpegOperations.ts
index e6d3a779..35dd5809 100644
--- a/src/renderer/src/hooks/useFfmpegOperations.ts
+++ b/src/renderer/src/hooks/useFfmpegOperations.ts
@@ -5,18 +5,18 @@ import pMap from 'p-map';
import invariant from 'tiny-invariant';
import i18n from 'i18next';
-import { getSuffixedOutPath, transferTimestamps, getOutFileExtension, getOutDir, deleteDispositionValue, getHtml5ifiedPath, unlinkWithRetry, getFrameDuration, isMac, html5ifiedPrefix, html5dummySuffix, assertFileExists } from '../util';
+import { getSuffixedOutPath, transferTimestamps, getOutFileExtension, getOutDir, getHtml5ifiedPath, unlinkWithRetry, getFrameDuration, isMac, html5ifiedPrefix, html5dummySuffix, assertFileExists } from '../util';
import { isCuttingStart, isCuttingEnd, runFfmpegWithProgress, getFfCommandLine, getDuration, createChaptersFromSegments, readFileFfprobeMeta, getExperimentalArgs, getVideoTimescaleArgs, logStdoutStderr, runFfmpegConcat, RefuseOverwriteError, runFfmpeg } from '../ffmpeg';
import { getMapStreamsArgs, getStreamIdsToCopy } from '../util/streams';
import { needsSmartCut, getCodecParams } from '../smartcut';
import { getGuaranteedSegments, isDurationValid } from '../segments';
import type { FFprobeStream } from '../../../common/ffprobe';
import type { AvoidNegativeTs, FfmpegHwAccel, Html5ifyMode, PreserveMetadata } from '../../../common/types';
-import type { AllFilesMeta, Chapter, CopyfileStreams, CustomTagsByFile, LiteFFprobeStream, ParamsByStreamId, SegmentToExport } from '../types';
+import { deleteDispositionValue, type AllFilesMeta, type Chapter, type CopyfileStreams, type CustomTagsByFile, type LiteFFprobeStream, type ParamsByStreamId, type SegmentToExport } from '../types';
import type { LossyMode } from '../../../main';
import { UserFacingError } from '../../errors';
import mainApi from '../mainApi';
-import { getHwaccelArgs } from '../../../common/util';
+import { formatFfmpegTime, getHwaccelArgs } from '../../../common/util';
const { join, resolve, dirname } = window.require('path');
const { writeFile, mkdir, access, constants: { W_OK } } = window.require('fs/promises');
@@ -278,8 +278,8 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
if (detectedFps != null) cutDuration = Math.max(cutDuration, frameDuration); // ensure at least one frame duration
// Don't cut if no need: https://github.com/mifi/lossless-cut/issues/50
- const cutFromArgs = cuttingStart ? ['-ss', cutFromWithAdjustment.toFixed(5)] : [];
- const cutToArgs = cuttingEnd ? ['-t', cutDuration.toFixed(5)] : [];
+ const cutFromArgs = cuttingStart ? ['-ss', formatFfmpegTime(cutFromWithAdjustment)] : [];
+ const cutToArgs = cuttingEnd ? ['-t', formatFfmpegTime(cutDuration)] : [];
const copyFileStreamsFiltered = copyFileStreams.filter(({ streamIds }) => streamIds.length > 0);
@@ -289,7 +289,15 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// If cutting multiple files, `-ss` must be before `-i`, regardless of `ssBeforeInput` choice
// and it seems that `-t` must be after `-i` #896
const inputFilesArgs = copyFileStreamsFiltered.length > 1
- ? flatMap(copyFileStreamsFiltered, ({ path }) => [...cutFromArgs, '-i', path, ...cutToArgs])
+ ? flatMap(copyFileStreamsFiltered, ({ streamIds, path }) => {
+ // don't "cut"/seek cover art or images attached by users, see https://github.com/mifi/lossless-cut/issues/2884
+ const streamParams = streamIds.map((streamId) => paramsByStreamId.get(path)?.get(streamId));
+ if (streamIds.length === 1 && streamParams[0]?.disposition === 'attached_pic') {
+ return ['-i', path];
+ }
+
+ return [...cutFromArgs, '-i', path, ...cutToArgs];
+ })
: [
...(ssBeforeInput ? cutFromArgs : []),
'-i', copyFileStreamsFiltered[0]!.path,
@@ -509,10 +517,10 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
// No progress if we set loglevel warning :(
// '-loglevel', 'warning',
- '-ss', cutFrom.toFixed(5), // if we don't -ss before -i, seeking will be slow for long files, see https://github.com/mifi/lossless-cut/issues/126#issuecomment-1135451043
+ '-ss', formatFfmpegTime(cutFrom), // if we don't -ss before -i, seeking will be slow for long files, see https://github.com/mifi/lossless-cut/issues/126#issuecomment-1135451043
'-i', filePath,
'-ss', '0', // If we don't do this, the output seems to start with an empty black after merging with the encoded part
- '-t', (cutTo - cutFrom).toFixed(5),
+ '-t', formatFfmpegTime(cutTo - cutFrom),
...mapStreamsArgs,
@@ -717,7 +725,7 @@ function useFfmpegOperations({ filePath, treatInputFileModifiedTimeAsStart, trea
if (await shouldSkipExistingFile(mergedOutFilePath)) return;
- const chapters = await createChaptersFromSegments({ segmentPaths, chapterNames });
+ const chapters = await createChaptersFromSegments({ paths: segmentPaths, defaultChapterNames: chapterNames });
const metadataFromPath = segmentPaths[0];
invariant(metadataFromPath != null);
diff --git a/src/renderer/src/hooks/useKeyframes.ts b/src/renderer/src/hooks/useKeyframes.ts
index 4e14b792..3cca5a6d 100644
--- a/src/renderer/src/hooks/useKeyframes.ts
+++ b/src/renderer/src/hooks/useKeyframes.ts
@@ -42,7 +42,7 @@ function useKeyframes({ keyframesEnabled, filePath, commandedTime, videoStream,
return map;
}, [detectedFps, neighbouringKeyFrames]);
- const findNearestKeyFrameTime = useCallback(({ time, direction }: { time: number, direction: number }) => ffmpegFindNearestKeyFrameTime({ frames: neighbouringKeyFrames, time, direction, fps: detectedFps }), [neighbouringKeyFrames, detectedFps]);
+ const findNearestKeyFrameTime = useCallback(({ time, direction }: { time: number, direction: number }) => ffmpegFindNearestKeyFrameTime({ frames: neighbouringKeyFrames, time, direction }), [neighbouringKeyFrames]);
useEffect(() => setNeighbouringKeyFrames({}), [filePath, videoStream]);
diff --git a/src/renderer/src/hooks/useSegments.tsx b/src/renderer/src/hooks/useSegments.tsx
index 4371ef80..ac3c6ad5 100644
--- a/src/renderer/src/hooks/useSegments.tsx
+++ b/src/renderer/src/hooks/useSegments.tsx
@@ -12,7 +12,7 @@ import TextInput from '../components/TextInput';
import { detectSceneChanges as ffmpegDetectSceneChanges, readFrames, mapTimesToSegments, findKeyframeNearTime } from '../ffmpeg';
import { getFileSize, shuffleArray } from '../util';
import { errorToast } from '../swal';
-import { createNumSegments as createNumSegmentsDialog, createFixedByteSixedSegments as createFixedByteSixedSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, askForSegmentDuration, toastError } from '../dialogs';
+import { createNumSegments as createNumSegmentsDialog, createFixedByteSixedSegments as createFixedByteSixedSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, askForAlignSegments, selectSegmentsByLabelDialog, askForSegmentDuration, toastError } from '../dialogs';
import { createSegment, sortSegments, invertSegments, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, addSegmentColorIndex, filterNonMarkers, makeDurationSegments, isInitialSegment } from '../segments';
import type { FfmpegDialog } from '../ffmpegParameters';
import { parameters as allFfmpegParameters, getHint, getLabel } from '../ffmpegParameters';
@@ -495,20 +495,6 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
safeSetCutSegments(newSegments, fileDuration);
}, [cutSegments, fileDuration, safeSetCutSegments]);
- const shiftAllSegmentTimes = useCallback(async () => {
- const shift = await askForShiftSegments({ inputPlaceholder: timecodePlaceholder, parseTimecode });
- if (shift == null) return;
-
- const { shiftAmount, shiftKeys } = shift;
- await modifySelectedSegmentTimes((segment) => {
- const newSegment = { ...segment };
- shiftKeys.forEach((key) => {
- if (newSegment[key] != null) newSegment[key] += shiftAmount;
- });
- return newSegment;
- });
- }, [modifySelectedSegmentTimes, parseTimecode, timecodePlaceholder]);
-
const alignSegmentTimesToKeyframes = useCallback(async () => {
if (!videoStream || workingRef.current) return;
try {
@@ -986,7 +972,7 @@ function useSegments({ filePath, workingRef, setWorking, setProgress, videoStrea
fillSegmentsGaps,
combineOverlappingSegments,
combineSelectedSegments,
- shiftAllSegmentTimes,
+ modifySelectedSegmentTimes,
alignSegmentTimesToKeyframes,
updateSegOrder,
updateSegOrders,
diff --git a/src/renderer/src/hooks/useTimecode.tsx b/src/renderer/src/hooks/useTimecode.tsx
index 5ae69ef9..c078d89a 100644
--- a/src/renderer/src/hooks/useTimecode.tsx
+++ b/src/renderer/src/hooks/useTimecode.tsx
@@ -54,16 +54,6 @@ export default ({ detectedFps, timecodeFormat, showGenericDialog }: {
return parseDuration(val);
}, [detectedFps, frameCountToDuration, timecodeFormat]);
- const formatTimeAndFrames = useCallback((seconds: number) => {
- const frameCount = getFrameCount(seconds);
-
- const timeStr = timecodeFormat === 'timecodeWithFramesFraction'
- ? formatDuration({ seconds, fps: detectedFps })
- : formatDuration({ seconds });
-
- return `${timeStr} (${frameCount ?? '0'})`;
- }, [detectedFps, timecodeFormat, getFrameCount]);
-
const promptTimecode = useCallback(async ({ initialValue, title, description, inputPlaceholder, allowRelative = false }: {
initialValue?: string | undefined,
title: string,
@@ -141,7 +131,6 @@ export default ({ detectedFps, timecodeFormat, showGenericDialog }: {
return {
parseTimecode,
formatTimecode,
- formatTimeAndFrames,
timecodePlaceholder,
getFrameCount,
promptTimecode,
diff --git a/src/renderer/src/index.tsx b/src/renderer/src/index.tsx
index 2d5dd97d..5e955eb5 100644
--- a/src/renderer/src/index.tsx
+++ b/src/renderer/src/index.tsx
@@ -22,6 +22,8 @@ import '@fontsource/open-sans/700-italic.css';
import '@fontsource/open-sans/800.css';
import '@fontsource/open-sans/800-italic.css';
+import '@radix-ui/themes/styles.css';
+
import type { KeyboardLayoutMap } from './types';
import App from './App';
import ErrorBoundary from './ErrorBoundary';
diff --git a/src/renderer/src/segments.test.ts b/src/renderer/src/segments.test.ts
index 66457271..35be671a 100644
--- a/src/renderer/src/segments.test.ts
+++ b/src/renderer/src/segments.test.ts
@@ -101,57 +101,6 @@ it('detects overlapping segments', () => {
]);
});
-test('combineOverlappingSegments', () => {
- const segments = [
- {
- start: 0,
- },
- {
- start: 838,
- end: 1101,
- },
- {
- start: 1101,
- end: 1244,
- },
- {
- start: 1216,
- end: 1487,
- },
- {
- start: 1487,
- },
- {
- start: 1625,
- },
- {
- start: 503,
- end: 669,
- },
- {
- start: 392,
- end: 716,
- },
- {
- start: 229,
- end: 784,
- },
- {
- start: 0,
- end: 87,
- },
- {
- start: 1561,
- end: 1831,
- },
- {
- start: 2027,
- },
- ];
-
- expect(combineOverlappingSegments(segments)).toMatchSnapshot();
-});
-
it('detects overlapping segments, undefined end', () => {
expect(partitionIntoOverlappingRanges([
{ start: 1, end: undefined },
@@ -213,3 +162,77 @@ describe('invertSegments', () => {
], false, false)).toMatchSnapshot();
});
});
+
+describe('combineOverlappingSegments', () => {
+ test('non touching', () => {
+ expect(combineOverlappingSegments([
+ { start: 1, end: 2 },
+ { start: 3, end: 4 },
+ ])).toMatchSnapshot();
+ });
+ test('overlapping and non overlapping', () => {
+ expect(combineOverlappingSegments([
+ { start: 1, end: 2 },
+ { start: 1.5, end: 3 },
+ { start: 4, end: 5 },
+ ])).toMatchSnapshot();
+ });
+ test('adjacent', () => {
+ expect(combineOverlappingSegments([
+ { start: 1, end: 2 },
+ { start: 2, end: 3 },
+ { start: 3, end: 4 },
+ ])).toMatchSnapshot();
+ });
+
+ test('various', () => {
+ const segments = [
+ {
+ start: 0,
+ },
+ {
+ start: 838,
+ end: 1101,
+ },
+ {
+ start: 1101,
+ end: 1244,
+ },
+ {
+ start: 1216,
+ end: 1487,
+ },
+ {
+ start: 1487,
+ },
+ {
+ start: 1625,
+ },
+ {
+ start: 503,
+ end: 669,
+ },
+ {
+ start: 392,
+ end: 716,
+ },
+ {
+ start: 229,
+ end: 784,
+ },
+ {
+ start: 0,
+ end: 87,
+ },
+ {
+ start: 1561,
+ end: 1831,
+ },
+ {
+ start: 2027,
+ },
+ ];
+
+ expect(combineOverlappingSegments(segments)).toMatchSnapshot();
+ });
+});
diff --git a/src/renderer/src/segments.ts b/src/renderer/src/segments.ts
index c85adde1..f3bd3a4c 100644
--- a/src/renderer/src/segments.ts
+++ b/src/renderer/src/segments.ts
@@ -129,8 +129,8 @@ export function combineOverlappingSegments(existingSegmen
const currentSegmentEndOrStart = currentSegment.end ?? currentSegment.start;
- // Check if the current segment overlaps or is adjacent to the next segment
- if (currentSegmentEndOrStart >= nextSegment.start) {
+ // Check if the current segment overlaps with the next segment
+ if (currentSegmentEndOrStart > nextSegment.start) {
currentSegment = {
...currentSegment,
end: Math.max(currentSegmentEndOrStart, nextSegment.end ?? nextSegment.start),
diff --git a/src/renderer/src/types.ts b/src/renderer/src/types.ts
index 77a727be..f520b72c 100644
--- a/src/renderer/src/types.ts
+++ b/src/renderer/src/types.ts
@@ -175,9 +175,15 @@ export interface BsfAspectRatioParams {
den: number;
}
+export const dispositionOptions = ['default', 'dub', 'original', 'comment', 'lyrics', 'karaoke', 'forced', 'hearing_impaired', 'visual_impaired', 'clean_effects', 'attached_pic', 'captions', 'descriptions', 'dependent', 'metadata'] as const;
+export const contentDispositionOptionsSchema = z.enum(dispositionOptions);
+export type ContentDispositionOptions = z.infer;
+export const deleteDispositionValue = 'llc_disposition_remove' as const;
+
+
export interface StreamParams {
customTags?: Record,
- disposition?: string,
+ disposition?: ContentDispositionOptions | 'llc_disposition_remove' | undefined,
bsfH264Mp4toannexb?: boolean,
bsfHevcMp4toannexb?: boolean,
bsfHevcAudInsert?: boolean,
diff --git a/src/renderer/src/util.ts b/src/renderer/src/util.ts
index 017896bf..6799528d 100644
--- a/src/renderer/src/util.ts
+++ b/src/renderer/src/util.ts
@@ -273,9 +273,6 @@ export function getHtml5ifiedPath(cod: string | undefined, fp: string, type: Exc
return getSuffixedOutPath({ customOutDir: cod, filePath: fp, nameSuffix: `${html5ifiedPrefix}${type}.${ext}` });
}
-
-export const deleteDispositionValue = 'llc_disposition_remove';
-
export const mirrorTransform = 'matrix(-1, 0, 0, 1, 0, 0)';
// todo this is not a correct assumption
diff --git a/src/renderer/src/util/streams.ts b/src/renderer/src/util/streams.ts
index 6ed4d19c..8bdfae30 100644
--- a/src/renderer/src/util/streams.ts
+++ b/src/renderer/src/util/streams.ts
@@ -197,10 +197,18 @@ function getPerStreamFlags({ stream, outputIndex, outFormat, manuallyCopyDisposi
return args;
}
+export type AllFilesStreamMeta = Record>;
+
+export const getStreamById = ({ allFilesMeta, streamId, path }: {
+ allFilesMeta: AllFilesStreamMeta,
+ streamId: number,
+ path: string,
+}) => allFilesMeta?.[path]?.streams.find((s) => s.index === streamId);
+
export function getMapStreamsArgs({ startIndex = 0, outFormat, allFilesMeta, copyFileStreams, manuallyCopyDisposition, getVideoArgs, needFlac }: {
startIndex?: number,
outFormat: string | undefined,
- allFilesMeta: Record>,
+ allFilesMeta: AllFilesStreamMeta,
copyFileStreams: CopyfileStreams,
manuallyCopyDisposition?: boolean,
getVideoArgs?: GetVideoArgsFn,
@@ -211,9 +219,9 @@ export function getMapStreamsArgs({ startIndex = 0, outFormat, allFilesMeta, cop
copyFileStreams.forEach(({ streamIds, path }, fileIndex) => {
streamIds.forEach((streamId) => {
- const { streams } = allFilesMeta[path]!;
- const stream = streams.find((s) => s.index === streamId);
+ const stream = getStreamById({ allFilesMeta, streamId, path });
invariant(stream != null);
+
args = [
...args,
'-map', `${fileIndex}:${streamId}`,
diff --git a/yarn.lock b/yarn.lock
index dcb210f2..018df5d7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1845,6 +1845,13 @@ __metadata:
languageName: node
linkType: hard
+"@nodable/entities@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "@nodable/entities@npm:2.1.0"
+ checksum: 10/355c55e82aebe45d4b962d16530951df51e19e3e63a27ea61ad3260c0807064619b270b9c83db10e8394f42760abd5b7f7c5b5117678c4246ce8364a4aafc637
+ languageName: node
+ linkType: hard
+
"@nodelib/fs.scandir@npm:2.1.5":
version: 2.1.5
resolution: "@nodelib/fs.scandir@npm:2.1.5"
@@ -2350,6 +2357,13 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/number@npm:1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/number@npm:1.1.1"
+ checksum: 10/58717faf3f7aa180fdfcde7083cae0bc06677cbd08fd2bed5a3f8820deeb6f514f7d475f1fbb61e1f9a16cb2e7daf1000b2c614b0de3520fccfc04e3576e4566
+ languageName: node
+ linkType: hard
+
"@radix-ui/primitive@npm:1.1.3":
version: 1.1.3
resolution: "@radix-ui/primitive@npm:1.1.3"
@@ -2357,7 +2371,53 @@ __metadata:
languageName: node
linkType: hard
-"@radix-ui/react-alert-dialog@npm:^1.1.15":
+"@radix-ui/react-accessible-icon@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-accessible-icon@npm:1.1.7"
+ dependencies:
+ "@radix-ui/react-visually-hidden": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/716230e25f7342516f8cd5530fed815356c79d2ea3c5cc31129749e49cd6fd20bea548781b04349fe8136fd71caabf693b1268f7fe8d32dcd765fa3e051dbc19
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-accordion@npm:1.2.12":
+ version: 1.2.12
+ resolution: "@radix-ui/react-accordion@npm:1.2.12"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collapsible": "npm:1.1.12"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/5cf644dcddd2d29ceba2e82043e574cb25745ad42ecdf47e8e212732fec012f8121f49c7fa8527deb28fbc37d3bd4fed2bbe61ed06a56c69c8aaad1ade6db43b
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-alert-dialog@npm:1.1.15, @radix-ui/react-alert-dialog@npm:^1.1.15":
version: 1.1.15
resolution: "@radix-ui/react-alert-dialog@npm:1.1.15"
dependencies:
@@ -2400,7 +2460,49 @@ __metadata:
languageName: node
linkType: hard
-"@radix-ui/react-checkbox@npm:^1.3.3":
+"@radix-ui/react-aspect-ratio@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-aspect-ratio@npm:1.1.7"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/50e84b77df4f777f03e5305ef6ca1b02824babc99bec5b7a2484d5c45c83a606375d1d3da63f7ac68d03b6abb27c3ddc0cbb02296a4315715116c221e7d61582
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-avatar@npm:1.1.10":
+ version: 1.1.10
+ resolution: "@radix-ui/react-avatar@npm:1.1.10"
+ dependencies:
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-is-hydrated": "npm:0.1.0"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/5485763c44793aa833572989913b491b40470b013e0be0dd7e72392ac9e80e91bd6c48c6c7f7053b906b70a13829a6cb30e268b8daf71359ac86cf27c300b82f
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-checkbox@npm:1.3.3, @radix-ui/react-checkbox@npm:^1.3.3":
version: 1.3.3
resolution: "@radix-ui/react-checkbox@npm:1.3.3"
dependencies:
@@ -2426,6 +2528,32 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-collapsible@npm:1.1.12":
+ version: 1.1.12
+ resolution: "@radix-ui/react-collapsible@npm:1.1.12"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/13dfea611da7201b6db3dd83470ac644fbf3b17f0fe2214914cfe703493b97b4d66aca000fd84eda64ed1cfb8d356d7088e1bc5ae7f0aad52d723c5ac5d9381f
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-collection@npm:1.1.7":
version: 1.1.7
resolution: "@radix-ui/react-collection@npm:1.1.7"
@@ -2461,6 +2589,30 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-context-menu@npm:2.2.16":
+ version: 2.2.16
+ resolution: "@radix-ui/react-context-menu@npm:2.2.16"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-menu": "npm:2.1.16"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/3215af17c6481d517b8df139543485aac77fd540012192394437bd6487d4a7b221e683cc6e08959db41c0238296ae94fae3a5715495caedcd79afc80b7178f5d
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-context@npm:1.1.2":
version: 1.1.2
resolution: "@radix-ui/react-context@npm:1.1.2"
@@ -2542,7 +2694,7 @@ __metadata:
languageName: node
linkType: hard
-"@radix-ui/react-dropdown-menu@npm:^2.1.16":
+"@radix-ui/react-dropdown-menu@npm:2.1.16, @radix-ui/react-dropdown-menu@npm:^2.1.16":
version: 2.1.16
resolution: "@radix-ui/react-dropdown-menu@npm:2.1.16"
dependencies:
@@ -2550,9 +2702,541 @@ __metadata:
"@radix-ui/react-compose-refs": "npm:1.1.2"
"@radix-ui/react-context": "npm:1.1.2"
"@radix-ui/react-id": "npm:1.1.1"
- "@radix-ui/react-menu": "npm:2.1.16"
+ "@radix-ui/react-menu": "npm:2.1.16"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/da215196b5dde5619cdb424b1b5236159e4bb949974b7f4ffbf047d467c55116229a8f9cf07eae6457afefb4a2b07888bb30542f303045e05d90a4b072941ae2
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-focus-guards@npm:1.1.3":
+ version: 1.1.3
+ resolution: "@radix-ui/react-focus-guards@npm:1.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10/b57878f6cf0ebc3e8d7c5c6bbaad44598daac19c921551ca541c104201048a9a902f3d69196e7a09995fd46e998c309aab64dc30fa184b3609d67d187a6a9c24
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-focus-scope@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-focus-scope@npm:1.1.7"
+ dependencies:
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/2a7cd00e39e01756999ebf0bdb3401d6a8efa489a7b19e6b629b40bad3022b7b1f616555ccb4b0505bc0ba53e13a1fb51be905db138b16ec39c4fe319fe701d3
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-form@npm:0.1.8":
+ version: 0.1.8
+ resolution: "@radix-ui/react-form@npm:0.1.8"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-label": "npm:2.1.7"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/62b57000820150b7938e71f324abcf562cec69e3352b52b6d28436bffc52243ae938e6b22555118431c973491f48050a959c4489880b2af113729af59a8489f8
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-hover-card@npm:1.1.15":
+ version: 1.1.15
+ resolution: "@radix-ui/react-hover-card@npm:1.1.15"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-popper": "npm:1.2.8"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/49811b05853c81e30c04cf5eb42792195ea8700a9ec79e573a04085adf457211fb847024f76160b7bb569a8221e179ed8ed46ae3c2ea5f454c044d5cf8e9042b
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-id@npm:1.1.1":
+ version: 1.1.1
+ resolution: "@radix-ui/react-id@npm:1.1.1"
+ dependencies:
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10/8d68e200778eb3038906870fc869b3d881f4a46715fb20cddd9c76cba42fdaaa4810a3365b6ec2daf0f185b9201fc99d009167f59c7921bc3a139722c2e976db
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-label@npm:2.1.7":
+ version: 2.1.7
+ resolution: "@radix-ui/react-label@npm:2.1.7"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/6fe47ff695ac127a87a3ee77489fb345a89515edc8df4b2b290c801a9ae14ad934cc64ddad7638cddb71b064ff898bd1c2ac88c16dd1b8236a0939d7fea95e3b
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-menu@npm:2.1.16":
+ version: 2.1.16
+ resolution: "@radix-ui/react-menu@npm:2.1.16"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-focus-guards": "npm:1.1.3"
+ "@radix-ui/react-focus-scope": "npm:1.1.7"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-popper": "npm:1.2.8"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ aria-hidden: "npm:^1.2.4"
+ react-remove-scroll: "npm:^2.6.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/2ffdfa08822c8c4ffc265d02d16c83d725114f9c0e9b510e73e431306dedddd507ef2861ccd67ec8c0d21cb24cd6401e42f16f3e65b30be627c7e22159151e40
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-menubar@npm:1.1.16":
+ version: 1.1.16
+ resolution: "@radix-ui/react-menubar@npm:1.1.16"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-menu": "npm:2.1.16"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/c36145d72baa91b0f4a197827257184edcc8cabadb11c35b35d4908a3eaf2e976029dd59e0c9a0d1318fdc11b3156a59a51938d707262ac5c939a522c17249f1
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-navigation-menu@npm:1.2.14":
+ version: 1.2.14
+ resolution: "@radix-ui/react-navigation-menu@npm:1.2.14"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/react-use-previous": "npm:1.1.1"
+ "@radix-ui/react-visually-hidden": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/530c04bae6c92416682fe85a7a10bf1d29de69d4e1dbf7d8c7948e99756f5ba85da11cdb62b4dc5c2feaa47c5feb47767e352319d22d9e21a19946455e8adc1c
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-one-time-password-field@npm:0.1.8":
+ version: 0.1.8
+ resolution: "@radix-ui/react-one-time-password-field@npm:0.1.8"
+ dependencies:
+ "@radix-ui/number": "npm:1.1.1"
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-effect-event": "npm:0.0.2"
+ "@radix-ui/react-use-is-hydrated": "npm:0.1.0"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/cb6849bdd8180faf855c2c43784a6a64893d0515e2d3fb7a1e8bbd47317e6d7888bccc4e37d0bffd2269eeff1dbfecbbd896347dca54d4a00d0c0db2a7dfbdfc
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-password-toggle-field@npm:0.1.3":
+ version: 0.1.3
+ resolution: "@radix-ui/react-password-toggle-field@npm:0.1.3"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-effect-event": "npm:0.0.2"
+ "@radix-ui/react-use-is-hydrated": "npm:0.1.0"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/7f077d1cd9fdd955cfc0142da8ab4f97f4d2c1a9f3b5770c1c4ba31df7b4572818d56a2cddc9c06a8d7fd36be9e63b98dce566ff75035293623f3267be6b0334
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-popover@npm:1.1.15":
+ version: 1.1.15
+ resolution: "@radix-ui/react-popover@npm:1.1.15"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-focus-guards": "npm:1.1.3"
+ "@radix-ui/react-focus-scope": "npm:1.1.7"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-popper": "npm:1.2.8"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ aria-hidden: "npm:^1.2.4"
+ react-remove-scroll: "npm:^2.6.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/0ea7c8bb827e44d5c02b3f7193d9ac8085c71a01bf601b1afeb2bb0ec0124756e03db3471606e89e4d014e4de7c7066c8e2e9b81bb4b31ea321890ec33421f31
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-popper@npm:1.2.8":
+ version: 1.2.8
+ resolution: "@radix-ui/react-popper@npm:1.2.8"
+ dependencies:
+ "@floating-ui/react-dom": "npm:^2.0.0"
+ "@radix-ui/react-arrow": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/react-use-rect": "npm:1.1.1"
+ "@radix-ui/react-use-size": "npm:1.1.1"
+ "@radix-ui/rect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/01366054e1e63dd9394f77afb9da3367709478a5adf4436c080fc5bbe9456170192ff9d1425d9fae5b246e1ba95173848f84b6f2a06b21b47d966367ec7cb997
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-portal@npm:1.1.9":
+ version: 1.1.9
+ resolution: "@radix-ui/react-portal@npm:1.1.9"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/bd6be39bf021d5c917e2474ecba411e2625171f7ef96862b9af04bbd68833bb3662a7f1fbdeb5a7a237111b10e811e76d2cd03e957dadd6e668ef16541bfbd68
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-presence@npm:1.1.5":
+ version: 1.1.5
+ resolution: "@radix-ui/react-presence@npm:1.1.5"
+ dependencies:
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/4cdb05844c18877efb4b9739b46b7e5850b81d7ede994e75b5d62e8153a43c6e16b3ff9e55ff716e20b74b99b9415a94e97fd636bcb8698d5bbf7ab7b8663f9b
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-primitive@npm:2.1.3":
+ version: 2.1.3
+ resolution: "@radix-ui/react-primitive@npm:2.1.3"
+ dependencies:
+ "@radix-ui/react-slot": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/1dbbf932a3527f4e62f210bb72944eff605c3e38c8d3275ed5a5c570c02820ab156169756a65ad9a638d2089a828a04a7903795377384e98c87d0ca456303253
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-progress@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-progress@npm:1.1.7"
+ dependencies:
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/ecdcebd586c17150351d22b9441928c07d62d7dd99688fc3ef8f04798e8a70ae436d2c6e7b30d0f266f7974f895c46bb347846ecf2617ab4196e937fe4d50a0a
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-radio-group@npm:1.3.8":
+ version: 1.3.8
+ resolution: "@radix-ui/react-radio-group@npm:1.3.8"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-previous": "npm:1.1.1"
+ "@radix-ui/react-use-size": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/8fd81aa68569919975142d9fb8850041b2e402bfff9c49115efd3aaa1e45060020744bf9f68a6695a468bcefd1c26402bdd7c9f06ce5b7b43ecad8bc6aa4e9e3
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-roving-focus@npm:1.1.11":
+ version: 1.1.11
+ resolution: "@radix-ui/react-roving-focus@npm:1.1.11"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/0eddafa942332c95622ab8b53cce2fa25fd0dcaf4797218e9e6725da0734a81a438852cdcb3f588521018f68d38c6c5e50c64fda78c655f4e69dd45681ecc5e7
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-scroll-area@npm:1.2.10":
+ version: 1.2.10
+ resolution: "@radix-ui/react-scroll-area@npm:1.2.10"
+ dependencies:
+ "@radix-ui/number": "npm:1.1.1"
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/c3f311ad7f6c957f460e1455dca8deadde970a6511cad0f4b1a8f4779fefac46b0decd1a8046f0d9c357c40805e6313775fad153e881ead0b3f83a4868817e70
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-select@npm:2.2.6":
+ version: 2.2.6
+ resolution: "@radix-ui/react-select@npm:2.2.6"
+ dependencies:
+ "@radix-ui/number": "npm:1.1.1"
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-focus-guards": "npm:1.1.3"
+ "@radix-ui/react-focus-scope": "npm:1.1.7"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-popper": "npm:1.2.8"
+ "@radix-ui/react-portal": "npm:1.1.9"
"@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
"@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/react-use-previous": "npm:1.1.1"
+ "@radix-ui/react-visually-hidden": "npm:1.2.3"
+ aria-hidden: "npm:^1.2.4"
+ react-remove-scroll: "npm:^2.6.3"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
@@ -2563,30 +3247,44 @@ __metadata:
optional: true
"@types/react-dom":
optional: true
- checksum: 10/da215196b5dde5619cdb424b1b5236159e4bb949974b7f4ffbf047d467c55116229a8f9cf07eae6457afefb4a2b07888bb30542f303045e05d90a4b072941ae2
+ checksum: 10/ef6df1a6411a965d30f8e14387058ece020469897ad2089d1e1019d8e37487e6b3c0f9683d7c6ec217698fa85fedd419738cab089a5ebc49a04405e63aac0bf0
languageName: node
linkType: hard
-"@radix-ui/react-focus-guards@npm:1.1.3":
- version: 1.1.3
- resolution: "@radix-ui/react-focus-guards@npm:1.1.3"
+"@radix-ui/react-separator@npm:1.1.7":
+ version: 1.1.7
+ resolution: "@radix-ui/react-separator@npm:1.1.7"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.1.3"
peerDependencies:
"@types/react": "*"
+ "@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
- checksum: 10/b57878f6cf0ebc3e8d7c5c6bbaad44598daac19c921551ca541c104201048a9a902f3d69196e7a09995fd46e998c309aab64dc30fa184b3609d67d187a6a9c24
+ "@types/react-dom":
+ optional: true
+ checksum: 10/c5c19b7991bf5395eb2b849145deeb9aa307d9fcf220380497992ff2a301753ab477d0329af51b6d500cd3c1d777edbd2504b544d9254542fb5f829ac5ddbcf3
languageName: node
linkType: hard
-"@radix-ui/react-focus-scope@npm:1.1.7":
- version: 1.1.7
- resolution: "@radix-ui/react-focus-scope@npm:1.1.7"
+"@radix-ui/react-slider@npm:1.3.6":
+ version: 1.3.6
+ resolution: "@radix-ui/react-slider@npm:1.3.6"
dependencies:
+ "@radix-ui/number": "npm:1.1.1"
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
"@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
"@radix-ui/react-primitive": "npm:2.1.3"
- "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/react-use-previous": "npm:1.1.1"
+ "@radix-ui/react-use-size": "npm:1.1.1"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
@@ -2597,47 +3295,36 @@ __metadata:
optional: true
"@types/react-dom":
optional: true
- checksum: 10/2a7cd00e39e01756999ebf0bdb3401d6a8efa489a7b19e6b629b40bad3022b7b1f616555ccb4b0505bc0ba53e13a1fb51be905db138b16ec39c4fe319fe701d3
+ checksum: 10/3817eb89cf077bb2610383e3fa1651e624b1638f3a83e464330a6f5cc41cc7c7556a62dda337ece7a80ebde470562669d48a6b014a0b472fc35d608103b9ecbd
languageName: node
linkType: hard
-"@radix-ui/react-id@npm:1.1.1":
- version: 1.1.1
- resolution: "@radix-ui/react-id@npm:1.1.1"
+"@radix-ui/react-slot@npm:1.2.3":
+ version: 1.2.3
+ resolution: "@radix-ui/react-slot@npm:1.2.3"
dependencies:
- "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
"@types/react":
optional: true
- checksum: 10/8d68e200778eb3038906870fc869b3d881f4a46715fb20cddd9c76cba42fdaaa4810a3365b6ec2daf0f185b9201fc99d009167f59c7921bc3a139722c2e976db
+ checksum: 10/fe484c2741e31d9c20a8fb53c5790a73c0664e2bea35e27f4d484a90c42135fcfffe11a08abfcacb7a8ee2faf013471f0e856818f3ddac8ac51ceb8869e0fd08
languageName: node
linkType: hard
-"@radix-ui/react-menu@npm:2.1.16":
- version: 2.1.16
- resolution: "@radix-ui/react-menu@npm:2.1.16"
+"@radix-ui/react-switch@npm:1.2.6, @radix-ui/react-switch@npm:^1.2.6":
+ version: 1.2.6
+ resolution: "@radix-ui/react-switch@npm:1.2.6"
dependencies:
"@radix-ui/primitive": "npm:1.1.3"
- "@radix-ui/react-collection": "npm:1.1.7"
"@radix-ui/react-compose-refs": "npm:1.1.2"
"@radix-ui/react-context": "npm:1.1.2"
- "@radix-ui/react-direction": "npm:1.1.1"
- "@radix-ui/react-dismissable-layer": "npm:1.1.11"
- "@radix-ui/react-focus-guards": "npm:1.1.3"
- "@radix-ui/react-focus-scope": "npm:1.1.7"
- "@radix-ui/react-id": "npm:1.1.1"
- "@radix-ui/react-popper": "npm:1.2.8"
- "@radix-ui/react-portal": "npm:1.1.9"
- "@radix-ui/react-presence": "npm:1.1.5"
"@radix-ui/react-primitive": "npm:2.1.3"
- "@radix-ui/react-roving-focus": "npm:1.1.11"
- "@radix-ui/react-slot": "npm:1.2.3"
- "@radix-ui/react-use-callback-ref": "npm:1.1.1"
- aria-hidden: "npm:^1.2.4"
- react-remove-scroll: "npm:^2.6.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-previous": "npm:1.1.1"
+ "@radix-ui/react-use-size": "npm:1.1.1"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
@@ -2648,24 +3335,22 @@ __metadata:
optional: true
"@types/react-dom":
optional: true
- checksum: 10/2ffdfa08822c8c4ffc265d02d16c83d725114f9c0e9b510e73e431306dedddd507ef2861ccd67ec8c0d21cb24cd6401e42f16f3e65b30be627c7e22159151e40
+ checksum: 10/d2f98e5bfa2c72effafa2848f771eb8d5aa2b669ff4d8e9408d25db31a10979b77f2d0e77f0415c1c8a13f009083dc48bb91fab326223830bdc834e58b7029ee
languageName: node
linkType: hard
-"@radix-ui/react-popper@npm:1.2.8":
- version: 1.2.8
- resolution: "@radix-ui/react-popper@npm:1.2.8"
+"@radix-ui/react-tabs@npm:1.1.13":
+ version: 1.1.13
+ resolution: "@radix-ui/react-tabs@npm:1.1.13"
dependencies:
- "@floating-ui/react-dom": "npm:^2.0.0"
- "@radix-ui/react-arrow": "npm:1.1.7"
- "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/primitive": "npm:1.1.3"
"@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-presence": "npm:1.1.5"
"@radix-ui/react-primitive": "npm:2.1.3"
- "@radix-ui/react-use-callback-ref": "npm:1.1.1"
- "@radix-ui/react-use-layout-effect": "npm:1.1.1"
- "@radix-ui/react-use-rect": "npm:1.1.1"
- "@radix-ui/react-use-size": "npm:1.1.1"
- "@radix-ui/rect": "npm:1.1.1"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
@@ -2676,16 +3361,26 @@ __metadata:
optional: true
"@types/react-dom":
optional: true
- checksum: 10/01366054e1e63dd9394f77afb9da3367709478a5adf4436c080fc5bbe9456170192ff9d1425d9fae5b246e1ba95173848f84b6f2a06b21b47d966367ec7cb997
+ checksum: 10/9f0940957fd25794f03e847bc3b75a2b8410cd3c2ec500710827b3a3a2f4904b7f20b5c4784908a8f88bcd59b90b49ede9fb983030843fafcb29817195b29acb
languageName: node
linkType: hard
-"@radix-ui/react-portal@npm:1.1.9":
- version: 1.1.9
- resolution: "@radix-ui/react-portal@npm:1.1.9"
+"@radix-ui/react-toast@npm:1.2.15":
+ version: 1.2.15
+ resolution: "@radix-ui/react-toast@npm:1.2.15"
dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.5"
"@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
"@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/react-visually-hidden": "npm:1.2.3"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
@@ -2696,16 +3391,21 @@ __metadata:
optional: true
"@types/react-dom":
optional: true
- checksum: 10/bd6be39bf021d5c917e2474ecba411e2625171f7ef96862b9af04bbd68833bb3662a7f1fbdeb5a7a237111b10e811e76d2cd03e957dadd6e668ef16541bfbd68
+ checksum: 10/d713dabd5bcaf5a20024278702b0d1cee966e8de5e6c5cc5e7f789ea89d90b20e5ddb28eea73d4aa2f41eb03de16dd27beaa6e2ff50e86cced0b1e96ce827f98
languageName: node
linkType: hard
-"@radix-ui/react-presence@npm:1.1.5":
- version: 1.1.5
- resolution: "@radix-ui/react-presence@npm:1.1.5"
+"@radix-ui/react-toggle-group@npm:1.1.11":
+ version: 1.1.11
+ resolution: "@radix-ui/react-toggle-group@npm:1.1.11"
dependencies:
- "@radix-ui/react-compose-refs": "npm:1.1.2"
- "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-toggle": "npm:1.1.10"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
@@ -2716,15 +3416,17 @@ __metadata:
optional: true
"@types/react-dom":
optional: true
- checksum: 10/4cdb05844c18877efb4b9739b46b7e5850b81d7ede994e75b5d62e8153a43c6e16b3ff9e55ff716e20b74b99b9415a94e97fd636bcb8698d5bbf7ab7b8663f9b
+ checksum: 10/b20e2aa2580624621b62e72d2e44585427807e6629186dc8a46909a3bd35d9122fb4837f8e8fa9856a24b54bb2a7aa8983007da8b459303495d0d55b14396b35
languageName: node
linkType: hard
-"@radix-ui/react-primitive@npm:2.1.3":
- version: 2.1.3
- resolution: "@radix-ui/react-primitive@npm:2.1.3"
+"@radix-ui/react-toggle@npm:1.1.10":
+ version: 1.1.10
+ resolution: "@radix-ui/react-toggle@npm:1.1.10"
dependencies:
- "@radix-ui/react-slot": "npm:1.2.3"
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
@@ -2735,23 +3437,21 @@ __metadata:
optional: true
"@types/react-dom":
optional: true
- checksum: 10/1dbbf932a3527f4e62f210bb72944eff605c3e38c8d3275ed5a5c570c02820ab156169756a65ad9a638d2089a828a04a7903795377384e98c87d0ca456303253
+ checksum: 10/ece154bd7a0ca765040fafbe4131f753457a3d37f73dec3ae94f08f17f3889272d06f33f305ad34c986925ce8a40532ee43f6bdb7e8b99fd8bac299b01d69204
languageName: node
linkType: hard
-"@radix-ui/react-roving-focus@npm:1.1.11":
+"@radix-ui/react-toolbar@npm:1.1.11":
version: 1.1.11
- resolution: "@radix-ui/react-roving-focus@npm:1.1.11"
+ resolution: "@radix-ui/react-toolbar@npm:1.1.11"
dependencies:
"@radix-ui/primitive": "npm:1.1.3"
- "@radix-ui/react-collection": "npm:1.1.7"
- "@radix-ui/react-compose-refs": "npm:1.1.2"
"@radix-ui/react-context": "npm:1.1.2"
"@radix-ui/react-direction": "npm:1.1.1"
- "@radix-ui/react-id": "npm:1.1.1"
"@radix-ui/react-primitive": "npm:2.1.3"
- "@radix-ui/react-use-callback-ref": "npm:1.1.1"
- "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-separator": "npm:1.1.7"
+ "@radix-ui/react-toggle-group": "npm:1.1.11"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
@@ -2762,36 +3462,26 @@ __metadata:
optional: true
"@types/react-dom":
optional: true
- checksum: 10/0eddafa942332c95622ab8b53cce2fa25fd0dcaf4797218e9e6725da0734a81a438852cdcb3f588521018f68d38c6c5e50c64fda78c655f4e69dd45681ecc5e7
- languageName: node
- linkType: hard
-
-"@radix-ui/react-slot@npm:1.2.3":
- version: 1.2.3
- resolution: "@radix-ui/react-slot@npm:1.2.3"
- dependencies:
- "@radix-ui/react-compose-refs": "npm:1.1.2"
- peerDependencies:
- "@types/react": "*"
- react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
- peerDependenciesMeta:
- "@types/react":
- optional: true
- checksum: 10/fe484c2741e31d9c20a8fb53c5790a73c0664e2bea35e27f4d484a90c42135fcfffe11a08abfcacb7a8ee2faf013471f0e856818f3ddac8ac51ceb8869e0fd08
+ checksum: 10/da022ae96f0a02da2c5890af3350f622425c8bb8564b1cae51ca28cfeb5dc73a8bb597bb762dfef2f738d497d207792e22231aec050c8d05071518b9c58f1f8b
languageName: node
linkType: hard
-"@radix-ui/react-switch@npm:^1.2.6":
- version: 1.2.6
- resolution: "@radix-ui/react-switch@npm:1.2.6"
+"@radix-ui/react-tooltip@npm:1.2.8":
+ version: 1.2.8
+ resolution: "@radix-ui/react-tooltip@npm:1.2.8"
dependencies:
"@radix-ui/primitive": "npm:1.1.3"
"@radix-ui/react-compose-refs": "npm:1.1.2"
"@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-id": "npm:1.1.1"
+ "@radix-ui/react-popper": "npm:1.2.8"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.5"
"@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-slot": "npm:1.2.3"
"@radix-ui/react-use-controllable-state": "npm:1.2.2"
- "@radix-ui/react-use-previous": "npm:1.1.1"
- "@radix-ui/react-use-size": "npm:1.1.1"
+ "@radix-ui/react-visually-hidden": "npm:1.2.3"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
@@ -2802,7 +3492,7 @@ __metadata:
optional: true
"@types/react-dom":
optional: true
- checksum: 10/d2f98e5bfa2c72effafa2848f771eb8d5aa2b669ff4d8e9408d25db31a10979b77f2d0e77f0415c1c8a13f009083dc48bb91fab326223830bdc834e58b7029ee
+ checksum: 10/e31857628d998b69616b8994f9627d387ed7bfa453b94e3b18ad2c04de83caf5fcca0ef2f304b1d343e00f183e937d883247d81e386dcc76c7c7c268484bc47c
languageName: node
linkType: hard
@@ -2865,6 +3555,21 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-use-is-hydrated@npm:0.1.0":
+ version: 0.1.0
+ resolution: "@radix-ui/react-use-is-hydrated@npm:0.1.0"
+ dependencies:
+ use-sync-external-store: "npm:^1.5.0"
+ peerDependencies:
+ "@types/react": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ checksum: 10/72e68a85a7a4a6dafd255a0cc87b6410bf0356c5e296e2eb82c265559408a735204cd150408b9c0d598057dafad3d51086e0362633bd728e95655b3bfd70ae26
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-use-layout-effect@npm:1.1.1":
version: 1.1.1
resolution: "@radix-ui/react-use-layout-effect@npm:1.1.1"
@@ -2921,6 +3626,25 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-visually-hidden@npm:1.2.3":
+ version: 1.2.3
+ resolution: "@radix-ui/react-visually-hidden@npm:1.2.3"
+ dependencies:
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/42296bde1ddf4af4e7445e914c35d6bc8406d6ede49f0a959a553e75b3ed21da09fda80a81c48d8ec058ed8129ce7137499d02ee26f90f0d3eaa2417922d6509
+ languageName: node
+ linkType: hard
+
"@radix-ui/rect@npm:1.1.1":
version: 1.1.1
resolution: "@radix-ui/rect@npm:1.1.1"
@@ -2928,6 +3652,28 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/themes@npm:^3.3.0":
+ version: 3.3.0
+ resolution: "@radix-ui/themes@npm:3.3.0"
+ dependencies:
+ "@radix-ui/colors": "npm:^3.0.0"
+ classnames: "npm:^2.3.2"
+ radix-ui: "npm:^1.1.3"
+ react-remove-scroll-bar: "npm:^2.3.8"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: 16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: 16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/f2126b0bf9d2076a33bb7ace72cfdca30bd005737a46c2e18812b07d8be70c4ab0b5c8d4c975cc7c60d3213de7b95f5b2c676796f34d45dddfcad276358c12b9
+ languageName: node
+ linkType: hard
+
"@react-leaflet/core@npm:^2.1.0":
version: 2.1.0
resolution: "@react-leaflet/core@npm:2.1.0"
@@ -3460,9 +4206,9 @@ __metadata:
linkType: hard
"@tootallnate/once@npm:2":
- version: 2.0.0
- resolution: "@tootallnate/once@npm:2.0.0"
- checksum: 10/ad87447820dd3f24825d2d947ebc03072b20a42bfc96cbafec16bff8bbda6c1a81fcb0be56d5b21968560c5359a0af4038a68ba150c3e1694fe4c109a063bed8
+ version: 2.0.1
+ resolution: "@tootallnate/once@npm:2.0.1"
+ checksum: 10/487b59b5adb8458dc13394a5aae997bf9705c51fa1e2396c50cd967019d06b273faba3c4d9e7895a996b9e9b055f1c55e53d822e54b3e9c298bcb4f6967cd0d5
languageName: node
linkType: hard
@@ -4273,9 +5019,9 @@ __metadata:
linkType: hard
"@xmldom/xmldom@npm:^0.8.8":
- version: 0.8.10
- resolution: "@xmldom/xmldom@npm:0.8.10"
- checksum: 10/62400bc5e0e75b90650e33a5ceeb8d94829dd11f9b260962b71a784cd014ddccec3e603fe788af9c1e839fa4648d8c521ebd80d8b752878d3a40edabc9ce7ccf
+ version: 0.8.13
+ resolution: "@xmldom/xmldom@npm:0.8.13"
+ checksum: 10/f8f3d56fa91d5026885c0c5c00b07eae47647bda0d742ecbf8e51e06bb287ab30222977b20529ee15c364031606225ebca58907a8ecc76a3add6b3f10e6ddfc6
languageName: node
linkType: hard
@@ -5282,6 +6028,13 @@ __metadata:
languageName: node
linkType: hard
+"classnames@npm:^2.3.2":
+ version: 2.5.1
+ resolution: "classnames@npm:2.5.1"
+ checksum: 10/58eb394e8817021b153bb6e7d782cfb667e4ab390cb2e9dac2fc7c6b979d1cc2b2a733093955fc5c94aa79ef5c8c89f11ab77780894509be6afbb91dddd79d15
+ languageName: node
+ linkType: hard
+
"clean-regexp@npm:^1.0.0":
version: 1.0.0
resolution: "clean-regexp@npm:1.0.0"
@@ -7152,22 +7905,26 @@ __metadata:
languageName: node
linkType: hard
-"fast-xml-builder@npm:^1.0.0":
- version: 1.0.0
- resolution: "fast-xml-builder@npm:1.0.0"
- checksum: 10/06c04d80545e5c9f4d1d6cca00567b5cc09953a92c6328fa48cfb4d7f42630313b8c2bb62e9cb81accee7bb5e1c5312fcae06c3d20dbe52d969a5938233316da
+"fast-xml-builder@npm:^1.1.7":
+ version: 1.1.9
+ resolution: "fast-xml-builder@npm:1.1.9"
+ dependencies:
+ path-expression-matcher: "npm:^1.1.3"
+ checksum: 10/44ef553aec4581b0fcc1b21bfd285aa9224eeb507620220228a9dd3dfd495068daaaf408b4cf9fd67354df558888680121c9d099229fdfd9672982531623781b
languageName: node
linkType: hard
-"fast-xml-parser@npm:^5.0.7, fast-xml-parser@npm:^5.4.1":
- version: 5.4.1
- resolution: "fast-xml-parser@npm:5.4.1"
+"fast-xml-parser@npm:^5.0.7, fast-xml-parser@npm:^5.7.0":
+ version: 5.7.3
+ resolution: "fast-xml-parser@npm:5.7.3"
dependencies:
- fast-xml-builder: "npm:^1.0.0"
- strnum: "npm:^2.1.2"
+ "@nodable/entities": "npm:^2.1.0"
+ fast-xml-builder: "npm:^1.1.7"
+ path-expression-matcher: "npm:^1.5.0"
+ strnum: "npm:^2.2.3"
bin:
fxparser: src/cli/cli.js
- checksum: 10/2b40067c3ad3542ca197d1353bcb0416cd5db20d5c66d74ac176b99af6ff9bd55a6182d36856a2fd477c95b8fc1f07405475f1662a31185480130ba7076c702a
+ checksum: 10/00a58655d0d58c1f914c7fd8e3a94e88799c3d473e29a6d2231dc02103df069e8c6043137cbec8df1cda6525a39914d1b84455a79530f63be266876a2211251c
languageName: node
linkType: hard
@@ -8087,10 +8844,10 @@ __metadata:
languageName: node
linkType: hard
-"i18next-fs-backend@npm:^2.6.1":
- version: 2.6.1
- resolution: "i18next-fs-backend@npm:2.6.1"
- checksum: 10/2c93cab111cee0472a2ff462cdaa3cac32a1d464fb7d8cb7a6c7171ac2fdaf39a6ba9e82a2700334389519cb228f2aef29dc44871635320140da65b5dfaadcbe
+"i18next-fs-backend@npm:^2.6.4":
+ version: 2.6.4
+ resolution: "i18next-fs-backend@npm:2.6.4"
+ checksum: 10/b618ff9af83b624ca43b530d8346182e911a8167e3970e54c1a417814a09a147994787d01c8bfed53aa09267a8301ad7404a32c20ff7f060a35d3efbb3d6c873
languageName: node
linkType: hard
@@ -8301,9 +9058,9 @@ __metadata:
linkType: hard
"ip-address@npm:^10.0.1":
- version: 10.1.0
- resolution: "ip-address@npm:10.1.0"
- checksum: 10/a6979629d1ad9c1fb424bc25182203fad739b40225aebc55ec6243bbff5035faf7b9ed6efab3a097de6e713acbbfde944baacfa73e11852bb43989c45a68d79e
+ version: 10.2.0
+ resolution: "ip-address@npm:10.2.0"
+ checksum: 10/12fec399e1af5753ac322e47a6d81a50d3a528b3abb17c09525b2a2edcaedcca628c40520706f7037bc4d8e951b0296c47e7b86d0a8e6e2335c8f0ba4afcfac1
languageName: node
linkType: hard
@@ -9074,10 +9831,10 @@ __metadata:
languageName: node
linkType: hard
-"lodash@npm:^4.17.15, lodash@npm:^4.17.23":
- version: 4.17.23
- resolution: "lodash@npm:4.17.23"
- checksum: 10/82504c88250f58da7a5a4289f57a4f759c44946c005dd232821c7688b5fcfbf4a6268f6a6cdde4b792c91edd2f3b5398c1d2a0998274432cff76def48735e233
+"lodash@npm:^4.17.15, lodash@npm:^4.18.1":
+ version: 4.18.1
+ resolution: "lodash@npm:4.18.1"
+ checksum: 10/306fea53dfd39dad1f03d45ba654a2405aebd35797b673077f401edb7df2543623dc44b9effbb98f69b32152295fff725a4cec99c684098947430600c6af0c3f
languageName: node
linkType: hard
@@ -9151,6 +9908,7 @@ __metadata:
"@radix-ui/react-dialog": "npm:^1.1.15"
"@radix-ui/react-dropdown-menu": "npm:^2.1.16"
"@radix-ui/react-switch": "npm:^1.2.6"
+ "@radix-ui/themes": "npm:^3.3.0"
"@tanstack/react-virtual": "npm:3.13.18"
"@tsconfig/node22": "npm:^22.0.5"
"@tsconfig/strictest": "npm:^2.0.8"
@@ -9200,17 +9958,17 @@ __metadata:
execa: "npm:^9.6.1"
express: "npm:^5.2.1"
express-async-handler: "npm:^1.2.0"
- fast-xml-parser: "npm:^5.4.1"
+ fast-xml-parser: "npm:^5.7.0"
file-type: "npm:^21.3.2"
i18next: "npm:^25.7.3"
i18next-cli: "npm:^1.34.1"
- i18next-fs-backend: "npm:^2.6.1"
+ i18next-fs-backend: "npm:^2.6.4"
icon-gen: "npm:^5.0.0"
immer: "npm:^11.1.3"
json5: "npm:^2.2.3"
ky: "npm:^1.14.2"
leaflet: "npm:^1.9.4"
- lodash: "npm:^4.17.23"
+ lodash: "npm:^4.18.1"
lodash.debounce: "npm:^4.0.8"
luxon: "npm:^3.7.2"
mime-types: "npm:^3.0.2"
@@ -9245,7 +10003,7 @@ __metadata:
typescript: "npm:~5.9.3"
use-debounce: "npm:^5.2.1"
use-trace-update: "npm:^1.3.3"
- vite: "npm:^7.3.0"
+ vite: "npm:^7.3.2"
vitest: "npm:^4.0.16"
winston: "npm:^3.19.0"
yargs: "npm:^18.0.0"
@@ -10728,6 +11486,13 @@ __metadata:
languageName: node
linkType: hard
+"path-expression-matcher@npm:^1.1.3, path-expression-matcher@npm:^1.5.0":
+ version: 1.5.0
+ resolution: "path-expression-matcher@npm:1.5.0"
+ checksum: 10/28303bb9ee6831e6df14c10cd3f3f7b2d7c8d7f788d8bdb7440136fd696064c82a3e264999a0764d28e39f698275fc03a5493bec93c57ef4a22566280367dd64
+ languageName: node
+ linkType: hard
+
"path-is-absolute@npm:^1.0.0":
version: 1.0.1
resolution: "path-is-absolute@npm:1.0.1"
@@ -10867,13 +11632,13 @@ __metadata:
linkType: hard
"postcss@npm:^8.5.6":
- version: 8.5.6
- resolution: "postcss@npm:8.5.6"
+ version: 8.5.14
+ resolution: "postcss@npm:8.5.14"
dependencies:
nanoid: "npm:^3.3.11"
picocolors: "npm:^1.1.1"
source-map-js: "npm:^1.2.1"
- checksum: 10/9e4fbe97574091e9736d0e82a591e29aa100a0bf60276a926308f8c57249698935f35c5d2f4e80de778d0cbb8dcffab4f383d85fd50c5649aca421c3df729b86
+ checksum: 10/2e3f4dea69692918fe9df5402beb0e54df84499995a094f2fbf63d1a9e38bc1b7a42854df47f09e02593213e01a5eb0627b1d1bd6d1b0ea90767b2e072f7167c
languageName: node
linkType: hard
@@ -11009,11 +11774,11 @@ __metadata:
linkType: hard
"qs@npm:^6.14.0":
- version: 6.14.2
- resolution: "qs@npm:6.14.2"
+ version: 6.15.2
+ resolution: "qs@npm:6.15.2"
dependencies:
side-channel: "npm:^1.1.0"
- checksum: 10/682933a85bb4b7bd0d66e13c0a40d9e612b5e4bcc2cb9238f711a9368cd22d91654097a74fff93551e58146db282c56ac094957dfdc60ce64ea72c3c9d7779ac
+ checksum: 10/7a934b2dba40654cf9878dab7c1e7ad56c6186265509727fbece67dadc38fd5d67715b61b6ef938e5a0475faeee701d18ec0a7ce420ad64367caad7f1bdb66e2
languageName: node
linkType: hard
@@ -11031,6 +11796,79 @@ __metadata:
languageName: node
linkType: hard
+"radix-ui@npm:^1.1.3":
+ version: 1.4.3
+ resolution: "radix-ui@npm:1.4.3"
+ dependencies:
+ "@radix-ui/primitive": "npm:1.1.3"
+ "@radix-ui/react-accessible-icon": "npm:1.1.7"
+ "@radix-ui/react-accordion": "npm:1.2.12"
+ "@radix-ui/react-alert-dialog": "npm:1.1.15"
+ "@radix-ui/react-arrow": "npm:1.1.7"
+ "@radix-ui/react-aspect-ratio": "npm:1.1.7"
+ "@radix-ui/react-avatar": "npm:1.1.10"
+ "@radix-ui/react-checkbox": "npm:1.3.3"
+ "@radix-ui/react-collapsible": "npm:1.1.12"
+ "@radix-ui/react-collection": "npm:1.1.7"
+ "@radix-ui/react-compose-refs": "npm:1.1.2"
+ "@radix-ui/react-context": "npm:1.1.2"
+ "@radix-ui/react-context-menu": "npm:2.2.16"
+ "@radix-ui/react-dialog": "npm:1.1.15"
+ "@radix-ui/react-direction": "npm:1.1.1"
+ "@radix-ui/react-dismissable-layer": "npm:1.1.11"
+ "@radix-ui/react-dropdown-menu": "npm:2.1.16"
+ "@radix-ui/react-focus-guards": "npm:1.1.3"
+ "@radix-ui/react-focus-scope": "npm:1.1.7"
+ "@radix-ui/react-form": "npm:0.1.8"
+ "@radix-ui/react-hover-card": "npm:1.1.15"
+ "@radix-ui/react-label": "npm:2.1.7"
+ "@radix-ui/react-menu": "npm:2.1.16"
+ "@radix-ui/react-menubar": "npm:1.1.16"
+ "@radix-ui/react-navigation-menu": "npm:1.2.14"
+ "@radix-ui/react-one-time-password-field": "npm:0.1.8"
+ "@radix-ui/react-password-toggle-field": "npm:0.1.3"
+ "@radix-ui/react-popover": "npm:1.1.15"
+ "@radix-ui/react-popper": "npm:1.2.8"
+ "@radix-ui/react-portal": "npm:1.1.9"
+ "@radix-ui/react-presence": "npm:1.1.5"
+ "@radix-ui/react-primitive": "npm:2.1.3"
+ "@radix-ui/react-progress": "npm:1.1.7"
+ "@radix-ui/react-radio-group": "npm:1.3.8"
+ "@radix-ui/react-roving-focus": "npm:1.1.11"
+ "@radix-ui/react-scroll-area": "npm:1.2.10"
+ "@radix-ui/react-select": "npm:2.2.6"
+ "@radix-ui/react-separator": "npm:1.1.7"
+ "@radix-ui/react-slider": "npm:1.3.6"
+ "@radix-ui/react-slot": "npm:1.2.3"
+ "@radix-ui/react-switch": "npm:1.2.6"
+ "@radix-ui/react-tabs": "npm:1.1.13"
+ "@radix-ui/react-toast": "npm:1.2.15"
+ "@radix-ui/react-toggle": "npm:1.1.10"
+ "@radix-ui/react-toggle-group": "npm:1.1.11"
+ "@radix-ui/react-toolbar": "npm:1.1.11"
+ "@radix-ui/react-tooltip": "npm:1.2.8"
+ "@radix-ui/react-use-callback-ref": "npm:1.1.1"
+ "@radix-ui/react-use-controllable-state": "npm:1.2.2"
+ "@radix-ui/react-use-effect-event": "npm:0.0.2"
+ "@radix-ui/react-use-escape-keydown": "npm:1.1.1"
+ "@radix-ui/react-use-is-hydrated": "npm:0.1.0"
+ "@radix-ui/react-use-layout-effect": "npm:1.1.1"
+ "@radix-ui/react-use-size": "npm:1.1.1"
+ "@radix-ui/react-visually-hidden": "npm:1.2.3"
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 10/de5a0e2df7bfbaac889c15499c75c5216729bd2db2c4ca63b2718454a95bebbe38f6305df00efca72aa949d39bf1f4fd14057c3a261b75fb1cfae5103490da7b
+ languageName: node
+ linkType: hard
+
"range-parser@npm:^1.2.1":
version: 1.2.1
resolution: "range-parser@npm:1.2.1"
@@ -11162,7 +12000,7 @@ __metadata:
languageName: node
linkType: hard
-"react-remove-scroll-bar@npm:^2.3.7":
+"react-remove-scroll-bar@npm:^2.3.7, react-remove-scroll-bar@npm:^2.3.8":
version: 2.3.8
resolution: "react-remove-scroll-bar@npm:2.3.8"
dependencies:
@@ -12680,10 +13518,10 @@ __metadata:
languageName: node
linkType: hard
-"strnum@npm:^2.1.2":
- version: 2.1.2
- resolution: "strnum@npm:2.1.2"
- checksum: 10/7d894dff385e3a5c5b29c012cf0a7ea7962a92c6a299383c3d6db945ad2b6f3e770511356a9774dbd54444c56af1dc7c435dad6466c47293c48173274dd6c631
+"strnum@npm:^2.2.3":
+ version: 2.3.0
+ resolution: "strnum@npm:2.3.0"
+ checksum: 10/ce79c86bb2b96f053eb28e14924c13604e22977dcdece9aa914c25e16cc5c4bbe048976fe0b2a4decf08a1e13600b820749cea25463fc0e5fee3078339e0a457
languageName: node
linkType: hard
@@ -13504,7 +14342,7 @@ __metadata:
languageName: node
linkType: hard
-"use-sync-external-store@npm:^1.6.0":
+"use-sync-external-store@npm:^1.5.0, use-sync-external-store@npm:^1.6.0":
version: 1.6.0
resolution: "use-sync-external-store@npm:1.6.0"
peerDependencies:
@@ -13582,9 +14420,9 @@ __metadata:
languageName: node
linkType: hard
-"vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.3.0":
- version: 7.3.0
- resolution: "vite@npm:7.3.0"
+"vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.3.2":
+ version: 7.3.2
+ resolution: "vite@npm:7.3.2"
dependencies:
esbuild: "npm:^0.27.0"
fdir: "npm:^6.5.0"
@@ -13633,7 +14471,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
- checksum: 10/044490133aaf4cc024700995edaac10ff182262c721a44a8ac7839207b3e5af435c8b9dd8ee4658dc0f47147fa4211632cca3120177aa4bab99a7cb095e5149e
+ checksum: 10/c5f7a9a60011c41c836cedf31c8ee7624102aff9b6a7f3aab2ff47639721bba0916f81994c3a3ea6577a16c4f0dfee1e7dbd244e0da8edd5954e3c6d48daaaa2
languageName: node
linkType: hard