feature 10,11
This commit is contained in:
@ -28,9 +28,9 @@ Features are grouped by priority. Check off items as they are implemented.
|
|||||||
|
|
||||||
## 🟡 Medium Priority — Widely expected features
|
## 🟡 Medium Priority — Widely expected features
|
||||||
|
|
||||||
- [ ] [#010] **Transcript search (Ctrl+F)** — find words/phrases in the transcript and highlight matches. Pure frontend. Critical for long-form content. Jump between matches with Enter.
|
- [x] [#010] **Transcript search (Ctrl+F)** — find words/phrases in the transcript and highlight matches. Pure frontend. Critical for long-form content. Jump between matches with Enter.
|
||||||
|
|
||||||
- [ ] [#011] **Mark In / Out + delete (I / O keys)** — keyboard shortcuts to mark a time range on the timeline, then delete it. Faster than click-dragging words. Store the in/out points in state, `Delete` removes them.
|
- [x] [#011] **Mark In / Out + delete (I / O keys)** — keyboard shortcuts to mark a time range on the timeline, then delete it. Faster than click-dragging words. Store the in/out points in state, `Delete` removes them.
|
||||||
|
|
||||||
- [ ] [#012] **Low-confidence word highlighting** — WhisperX already returns `confidence` per word. Words below a threshold (e.g. < 0.6) should be visually underlined or tinted so the user knows where to double-check.
|
- [ ] [#012] **Low-confidence word highlighting** — WhisperX already returns `confidence` per word. Words below a threshold (e.g. < 0.6) should be visually underlined or tinted so the user knows where to double-check.
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useRef, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useRef, useEffect, useMemo, useState } from 'react';
|
||||||
import { useEditorStore } from '../store/editorStore';
|
import { useEditorStore } from '../store/editorStore';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
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 {
|
interface TranscriptEditorProps {
|
||||||
cutMode: boolean;
|
cutMode: boolean;
|
||||||
@ -45,8 +45,72 @@ export default function TranscriptEditor({
|
|||||||
const virtuosoRef = useRef<any>(null);
|
const virtuosoRef = useRef<any>(null);
|
||||||
const zoneDragStart = useRef<number | null>(null);
|
const zoneDragStart = useRef<number | null>(null);
|
||||||
const [zoneDragRange, setZoneDragRange] = useState<{ start: number; end: 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 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);
|
const [activeWordIndex, setActiveWordIndex] = useState(-1);
|
||||||
|
|
||||||
@ -252,6 +316,8 @@ export default function TranscriptEditor({
|
|||||||
const muteRange = getMuteRangeForWord(globalIndex);
|
const muteRange = getMuteRangeForWord(globalIndex);
|
||||||
const gainRange = getGainRangeForWord(globalIndex);
|
const gainRange = getGainRangeForWord(globalIndex);
|
||||||
const speedRange = getSpeedRangeForWord(globalIndex);
|
const speedRange = getSpeedRangeForWord(globalIndex);
|
||||||
|
const isSearchMatch = matchSet.has(globalIndex);
|
||||||
|
const isActiveSearchMatch = matchIndices.length > 0 && matchIndices[safeActiveMatchIdx] === globalIndex;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@ -272,6 +338,8 @@ export default function TranscriptEditor({
|
|||||||
${isZoneDragSelected && muteMode ? 'bg-blue-500/30 ring-1 ring-blue-400/60' : ''}
|
${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 && gainMode ? 'bg-amber-500/30 ring-1 ring-amber-400/60' : ''}
|
||||||
${isZoneDragSelected && speedMode ? 'bg-emerald-500/30 ring-1 ring-emerald-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' : ''}
|
${isSelected && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-word-selected text-white' : ''}
|
||||||
${isActive && !isSelected && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-accent/20 text-editor-accent' : ''}
|
${isActive && !isSelected && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-accent/20 text-editor-accent' : ''}
|
||||||
${isHovered && !isSelected && !isActive && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-word-hover' : ''}
|
${isHovered && !isSelected && !isActive && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-word-hover' : ''}
|
||||||
@ -299,12 +367,63 @@ export default function TranscriptEditor({
|
|||||||
</div>
|
</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 (
|
return (
|
||||||
<div className="flex-1 flex flex-col min-h-0">
|
<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 && (
|
{selectedWordIndices.length > 0 && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -243,6 +243,8 @@ export default function WaveformTimeline({
|
|||||||
const muteRanges = useEditorStore((s) => s.muteRanges);
|
const muteRanges = useEditorStore((s) => s.muteRanges);
|
||||||
const gainRanges = useEditorStore((s) => s.gainRanges);
|
const gainRanges = useEditorStore((s) => s.gainRanges);
|
||||||
const speedRanges = useEditorStore((s) => s.speedRanges);
|
const speedRanges = useEditorStore((s) => s.speedRanges);
|
||||||
|
const markInTime = useEditorStore((s) => s.markInTime);
|
||||||
|
const markOutTime = useEditorStore((s) => s.markOutTime);
|
||||||
const setCurrentTime = useEditorStore((s) => s.setCurrentTime);
|
const setCurrentTime = useEditorStore((s) => s.setCurrentTime);
|
||||||
const addCutRange = useEditorStore((s) => s.addCutRange);
|
const addCutRange = useEditorStore((s) => s.addCutRange);
|
||||||
const addMuteRange = useEditorStore((s) => s.addMuteRange);
|
const addMuteRange = useEditorStore((s) => s.addMuteRange);
|
||||||
@ -570,6 +572,33 @@ export default function WaveformTimeline({
|
|||||||
ctx.strokeRect(x1, waveTop, x2 - x1, waveH);
|
ctx.strokeRect(x1, waveTop, x2 - x1, waveH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw mark in/out range and markers
|
||||||
|
if (markInTime !== null && markOutTime !== null) {
|
||||||
|
const x1 = (sourceToDisplayTime(Math.min(markInTime, markOutTime), timelineSegments, dur) - scroll) * pxPerSec;
|
||||||
|
const x2 = (sourceToDisplayTime(Math.max(markInTime, markOutTime), timelineSegments, dur) - scroll) * pxPerSec;
|
||||||
|
ctx.fillStyle = 'rgba(250, 204, 21, 0.14)';
|
||||||
|
ctx.fillRect(x1, waveTop, x2 - x1, waveH);
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawMarkLine = (time: number, label: string) => {
|
||||||
|
const x = (sourceToDisplayTime(time, timelineSegments, dur) - scroll) * pxPerSec;
|
||||||
|
if (x < -4 || x > width + 4) return;
|
||||||
|
ctx.strokeStyle = '#facc15';
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, 0);
|
||||||
|
ctx.lineTo(x, height);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.fillStyle = '#facc15';
|
||||||
|
ctx.font = '9px sans-serif';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
ctx.fillText(label, Math.min(width - 12, Math.max(2, x + 2)), 2);
|
||||||
|
ctx.textBaseline = 'alphabetic';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (markInTime !== null) drawMarkLine(markInTime, 'I');
|
||||||
|
if (markOutTime !== null) drawMarkLine(markOutTime, 'O');
|
||||||
|
|
||||||
const mid = waveTop + waveH / 2;
|
const mid = waveTop + waveH / 2;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.strokeStyle = '#4a4d5e';
|
ctx.strokeStyle = '#4a4d5e';
|
||||||
@ -605,6 +634,8 @@ export default function WaveformTimeline({
|
|||||||
gainMode,
|
gainMode,
|
||||||
speedMode,
|
speedMode,
|
||||||
selectedZone,
|
selectedZone,
|
||||||
|
markInTime,
|
||||||
|
markOutTime,
|
||||||
displayDuration,
|
displayDuration,
|
||||||
showCutZones,
|
showCutZones,
|
||||||
showMuteZones,
|
showMuteZones,
|
||||||
@ -1149,6 +1180,8 @@ export default function WaveformTimeline({
|
|||||||
{muteMode && <span className="text-[10px] text-blue-400">Mute mode</span>}
|
{muteMode && <span className="text-[10px] text-blue-400">Mute mode</span>}
|
||||||
{gainMode && <span className="text-[10px] text-amber-400">Gain mode ({gainModeDb.toFixed(1)} dB)</span>}
|
{gainMode && <span className="text-[10px] text-amber-400">Gain mode ({gainModeDb.toFixed(1)} dB)</span>}
|
||||||
{speedMode && <span className="text-[10px] text-emerald-400">Speed mode ({speedModeValue.toFixed(2)}x)</span>}
|
{speedMode && <span className="text-[10px] text-emerald-400">Speed mode ({speedModeValue.toFixed(2)}x)</span>}
|
||||||
|
{markInTime !== null && <span className="text-[10px] text-yellow-300">I {markInTime.toFixed(2)}s</span>}
|
||||||
|
{markOutTime !== null && <span className="text-[10px] text-yellow-300">O {markOutTime.toFixed(2)}s</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -3,6 +3,11 @@ import { useEditorStore } from '../store/editorStore';
|
|||||||
|
|
||||||
export function useKeyboardShortcuts() {
|
export function useKeyboardShortcuts() {
|
||||||
const addCutRange = useEditorStore((s) => s.addCutRange);
|
const addCutRange = useEditorStore((s) => s.addCutRange);
|
||||||
|
const markInTime = useEditorStore((s) => s.markInTime);
|
||||||
|
const markOutTime = useEditorStore((s) => s.markOutTime);
|
||||||
|
const setMarkInTime = useEditorStore((s) => s.setMarkInTime);
|
||||||
|
const setMarkOutTime = useEditorStore((s) => s.setMarkOutTime);
|
||||||
|
const clearMarkRange = useEditorStore((s) => s.clearMarkRange);
|
||||||
const selectedWordIndices = useEditorStore((s) => s.selectedWordIndices);
|
const selectedWordIndices = useEditorStore((s) => s.selectedWordIndices);
|
||||||
const words = useEditorStore((s) => s.words);
|
const words = useEditorStore((s) => s.words);
|
||||||
|
|
||||||
@ -38,6 +43,17 @@ export function useKeyboardShortcuts() {
|
|||||||
const startTime = words[sorted[0]].start;
|
const startTime = words[sorted[0]].start;
|
||||||
const endTime = words[sorted[sorted.length - 1]].end;
|
const endTime = words[sorted[sorted.length - 1]].end;
|
||||||
addCutRange(startTime, endTime);
|
addCutRange(startTime, endTime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markInTime !== null && markOutTime !== null) {
|
||||||
|
e.preventDefault();
|
||||||
|
const start = Math.min(markInTime, markOutTime);
|
||||||
|
const end = Math.max(markInTime, markOutTime);
|
||||||
|
if (end - start >= 0.01) {
|
||||||
|
addCutRange(start, end);
|
||||||
|
}
|
||||||
|
clearMarkRange();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -103,17 +119,17 @@ export function useKeyboardShortcuts() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- [ mark in-point (home) ---
|
// --- I: mark in-point ---
|
||||||
case e.key === '[': {
|
case e.key === 'i' || e.key === 'I': {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (video) video.currentTime = 0;
|
if (video) setMarkInTime(video.currentTime);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- ] mark out-point (end) ---
|
// --- O: mark out-point ---
|
||||||
case e.key === ']': {
|
case e.key === 'o' || e.key === 'O': {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (video) video.currentTime = video.duration;
|
if (video) setMarkOutTime(video.currentTime);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +165,7 @@ export function useKeyboardShortcuts() {
|
|||||||
|
|
||||||
window.addEventListener('keydown', handler);
|
window.addEventListener('keydown', handler);
|
||||||
return () => window.removeEventListener('keydown', handler);
|
return () => window.removeEventListener('keydown', handler);
|
||||||
}, [addCutRange, selectedWordIndices, words]);
|
}, [addCutRange, markInTime, markOutTime, setMarkInTime, setMarkOutTime, clearMarkRange, selectedWordIndices, words]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveProject() {
|
async function saveProject() {
|
||||||
@ -210,6 +226,7 @@ function toggleCheatsheet() {
|
|||||||
['K', 'Pause'],
|
['K', 'Pause'],
|
||||||
['L', 'Forward / Speed up'],
|
['L', 'Forward / Speed up'],
|
||||||
['\u2190 / \u2192', 'Seek \u00b15 seconds'],
|
['\u2190 / \u2192', 'Seek \u00b15 seconds'],
|
||||||
|
['I / O', 'Mark in / out points'],
|
||||||
['Delete', 'Cut selected words'],
|
['Delete', 'Cut selected words'],
|
||||||
['Ctrl+Z', 'Undo'],
|
['Ctrl+Z', 'Undo'],
|
||||||
['Ctrl+Shift+Z', 'Redo'],
|
['Ctrl+Shift+Z', 'Redo'],
|
||||||
|
|||||||
@ -33,6 +33,8 @@ interface EditorState {
|
|||||||
currentTime: number;
|
currentTime: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
|
markInTime: number | null;
|
||||||
|
markOutTime: number | null;
|
||||||
|
|
||||||
selectedWordIndices: number[];
|
selectedWordIndices: number[];
|
||||||
hoveredWordIndex: number | null;
|
hoveredWordIndex: number | null;
|
||||||
@ -58,6 +60,9 @@ interface EditorActions {
|
|||||||
setCurrentTime: (time: number) => void;
|
setCurrentTime: (time: number) => void;
|
||||||
setDuration: (duration: number) => void;
|
setDuration: (duration: number) => void;
|
||||||
setIsPlaying: (playing: boolean) => void;
|
setIsPlaying: (playing: boolean) => void;
|
||||||
|
setMarkInTime: (time: number | null) => void;
|
||||||
|
setMarkOutTime: (time: number | null) => void;
|
||||||
|
clearMarkRange: () => void;
|
||||||
setSelectedWordIndices: (indices: number[]) => void;
|
setSelectedWordIndices: (indices: number[]) => void;
|
||||||
setHoveredWordIndex: (index: number | null) => void;
|
setHoveredWordIndex: (index: number | null) => void;
|
||||||
deleteSelectedWords: () => void;
|
deleteSelectedWords: () => void;
|
||||||
@ -120,6 +125,8 @@ const initialState: EditorState = {
|
|||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
|
markInTime: null,
|
||||||
|
markOutTime: null,
|
||||||
selectedWordIndices: [],
|
selectedWordIndices: [],
|
||||||
hoveredWordIndex: null,
|
hoveredWordIndex: null,
|
||||||
isTranscribing: false,
|
isTranscribing: false,
|
||||||
@ -232,6 +239,9 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
|||||||
setCurrentTime: (time) => set({ currentTime: time }),
|
setCurrentTime: (time) => set({ currentTime: time }),
|
||||||
setDuration: (duration) => set({ duration }),
|
setDuration: (duration) => set({ duration }),
|
||||||
setIsPlaying: (playing) => set({ isPlaying: playing }),
|
setIsPlaying: (playing) => set({ isPlaying: playing }),
|
||||||
|
setMarkInTime: (time) => set({ markInTime: time }),
|
||||||
|
setMarkOutTime: (time) => set({ markOutTime: time }),
|
||||||
|
clearMarkRange: () => set({ markInTime: null, markOutTime: null }),
|
||||||
setSelectedWordIndices: (indices) => set({ selectedWordIndices: indices }),
|
setSelectedWordIndices: (indices) => set({ selectedWordIndices: indices }),
|
||||||
setHoveredWordIndex: (index) => set({ hoveredWordIndex: index }),
|
setHoveredWordIndex: (index) => set({ hoveredWordIndex: index }),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user