improved zone handling
This commit is contained in:
@ -108,6 +108,34 @@ Must-have plan is complete when all are true:
|
|||||||
2. Feature PRs without spec updates are blocked.
|
2. Feature PRs without spec updates are blocked.
|
||||||
3. Backend router contracts cover core success and error paths.
|
3. Backend router contracts cover core success and error paths.
|
||||||
4. Frontend has at least one stable test command integrated into validation.
|
4. Frontend has at least one stable test command integrated into validation.
|
||||||
|
|
||||||
|
## 7. AI Tools Validation Strategy
|
||||||
|
|
||||||
|
Required:
|
||||||
|
|
||||||
|
1. **Per-edit validation**: After each code change (file edit, replacement, or creation), validate immediately with appropriate tools.
|
||||||
|
2. **Tool selection by change type**:
|
||||||
|
- Frontend changes: ESLint (`npm run -s lint`), then TypeScript build (`npm run build`)
|
||||||
|
- Backend changes: Syntax check via Python import, then run relevant test suite
|
||||||
|
- Type/interface changes: Full type check via build or `tsc -b`
|
||||||
|
3. **Failure handling**: If validation fails, fix immediately before proceeding to next edit.
|
||||||
|
4. **Documentation updates**: When changing architecture, always update [.github/copilot-instructions.md](.github/copilot-instructions.md) as part of the same PR.
|
||||||
|
5. **Large multi-edit operations**: Use `multi_replace_string_in_file` to batch independent edits and reduce tool call overhead.
|
||||||
|
6. **Error collection**: Use `get_errors` tool to identify issues across multiple files in one call post-change.
|
||||||
|
|
||||||
|
Current implementation:
|
||||||
|
|
||||||
|
1. Electron removal completed with post-edit lint and build validation at each phase.
|
||||||
|
2. Zone editor feature implemented with immediate lint/build validation after component creation and UI integration.
|
||||||
|
3. Validation tools: `npm run -s lint`, `npm run build`, `get_errors`, `run_in_terminal` for test scripts.
|
||||||
|
|
||||||
|
Best practices established:
|
||||||
|
|
||||||
|
- Always run lint before build to catch TypeScript errors early
|
||||||
|
- Run full build after component changes to verify tree-shaking and bundling
|
||||||
|
- Use `get_errors` for multi-file error detection rather than sequential file reads
|
||||||
|
- Batch unrelated edits with `multi_replace_string_in_file` for efficiency
|
||||||
|
- Cache key decisions in session memory to avoid repeated exploration
|
||||||
5. AI policy + diagnostics workflow are active.
|
5. AI policy + diagnostics workflow are active.
|
||||||
|
|
||||||
## Current State Summary
|
## Current State Summary
|
||||||
|
|||||||
@ -44,6 +44,8 @@ Features are grouped by priority. Check off items as they are implemented.
|
|||||||
|
|
||||||
- [ ] [#017] **Chapters** — group markers into named chapter ranges. Useful for podcasts and lectures. Exportable as YouTube chapter timestamps in the description.
|
- [ ] [#017] **Chapters** — group markers into named chapter ranges. Useful for podcasts and lectures. Exportable as YouTube chapter timestamps in the description.
|
||||||
|
|
||||||
|
- [ ] [#041] **Customizable hotkeys / keymap editor (left-hand focused)** — allow users to view, remap, and reset keyboard shortcuts (transport, edit, save/export, zone tools), with a default preset optimized for left-hand reach (Q/W/E/R/A/S/D/F/Z/X/C/V + modifiers). Include conflict detection, an alternate standard preset, and one-click "restore defaults".
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🟢 Lower Priority — Differentiating / power features
|
## 🟢 Lower Priority — Differentiating / power features
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import ExportDialog from './components/ExportDialog';
|
|||||||
import SettingsPanel from './components/SettingsPanel';
|
import SettingsPanel from './components/SettingsPanel';
|
||||||
import DevPanel from './components/DevPanel';
|
import DevPanel from './components/DevPanel';
|
||||||
import SilenceTrimmerPanel from './components/SilenceTrimmerPanel';
|
import SilenceTrimmerPanel from './components/SilenceTrimmerPanel';
|
||||||
import VolumePanel from './components/VolumePanel';
|
import ZoneEditor from './components/ZoneEditor';
|
||||||
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
|
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
|
||||||
import {
|
import {
|
||||||
Film,
|
Film,
|
||||||
@ -20,15 +20,15 @@ import {
|
|||||||
Save,
|
Save,
|
||||||
Scissors,
|
Scissors,
|
||||||
VolumeX,
|
VolumeX,
|
||||||
Volume2,
|
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
FilePlus2,
|
FilePlus2,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
|
Grid3x3,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const LAST_MEDIA_PATH_KEY = 'talkedit:lastMediaPath';
|
const LAST_MEDIA_PATH_KEY = 'talkedit:lastMediaPath';
|
||||||
|
|
||||||
type Panel = 'ai' | 'settings' | 'export' | 'silence' | 'volume' | null;
|
type Panel = 'ai' | 'settings' | 'export' | 'silence' | 'zones' | null;
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const {
|
const {
|
||||||
@ -459,17 +459,29 @@ export default function App() {
|
|||||||
onClick={handleMute}
|
onClick={handleMute}
|
||||||
active={muteMode}
|
active={muteMode}
|
||||||
/>
|
/>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<ToolbarButton
|
||||||
|
icon={<SlidersHorizontal className="w-4 h-4" />}
|
||||||
|
label="Gain Zone"
|
||||||
|
onClick={handleGain}
|
||||||
|
active={gainMode}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={-24}
|
||||||
|
max={24}
|
||||||
|
step={0.5}
|
||||||
|
value={gainModeDb}
|
||||||
|
onChange={(e) => setGainModeDb(Math.max(-24, Math.min(24, Number(e.target.value) || 0)))}
|
||||||
|
className="w-16 px-1.5 py-1 text-xs bg-editor-surface border border-editor-border rounded text-editor-text focus:outline-none focus:border-editor-accent"
|
||||||
|
title="Gain dB for new gain zones"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={<SlidersHorizontal className="w-4 h-4" />}
|
icon={<Grid3x3 className="w-4 h-4" />}
|
||||||
label="Gain Zone"
|
label="Zones"
|
||||||
onClick={handleGain}
|
active={activePanel === 'zones'}
|
||||||
active={gainMode}
|
onClick={() => togglePanel('zones')}
|
||||||
/>
|
|
||||||
<ToolbarButton
|
|
||||||
icon={<Volume2 className="w-4 h-4" />}
|
|
||||||
label="Volume"
|
|
||||||
active={activePanel === 'volume'}
|
|
||||||
onClick={() => togglePanel('volume')}
|
|
||||||
disabled={!videoPath}
|
disabled={!videoPath}
|
||||||
/>
|
/>
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
@ -571,7 +583,12 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : words.length > 0 ? (
|
) : words.length > 0 ? (
|
||||||
<TranscriptEditor />
|
<TranscriptEditor
|
||||||
|
cutMode={cutMode}
|
||||||
|
muteMode={muteMode}
|
||||||
|
gainMode={gainMode}
|
||||||
|
gainModeDb={gainModeDb}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 flex items-center justify-center text-editor-text-muted text-sm">
|
<div className="flex-1 flex items-center justify-center text-editor-text-muted text-sm">
|
||||||
No transcript yet
|
No transcript yet
|
||||||
@ -589,13 +606,8 @@ export default function App() {
|
|||||||
{/* Right panel (AI / Export / Settings) */}
|
{/* Right panel (AI / Export / Settings) */}
|
||||||
{activePanel && (
|
{activePanel && (
|
||||||
<div className="w-80 border-l border-editor-border overflow-y-auto shrink-0">
|
<div className="w-80 border-l border-editor-border overflow-y-auto shrink-0">
|
||||||
{activePanel === 'volume' && (
|
{activePanel === 'zones' && (
|
||||||
<VolumePanel
|
<ZoneEditor />
|
||||||
gainMode={gainMode}
|
|
||||||
onToggleGainMode={handleGain}
|
|
||||||
timelineGainDb={gainModeDb}
|
|
||||||
onTimelineGainDbChange={setGainModeDb}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{activePanel === 'silence' && <SilenceTrimmerPanel />}
|
{activePanel === 'silence' && <SilenceTrimmerPanel />}
|
||||||
{activePanel === 'ai' && <AIPanel />}
|
{activePanel === 'ai' && <AIPanel />}
|
||||||
|
|||||||
@ -3,7 +3,14 @@ import { useEditorStore } from '../store/editorStore';
|
|||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import { Scissors, VolumeX, SlidersHorizontal, RotateCcw } from 'lucide-react';
|
import { Scissors, VolumeX, SlidersHorizontal, RotateCcw } from 'lucide-react';
|
||||||
|
|
||||||
export default function TranscriptEditor() {
|
interface TranscriptEditorProps {
|
||||||
|
cutMode: boolean;
|
||||||
|
muteMode: boolean;
|
||||||
|
gainMode: boolean;
|
||||||
|
gainModeDb: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TranscriptEditor({ cutMode, muteMode, gainMode, gainModeDb }: TranscriptEditorProps) {
|
||||||
const words = useEditorStore((s) => s.words);
|
const words = useEditorStore((s) => s.words);
|
||||||
const segments = useEditorStore((s) => s.segments);
|
const segments = useEditorStore((s) => s.segments);
|
||||||
const deletedRanges = useEditorStore((s) => s.deletedRanges);
|
const deletedRanges = useEditorStore((s) => s.deletedRanges);
|
||||||
@ -26,6 +33,8 @@ export default function TranscriptEditor() {
|
|||||||
const selectionStart = useRef<number | null>(null);
|
const selectionStart = useRef<number | null>(null);
|
||||||
const wasDragging = useRef(false);
|
const wasDragging = useRef(false);
|
||||||
const virtuosoRef = useRef<any>(null);
|
const virtuosoRef = useRef<any>(null);
|
||||||
|
const zoneDragStart = useRef<number | null>(null);
|
||||||
|
const [zoneDragRange, setZoneDragRange] = useState<{ start: number; end: number } | null>(null);
|
||||||
|
|
||||||
const deletedSet = useMemo(() => {
|
const deletedSet = useMemo(() => {
|
||||||
const s = new Set<number>();
|
const s = new Set<number>();
|
||||||
@ -74,6 +83,14 @@ export default function TranscriptEditor() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cutMode || muteMode || gainMode) {
|
||||||
|
zoneDragStart.current = index;
|
||||||
|
setZoneDragRange({ start: index, end: index });
|
||||||
|
selectionStart.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
wasDragging.current = false;
|
wasDragging.current = false;
|
||||||
if (e.shiftKey && selectedWordIndices.length > 0) {
|
if (e.shiftKey && selectedWordIndices.length > 0) {
|
||||||
const first = selectedWordIndices[0];
|
const first = selectedWordIndices[0];
|
||||||
@ -87,12 +104,19 @@ export default function TranscriptEditor() {
|
|||||||
setSelectedWordIndices([index]);
|
setSelectedWordIndices([index]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[words, selectedWordIndices, setSelectedWordIndices],
|
[words, selectedWordIndices, setSelectedWordIndices, cutMode, muteMode, gainMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleWordMouseEnter = useCallback(
|
const handleWordMouseEnter = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
setHoveredWordIndex(index);
|
setHoveredWordIndex(index);
|
||||||
|
if (zoneDragStart.current !== null) {
|
||||||
|
setZoneDragRange({
|
||||||
|
start: Math.min(zoneDragStart.current, index),
|
||||||
|
end: Math.max(zoneDragStart.current, index),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (selectionStart.current !== null) {
|
if (selectionStart.current !== null) {
|
||||||
wasDragging.current = true;
|
wasDragging.current = true;
|
||||||
const start = Math.min(selectionStart.current, index);
|
const start = Math.min(selectionStart.current, index);
|
||||||
@ -106,8 +130,19 @@ export default function TranscriptEditor() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleMouseUp = useCallback(() => {
|
const handleMouseUp = useCallback(() => {
|
||||||
|
if (zoneDragStart.current !== null && zoneDragRange) {
|
||||||
|
const startWord = words[zoneDragRange.start];
|
||||||
|
const endWord = words[zoneDragRange.end];
|
||||||
|
if (startWord && endWord) {
|
||||||
|
if (cutMode) addCutRange(startWord.start, endWord.end);
|
||||||
|
if (muteMode) addMuteRange(startWord.start, endWord.end);
|
||||||
|
if (gainMode) addGainRange(startWord.start, endWord.end, gainModeDb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zoneDragStart.current = null;
|
||||||
|
setZoneDragRange(null);
|
||||||
selectionStart.current = null;
|
selectionStart.current = null;
|
||||||
}, []);
|
}, [zoneDragRange, words, cutMode, muteMode, gainMode, gainModeDb, addCutRange, addMuteRange, addGainRange]);
|
||||||
|
|
||||||
const handleClickOutside = useCallback(
|
const handleClickOutside = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
@ -148,8 +183,8 @@ export default function TranscriptEditor() {
|
|||||||
const sorted = [...selectedWordIndices].sort((a, b) => a - b);
|
const sorted = [...selectedWordIndices].sort((a, b) => a - b);
|
||||||
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;
|
||||||
addGainRange(startTime, endTime, 3);
|
addGainRange(startTime, endTime, gainModeDb);
|
||||||
}, [selectedWordIndices, words, addGainRange]);
|
}, [selectedWordIndices, words, addGainRange, gainModeDb]);
|
||||||
|
|
||||||
const getCutRangeForWord = useCallback(
|
const getCutRangeForWord = useCallback(
|
||||||
(wordIndex: number) => {
|
(wordIndex: number) => {
|
||||||
@ -196,6 +231,9 @@ export default function TranscriptEditor() {
|
|||||||
const isSelected = selectedSet.has(globalIndex);
|
const isSelected = selectedSet.has(globalIndex);
|
||||||
const isActive = globalIndex === activeWordIndex;
|
const isActive = globalIndex === activeWordIndex;
|
||||||
const isHovered = globalIndex === hoveredWordIndex;
|
const isHovered = globalIndex === hoveredWordIndex;
|
||||||
|
const isZoneDragSelected = zoneDragRange
|
||||||
|
? globalIndex >= zoneDragRange.start && globalIndex <= zoneDragRange.end
|
||||||
|
: false;
|
||||||
const deletedRange = isDeleted ? getRangeForWord(globalIndex) : null;
|
const deletedRange = isDeleted ? getRangeForWord(globalIndex) : null;
|
||||||
const cutRange = getCutRangeForWord(globalIndex);
|
const cutRange = getCutRangeForWord(globalIndex);
|
||||||
const muteRange = getMuteRangeForWord(globalIndex);
|
const muteRange = getMuteRangeForWord(globalIndex);
|
||||||
@ -216,6 +254,9 @@ export default function TranscriptEditor() {
|
|||||||
${cutRange ? 'bg-red-500/20 text-red-100' : ''}
|
${cutRange ? 'bg-red-500/20 text-red-100' : ''}
|
||||||
${muteRange ? 'bg-blue-500/20 text-blue-100' : ''}
|
${muteRange ? 'bg-blue-500/20 text-blue-100' : ''}
|
||||||
${gainRange ? 'bg-amber-500/20 text-amber-100' : ''}
|
${gainRange ? 'bg-amber-500/20 text-amber-100' : ''}
|
||||||
|
${isZoneDragSelected && cutMode ? 'bg-red-500/30 ring-1 ring-red-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' : ''}
|
||||||
${isSelected && !isDeleted && !cutRange && !muteRange && !gainRange ? 'bg-editor-word-selected text-white' : ''}
|
${isSelected && !isDeleted && !cutRange && !muteRange && !gainRange ? 'bg-editor-word-selected text-white' : ''}
|
||||||
${isActive && !isDeleted && !isSelected && !cutRange && !muteRange && !gainRange ? 'bg-editor-accent/20 text-editor-accent' : ''}
|
${isActive && !isDeleted && !isSelected && !cutRange && !muteRange && !gainRange ? 'bg-editor-accent/20 text-editor-accent' : ''}
|
||||||
${isHovered && !isDeleted && !isSelected && !isActive && !cutRange && !muteRange && !gainRange ? 'bg-editor-word-hover' : ''}
|
${isHovered && !isDeleted && !isSelected && !isActive && !cutRange && !muteRange && !gainRange ? 'bg-editor-word-hover' : ''}
|
||||||
@ -253,7 +294,7 @@ export default function TranscriptEditor() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[segments, deletedSet, selectedSet, activeWordIndex, hoveredWordIndex, handleWordMouseDown, handleWordMouseEnter, setHoveredWordIndex, getRangeForWord, getCutRangeForWord, getMuteRangeForWord, getGainRangeForWord, restoreRange, removeCutRange, removeMuteRange, removeGainRange],
|
[segments, deletedSet, selectedSet, activeWordIndex, hoveredWordIndex, handleWordMouseDown, handleWordMouseEnter, setHoveredWordIndex, getRangeForWord, getCutRangeForWord, getMuteRangeForWord, getGainRangeForWord, restoreRange, removeCutRange, removeMuteRange, removeGainRange, zoneDragRange, cutMode, muteMode, gainMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -283,7 +324,7 @@ export default function TranscriptEditor() {
|
|||||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-amber-500/20 text-amber-300 rounded hover:bg-amber-500/30 transition-colors"
|
className="flex items-center gap-1 px-2 py-1 text-xs bg-amber-500/20 text-amber-300 rounded hover:bg-amber-500/30 transition-colors"
|
||||||
>
|
>
|
||||||
<SlidersHorizontal className="w-3 h-3" />
|
<SlidersHorizontal className="w-3 h-3" />
|
||||||
Gain (+3 dB)
|
Gain ({gainModeDb > 0 ? '+' : ''}{gainModeDb.toFixed(1)} dB)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -147,6 +147,7 @@ export default function WaveformTimeline({
|
|||||||
const isDraggingRef = useRef(false);
|
const isDraggingRef = useRef(false);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const selectionStartRef = useRef<number | null>(null);
|
const selectionStartRef = useRef<number | null>(null);
|
||||||
|
const selectionEndRef = useRef<number | null>(null);
|
||||||
const [selectionStart, setSelectionStart] = useState<number | null>(null);
|
const [selectionStart, setSelectionStart] = useState<number | null>(null);
|
||||||
const [selectionEnd, setSelectionEnd] = useState<number | null>(null);
|
const [selectionEnd, setSelectionEnd] = useState<number | null>(null);
|
||||||
const [selectedZone, setSelectedZone] = useState<{type: 'cut' | 'mute' | 'gain', id: string} | null>(null);
|
const [selectedZone, setSelectedZone] = useState<{type: 'cut' | 'mute' | 'gain', id: string} | null>(null);
|
||||||
@ -809,6 +810,7 @@ export default function WaveformTimeline({
|
|||||||
// Range selection mode
|
// Range selection mode
|
||||||
const startTime = clientXToTime(e.clientX);
|
const startTime = clientXToTime(e.clientX);
|
||||||
selectionStartRef.current = startTime;
|
selectionStartRef.current = startTime;
|
||||||
|
selectionEndRef.current = startTime;
|
||||||
setSelectionStart(startTime);
|
setSelectionStart(startTime);
|
||||||
setSelectionEnd(startTime);
|
setSelectionEnd(startTime);
|
||||||
isDraggingRef.current = true;
|
isDraggingRef.current = true;
|
||||||
@ -817,6 +819,7 @@ export default function WaveformTimeline({
|
|||||||
const onMove = (ev: MouseEvent) => {
|
const onMove = (ev: MouseEvent) => {
|
||||||
if (!isDraggingRef.current) return;
|
if (!isDraggingRef.current) return;
|
||||||
const currentTime = clientXToTime(ev.clientX);
|
const currentTime = clientXToTime(ev.clientX);
|
||||||
|
selectionEndRef.current = currentTime;
|
||||||
setSelectionEnd(currentTime);
|
setSelectionEnd(currentTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -824,21 +827,23 @@ export default function WaveformTimeline({
|
|||||||
isDraggingRef.current = false;
|
isDraggingRef.current = false;
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
|
|
||||||
if (selectionStartRef.current !== null && selectionEnd !== null) {
|
if (selectionStartRef.current !== null && selectionEndRef.current !== null) {
|
||||||
const start = Math.min(selectionStartRef.current, selectionEnd);
|
const start = Math.min(selectionStartRef.current, selectionEndRef.current);
|
||||||
const end = Math.max(selectionStartRef.current, selectionEnd);
|
const end = Math.max(selectionStartRef.current, selectionEndRef.current);
|
||||||
|
const minDuration = 0.01;
|
||||||
|
|
||||||
if (cutMode) {
|
if (end - start >= minDuration && cutMode) {
|
||||||
addCutRange(start, end);
|
addCutRange(start, end);
|
||||||
} else if (muteMode) {
|
} else if (end - start >= minDuration && muteMode) {
|
||||||
addMuteRange(start, end);
|
addMuteRange(start, end);
|
||||||
} else if (gainMode) {
|
} else if (end - start >= minDuration && gainMode) {
|
||||||
addGainRange(start, end, gainModeDb);
|
addGainRange(start, end, gainModeDb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset selection
|
// Reset selection
|
||||||
selectionStartRef.current = null;
|
selectionStartRef.current = null;
|
||||||
|
selectionEndRef.current = null;
|
||||||
setSelectionStart(null);
|
setSelectionStart(null);
|
||||||
setSelectionEnd(null);
|
setSelectionEnd(null);
|
||||||
|
|
||||||
@ -868,7 +873,7 @@ export default function WaveformTimeline({
|
|||||||
window.addEventListener('mouseup', onUp);
|
window.addEventListener('mouseup', onUp);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[cutMode, muteMode, gainMode, gainModeDb, clientXToTime, seekToClientX, addCutRange, addMuteRange, addGainRange, selectionEnd, getZoneAtPosition, cutRanges, muteRanges, gainRanges, duration, updateCutRange, updateMuteRange, updateGainRangeBounds],
|
[cutMode, muteMode, gainMode, gainModeDb, clientXToTime, seekToClientX, addCutRange, addMuteRange, addGainRange, getZoneAtPosition, cutRanges, muteRanges, gainRanges, duration, updateCutRange, updateMuteRange, updateGainRangeBounds],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle keyboard shortcuts for zone editing
|
// Handle keyboard shortcuts for zone editing
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AIPanel.tsx","./src/components/DevPanel.tsx","./src/components/ExportDialog.tsx","./src/components/SettingsPanel.tsx","./src/components/SilenceTrimmerPanel.tsx","./src/components/TranscriptEditor.tsx","./src/components/VideoPlayer.tsx","./src/components/VolumePanel.tsx","./src/components/WaveformTimeline.tsx","./src/hooks/useKeyboardShortcuts.ts","./src/hooks/useVideoSync.ts","./src/lib/dev-logger.ts","./src/lib/tauri-bridge.ts","./src/store/aiStore.ts","./src/store/editorStore.test.ts","./src/store/editorStore.ts","./src/types/project.ts"],"version":"5.9.3"}
|
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AIPanel.tsx","./src/components/DevPanel.tsx","./src/components/ExportDialog.tsx","./src/components/SettingsPanel.tsx","./src/components/SilenceTrimmerPanel.tsx","./src/components/TranscriptEditor.tsx","./src/components/VideoPlayer.tsx","./src/components/VolumePanel.tsx","./src/components/WaveformTimeline.tsx","./src/components/ZoneEditor.tsx","./src/hooks/useKeyboardShortcuts.ts","./src/hooks/useVideoSync.ts","./src/lib/dev-logger.ts","./src/lib/tauri-bridge.ts","./src/store/aiStore.ts","./src/store/editorStore.test.ts","./src/store/editorStore.ts","./src/types/project.ts"],"version":"5.9.3"}
|
||||||
Reference in New Issue
Block a user