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

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, ...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>

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

View File

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

View File

@ -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'),

View File

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

View File

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

View File

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