diff --git a/src/renderer/features/tag-editor/components/add-field-input.tsx b/src/renderer/features/tag-editor/components/add-field-input.tsx
new file mode 100644
index 000000000..81b514a86
--- /dev/null
+++ b/src/renderer/features/tag-editor/components/add-field-input.tsx
@@ -0,0 +1,109 @@
+import { useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { KNOWN_TAG_MAP, resolveTagKey } from '../utils/known-tags';
+
+import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
+import { Autocomplete } from '/@/shared/components/autocomplete/autocomplete';
+import { Group } from '/@/shared/components/group/group';
+
+interface AddFieldInputProps {
+ availableFields: Array<{ label: string; value: string }>;
+ existingFieldKeys: string[];
+ onAddField: (key: string) => void;
+}
+
+export const AddFieldInput = ({
+ availableFields,
+ existingFieldKeys,
+ onAddField,
+}: AddFieldInputProps) => {
+ const { t } = useTranslation();
+ const skipNextEnterRef = useRef(false);
+ const skipNextOnChangeResetRef = useRef(false);
+ const [input, setInput] = useState('');
+ const [duplicateAttempted, setDuplicateAttempted] = useState(false);
+
+ const trimmedInput = input.trim();
+ const customKeyError =
+ trimmedInput && !KNOWN_TAG_MAP.has(trimmedInput)
+ ? trimmedInput.includes('=')
+ ? "Tag key cannot contain '='"
+ : // eslint-disable-next-line no-control-regex
+ /[^\x00-\x7F]/.test(trimmedInput)
+ ? 'Tag key must use ASCII characters only'
+ : null
+ : null;
+
+ const resolvedInputKey = trimmedInput ? resolveTagKey(trimmedInput) : '';
+ const duplicateError =
+ trimmedInput && !customKeyError && existingFieldKeys.includes(resolvedInputKey)
+ ? 'Field already exists'
+ : null;
+ const fieldError = customKeyError ?? (duplicateAttempted ? duplicateError : null);
+
+ const addField = (key: string): boolean => {
+ const trimmed = key.trim();
+ if (!trimmed) return false;
+ if (
+ !KNOWN_TAG_MAP.has(trimmed) &&
+ (trimmed.includes('=') || // eslint-disable-next-line no-control-regex
+ /[^\x00-\x7F]/.test(trimmed))
+ )
+ return false;
+
+ const normalizedKey = resolveTagKey(trimmed);
+ if (existingFieldKeys.includes(normalizedKey)) {
+ setDuplicateAttempted(true);
+ return false;
+ }
+
+ setDuplicateAttempted(false);
+ setInput('');
+ onAddField(normalizedKey);
+ return true;
+ };
+
+ return (
+
+ {
+ setInput(value);
+ if (skipNextOnChangeResetRef.current) {
+ skipNextOnChangeResetRef.current = false;
+ } else {
+ setDuplicateAttempted(false);
+ }
+ }}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ if (skipNextEnterRef.current) {
+ skipNextEnterRef.current = false;
+ return;
+ }
+ addField(input);
+ }
+ }}
+ onOptionSubmit={(value) => {
+ skipNextEnterRef.current = true;
+ skipNextOnChangeResetRef.current = true;
+ const added = addField(value);
+ if (added) {
+ queueMicrotask(() => setInput(''));
+ }
+ }}
+ placeholder={t('page.itemDetail.addField', 'Add field…')}
+ value={input}
+ />
+ addField(input)}
+ variant="filled"
+ />
+
+ );
+};
diff --git a/src/renderer/features/tag-editor/components/song-edit-modal.tsx b/src/renderer/features/tag-editor/components/song-edit-modal.tsx
index 9ca239b51..62ef0254c 100644
--- a/src/renderer/features/tag-editor/components/song-edit-modal.tsx
+++ b/src/renderer/features/tag-editor/components/song-edit-modal.tsx
@@ -1,17 +1,14 @@
import { closeAllModals } from '@mantine/modals';
-import { useRef, useState } from 'react';
+import { useRef } from 'react';
import { useTranslation } from 'react-i18next';
-import { RiAddLine } from 'react-icons/ri';
import { useMetadataEditor } from '../hooks/use-metadata-editor';
-import { KNOWN_TAG_MAP, resolveTagKey } from '../utils/known-tags';
+import { AddFieldInput } from './add-field-input';
import { ArtworkPanel } from './artwork-panel';
import styles from './song-edit-modal.module.css';
import { TagEditorSettings } from './tag-editor-settings';
import { TagFieldRow } from './tag-field-row';
-import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
-import { Autocomplete } from '/@/shared/components/autocomplete/autocomplete';
import { Button } from '/@/shared/components/button/button';
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
import { Group } from '/@/shared/components/group/group';
@@ -25,10 +22,6 @@ import { Song } from '/@/shared/types/domain-types';
export const SongEditModal = ({ songs }: { songs: Song[] }) => {
const { t } = useTranslation();
const tableContainerRef = useRef(null);
- const skipNextEnterRef = useRef(false);
- const skipNextOnChangeResetRef = useRef(false);
- const [addFieldInput, setAddFieldInput] = useState('');
- const [duplicateAttempted, setDuplicateAttempted] = useState(false);
const editor = useMetadataEditor({
browser: window.api.browser,
@@ -36,52 +29,15 @@ export const SongEditModal = ({ songs }: { songs: Song[] }) => {
utils: window.api.utils,
});
- const trimmedInput = addFieldInput.trim();
- const customKeyError =
- trimmedInput && !KNOWN_TAG_MAP.has(trimmedInput)
- ? trimmedInput.includes('=')
- ? "Tag key cannot contain '='"
- : // eslint-disable-next-line no-control-regex
- /[^\x00-\x7F]/.test(trimmedInput)
- ? 'Tag key must use ASCII characters only'
- : null
- : null;
-
- const resolvedInputKey = trimmedInput ? resolveTagKey(trimmedInput) : '';
- const duplicateError =
- trimmedInput &&
- !customKeyError &&
- editor.sortedFieldEntries.some(([k]) => k === resolvedInputKey)
- ? 'Field already exists'
- : null;
- const fieldError = customKeyError ?? (duplicateAttempted ? duplicateError : null);
-
- const handleAddField = (key: string): boolean => {
- const trimmed = key.trim();
- if (!trimmed) return false;
- if (
- !KNOWN_TAG_MAP.has(trimmed) &&
- (trimmed.includes('=') || // eslint-disable-next-line no-control-regex
- /[^\x00-\x7F]/.test(trimmed))
- )
- return false;
- const normalizedKey = resolveTagKey(trimmed);
- if (editor.sortedFieldEntries.some(([k]) => k === normalizedKey)) {
- setDuplicateAttempted(true);
- return false;
- }
- setDuplicateAttempted(false);
- editor.handleAddField(normalizedKey);
- setAddFieldInput('');
-
+ const handleAddField = (key: string) => {
+ editor.handleAddField(key);
requestAnimationFrame(() => {
const row = tableContainerRef.current?.querySelector(
- `[data-field-key="${normalizedKey}"]`,
+ `[data-field-key="${key}"]`,
);
row?.scrollIntoView({ block: 'nearest' });
row?.querySelector('input, textarea')?.focus();
});
- return true;
};
// While loading, shows a spinner
@@ -125,46 +81,10 @@ export const SongEditModal = ({ songs }: { songs: Song[] }) => {
{editor.readWarning}
)}
- {
- setAddFieldInput(v);
- if (skipNextOnChangeResetRef.current) {
- skipNextOnChangeResetRef.current = false;
- } else {
- setDuplicateAttempted(false);
- }
- }}
- onKeyDown={(e) => {
- if (e.key === 'Enter') {
- if (skipNextEnterRef.current) {
- skipNextEnterRef.current = false;
- return;
- }
- handleAddField(addFieldInput);
- }
- }}
- onOptionSubmit={(value) => {
- skipNextEnterRef.current = true;
- skipNextOnChangeResetRef.current = true;
- const added = handleAddField(value);
- if (added) {
- queueMicrotask(() => setAddFieldInput(''));
- }
- }}
- placeholder={t('page.itemDetail.addField', 'Add field…')}
- rightSection={
- handleAddField(addFieldInput)}
- variant="filled"
- >
-
-
- }
- rightSectionPointerEvents="all"
- value={addFieldInput}
+ key)}
+ onAddField={handleAddField}
/>