mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-22 10:22:31 +01:00
parent
8217be4f14
commit
3f4c214287
@ -136,7 +136,8 @@
|
|||||||
"semver": "^7.5.2",
|
"semver": "^7.5.2",
|
||||||
"string-to-stream": "^3.0.1",
|
"string-to-stream": "^3.0.1",
|
||||||
"winston": "^3.8.1",
|
"winston": "^3.8.1",
|
||||||
"yargs-parser": "^21.1.1"
|
"yargs-parser": "^21.1.1",
|
||||||
|
"zod": "^3.22.5"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"directories": {
|
"directories": {
|
||||||
|
@ -400,11 +400,11 @@ const SegmentList = memo(({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [editingTag, setEditingTag] = useState();
|
const [editingTag, setEditingTag] = useState<string>();
|
||||||
|
|
||||||
const onTagChange = useCallback((tag: string, value: string) => setEditingSegmentTags((existingTags) => ({
|
const onTagsChange = useCallback((keyValues: Record<string, string>) => setEditingSegmentTags((existingTags) => ({
|
||||||
...existingTags,
|
...existingTags,
|
||||||
[tag]: value,
|
...keyValues,
|
||||||
})), [setEditingSegmentTags]);
|
})), [setEditingSegmentTags]);
|
||||||
|
|
||||||
const onTagReset = useCallback((tag: string) => setEditingSegmentTags((tags) => {
|
const onTagReset = useCallback((tag: string) => setEditingSegmentTags((tags) => {
|
||||||
@ -437,7 +437,7 @@ const SegmentList = memo(({
|
|||||||
onCloseComplete={onSegmentTagsCloseComplete}
|
onCloseComplete={onSegmentTagsCloseComplete}
|
||||||
>
|
>
|
||||||
<div style={{ color: 'black' }}>
|
<div style={{ color: 'black' }}>
|
||||||
<TagEditor customTags={editingSegmentTags} editingTag={editingTag} setEditingTag={setEditingTag} onTagChange={onTagChange} onTagReset={onTagReset} addTagTitle={t('Add segment tag')} addTagText={t('Enter tag key')} />
|
<TagEditor customTags={editingSegmentTags} editingTag={editingTag} setEditingTag={setEditingTag} onTagsChange={onTagsChange} onTagReset={onTagReset} addTagTitle={t('Add segment tag')} addTagText={t('Enter tag key')} />
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ const CopyClipboardButton = memo(({ text, style }: { text: string, style?: Motio
|
|||||||
<motion.span animate={animation} style={{ display: 'inline-block', cursor: 'pointer', ...style }}>
|
<motion.span animate={animation} style={{ display: 'inline-block', cursor: 'pointer', ...style }}>
|
||||||
<FaClipboard title={t('Copy to clipboard')} onClick={onClick} />
|
<FaClipboard title={t('Copy to clipboard')} onClick={onClick} />
|
||||||
</motion.span>
|
</motion.span>
|
||||||
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,46 +1,70 @@
|
|||||||
import { memo, useRef, useState, useMemo, useCallback, useEffect } from 'react';
|
import { memo, useRef, useState, useMemo, useCallback, useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { TextInput, TrashIcon, TickIcon, EditIcon, PlusIcon, Button, IconButton } from 'evergreen-ui';
|
import { TextInput, TrashIcon, TickIcon, EditIcon, PlusIcon, Button, IconButton } from 'evergreen-ui';
|
||||||
|
import invariant from 'tiny-invariant';
|
||||||
|
|
||||||
import { askForMetadataKey } from '../dialogs';
|
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 activeColor = '#429777';
|
||||||
|
|
||||||
const emptyObject = {};
|
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<string, string>) => void, onTagReset: (tag: string) => void, addTagTitle: string, addTagText: string,
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const ref = useRef();
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const [editingTagVal, setEditingTagVal] = useState();
|
const [editingTagVal, setEditingTagVal] = useState<string>();
|
||||||
const [newTag, setNewTag] = useState();
|
const [newTag, setNewTag] = useState<string>();
|
||||||
|
|
||||||
const mergedTags = useMemo(() => ({ ...existingTags, ...customTags, ...(newTag ? { [newTag]: '' } : {}) }), [customTags, existingTags, newTag]);
|
const mergedTags = useMemo(() => ({ ...existingTags, ...customTags, ...(newTag ? { [newTag]: '' } : undefined) }), [customTags, existingTags, newTag]);
|
||||||
|
|
||||||
const onResetClick = useCallback(() => {
|
const onResetClick = useCallback(() => {
|
||||||
|
invariant(editingTag != null);
|
||||||
onTagReset(editingTag);
|
onTagReset(editingTag);
|
||||||
setEditingTag();
|
setEditingTag(undefined);
|
||||||
setNewTag();
|
setNewTag(undefined);
|
||||||
}, [editingTag, onTagReset, setEditingTag]);
|
}, [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) {
|
if (newTag) {
|
||||||
onTagChange(editingTag, editingTagVal);
|
invariant(editingTag != null);
|
||||||
setEditingTag();
|
invariant(editingTagVal != null);
|
||||||
setNewTag();
|
onTagsChange({ [editingTag]: editingTagVal });
|
||||||
|
setEditingTag(undefined);
|
||||||
|
setNewTag(undefined);
|
||||||
} else if (editingTag != null) {
|
} else if (editingTag != null) {
|
||||||
if (editingTagVal !== existingTags[editingTag]) {
|
if (editingTagVal !== existingTags[editingTag]) {
|
||||||
onTagChange(editingTag, editingTagVal);
|
invariant(editingTag != null);
|
||||||
setEditingTag();
|
invariant(editingTagVal != null);
|
||||||
|
onTagsChange({ [editingTag]: editingTagVal });
|
||||||
|
setEditingTag(undefined);
|
||||||
} else { // If not actually changed, no need to update
|
} else { // If not actually changed, no need to update
|
||||||
onResetClick();
|
onResetClick();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setEditingTag(tag);
|
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) {
|
function onSubmit(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -69,7 +93,7 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<table style={{ color: 'black' }}>
|
<table style={{ color: 'black', marginBottom: 10 }}>
|
||||||
<tbody>
|
<tbody>
|
||||||
{Object.keys(mergedTags).map((tag) => {
|
{Object.keys(mergedTags).map((tag) => {
|
||||||
const editingThis = tag === editingTag;
|
const editingThis = tag === editingTag;
|
||||||
@ -87,7 +111,7 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi
|
|||||||
<TextInput ref={ref} placeholder={t('Enter value')} value={editingTagVal || ''} onChange={(e) => setEditingTagVal(e.target.value)} />
|
<TextInput ref={ref} placeholder={t('Enter value')} value={editingTagVal || ''} onChange={(e) => setEditingTagVal(e.target.value)} />
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<span style={{ padding: '.5em 0', color: thisTagCustom ? activeColor : undefined, fontWeight: thisTagCustom ? 'bold' : undefined }}>{mergedTags[tag] || `<${t('empty')}>`}</span>
|
<span style={{ padding: '.5em 0', color: thisTagCustom ? activeColor : undefined, fontWeight: thisTagCustom ? 'bold' : undefined }}>{mergedTags[tag] ? String(mergedTags[tag]) : `<${t('empty')}>`}</span>
|
||||||
)}
|
)}
|
||||||
{(editingTag == null || editingThis) && <IconButton icon={Icon} title={t('Edit')} appearance="minimal" style={{ marginLeft: '.4em' }} onClick={() => onEditClick(tag)} intent={editingThis ? 'success' : 'none'} />}
|
{(editingTag == null || editingThis) && <IconButton icon={Icon} title={t('Edit')} appearance="minimal" style={{ marginLeft: '.4em' }} onClick={() => onEditClick(tag)} intent={editingThis ? 'success' : 'none'} />}
|
||||||
{editingThis && <IconButton icon={TrashIcon} title={thisTagCustom ? t('Delete') : t('Reset')} appearance="minimal" onClick={onResetClick} intent="danger" />}
|
{editingThis && <IconButton icon={TrashIcon} title={thisTagCustom ? t('Delete') : t('Reset')} appearance="minimal" onClick={onResetClick} intent="danger" />}
|
||||||
@ -98,7 +122,14 @@ function TagEditor({ existingTags = emptyObject, customTags = emptyObject, editi
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<Button style={{ marginTop: 10 }} iconBefore={PlusIcon} onClick={onAddPress}>{addTagTitle}</Button>
|
<Button iconBefore={PlusIcon} onClick={onAddPress}>{addTagTitle}</Button>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '1em' }}>
|
||||||
|
<span style={{ marginRight: '1em' }}>{t('Batch')}:</span>
|
||||||
|
<CopyClipboardButton text={JSON.stringify(mergedTags)} style={{ marginRight: '.3em', verticalAlign: 'middle' }} />
|
||||||
|
|
||||||
|
<Button appearance="minimal" onClick={onPasteClick}>{t('Paste')}</Button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -339,8 +339,8 @@ export async function askForAlignSegments() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function askForMetadataKey({ title, text }) {
|
export async function askForMetadataKey({ title, text }: { title: string, text: string }) {
|
||||||
const { value } = await Swal.fire({
|
const { value } = await Swal.fire<string>({
|
||||||
title,
|
title,
|
||||||
text,
|
text,
|
||||||
input: 'text',
|
input: 'text',
|
||||||
@ -352,7 +352,7 @@ export async function askForMetadataKey({ title, text }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function confirmExtractAllStreamsDialog() {
|
export async function confirmExtractAllStreamsDialog() {
|
||||||
const { value } = await Swal.fire({
|
const { value } = await Swal.fire<string>({
|
||||||
text: i18n.t('Please confirm that you want to extract all tracks as separate files'),
|
text: i18n.t('Please confirm that you want to extract all tracks as separate files'),
|
||||||
showCancelButton: true,
|
showCancelButton: true,
|
||||||
confirmButtonText: i18n.t('Extract all tracks'),
|
confirmButtonText: i18n.t('Extract all tracks'),
|
||||||
|
@ -49,7 +49,7 @@ export const swalToastOptions: SweetAlertOptions = {
|
|||||||
|
|
||||||
export const toast = Swal.mixin(swalToastOptions);
|
export const toast = Swal.mixin(swalToastOptions);
|
||||||
|
|
||||||
export const errorToast = (text) => toast.fire({
|
export const errorToast = (text: string) => toast.fire({
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
text,
|
text,
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import type { MenuItem, MenuItemConstructorOptions } from 'electron';
|
import type { MenuItem, MenuItemConstructorOptions } from 'electron';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
|
||||||
export interface ChromiumHTMLVideoElement extends HTMLVideoElement {
|
export interface ChromiumHTMLVideoElement extends HTMLVideoElement {
|
||||||
@ -23,8 +24,9 @@ export interface ApparentSegmentBase extends SegmentColorIndex {
|
|||||||
end: number,
|
end: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const segmentTagsSchema = z.record(z.string(), z.string());
|
||||||
|
|
||||||
export type SegmentTags = Record<string, unknown>;
|
export type SegmentTags = z.infer<typeof segmentTagsSchema>
|
||||||
|
|
||||||
export type EditingSegmentTags = Record<string, SegmentTags>
|
export type EditingSegmentTags = Record<string, SegmentTags>
|
||||||
|
|
||||||
|
@ -7996,6 +7996,7 @@ __metadata:
|
|||||||
vitest: "npm:^1.2.2"
|
vitest: "npm:^1.2.2"
|
||||||
winston: "npm:^3.8.1"
|
winston: "npm:^3.8.1"
|
||||||
yargs-parser: "npm:^21.1.1"
|
yargs-parser: "npm:^21.1.1"
|
||||||
|
zod: "npm:^3.22.5"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@ -12397,3 +12398,10 @@ __metadata:
|
|||||||
checksum: 2cac84540f65c64ccc1683c267edce396b26b1e931aa429660aefac8fbe0188167b7aee815a3c22fa59a28a58d898d1a2b1825048f834d8d629f4c2a5d443801
|
checksum: 2cac84540f65c64ccc1683c267edce396b26b1e931aa429660aefac8fbe0188167b7aee815a3c22fa59a28a58d898d1a2b1825048f834d8d629f4c2a5d443801
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"zod@npm:^3.22.5":
|
||||||
|
version: 3.22.5
|
||||||
|
resolution: "zod@npm:3.22.5"
|
||||||
|
checksum: a60c1b55c4cc824a5d0432ee29d93b087b5d8a1bd2d0f4cd6e7ffe5b602da9cab2f2c27b1ae6c96d88d9f778cc933cead70e08b7944a98893576c61dca5e0c74
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
Loading…
Reference in New Issue
Block a user