From b5028dc1421ff889340cc6d2834da3b9b26f0703 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Thu, 16 May 2024 14:10:00 +0200 Subject: [PATCH] allow selecting segments by expression also remove select by tags dialog to reduce code it's covered by expression fixes #1999 --- package.json | 1 + src/renderer/src/App.tsx | 4 +- src/renderer/src/SegmentList.tsx | 14 ++--- src/renderer/src/dialogs/index.tsx | 69 ++++++++++++++-------- src/renderer/src/hooks/useSegments.ts | 50 ++++++++++++---- yarn.lock | 85 +++++++++++++++++++++++++++ 6 files changed, 181 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index c6de85e4..88947b05 100644 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "i18next-fs-backend": "^2.1.1", "json5": "^2.2.2", "lodash": "^4.17.19", + "mathjs": "^12.4.2", "mime-types": "^2.1.14", "morgan": "^1.10.0", "semver": "^7.6.0", diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index d3d7b716..9a8d5f81 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -406,7 +406,7 @@ function App() { }, [detectedFps, timecodeFormat, getFrameCount]); const { - cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, createNumSegments, createFixedDurationSegments, createRandomSegments, apparentCutSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, currentApparentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, onSelectSegmentsByTag, toggleSegmentSelected, selectOnlySegment, getApparentCutSegmentById, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, + cutSegments, cutSegmentsHistory, createSegmentsFromKeyframes, shuffleSegments, detectBlackScenes, detectSilentScenes, detectSceneChanges, removeCutSegment, invertAllSegments, fillSegmentsGaps, combineOverlappingSegments, combineSelectedSegments, shiftAllSegmentTimes, alignSegmentTimesToKeyframes, updateSegOrder, updateSegOrders, reorderSegsByStartTime, addSegment, setCutStart, setCutEnd, onLabelSegment, splitCurrentSegment, createNumSegments, createFixedDurationSegments, createRandomSegments, apparentCutSegments, haveInvalidSegs, currentSegIndexSafe, currentCutSeg, currentApparentCutSeg, inverseCutSegments, clearSegments, loadCutSegments, isSegmentSelected, setCutTime, setCurrentSegIndex, onLabelSelectedSegments, deselectAllSegments, selectAllSegments, selectOnlyCurrentSegment, toggleCurrentSegmentSelected, invertSelectedSegments, removeSelectedSegments, setDeselectedSegmentIds, onSelectSegmentsByLabel, onSelectSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, getApparentCutSegmentById, selectedSegments, selectedSegmentsOrInverse, nonFilteredSegmentsOrInverse, segmentsToExport, duplicateCurrentSegment, duplicateSegment, updateSegAtIndex, } = useSegments({ filePath, workingRef, setWorking, setCutProgress, videoStream: activeVideoStream, duration, getRelevantTime, maxLabelLength, checkFileOpened, invertCutSegments, segmentsToChaptersOnly, timecodePlaceholder, parseTimecode }); @@ -2637,7 +2637,7 @@ function App() { jumpSegStart={jumpSegStart} jumpSegEnd={jumpSegEnd} onSelectSegmentsByLabel={onSelectSegmentsByLabel} - onSelectSegmentsByTag={onSelectSegmentsByTag} + onSelectSegmentsByExpr={onSelectSegmentsByExpr} onLabelSelectedSegments={onLabelSelectedSegments} updateSegAtIndex={updateSegAtIndex} editingSegmentTags={editingSegmentTags} diff --git a/src/renderer/src/SegmentList.tsx b/src/renderer/src/SegmentList.tsx index dd758bee..5783b1e9 100644 --- a/src/renderer/src/SegmentList.tsx +++ b/src/renderer/src/SegmentList.tsx @@ -44,7 +44,7 @@ const Segment = memo(({ onToggleSegmentSelected, onDeselectAllSegments, onSelectSegmentsByLabel, - onSelectSegmentsByTag, + onSelectSegmentsByExpr, onSelectAllSegments, jumpSegStart, jumpSegEnd, @@ -71,7 +71,7 @@ const Segment = memo(({ onToggleSegmentSelected: UseSegments['toggleSegmentSelected'], onDeselectAllSegments: UseSegments['deselectAllSegments'], onSelectSegmentsByLabel: UseSegments['onSelectSegmentsByLabel'], - onSelectSegmentsByTag: UseSegments['onSelectSegmentsByTag'], + onSelectSegmentsByExpr: UseSegments['onSelectSegmentsByExpr'], onSelectAllSegments: UseSegments['selectAllSegments'], jumpSegStart: (i: number) => void, jumpSegEnd: (i: number) => void, @@ -109,7 +109,7 @@ const Segment = memo(({ { label: t('Select all segments'), click: () => onSelectAllSegments() }, { label: t('Deselect all segments'), click: () => onDeselectAllSegments() }, { label: t('Select segments by label'), click: () => onSelectSegmentsByLabel() }, - { label: t('Select segments by tag'), click: () => onSelectSegmentsByTag() }, + { label: t('Select segments by expression'), click: () => onSelectSegmentsByExpr() }, { label: t('Invert selected segments'), click: () => onInvertSelectedSegments() }, { type: 'separator' }, @@ -128,7 +128,7 @@ const Segment = memo(({ { label: t('Segment tags'), click: () => onEditSegmentTags(index) }, { label: t('Extract frames as image files'), click: () => onExtractSegmentFramesAsImages([seg.segId]) }, ]; - }, [invertCutSegments, t, addSegment, onLabelSelectedSegments, onRemoveSelected, updateSegOrder, index, jumpSegStart, jumpSegEnd, onLabelPress, onRemovePress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByTag, onInvertSelectedSegments, onReorderPress, onEditSegmentTags, onExtractSegmentFramesAsImages]); + }, [invertCutSegments, t, addSegment, onLabelSelectedSegments, onRemoveSelected, updateSegOrder, index, jumpSegStart, jumpSegEnd, onLabelPress, onRemovePress, onDuplicateSegmentClick, seg, onSelectSingleSegment, onSelectAllSegments, onDeselectAllSegments, onSelectSegmentsByLabel, onSelectSegmentsByExpr, onInvertSelectedSegments, onReorderPress, onEditSegmentTags, onExtractSegmentFramesAsImages]); useContextMenu(ref, contextMenuTemplate); @@ -243,7 +243,7 @@ const SegmentList = memo(({ onDeselectAllSegments, onSelectAllSegments, onSelectSegmentsByLabel, - onSelectSegmentsByTag, + onSelectSegmentsByExpr, onExtractSegmentFramesAsImages, onLabelSelectedSegments, onInvertSelectedSegments, @@ -281,7 +281,7 @@ const SegmentList = memo(({ onDeselectAllSegments: UseSegments['deselectAllSegments'], onSelectAllSegments: UseSegments['selectAllSegments'], onSelectSegmentsByLabel: UseSegments['onSelectSegmentsByLabel'], - onSelectSegmentsByTag: UseSegments['onSelectSegmentsByTag'], + onSelectSegmentsByExpr: UseSegments['onSelectSegmentsByExpr'], onExtractSegmentFramesAsImages: (segIds: string[]) => Promise, onLabelSelectedSegments: UseSegments['onLabelSelectedSegments'], onInvertSelectedSegments: UseSegments['invertSelectedSegments'], @@ -487,7 +487,7 @@ const SegmentList = memo(({ onSelectAllSegments={onSelectAllSegments} onEditSegmentTags={onEditSegmentTags} onSelectSegmentsByLabel={onSelectSegmentsByLabel} - onSelectSegmentsByTag={onSelectSegmentsByTag} + onSelectSegmentsByExpr={onSelectSegmentsByExpr} onExtractSegmentFramesAsImages={onExtractSegmentFramesAsImages} onLabelSelectedSegments={onLabelSelectedSegments} onInvertSelectedSegments={onInvertSelectedSegments} diff --git a/src/renderer/src/dialogs/index.tsx b/src/renderer/src/dialogs/index.tsx index c4045160..6b3e47ab 100644 --- a/src/renderer/src/dialogs/index.tsx +++ b/src/renderer/src/dialogs/index.tsx @@ -434,7 +434,7 @@ export async function createFixedDurationSegments({ fileDuration, inputPlacehold return edl; } -export async function createRandomSegments(fileDuration) { +export async function createRandomSegments(fileDuration: number) { const response = await askForSegmentsRandomDurationRange(); if (response == null) return undefined; @@ -451,14 +451,14 @@ export async function createRandomSegments(fileDuration) { return edl; } -const MovSuggestion = ({ fileFormat }) => (fileFormat === 'mp4' ?
  • Change output Format from MP4 to MOV
  • : null); +const MovSuggestion = ({ fileFormat }: { fileFormat: string | undefined }) => (fileFormat === 'mp4' ?
  • Change output Format from MP4 to MOV
  • : null); const OutputFormatSuggestion = () =>
  • Select a different output Format (matroska and mp4 support most codecs)
  • ; const WorkingDirectorySuggestion = () =>
  • Set a different Working directory
  • ; const DifferentFileSuggestion = () =>
  • Try with a Different file
  • ; const HelpSuggestion = () =>
  • See Help menu
  • ; const ErrorReportSuggestion = () =>
  • If nothing helps, you can send an Error report
  • ; -export async function showExportFailedDialog({ fileFormat, safeOutputFileName }) { +export async function showExportFailedDialog({ fileFormat, safeOutputFileName }: { fileFormat: string | undefined, safeOutputFileName: boolean }) { const html = (
    Try one of the following before exporting again: @@ -480,7 +480,7 @@ export async function showExportFailedDialog({ fileFormat, safeOutputFileName }) return value; } -export async function showConcatFailedDialog({ fileFormat }) { +export async function showConcatFailedDialog({ fileFormat }: { fileFormat: string | undefined }) { const html = (
    Try each of the following before merging again: @@ -517,7 +517,7 @@ export function openYouTubeChaptersDialog(text: string) { }); } -export async function labelSegmentDialog({ currentName, maxLength }) { +export async function labelSegmentDialog({ currentName, maxLength }: { currentName: string, maxLength: number }) { const { value } = await Swal.fire({ showCancelButton: true, title: i18n.t('Label current segment'), @@ -528,7 +528,7 @@ export async function labelSegmentDialog({ currentName, maxLength }) { return value; } -export async function selectSegmentsByLabelDialog(currentName) { +export async function selectSegmentsByLabelDialog(currentName: string) { const { value } = await Swal.fire({ showCancelButton: true, title: i18n.t('Select segments by label'), @@ -538,24 +538,47 @@ export async function selectSegmentsByLabelDialog(currentName) { return value; } -export async function selectSegmentsByTagDialog() { - const { value: value1 } = await Swal.fire({ - showCancelButton: true, - title: i18n.t('Select segments by tag'), - text: i18n.t('Enter tag name (in the next dialog you\'ll enter tag value)'), - input: 'text', - }); - if (!value1) return undefined; +export async function selectSegmentsByExprDialog(inputValidator: (v: string) => string | undefined) { + const examples = { + duration: { name: i18n.t('Segment duration less than 5 seconds'), code: 'segment.duration < 5' }, + start: { name: i18n.t('Segment starts after 00:60'), code: 'segment.start > 60' }, + label: { name: i18n.t('Segment label'), code: "equalText(segment.label, 'My label')" }, + tag: { name: i18n.t('Segment tag value'), code: "equalText(segment.tags.myTag, 'tag value')" }, + }; - const { value: value2 } = await Swal.fire({ - showCancelButton: true, - title: i18n.t('Select segments by tag'), - text: i18n.t('Enter tag value'), - input: 'text', - }); - if (!value2) return undefined; + function addExample(type: string) { + Swal.getInput()!.value = examples[type]?.code ?? ''; + } - return { tagName: value1, tagValue: value2 }; + const { value } = await ReactSwal.fire({ + showCancelButton: true, + title: i18n.t('Select segments by expression'), + input: 'text', + html: ( +
    +
    + {i18n.t('Enter an expression which will be evaluated for each segment. Segments for which the expression evaluates to "true" will be selected. For available syntax, see {{url}}.', { url: 'https://mathjs.org/' })} +
    + +
    {i18n.t('Variables')}:
    + +
    + segment.label, segment.start, segment.end, segment.duration +
    + +
    {i18n.t('Examples')}:
    + + {Object.entries(examples).map(([key, { name }]) => ( + + ))} +
    + ), + inputPlaceholder: 'segment.duration < 5', + inputValidator, + }); + return value; } export function showJson5Dialog({ title, json }: { title: string, json: unknown }) { @@ -631,7 +654,7 @@ export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }) { const fps = detectedFps || 1; const currentFps = fps * outputPlaybackRate; - function parseValue(v) { + function parseValue(v: string) { const newFps = parseFloat(v); if (!Number.isNaN(newFps)) { return newFps / fps; diff --git a/src/renderer/src/hooks/useSegments.ts b/src/renderer/src/hooks/useSegments.ts index d0f70f9c..453eb455 100644 --- a/src/renderer/src/hooks/useSegments.ts +++ b/src/renderer/src/hooks/useSegments.ts @@ -3,15 +3,15 @@ import { useStateWithHistory } from 'react-use/lib/useStateWithHistory'; import i18n from 'i18next'; import pMap from 'p-map'; import invariant from 'tiny-invariant'; - +import { evaluate } from 'mathjs'; import sortBy from 'lodash/sortBy'; import { detectSceneChanges as ffmpegDetectSceneChanges, readFrames, mapTimesToSegments, findKeyframeNearTime } from '../ffmpeg'; import { handleError, shuffleArray } from '../util'; import { errorToast } from '../swal'; import { showParametersDialog } from '../dialogs/parameters'; -import { createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, selectSegmentsByTagDialog } from '../dialogs'; -import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, getSegmentTags, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2, addSegmentColorIndex } from '../segments'; +import { createNumSegments as createNumSegmentsDialog, createFixedDurationSegments as createFixedDurationSegmentsDialog, createRandomSegments as createRandomSegmentsDialog, labelSegmentDialog, askForShiftSegments, askForAlignSegments, selectSegmentsByLabelDialog, selectSegmentsByExprDialog } from '../dialogs'; +import { createSegment, findSegmentsAtCursor, sortSegments, invertSegments, combineOverlappingSegments as combineOverlappingSegments2, combineSelectedSegments as combineSelectedSegments2, isDurationValid, getSegApparentStart, getSegApparentEnd as getSegApparentEnd2, addSegmentColorIndex } from '../segments'; import * as ffmpegParameters from '../ffmpeg-parameters'; import { maxSegmentsAllowed } from '../util/constants'; import { ParseTimecode, SegmentBase, SegmentToExport, StateSegment, UpdateSegAtIndex } from '../types'; @@ -454,7 +454,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt if (segments) loadCutSegments(segments); }, [checkFileOpened, duration, loadCutSegments]); - const enableSegments = useCallback((segmentsToEnable) => { + const enableSegments = useCallback((segmentsToEnable: { segId: string }[]) => { if (segmentsToEnable.length === 0 || segmentsToEnable.length === cutSegments.length) return; // no point setDeselectedSegmentIds((existing) => { const ret = { ...existing }; @@ -471,13 +471,43 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt enableSegments(segmentsToEnable); }, [currentCutSeg, cutSegments, enableSegments]); - const onSelectSegmentsByTag = useCallback(async () => { - const value = await selectSegmentsByTagDialog(); + const onSelectSegmentsByExpr = useCallback(async () => { + function matchSegment(seg: StateSegment, expr: string) { + const start = getSegApparentStart(seg); + const end = getSegApparentEnd(seg); + // must clone tags because scope is mutable (editable by expression) + const scopeSegment: { label: string, start: number, end: number, duration: number, tags: Record } = { label: seg.name, start, end, duration: end - start, tags: { ...seg.tags } }; + return evaluate(expr, { segment: scopeSegment }) === true; + } + + const getSegmentsToEnable = (expr: string) => cutSegments.filter((seg) => { + try { + return matchSegment(seg, expr); + } catch (err) { + if (err instanceof TypeError) { + return false; + } + throw err; + } + }); + + const value = await selectSegmentsByExprDialog((v: string) => { + try { + const segments = getSegmentsToEnable(v); + if (segments.length === 0) return i18n.t('No segments matched'); + return undefined; + } catch (err) { + if (err instanceof Error) { + return err.message; + } + throw err; + } + }); + if (value == null) return; - const { tagName, tagValue } = value; - const segmentsToEnable = cutSegments.filter((seg) => getSegmentTags(seg)[tagName] === tagValue); + const segmentsToEnable = getSegmentsToEnable(value); enableSegments(segmentsToEnable); - }, [cutSegments, enableSegments]); + }, [cutSegments, enableSegments, getSegApparentEnd]); const onLabelSelectedSegments = useCallback(async () => { if (selectedSegmentsRaw.length === 0) return; @@ -570,7 +600,7 @@ function useSegments({ filePath, workingRef, setWorking, setCutProgress, videoSt invertSelectedSegments, removeSelectedSegments, onSelectSegmentsByLabel, - onSelectSegmentsByTag, + onSelectSegmentsByExpr, toggleSegmentSelected, selectOnlySegment, setCutTime, diff --git a/yarn.lock b/yarn.lock index 7598a35e..8a693bf2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -545,6 +545,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.24.4": + version: 7.24.5 + resolution: "@babel/runtime@npm:7.24.5" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: e0f4f4d4503f7338749d1dd92361ad132d683bde64e6b61d6c855e100dcd01592295fcfdcc960c946b85ef7908dc2f501080da58447c05812cf3cd80c599bb62 + languageName: node + linkType: hard + "@babel/template@npm:^7.20.7": version: 7.20.7 resolution: "@babel/template@npm:7.20.7" @@ -3898,6 +3907,13 @@ __metadata: languageName: node linkType: hard +"complex.js@npm:^2.1.1": + version: 2.1.1 + resolution: "complex.js@npm:2.1.1" + checksum: 1905d5204dd8a4d6f591182aca2045986f1ff3c5373e455ccd10c6ee2905bf1d3811a313d38c68f8a8507523202f91e25177387e3adc386c1b5b5ec2f13a6dbb + languageName: node + linkType: hard + "compute-scroll-into-view@npm:^1.0.14, compute-scroll-into-view@npm:^1.0.17": version: 1.0.17 resolution: "compute-scroll-into-view@npm:1.0.17" @@ -4238,6 +4254,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.4.3": + version: 10.4.3 + resolution: "decimal.js@npm:10.4.3" + checksum: de663a7bc4d368e3877db95fcd5c87b965569b58d16cdc4258c063d231ca7118748738df17cd638f7e9dd0be8e34cec08d7234b20f1f2a756a52fc5a38b188d0 + languageName: node + linkType: hard + "decompress-response@npm:^6.0.0": version: 6.0.0 resolution: "decompress-response@npm:6.0.0" @@ -5195,6 +5218,13 @@ __metadata: languageName: node linkType: hard +"escape-latex@npm:^1.2.0": + version: 1.2.0 + resolution: "escape-latex@npm:1.2.0" + checksum: 73a787319f0965ecb8244bb38bf3a3cba872f0b9a5d3da8821140e9f39fe977045dc953a62b1a2bed4d12bfccbe75a7d8ec786412bf00739eaa2f627d0a8e0d6 + languageName: node + linkType: hard + "escape-string-regexp@npm:5.0.0": version: 5.0.0 resolution: "escape-string-regexp@npm:5.0.0" @@ -5945,6 +5975,13 @@ __metadata: languageName: node linkType: hard +"fraction.js@npm:4.3.4": + version: 4.3.4 + resolution: "fraction.js@npm:4.3.4" + checksum: 3a1e6b268038ffdea625fab6a8d155d7ab644d35d0c99bc59084bfd29fbc714f3a38381b0627751ddb5f188bcde0b3f48c27e80eeb2ecd440825a7d2cd2bf9f1 + languageName: node + linkType: hard + "framer-motion@npm:^9.0.3": version: 9.0.3 resolution: "framer-motion@npm:9.0.3" @@ -7498,6 +7535,13 @@ __metadata: languageName: node linkType: hard +"javascript-natural-sort@npm:^0.7.1": + version: 0.7.1 + resolution: "javascript-natural-sort@npm:0.7.1" + checksum: 7bf6eab67871865d347f09a95aa770f9206c1ab0226bcda6fdd9edec340bf41111a7f82abac30556aa16a21cfa3b2b1ca4a362c8b73dd5ce15220e5d31f49d79 + languageName: node + linkType: hard + "js-cookie@npm:^2.2.1": version: 2.2.1 resolution: "js-cookie@npm:2.2.1" @@ -7949,6 +7993,7 @@ __metadata: ky: "npm:^0.33.1" lodash: "npm:^4.17.19" luxon: "npm:^3.3.0" + mathjs: "npm:^12.4.2" mime-types: "npm:^2.1.14" mkdirp: "npm:^1.0.3" morgan: "npm:^1.10.0" @@ -8140,6 +8185,25 @@ __metadata: languageName: node linkType: hard +"mathjs@npm:^12.4.2": + version: 12.4.2 + resolution: "mathjs@npm:12.4.2" + dependencies: + "@babel/runtime": "npm:^7.24.4" + complex.js: "npm:^2.1.1" + decimal.js: "npm:^10.4.3" + escape-latex: "npm:^1.2.0" + fraction.js: "npm:4.3.4" + javascript-natural-sort: "npm:^0.7.1" + seedrandom: "npm:^3.0.5" + tiny-emitter: "npm:^2.1.0" + typed-function: "npm:^4.1.1" + bin: + mathjs: bin/cli.js + checksum: 4b88ac1b137d00b8f3d66f4d1662d3670399390b59623ecf3ab7d587ba18be7b97ce9c5b07e953029ac75f48567d675c99323889ae231eb071ddd84db5dd699c + languageName: node + linkType: hard + "mdn-data@npm:2.0.14": version: 2.0.14 resolution: "mdn-data@npm:2.0.14" @@ -10298,6 +10362,13 @@ __metadata: languageName: node linkType: hard +"seedrandom@npm:^3.0.5": + version: 3.0.5 + resolution: "seedrandom@npm:3.0.5" + checksum: acad5e516c04289f61c2fb9848f449b95f58362b75406b79ec51e101ec885293fc57e3675d2f39f49716336559d7190f7273415d185fead8cd27b171ebf7d8fb + languageName: node + linkType: hard + "semver-compare@npm:^1.0.0": version: 1.0.0 resolution: "semver-compare@npm:1.0.0" @@ -11209,6 +11280,13 @@ __metadata: languageName: node linkType: hard +"tiny-emitter@npm:^2.1.0": + version: 2.1.0 + resolution: "tiny-emitter@npm:2.1.0" + checksum: 75633f4de4f47f43af56aff6162f25b87be7efc6f669fda256658f3c3f4a216f23dc0d13200c6fafaaf1b0c7142f0201352fb06aec0b77f68aea96be898f4516 + languageName: node + linkType: hard + "tiny-invariant@npm:1.2.0": version: 1.2.0 resolution: "tiny-invariant@npm:1.2.0" @@ -11536,6 +11614,13 @@ __metadata: languageName: node linkType: hard +"typed-function@npm:^4.1.1": + version: 4.1.1 + resolution: "typed-function@npm:4.1.1" + checksum: 0ef538d5f02e5c40659cccc14b5f2727f0e4181f11d91bb7897327c33cc2893de7e92343b6b32e1bb15e44a215a1e92e27ab2aa1353b100a9a2697abf2989a0c + languageName: node + linkType: hard + "typedarray-to-buffer@npm:^3.1.5": version: 3.1.5 resolution: "typedarray-to-buffer@npm:3.1.5"