defaults to project folders; examining zones
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 { Trash2, RotateCcw } from 'lucide-react';
|
||||
import { Scissors, VolumeX, SlidersHorizontal, RotateCcw } from 'lucide-react';
|
||||
|
||||
export default function TranscriptEditor() {
|
||||
const words = useEditorStore((s) => s.words);
|
||||
@ -9,6 +9,7 @@ export default function TranscriptEditor() {
|
||||
const deletedRanges = useEditorStore((s) => s.deletedRanges);
|
||||
const cutRanges = useEditorStore((s) => s.cutRanges);
|
||||
const muteRanges = useEditorStore((s) => s.muteRanges);
|
||||
const gainRanges = useEditorStore((s) => s.gainRanges);
|
||||
const selectedWordIndices = useEditorStore((s) => s.selectedWordIndices);
|
||||
const hoveredWordIndex = useEditorStore((s) => s.hoveredWordIndex);
|
||||
const setSelectedWordIndices = useEditorStore((s) => s.setSelectedWordIndices);
|
||||
@ -16,7 +17,10 @@ export default function TranscriptEditor() {
|
||||
const restoreRange = useEditorStore((s) => s.restoreRange);
|
||||
const removeCutRange = useEditorStore((s) => s.removeCutRange);
|
||||
const removeMuteRange = useEditorStore((s) => s.removeMuteRange);
|
||||
const removeGainRange = useEditorStore((s) => s.removeGainRange);
|
||||
const addCutRange = useEditorStore((s) => s.addCutRange);
|
||||
const addMuteRange = useEditorStore((s) => s.addMuteRange);
|
||||
const addGainRange = useEditorStore((s) => s.addGainRange);
|
||||
const getWordAtTime = useEditorStore((s) => s.getWordAtTime);
|
||||
|
||||
const selectionStart = useRef<number | null>(null);
|
||||
@ -131,6 +135,22 @@ export default function TranscriptEditor() {
|
||||
addCutRange(startTime, endTime);
|
||||
}, [selectedWordIndices, words, addCutRange]);
|
||||
|
||||
const muteSelectedWords = useCallback(() => {
|
||||
if (selectedWordIndices.length === 0) return;
|
||||
const sorted = [...selectedWordIndices].sort((a, b) => a - b);
|
||||
const startTime = words[sorted[0]].start;
|
||||
const endTime = words[sorted[sorted.length - 1]].end;
|
||||
addMuteRange(startTime, endTime);
|
||||
}, [selectedWordIndices, words, addMuteRange]);
|
||||
|
||||
const gainSelectedWords = useCallback(() => {
|
||||
if (selectedWordIndices.length === 0) return;
|
||||
const sorted = [...selectedWordIndices].sort((a, b) => a - b);
|
||||
const startTime = words[sorted[0]].start;
|
||||
const endTime = words[sorted[sorted.length - 1]].end;
|
||||
addGainRange(startTime, endTime, 3);
|
||||
}, [selectedWordIndices, words, addGainRange]);
|
||||
|
||||
const getCutRangeForWord = useCallback(
|
||||
(wordIndex: number) => {
|
||||
const word = words[wordIndex];
|
||||
@ -149,6 +169,15 @@ export default function TranscriptEditor() {
|
||||
[words, muteRanges],
|
||||
);
|
||||
|
||||
const getGainRangeForWord = useCallback(
|
||||
(wordIndex: number) => {
|
||||
const word = words[wordIndex];
|
||||
if (!word) return null;
|
||||
return gainRanges.find((r) => word.start >= r.start && word.end <= r.end);
|
||||
},
|
||||
[words, gainRanges],
|
||||
);
|
||||
|
||||
const renderSegment = useCallback(
|
||||
(index: number) => {
|
||||
const segment = segments[index];
|
||||
@ -170,6 +199,7 @@ export default function TranscriptEditor() {
|
||||
const deletedRange = isDeleted ? getRangeForWord(globalIndex) : null;
|
||||
const cutRange = getCutRangeForWord(globalIndex);
|
||||
const muteRange = getMuteRangeForWord(globalIndex);
|
||||
const gainRange = getGainRangeForWord(globalIndex);
|
||||
|
||||
return (
|
||||
<span
|
||||
@ -185,9 +215,10 @@ export default function TranscriptEditor() {
|
||||
${isDeleted ? 'line-through text-editor-text-muted/40 bg-editor-word-deleted' : ''}
|
||||
${cutRange ? 'bg-red-500/20 text-red-100' : ''}
|
||||
${muteRange ? 'bg-blue-500/20 text-blue-100' : ''}
|
||||
${isSelected && !isDeleted && !cutRange && !muteRange ? 'bg-editor-word-selected text-white' : ''}
|
||||
${isActive && !isDeleted && !isSelected && !cutRange && !muteRange ? 'bg-editor-accent/20 text-editor-accent' : ''}
|
||||
${isHovered && !isDeleted && !isSelected && !isActive && !cutRange && !muteRange ? 'bg-editor-word-hover' : ''}
|
||||
${gainRange ? 'bg-amber-500/20 text-amber-100' : ''}
|
||||
${isSelected && !isDeleted && !cutRange && !muteRange && !gainRange ? 'bg-editor-word-selected text-white' : ''}
|
||||
${isActive && !isDeleted && !isSelected && !cutRange && !muteRange && !gainRange ? 'bg-editor-accent/20 text-editor-accent' : ''}
|
||||
${isHovered && !isDeleted && !isSelected && !isActive && !cutRange && !muteRange && !gainRange ? 'bg-editor-word-hover' : ''}
|
||||
`}
|
||||
>
|
||||
{word.word}{' '}
|
||||
@ -202,12 +233,13 @@ export default function TranscriptEditor() {
|
||||
<RotateCcw className="w-2.5 h-2.5" /> Restore
|
||||
</button>
|
||||
)}
|
||||
{(cutRange || muteRange) && isHovered && (
|
||||
{(cutRange || muteRange || gainRange) && isHovered && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (cutRange) removeCutRange(cutRange.id);
|
||||
if (muteRange) removeMuteRange(muteRange.id);
|
||||
if (gainRange) removeGainRange(gainRange.id);
|
||||
}}
|
||||
className="absolute -top-5 left-1/2 -translate-x-1/2 flex items-center gap-0.5 px-1.5 py-0.5 bg-editor-surface border border-editor-border rounded text-[10px] text-editor-success whitespace-nowrap z-10"
|
||||
>
|
||||
@ -221,23 +253,39 @@ export default function TranscriptEditor() {
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[segments, deletedSet, selectedSet, activeWordIndex, hoveredWordIndex, handleWordMouseDown, handleWordMouseEnter, setHoveredWordIndex, getRangeForWord, getCutRangeForWord, getMuteRangeForWord, restoreRange, removeCutRange, removeMuteRange],
|
||||
[segments, deletedSet, selectedSet, activeWordIndex, hoveredWordIndex, handleWordMouseDown, handleWordMouseEnter, setHoveredWordIndex, getRangeForWord, getCutRangeForWord, getMuteRangeForWord, getGainRangeForWord, restoreRange, removeCutRange, removeMuteRange, removeGainRange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-editor-border shrink-0">
|
||||
<span className="text-xs text-editor-text-muted flex-1">
|
||||
{words.length} words · {deletedRanges.length} cuts · {cutRanges.length} cut ranges · {muteRanges.length} mute ranges
|
||||
{words.length} words · {deletedRanges.length} cuts · {cutRanges.length} cut ranges · {muteRanges.length} mute ranges · {gainRanges.length} gain ranges
|
||||
</span>
|
||||
{selectedWordIndices.length > 0 && (
|
||||
<button
|
||||
onClick={cutSelectedWords}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-editor-danger/20 text-editor-danger rounded hover:bg-editor-danger/30 transition-colors"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Cut {selectedWordIndices.length} words
|
||||
</button>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={cutSelectedWords}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-red-500/20 text-red-300 rounded hover:bg-red-500/30 transition-colors"
|
||||
>
|
||||
<Scissors className="w-3 h-3" />
|
||||
Cut
|
||||
</button>
|
||||
<button
|
||||
onClick={muteSelectedWords}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-blue-500/20 text-blue-300 rounded hover:bg-blue-500/30 transition-colors"
|
||||
>
|
||||
<VolumeX className="w-3 h-3" />
|
||||
Mute
|
||||
</button>
|
||||
<button
|
||||
onClick={gainSelectedWords}
|
||||
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" />
|
||||
Gain (+3 dB)
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@ -42,15 +42,21 @@ window.electronAPI = {
|
||||
},
|
||||
|
||||
openProject: async (): Promise<string | null> => {
|
||||
const projectDir = await invoke<string>('get_projects_directory');
|
||||
const result = await open({
|
||||
multiple: false,
|
||||
defaultPath: projectDir,
|
||||
filters: PROJECT_FILTERS,
|
||||
});
|
||||
return typeof result === 'string' ? result : null;
|
||||
},
|
||||
|
||||
saveProject: async (): Promise<string | null> => {
|
||||
const result = await save({ filters: PROJECT_FILTERS });
|
||||
const projectDir = await invoke<string>('get_projects_directory');
|
||||
const result = await save({
|
||||
defaultPath: `${projectDir}/project.aive`,
|
||||
filters: PROJECT_FILTERS,
|
||||
});
|
||||
return result ?? null;
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user