1
0
mirror of https://github.com/mifi/lossless-cut.git synced 2024-11-22 10:22:31 +01:00

implement copy/paste segment tags

closes #1964
This commit is contained in:
Mikael Finstad 2024-04-21 20:50:58 +02:00
parent 8217be4f14
commit 3f4c214287
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
8 changed files with 70 additions and 29 deletions

View File

@ -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": {

View File

@ -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,
[tag]: value,
...keyValues,
})), [setEditingSegmentTags]);
const onTagReset = useCallback((tag: string) => setEditingSegmentTags((tags) => {
@ -437,7 +437,7 @@ const SegmentList = memo(({
onCloseComplete={onSegmentTagsCloseComplete}
>
<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>
</Dialog>

View File

@ -23,7 +23,6 @@ const CopyClipboardButton = memo(({ text, style }: { text: string, style?: Motio
<motion.span animate={animation} style={{ display: 'inline-block', cursor: 'pointer', ...style }}>
<FaClipboard title={t('Copy to clipboard')} onClick={onClick} />
</motion.span>
);
});

View File

@ -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<string, string>) => void, onTagReset: (tag: string) => void, addTagTitle: string, addTagText: string,
}) {
const { t } = useTranslation();
const ref = useRef();
const ref = useRef<HTMLInputElement>(null);
const [editingTagVal, setEditingTagVal] = useState();
const [newTag, setNewTag] = useState();
const [editingTagVal, setEditingTagVal] = useState<string>();
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(() => {
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 (
<>
<table style={{ color: 'black' }}>
<table style={{ color: 'black', marginBottom: 10 }}>
<tbody>
{Object.keys(mergedTags).map((tag) => {
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)} />
</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'} />}
{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>
</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>
</>
);
}

View File

@ -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<string>({
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<string>({
text: i18n.t('Please confirm that you want to extract all tracks as separate files'),
showCancelButton: true,
confirmButtonText: i18n.t('Extract all tracks'),

View File

@ -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,
});

View File

@ -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<string, unknown>;
export type SegmentTags = z.infer<typeof segmentTagsSchema>
export type EditingSegmentTags = Record<string, SegmentTags>

View File

@ -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