feature 10,11
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
import { useCallback, useRef, useEffect, useMemo, useState } from 'react';
|
||||
import { useEditorStore } from '../store/editorStore';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { Scissors, VolumeX, SlidersHorizontal, Gauge, RotateCcw } from 'lucide-react';
|
||||
import { Scissors, VolumeX, SlidersHorizontal, Gauge, RotateCcw, Search, ChevronUp, ChevronDown, X } from 'lucide-react';
|
||||
|
||||
interface TranscriptEditorProps {
|
||||
cutMode: boolean;
|
||||
@ -45,8 +45,72 @@ export default function TranscriptEditor({
|
||||
const virtuosoRef = useRef<any>(null);
|
||||
const zoneDragStart = useRef<number | null>(null);
|
||||
const [zoneDragRange, setZoneDragRange] = useState<{ start: number; end: number } | null>(null);
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [activeMatchIdx, setActiveMatchIdx] = useState(0);
|
||||
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const selectedSet = useMemo(() => new Set(selectedWordIndices), [selectedWordIndices]);
|
||||
const matchIndices = useMemo(() => {
|
||||
const q = searchQuery.trim().toLowerCase();
|
||||
if (!q) return [] as number[];
|
||||
const matches: number[] = [];
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
if (words[i].word.toLowerCase().includes(q)) matches.push(i);
|
||||
}
|
||||
return matches;
|
||||
}, [searchQuery, words]);
|
||||
const matchSet = useMemo(() => new Set(matchIndices), [matchIndices]);
|
||||
const safeActiveMatchIdx = matchIndices.length === 0
|
||||
? 0
|
||||
: Math.min(activeMatchIdx, matchIndices.length - 1);
|
||||
|
||||
const jumpToMatch = useCallback((idx: number) => {
|
||||
if (matchIndices.length === 0) return;
|
||||
const nextIdx = ((idx % matchIndices.length) + matchIndices.length) % matchIndices.length;
|
||||
setActiveMatchIdx(nextIdx);
|
||||
const wordIndex = matchIndices[nextIdx];
|
||||
const el = document.getElementById(`word-${wordIndex}`);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
||||
}
|
||||
}, [matchIndices]);
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
const isInInput = !!target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT');
|
||||
|
||||
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'f') {
|
||||
e.preventDefault();
|
||||
setSearchOpen(true);
|
||||
requestAnimationFrame(() => searchInputRef.current?.focus());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!searchOpen) return;
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
setSearchOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && !isInInput) {
|
||||
e.preventDefault();
|
||||
jumpToMatch(safeActiveMatchIdx + (e.shiftKey ? -1 : 1));
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Enter' && isInInput && target === searchInputRef.current) {
|
||||
e.preventDefault();
|
||||
jumpToMatch(safeActiveMatchIdx + (e.shiftKey ? -1 : 1));
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [jumpToMatch, searchOpen, safeActiveMatchIdx]);
|
||||
|
||||
const [activeWordIndex, setActiveWordIndex] = useState(-1);
|
||||
|
||||
@ -252,6 +316,8 @@ export default function TranscriptEditor({
|
||||
const muteRange = getMuteRangeForWord(globalIndex);
|
||||
const gainRange = getGainRangeForWord(globalIndex);
|
||||
const speedRange = getSpeedRangeForWord(globalIndex);
|
||||
const isSearchMatch = matchSet.has(globalIndex);
|
||||
const isActiveSearchMatch = matchIndices.length > 0 && matchIndices[safeActiveMatchIdx] === globalIndex;
|
||||
|
||||
return (
|
||||
<span
|
||||
@ -272,6 +338,8 @@ export default function TranscriptEditor({
|
||||
${isZoneDragSelected && muteMode ? 'bg-blue-500/30 ring-1 ring-blue-400/60' : ''}
|
||||
${isZoneDragSelected && gainMode ? 'bg-amber-500/30 ring-1 ring-amber-400/60' : ''}
|
||||
${isZoneDragSelected && speedMode ? 'bg-emerald-500/30 ring-1 ring-emerald-400/60' : ''}
|
||||
${isSearchMatch && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-accent/15 ring-1 ring-editor-accent/35' : ''}
|
||||
${isActiveSearchMatch && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-accent/35 ring-1 ring-editor-accent text-white' : ''}
|
||||
${isSelected && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-word-selected text-white' : ''}
|
||||
${isActive && !isSelected && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-accent/20 text-editor-accent' : ''}
|
||||
${isHovered && !isSelected && !isActive && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-word-hover' : ''}
|
||||
@ -299,12 +367,63 @@ export default function TranscriptEditor({
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[segments, selectedSet, activeWordIndex, hoveredWordIndex, handleWordMouseDown, handleWordMouseEnter, setHoveredWordIndex, getCutRangeForWord, getMuteRangeForWord, getGainRangeForWord, getSpeedRangeForWord, removeCutRange, removeMuteRange, removeGainRange, removeSpeedRange, zoneDragRange, cutMode, muteMode, gainMode, speedMode],
|
||||
[segments, selectedSet, matchSet, matchIndices, safeActiveMatchIdx, activeWordIndex, hoveredWordIndex, handleWordMouseDown, handleWordMouseEnter, setHoveredWordIndex, getCutRangeForWord, getMuteRangeForWord, getGainRangeForWord, getSpeedRangeForWord, removeCutRange, removeMuteRange, removeGainRange, removeSpeedRange, zoneDragRange, cutMode, muteMode, gainMode, speedMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex items-center justify-end gap-2 px-4 py-2 border-b border-editor-border shrink-0">
|
||||
<div className="flex items-center justify-between gap-2 px-4 py-2 border-b border-editor-border shrink-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchOpen(true);
|
||||
requestAnimationFrame(() => searchInputRef.current?.focus());
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-editor-text-muted hover:text-editor-text hover:bg-editor-surface rounded"
|
||||
title="Find (Ctrl+F)"
|
||||
>
|
||||
<Search className="w-3 h-3" />
|
||||
Find
|
||||
</button>
|
||||
{searchOpen && (
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 rounded border border-editor-border bg-editor-surface">
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
setActiveMatchIdx(0);
|
||||
}}
|
||||
placeholder="Search transcript"
|
||||
className="w-40 bg-transparent text-xs text-editor-text focus:outline-none"
|
||||
/>
|
||||
<span className="text-[10px] text-editor-text-muted min-w-[52px] text-right">
|
||||
{matchIndices.length === 0 ? '0/0' : `${safeActiveMatchIdx + 1}/${matchIndices.length}`}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => jumpToMatch(safeActiveMatchIdx - 1)}
|
||||
className="p-0.5 rounded hover:bg-editor-bg text-editor-text-muted hover:text-editor-text"
|
||||
title="Previous match (Shift+Enter)"
|
||||
>
|
||||
<ChevronUp className="w-3 h-3" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => jumpToMatch(safeActiveMatchIdx + 1)}
|
||||
className="p-0.5 rounded hover:bg-editor-bg text-editor-text-muted hover:text-editor-text"
|
||||
title="Next match (Enter)"
|
||||
>
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSearchOpen(false)}
|
||||
className="p-0.5 rounded hover:bg-editor-bg text-editor-text-muted hover:text-editor-text"
|
||||
title="Close search (Esc)"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{selectedWordIndices.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user