defaults to project folders; examining zones

This commit is contained in:
2026-04-15 17:31:41 -06:00
parent 024b9bd806
commit 48d761c713
3 changed files with 78 additions and 15 deletions

View File

@ -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 &middot; {deletedRanges.length} cuts &middot; {cutRanges.length} cut ranges &middot; {muteRanges.length} mute ranges {words.length} words &middot; {deletedRanges.length} cuts &middot; {cutRanges.length} cut ranges &middot; {muteRanges.length} mute ranges &middot; {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>

View File

@ -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;
}, },

View File

@ -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,