diff --git a/frontend/src/components/TranscriptEditor.tsx b/frontend/src/components/TranscriptEditor.tsx index 5113c43..8906ae2 100644 --- a/frontend/src/components/TranscriptEditor.tsx +++ b/frontend/src/components/TranscriptEditor.tsx @@ -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(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 ( {word.word}{' '} @@ -202,12 +233,13 @@ export default function TranscriptEditor() { Restore )} - {(cutRange || muteRange) && isHovered && ( + {(cutRange || muteRange || gainRange) && isHovered && ( +
+ + + +
)} diff --git a/frontend/src/lib/tauri-bridge.ts b/frontend/src/lib/tauri-bridge.ts index 8300b6d..f5c5639 100644 --- a/frontend/src/lib/tauri-bridge.ts +++ b/frontend/src/lib/tauri-bridge.ts @@ -42,15 +42,21 @@ window.electronAPI = { }, openProject: async (): Promise => { + const projectDir = await invoke('get_projects_directory'); const result = await open({ multiple: false, + defaultPath: projectDir, filters: PROJECT_FILTERS, }); return typeof result === 'string' ? result : null; }, saveProject: async (): Promise => { - const result = await save({ filters: PROJECT_FILTERS }); + const projectDir = await invoke('get_projects_directory'); + const result = await save({ + defaultPath: `${projectDir}/project.aive`, + filters: PROJECT_FILTERS, + }); return result ?? null; }, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 537e0e2..deabfe0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,14 @@ mod ai_provider; mod caption_generator; mod background_removal; +#[tauri::command] +fn get_projects_directory() -> Result { + 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. #[tauri::command] fn get_backend_url() -> String { @@ -234,6 +242,7 @@ pub fn run() { Ok(()) }) .invoke_handler(tauri::generate_handler![ + get_projects_directory, get_backend_url, encrypt_string, decrypt_string,