defaults to project folders; examining zones
This commit is contained in:
@ -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 { Trash2, RotateCcw } from 'lucide-react';
|
import { Scissors, VolumeX, SlidersHorizontal, RotateCcw } from 'lucide-react';
|
||||||
|
|
||||||
export default function TranscriptEditor() {
|
export default function TranscriptEditor() {
|
||||||
const words = useEditorStore((s) => s.words);
|
const words = useEditorStore((s) => s.words);
|
||||||
@ -9,6 +9,7 @@ export default function TranscriptEditor() {
|
|||||||
const deletedRanges = useEditorStore((s) => s.deletedRanges);
|
const deletedRanges = useEditorStore((s) => s.deletedRanges);
|
||||||
const cutRanges = useEditorStore((s) => s.cutRanges);
|
const cutRanges = useEditorStore((s) => s.cutRanges);
|
||||||
const muteRanges = useEditorStore((s) => s.muteRanges);
|
const muteRanges = useEditorStore((s) => s.muteRanges);
|
||||||
|
const gainRanges = useEditorStore((s) => s.gainRanges);
|
||||||
const selectedWordIndices = useEditorStore((s) => s.selectedWordIndices);
|
const selectedWordIndices = useEditorStore((s) => s.selectedWordIndices);
|
||||||
const hoveredWordIndex = useEditorStore((s) => s.hoveredWordIndex);
|
const hoveredWordIndex = useEditorStore((s) => s.hoveredWordIndex);
|
||||||
const setSelectedWordIndices = useEditorStore((s) => s.setSelectedWordIndices);
|
const setSelectedWordIndices = useEditorStore((s) => s.setSelectedWordIndices);
|
||||||
@ -16,7 +17,10 @@ export default function TranscriptEditor() {
|
|||||||
const restoreRange = useEditorStore((s) => s.restoreRange);
|
const restoreRange = useEditorStore((s) => s.restoreRange);
|
||||||
const removeCutRange = useEditorStore((s) => s.removeCutRange);
|
const removeCutRange = useEditorStore((s) => s.removeCutRange);
|
||||||
const removeMuteRange = useEditorStore((s) => s.removeMuteRange);
|
const removeMuteRange = useEditorStore((s) => s.removeMuteRange);
|
||||||
|
const removeGainRange = useEditorStore((s) => s.removeGainRange);
|
||||||
const addCutRange = useEditorStore((s) => s.addCutRange);
|
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 getWordAtTime = useEditorStore((s) => s.getWordAtTime);
|
||||||
|
|
||||||
const selectionStart = useRef<number | null>(null);
|
const selectionStart = useRef<number | null>(null);
|
||||||
@ -131,6 +135,22 @@ export default function TranscriptEditor() {
|
|||||||
addCutRange(startTime, endTime);
|
addCutRange(startTime, endTime);
|
||||||
}, [selectedWordIndices, words, addCutRange]);
|
}, [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(
|
const getCutRangeForWord = useCallback(
|
||||||
(wordIndex: number) => {
|
(wordIndex: number) => {
|
||||||
const word = words[wordIndex];
|
const word = words[wordIndex];
|
||||||
@ -149,6 +169,15 @@ export default function TranscriptEditor() {
|
|||||||
[words, muteRanges],
|
[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(
|
const renderSegment = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
const segment = segments[index];
|
const segment = segments[index];
|
||||||
@ -170,6 +199,7 @@ export default function TranscriptEditor() {
|
|||||||
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);
|
||||||
|
const gainRange = getGainRangeForWord(globalIndex);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@ -185,9 +215,10 @@ export default function TranscriptEditor() {
|
|||||||
${isDeleted ? 'line-through text-editor-text-muted/40 bg-editor-word-deleted' : ''}
|
${isDeleted ? 'line-through text-editor-text-muted/40 bg-editor-word-deleted' : ''}
|
||||||
${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' : ''}
|
||||||
${isSelected && !isDeleted && !cutRange && !muteRange ? 'bg-editor-word-selected text-white' : ''}
|
${gainRange ? 'bg-amber-500/20 text-amber-100' : ''}
|
||||||
${isActive && !isDeleted && !isSelected && !cutRange && !muteRange ? 'bg-editor-accent/20 text-editor-accent' : ''}
|
${isSelected && !isDeleted && !cutRange && !muteRange && !gainRange ? 'bg-editor-word-selected text-white' : ''}
|
||||||
${isHovered && !isDeleted && !isSelected && !isActive && !cutRange && !muteRange ? 'bg-editor-word-hover' : ''}
|
${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}{' '}
|
{word.word}{' '}
|
||||||
@ -202,12 +233,13 @@ export default function TranscriptEditor() {
|
|||||||
<RotateCcw className="w-2.5 h-2.5" /> Restore
|
<RotateCcw className="w-2.5 h-2.5" /> Restore
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{(cutRange || muteRange) && isHovered && (
|
{(cutRange || muteRange || gainRange) && isHovered && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (cutRange) removeCutRange(cutRange.id);
|
if (cutRange) removeCutRange(cutRange.id);
|
||||||
if (muteRange) removeMuteRange(muteRange.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"
|
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>
|
</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 (
|
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 gap-2 px-4 py-2 border-b border-editor-border shrink-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">
|
<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>
|
</span>
|
||||||
{selectedWordIndices.length > 0 && (
|
{selectedWordIndices.length > 0 && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={cutSelectedWords}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3 h-3" />
|
<Scissors className="w-3 h-3" />
|
||||||
Cut {selectedWordIndices.length} words
|
Cut
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -42,15 +42,21 @@ window.electronAPI = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
openProject: async (): Promise<string | null> => {
|
openProject: async (): Promise<string | null> => {
|
||||||
|
const projectDir = await invoke<string>('get_projects_directory');
|
||||||
const result = await open({
|
const result = await open({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
|
defaultPath: projectDir,
|
||||||
filters: PROJECT_FILTERS,
|
filters: PROJECT_FILTERS,
|
||||||
});
|
});
|
||||||
return typeof result === 'string' ? result : null;
|
return typeof result === 'string' ? result : null;
|
||||||
},
|
},
|
||||||
|
|
||||||
saveProject: async (): Promise<string | 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;
|
return result ?? null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,14 @@ mod ai_provider;
|
|||||||
mod caption_generator;
|
mod caption_generator;
|
||||||
mod background_removal;
|
mod background_removal;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_projects_directory() -> Result<String, String> {
|
||||||
|
let dir = paths::project_root().join("Projects");
|
||||||
|
std::fs::create_dir_all(&dir)
|
||||||
|
.map_err(|e| format!("Failed to create Projects directory: {e}"))?;
|
||||||
|
Ok(dir.to_string_lossy().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the backend URL.
|
/// Returns the backend URL.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_backend_url() -> String {
|
fn get_backend_url() -> String {
|
||||||
@ -234,6 +242,7 @@ pub fn run() {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
get_projects_directory,
|
||||||
get_backend_url,
|
get_backend_url,
|
||||||
encrypt_string,
|
encrypt_string,
|
||||||
decrypt_string,
|
decrypt_string,
|
||||||
|
|||||||
Reference in New Issue
Block a user