diff --git a/package.json b/package.json index 96f6e01d..ae8dc977 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,8 @@ "semver": "^7.5.2", "string-to-stream": "^3.0.1", "winston": "^3.8.1", - "yargs-parser": "^21.1.1" + "yargs-parser": "^21.1.1", + "zod": "^3.22.5" }, "build": { "directories": { diff --git a/src/renderer/src/SegmentList.tsx b/src/renderer/src/SegmentList.tsx index 302db799..18410bf7 100644 --- a/src/renderer/src/SegmentList.tsx +++ b/src/renderer/src/SegmentList.tsx @@ -400,11 +400,11 @@ const SegmentList = memo(({ ); } - const [editingTag, setEditingTag] = useState(); + const [editingTag, setEditingTag] = useState(); - const onTagChange = useCallback((tag: string, value: string) => setEditingSegmentTags((existingTags) => ({ + const onTagsChange = useCallback((keyValues: Record) => setEditingSegmentTags((existingTags) => ({ ...existingTags, - [tag]: value, + ...keyValues, })), [setEditingSegmentTags]); const onTagReset = useCallback((tag: string) => setEditingSegmentTags((tags) => { @@ -437,7 +437,7 @@ const SegmentList = memo(({ onCloseComplete={onSegmentTagsCloseComplete} >
- +
diff --git a/src/renderer/src/components/CopyClipboardButton.tsx b/src/renderer/src/components/CopyClipboardButton.tsx index fe3c0d87..a1295428 100644 --- a/src/renderer/src/components/CopyClipboardButton.tsx +++ b/src/renderer/src/components/CopyClipboardButton.tsx @@ -23,7 +23,6 @@ const CopyClipboardButton = memo(({ text, style }: { text: string, style?: Motio - ); }); diff --git a/src/renderer/src/components/TagEditor.jsx b/src/renderer/src/components/TagEditor.tsx similarity index 57% rename from src/renderer/src/components/TagEditor.jsx rename to src/renderer/src/components/TagEditor.tsx index 2ebb108f..5bb4dc63 100644 --- a/src/renderer/src/components/TagEditor.jsx +++ b/src/renderer/src/components/TagEditor.tsx @@ -1,46 +1,70 @@ import { memo, useRef, useState, useMemo, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { TextInput, TrashIcon, TickIcon, EditIcon, PlusIcon, Button, IconButton } from 'evergreen-ui'; +import invariant from 'tiny-invariant'; import { askForMetadataKey } from '../dialogs'; +import { SegmentTags, segmentTagsSchema } from '../types'; +import CopyClipboardButton from './CopyClipboardButton'; +import { errorToast } from '../swal'; +const { clipboard } = window.require('electron'); + const activeColor = '#429777'; const emptyObject = {}; -function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editingTag, setEditingTag, onTagChange, onTagReset, addTagTitle, addTagText }) { +function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editingTag, setEditingTag, onTagsChange, onTagReset, addTagTitle, addTagText }: { + existingTags?: SegmentTags, customTags?: SegmentTags | undefined, editingTag: string | undefined, setEditingTag: (v: string | undefined) => void, onTagsChange: (keyValues: Record) => void, onTagReset: (tag: string) => void, addTagTitle: string, addTagText: string, +}) { const { t } = useTranslation(); - const ref = useRef(); + const ref = useRef(null); - const [editingTagVal, setEditingTagVal] = useState(); - const [newTag, setNewTag] = useState(); + const [editingTagVal, setEditingTagVal] = useState(); + const [newTag, setNewTag] = useState(); - const mergedTags = useMemo(() => ({ ...existingTags, ...customTags, ...(newTag ? { [newTag]: '' } : {}) }), [customTags, existingTags, newTag]); + const mergedTags = useMemo(() => ({ ...existingTags, ...customTags, ...(newTag ? { [newTag]: '' } : undefined) }), [customTags, existingTags, newTag]); const onResetClick = useCallback(() => { + invariant(editingTag != null); onTagReset(editingTag); - setEditingTag(); - setNewTag(); + setEditingTag(undefined); + setNewTag(undefined); }, [editingTag, onTagReset, setEditingTag]); - const onEditClick = useCallback((tag) => { + const onPasteClick = useCallback(async () => { + const text = clipboard.readText(); + try { + const json = JSON.parse(text); + const newTags = segmentTagsSchema.parse(json); + onTagsChange(newTags); + } catch (e) { + if (e instanceof Error) errorToast(e.message); + } + }, [onTagsChange]); + + const onEditClick = useCallback((tag?: string) => { if (newTag) { - onTagChange(editingTag, editingTagVal); - setEditingTag(); - setNewTag(); + invariant(editingTag != null); + invariant(editingTagVal != null); + onTagsChange({ [editingTag]: editingTagVal }); + setEditingTag(undefined); + setNewTag(undefined); } else if (editingTag != null) { if (editingTagVal !== existingTags[editingTag]) { - onTagChange(editingTag, editingTagVal); - setEditingTag(); + invariant(editingTag != null); + invariant(editingTagVal != null); + onTagsChange({ [editingTag]: editingTagVal }); + setEditingTag(undefined); } else { // If not actually changed, no need to update onResetClick(); } } else { setEditingTag(tag); - setEditingTagVal(mergedTags[tag]); + setEditingTagVal(tag && String(mergedTags[tag])); } - }, [editingTag, editingTagVal, existingTags, mergedTags, newTag, onResetClick, onTagChange, setEditingTag]); + }, [editingTag, editingTagVal, existingTags, mergedTags, newTag, onResetClick, onTagsChange, setEditingTag]); function onSubmit(e) { e.preventDefault(); @@ -69,7 +93,7 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi return ( <> - +
{Object.keys(mergedTags).map((tag) => { const editingThis = tag === editingTag; @@ -87,7 +111,7 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi setEditingTagVal(e.target.value)} /> ) : ( - {mergedTags[tag] || `<${t('empty')}>`} + {mergedTags[tag] ? String(mergedTags[tag]) : `<${t('empty')}>`} )} {(editingTag == null || editingThis) && onEditClick(tag)} intent={editingThis ? 'success' : 'none'} />} {editingThis && } @@ -98,7 +122,14 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi
- + + +
+ {t('Batch')}: + + + +
); } diff --git a/src/renderer/src/dialogs/index.tsx b/src/renderer/src/dialogs/index.tsx index 0001f809..8a727420 100644 --- a/src/renderer/src/dialogs/index.tsx +++ b/src/renderer/src/dialogs/index.tsx @@ -339,8 +339,8 @@ export async function askForAlignSegments() { }; } -export async function askForMetadataKey({ title, text }) { - const { value } = await Swal.fire({ +export async function askForMetadataKey({ title, text }: { title: string, text: string }) { + const { value } = await Swal.fire({ title, text, input: 'text', @@ -352,7 +352,7 @@ export async function askForMetadataKey({ title, text }) { } export async function confirmExtractAllStreamsDialog() { - const { value } = await Swal.fire({ + const { value } = await Swal.fire({ text: i18n.t('Please confirm that you want to extract all tracks as separate files'), showCancelButton: true, confirmButtonText: i18n.t('Extract all tracks'), diff --git a/src/renderer/src/swal.ts b/src/renderer/src/swal.ts index 2c2c9fcc..08ebe0a9 100644 --- a/src/renderer/src/swal.ts +++ b/src/renderer/src/swal.ts @@ -49,7 +49,7 @@ export const swalToastOptions: SweetAlertOptions = { export const toast = Swal.mixin(swalToastOptions); -export const errorToast = (text) => toast.fire({ +export const errorToast = (text: string) => toast.fire({ icon: 'error', text, }); diff --git a/src/renderer/src/types.ts b/src/renderer/src/types.ts index 9f27e230..d9b9561e 100644 --- a/src/renderer/src/types.ts +++ b/src/renderer/src/types.ts @@ -1,4 +1,5 @@ import type { MenuItem, MenuItemConstructorOptions } from 'electron'; +import { z } from 'zod'; export interface ChromiumHTMLVideoElement extends HTMLVideoElement { @@ -23,8 +24,9 @@ export interface ApparentSegmentBase extends SegmentColorIndex { end: number, } +export const segmentTagsSchema = z.record(z.string(), z.string()); -export type SegmentTags = Record; +export type SegmentTags = z.infer export type EditingSegmentTags = Record diff --git a/yarn.lock b/yarn.lock index 954dd0a2..d416776b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7996,6 +7996,7 @@ __metadata: vitest: "npm:^1.2.2" winston: "npm:^3.8.1" yargs-parser: "npm:^21.1.1" + zod: "npm:^3.22.5" languageName: unknown linkType: soft @@ -12397,3 +12398,10 @@ __metadata: checksum: 2cac84540f65c64ccc1683c267edce396b26b1e931aa429660aefac8fbe0188167b7aee815a3c22fa59a28a58d898d1a2b1825048f834d8d629f4c2a5d443801 languageName: node linkType: hard + +"zod@npm:^3.22.5": + version: 3.22.5 + resolution: "zod@npm:3.22.5" + checksum: a60c1b55c4cc824a5d0432ee29d93b087b5d8a1bd2d0f4cd6e7ffe5b602da9cab2f2c27b1ae6c96d88d9f778cc933cead70e08b7944a98893576c61dca5e0c74 + languageName: node + linkType: hard