Function bodies 405 total
useTauriCommand function · typescript · L58-L95 (38 LOC).claude/skills/tauri-desktop-app/hooks/useTauriCommand.ts
export function useTauriCommand<T = unknown>(
options: UseCommandOptions<T>
): CommandState<T> {
const { command, args: defaultArgs, onSuccess, onError } = options;
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const execute = useCallback(
async (args?: Record<string, unknown>) => {
setIsLoading(true);
setError(null);
try {
const result = await invoke<T>(command, args || defaultArgs);
setData(result);
onSuccess?.(result);
return result;
} catch (err) {
const errorMsg = String(err);
setError(errorMsg);
onError?.(errorMsg);
throw err;
} finally {
setIsLoading(false);
}
},
[command, defaultArgs, onSuccess, onError]
);
const reset = useCallback(() => {
setData(null);
setIsLoading(false);
setError(null);
}, []);
return { data, useTauriMutation function · typescript · L123-L152 (30 LOC).claude/skills/tauri-desktop-app/hooks/useTauriCommand.ts
export function useTauriMutation<T = unknown>(
options: UseMutationOptions<T>
): MutationState<T> {
const { command, onSuccess, onError } = options;
const [isMutating, setIsMutating] = useState(false);
const [error, setError] = useState<string | null>(null);
const execute = useCallback(
async (args: Record<string, unknown>) => {
setIsMutating(true);
setError(null);
try {
const result = await invoke<T>(command, args);
onSuccess?.(result);
return result;
} catch (err) {
const errorMsg = String(err);
setError(errorMsg);
onError?.(errorMsg);
throw err;
} finally {
setIsMutating(false);
}
},
[command, onSuccess, onError]
);
return { execute, isMutating, error };
}useTauriBatch function · typescript · L185-L213 (29 LOC).claude/skills/tauri-desktop-app/hooks/useTauriCommand.ts
export function useTauriBatch<T = unknown>(
options: BatchOptions<T>
): BatchState {
const { command, onSuccess, onError } = options;
const [isBatching, setIsBatching] = useState(false);
const [error, setError] = useState<string | null>(null);
const execute = useCallback(
async (items: T[]) => {
setIsBatching(true);
setError(null);
try {
await invoke(command, { items });
onSuccess?.();
} catch (err) {
const errorMsg = String(err);
setError(errorMsg);
onError?.(errorMsg);
throw err;
} finally {
setIsBatching(false);
}
},
[command, onSuccess, onError]
);
return { execute, isBatching, error };
}useTauriEvent function · typescript · L50-L95 (46 LOC).claude/skills/tauri-desktop-app/hooks/useTauriEvent.ts
export function useTauriEvent<T = unknown>(
eventName: string,
options: UseEventOptions<T> = {}
): EventState<T> {
const { defaultValue, onEvent, enabled = true } = options;
const [data, setData] = useState<T | null>(defaultValue ?? null);
const [isListening, setIsListening] = useState(false);
const startListening = useCallback(() => {
let unlisten: UnlistenFn | null = null;
listen<T>(eventName, (event) => {
setData(event.payload);
onEvent?.(event.payload);
})
.then((fn) => {
unlisten = fn;
setIsListening(true);
})
.catch((err) => {
console.error(`Failed to listen to ${eventName}:`, err);
});
return () => {
unlisten?.then((fn) => fn());
setIsListening(false);
};
}, [eventName, onEvent]);
useEffect(() => {
if (!enabled) return;
const cleanup = startListening();
return () => cleanup?.();
}, [enabled, startListening]);
const toggle = useCallback((enabled: booleuseTauriEvents function · typescript · L113-L144 (32 LOC).claude/skills/tauri-desktop-app/hooks/useTauriEvent.ts
export function useTauriEvents<T = unknown>(
handlers: EventHandlers<T>,
enabled = true
): boolean {
const [isListening, setIsListening] = useState(false);
useEffect(() => {
if (!enabled) return;
const unlisteners: Promise<UnlistenFn>[] = [];
Object.entries(handlers).forEach(([eventName, handler]) => {
const promise = listen<T>(eventName, (event) => {
handler(event.payload);
});
unlisteners.push(promise);
});
Promise.all(unlisteners).then(() => {
setIsListening(true);
});
return () => {
Promise.all(unlisteners).then((fns) => {
fns.forEach((fn) => fn());
});
setIsListening(false);
};
}, [handlers, enabled]);
return isListening;
}useCommandWithEvents function · typescript · L191-L243 (53 LOC).claude/skills/tauri-desktop-app/hooks/useTauriEvent.ts
export function useCommandWithEvents<TData = unknown, TProgress = number>(
options: CommandWithEventOptions<TData, TProgress>
) {
const {
command,
progressEvent,
completeEvent,
errorEvent,
onProgress,
onComplete,
onError,
} = options;
const [isRunning, setIsRunning] = useState(false);
// Listen to progress events
useTauriEvent<TProgress>(progressEvent, {
onEvent: (progress) => {
onProgress?.(progress);
},
});
// Listen to complete events
useTauriEvent<TData>(completeEvent || "", {
onEvent: (data) => {
setIsRunning(false);
onComplete?.(data);
},
});
// Listen to error events
useTauriEvent<string>(errorEvent || "", {
onEvent: (error) => {
setIsRunning(false);
onError?.(error);
},
});
const execute = useCallback(
async (args?: Record<string, unknown>) => {
setIsRunning(true);
try {
await invoke(command, args);
} catch (err) {
setIsRunninguseTauriLoad function · typescript · L48-L86 (39 LOC).claude/skills/tauri-desktop-app/hooks/useTauriLoad.ts
export function useTauriLoad<T = unknown>(
options: UseLoadOptions<T>
): LoadState<T> {
const {
loadOnMount = true,
defaultValue,
onSuccess,
onError,
...commandOptions
} = options;
const { data, isLoading, error, execute } = useTauriCommand<T>({
...commandOptions,
onSuccess,
onError,
});
const [isInitialLoadDone, setIsInitialLoadDone] = useState(!loadOnMount);
useEffect(() => {
if (loadOnMount && !isInitialLoadDone) {
execute()
.then(() => setIsInitialLoadDone(true))
.catch(() => setIsInitialLoadDone(true));
}
}, [loadOnMount, execute, isInitialLoadDone]);
const reload = useCallback(async () => {
await execute();
}, [execute]);
return {
data: (data ?? defaultValue) as T,
isLoading,
error,
reload,
isInitialLoadDone,
};
}Repobility analyzer · published findings · https://repobility.com
useTauriAutoRefresh function · typescript · L108-L134 (27 LOC).claude/skills/tauri-desktop-app/hooks/useTauriLoad.ts
export function useTauriAutoRefresh<T = unknown>(
options: UseAutoRefreshOptions<T>
): LoadState<T> {
const { eventName, ...loadOptions } = options;
const { data, isLoading, error, reload, isInitialLoadDone } =
useTauriLoad<T>(loadOptions);
useEffect(() => {
if (!eventName) return;
const unlisten = listen(eventName, () => {
reload();
});
return () => {
unlisten.then((fn) => fn());
};
}, [eventName, reload]);
return {
data: (data ?? loadOptions.defaultValue) as T,
isLoading,
error,
reload,
isInitialLoadDone,
};
}useTauriMutation function · typescript · L46-L85 (40 LOC).claude/skills/tauri-desktop-app/hooks/useTauriMutation.ts
export function useTauriMutation<TData = unknown, TVariables = void>(
options: UseMutationOptions<TData, TVariables>
): MutationState<TData, TVariables> {
const { command, onSuccess, onError, onSettled } = options;
const [isMutating, setIsMutating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<TData | null>(null);
const execute = useCallback(
async (variables: TVariables): Promise<TData> => {
setIsMutating(true);
setError(null);
try {
const result = await invoke<TData>(command, variables);
setData(result);
onSuccess?.(result, variables);
onSettled?.(result, null, variables);
return result;
} catch (err) {
const errorMsg = String(err);
setError(errorMsg);
onError?.(errorMsg, variables);
onSettled?.(undefined, errorMsg, variables);
throw err;
} finally {
setIsMutating(false);
}
},
useTauriBatchMutation function · typescript · L115-L154 (40 LOC).claude/skills/tauri-desktop-app/hooks/useTauriMutation.ts
export function useTauriBatchMutation<TData = unknown, TItem = unknown>(
options: UseBatchMutationOptions<TData, TItem>
): BatchMutationState<TData, TItem> {
const { command, onProgress, onSuccess, onError } = options;
const [isMutating, setIsMutating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [progress, setProgress] = useState({ completed: 0, total: 0 });
const execute = useCallback(
async (items: TItem[]): Promise<TData[]> => {
setIsMutating(true);
setError(null);
setProgress({ completed: 0, total: items.length });
try {
const results: TData[] = [];
for (const item of items) {
const result = await invoke<TData>(command, { item });
results.push(result);
setProgress((prev) => ({ ...prev, completed: prev.completed + 1 }));
onProgress?.(results.length, items.length);
}
onSuccess?.(results);
return results;
} catch (err) logError function · typescript · L35-L45 (11 LOC).claude/skills/tauri-desktop-app/utils/toastr.tsx
async function logError(message: string, context?: string): Promise<void> {
try {
await invoke("log_error_command", {
message,
context: context || null,
stack: new Error().stack,
});
} catch {
// Silently fail if logging fails
}
}showToastr function · typescript · L52-L68 (17 LOC).claude/skills/tauri-desktop-app/utils/toastr.tsx
export async function showToastr({
type,
title,
message,
duration,
context,
}: ToastrOptions): Promise<void> {
// Log errors to backend
if (type === "error") {
const fullMessage = message ? `${title}: ${message}` : title;
await logError(fullMessage, context);
}
// TODO: Replace with actual toast implementation
// Options: toastr, react-toastify, sonner, etc.
console.log(`[${type.toUpperCase()}] ${title}${message ? `: ${message}` : ""}`);
}showSuccess function · typescript · L73-L75 (3 LOC).claude/skills/tauri-desktop-app/utils/toastr.tsx
export function showSuccess(title: string, message?: string): void {
showToastr({ type: "success", title, message });
}showError function · typescript · L81-L87 (7 LOC).claude/skills/tauri-desktop-app/utils/toastr.tsx
export function showError(
title: string,
message?: string,
context?: string
): void {
showToastr({ type: "error", title, message, context });
}showInfo function · typescript · L92-L94 (3 LOC).claude/skills/tauri-desktop-app/utils/toastr.tsx
export function showInfo(title: string, message?: string): void {
showToastr({ type: "info", title, message });
}Repobility · code-quality intelligence platform · https://repobility.com
showWarning function · typescript · L99-L101 (3 LOC).claude/skills/tauri-desktop-app/utils/toastr.tsx
export function showWarning(title: string, message?: string): void {
showToastr({ type: "warning", title, message });
}useToastr function · typescript · L121-L129 (9 LOC).claude/skills/tauri-desktop-app/utils/toastr.tsx
export function useToastr() {
return {
showToastr: useCallback(showToastr, []),
showSuccess: useCallback(showSuccess, []),
showError: useCallback(showError, []),
showInfo: useCallback(showInfo, []),
showWarning: useCallback(showWarning, []),
};
}withErrorHandling function · typescript · L146-L156 (11 LOC).claude/skills/tauri-desktop-app/utils/toastr.tsx
export function withErrorHandling<P extends object>(
Component: React.ComponentType<P>
): React.ComponentType<P> {
return function WrappedComponent(props: P) {
return (
<ErrorBoundary>
<Component {...props} />
</ErrorBoundary>
);
};
}useAsyncOperation function · typescript · L217-L244 (28 LOC).claude/skills/tauri-desktop-app/utils/toastr.tsx
export function useAsyncOperation<T>({
operation,
onSuccess,
onError,
}: UseAsyncOperationOptions<T>): AsyncOperationState<T> {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [data, setData] = useState<T | null>(null);
const execute = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const result = await operation();
setData(result);
onSuccess?.(result);
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
onError?.(error);
} finally {
setIsLoading(false);
}
}, [operation, onSuccess, onError]);
return { execute, isLoading, error, data };
}AnalysisPanel function · typescript · L39-L249 (211 LOC)src/components/AnalysisPanel.tsx
export function AnalysisPanel({
clipId,
videoPath,
isAnalyzing,
progress,
stage,
segments,
statistics,
error,
onAnalyze,
onSegmentToggle,
onReset,
}: AnalysisPanelProps) {
const [filterType, setFilterType] = useState<"all" | "filler" | "silence">("all");
// Filter segments by type
const filteredSegments = useMemo(() => {
if (filterType === "all") return segments;
return segments.filter((s) => {
if (filterType === "filler") return s.type === "filler_word";
if (filterType === "silence") return s.type === "silence";
return true;
});
}, [segments, filterType]);
// Count segments by type
const fillerCount = useMemo(
() => segments.filter((s) => s.type === "filler_word").length,
[segments]
);
const silenceCount = useMemo(
() => segments.filter((s) => s.type === "silence").length,
[segments]
);
// Handle analyze button click
const handleAnalyze = useCallback(() => {
if (clipId && videoPath) {
ClipManager function · typescript · L21-L302 (282 LOC)src/components/ClipManager.tsx
export function ClipManager({
clips,
activeClipId,
onClipSelect,
onClipAdd,
onClipRemove,
onClipReorder,
}: ClipManagerProps) {
const [isImporting, setIsImporting] = useState(false);
const [draggedId, setDraggedId] = useState<string | null>(null);
const dragOverIdRef = useRef<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
// Handle file input change
const handleFileInputChange = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
setIsImporting(true);
try {
// In Tauri, file input returns the file path
for (let i = 0; i < files.length; i++) {
const file = files[i];
const filePath = (file as any).path || file.name;
await onClipAdd(filePath);
}
} catch (error) {
console.error("Failed to import video:", error);
} finally {
setIsImporting(false)ClipsPanel function · typescript · L34-L312 (279 LOC)src/components/ClipsPanel.tsx
export function ClipsPanel({
clips,
activeClipId,
onClipSelect,
onClipAdd,
onClipRemove,
onClipReorder,
isVisible = true,
onSwitchToTranscript,
}: ClipsPanelProps) {
const [isImporting, setIsImporting] = useState(false);
const [draggedId, setDraggedId] = useState<string | null>(null);
const dragOverIdRef = useRef<string | null>(null);
// Handle click on import button - use Tauri dialog
const handleImportClick = useCallback(async () => {
setIsImporting(true);
try {
const selected = await open({
multiple: true,
filters: [
{
name: 'Video',
extensions: ['mp4', 'mov', 'avi', 'mkv', 'webm']
}
]
});
if (selected) {
const files = Array.isArray(selected) ? selected : [selected];
for (const filePath of files) {
await onClipAdd(filePath);
}
}
} catch (error) {
console.error("Failed to import video:", error);
} finally {
CrashRecoveryDialog function · typescript · L29-L80 (52 LOC)src/components/CrashRecoveryDialog.tsx
export function CrashRecoveryDialog({
isOpen,
autosaveTime,
onRestore,
onDismiss,
}: CrashRecoveryDialogProps) {
const formattedTime = new Date(autosaveTime).toLocaleString();
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onDismiss()}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<div className="flex items-center gap-2 text-amber-500">
<AlertTriangle className="w-5 h-5" />
<DialogTitle>Crash Recovery</DialogTitle>
</div>
<DialogDescription>
The app didn't close properly. An autosaved project is available.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="flex items-start gap-3 p-4 rounded-lg bg-amber-500/10 border border-amber-500/20">
<Clock className="w-5 h-5 text-amber-500 shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium text-Want this analysis on your repo? https://repobility.com/scan/
ExportProgressModal function · typescript · L62-L234 (173 LOC)src/components/ExportProgressModal.tsx
export function ExportProgressModal({
open,
progress,
outputPath,
fileSize,
onCancel,
onComplete,
}: ExportProgressModalProps) {
const isCompleted = progress?.stage === "completed";
const progressPercent = progress?.progress ?? 0;
const stage = progress?.stage ?? "preparing";
const [copied, setCopied] = useState(false);
/**
* Format seconds into human-readable time string.
*/
const formatTime = (seconds: number): string => {
if (seconds < 60) return `${Math.round(seconds)}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
};
/**
* Format bytes into human-readable file size string.
*/
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024ExportSettingsDialog function · typescript · L88-L253 (166 LOC)src/components/ExportSettingsDialog.tsx
export function ExportSettingsDialog({
open,
onOpenChange,
onStartExport,
sourceDuration = 0,
sourceResolution: _sourceResolution = "1080p",
}: ExportSettingsDialogProps) {
const [settings, setSettings] = useState<ExportSettings>({
...DEFAULT_EXPORT_SETTINGS,
});
/**
* Estimate output file size based on duration, quality bitrate, and resolution.
* Formula: (bitrate_kbps * resolutionMultiplier * duration) / 8 / 1024 = size in MB
* Note: bitrate is in kilobits/second (kbps), need to convert to bytes then MB
*/
const estimateFileSize = (): string => {
if (!sourceDuration) return "~0 MB";
const bitrate = QUALITY_PRESETS.find(q => q.value === settings.quality)?.bitrate || 5000;
const resolutionMult = settings.resolution === "4k" ? 1.5 : settings.resolution === "2k" ? 1.2 : 1;
// Convert kbps to MB: (kbps * seconds) / 8 = KB, then / 1024 = MB
const sizeMb = (bitrate * resolutionMult * sourceDuration) / 8 / 1024;
return `~${Math.roundNewProjectDialog function · typescript · L28-L102 (75 LOC)src/components/NewProjectDialog.tsx
export function NewProjectDialog({
isOpen,
onClose,
onCreate,
}: NewProjectDialogProps) {
const [name, setName] = useState("Untitled Project");
const [description, setDescription] = useState("");
const handleCreate = useCallback(() => {
if (name.trim()) {
onCreate(name.trim(), description.trim());
// Reset for next time
setDescription("");
}
}, [name, description, onCreate]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
handleCreate();
}
},
[handleCreate]
);
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<div className="flex items-center gap-2">
<FolderOpen className="w-5 h-5 text-primary" />
<DialogTitle>New Project</DialogTitle>
</div>
<DialogDescription>
Create formatBytes function · typescript · L41-L47 (7 LOC)src/components/SettingsDialog.tsx
function formatBytes(bytes: number): string {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
}SettingsDialog function · typescript · L49-L414 (366 LOC)src/components/SettingsDialog.tsx
export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
const { theme, setTheme } = useTheme();
const [models, setModels] = useState<ModelInfo[]>([]);
const [activeModel, setActiveModel] = useState<string>("small");
const [isLoading, setIsLoading] = useState(true);
const [downloadingModel, setDownloadingModel] = useState<string | null>(null);
const [downloadProgress, setDownloadProgress] = useState<number>(0);
const [downloadedBytes, setDownloadedBytes] = useState<number>(0);
const [totalBytes, setTotalBytes] = useState<number>(0);
const [downloadStage, setDownloadStage] = useState<string>("");
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
// Load models on mount
useEffect(() => {
if (isOpen) {
loadModels();
}
}, [isOpen]);
// Listen for download progress
useTauriEvent<ModelDownloadProgress>("model-download-progress", {
onEvent: (dTimeline function · typescript · L40-L435 (396 LOC)src/components/Timeline.tsx
export function Timeline({
duration,
currentTime,
waveform,
segments,
zoom,
onSeek,
onSegmentToggle,
onZoomChange,
clipBoundaries = [],
activeClipId,
}: TimelineProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [hoverTime, setHoverTime] = useState<number | null>(null);
const [hoveredSegmentId, setHoveredSegmentId] = useState<string | null>(null);
// Calculate dimensions
const width = duration * zoom;
// Convert time to position
const timeToPosition = useCallback((time: number) => time * zoom, [zoom]);
// Convert position to time
const positionToTime = useCallback((pos: number) => pos / zoom, [zoom]);
// Handle seek via click/drag
const handleSeek = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const time = MaTopBar function · typescript · L60-L322 (263 LOC)src/components/TopBar.tsx
export function TopBar({
projectName = "Untitled Project",
onProjectNameChange,
onExport,
phase = "Phase 3: ML Integration",
leftPanelVisible = true,
rightPanelVisible = true,
onToggleLeftPanel,
onToggleRightPanel,
onOpenSettings,
onNewProject,
onSaveProject,
onOpenProject,
recentProjects,
onOpenRecentProject,
videoPath = "",
transcriptWords = [],
deletedWordIds,
videoDuration = 0,
videoResolution = "1080p",
}: TopBarProps) {
const [isEditing, setIsEditing] = useState(false);
const [editedName, setEditedName] = useState(projectName);
const [showExportDialog, setShowExportDialog] = useState(false);
// Export hook integration
const { isExporting, progress, exportOutputPath, exportFileSize, startExport, cancelExport, resetExport } = useExport();
const handleStartEdit = useCallback(() => {
setIsEditing(true);
setEditedName(projectName);
}, [projectName]);
const handleSaveEdit = useCallback(() => {
setIsEditing(false);
TranscriptEditor function · typescript · L59-L399 (341 LOC)src/components/TranscriptEditor.tsx
export function TranscriptEditor({
className,
onSwitchToClips,
currentTime = 0,
onSeek,
removedSegments = [],
}: TranscriptEditorProps) {
// Select store state and actions
const {
words,
paragraphs,
deletedWordIds,
selectedWordIds,
editMode,
toggleWordSelection,
deleteWord,
restoreWord,
deleteSelectedWords,
setEditMode,
deleteParagraph,
restoreParagraph,
clearSelection,
} = useTranscriptStore();
// Group words by clip ID using useMemo for performance
const clipsGrouped = useMemo(() => {
const groups = new Map<string, ClipWords>();
for (const word of words) {
const clipId = word.id.split("-")[0]; // Extract clip ID from word ID
const clipName = `Clip ${clipId}`; // Default clip name
if (!groups.has(clipId)) {
groups.set(clipId, {
clipId,
clipName,
words: [],
});
}
groups.get(clipId)!.words.push(word);
}
return Array.fromAbout: code-quality intelligence by Repobility · https://repobility.com
TranscriptionProgressModal function · typescript · L55-L211 (157 LOC)src/components/TranscriptionProgressModal.tsx
export function TranscriptionProgressModal({
open,
onOpenChange,
clipId,
}: TranscriptionProgressModalProps) {
const [progress, setProgress] = useState<number>(0);
const [stage, setStage] = useState<string>("");
const [error, setError] = useState<string | null>(null);
// Reset state when modal opens
useEffect(() => {
if (open) {
setProgress(0);
setStage("");
setError(null);
}
}, [open]);
// Listen for transcription progress events
useEffect(() => {
let unlistenProgress: UnlistenFn | null = null;
let unlistenComplete: UnlistenFn | null = null;
let unlistenError: UnlistenFn | null = null;
const setupListeners = async () => {
// Listen to progress updates
unlistenProgress = await listen<TranscriptionProgressEvent>(
"transcription-progress",
(event) => {
// Only handle events for this clip if clipId is specified
if (clipId && event.payload.clip_id !== clipId) return;
TranscriptPanel function · typescript · L47-L424 (378 LOC)src/components/TranscriptPanel.tsx
export function TranscriptPanel({
clips = [],
segments = [],
currentTime = 0,
onWordClick,
onSegmentToggle,
isVisible = true,
activeClipId,
onImportVideo,
}: TranscriptPanelProps) {
const [searchQuery, setSearchQuery] = useState("");
const [selectedWordId, setSelectedWordId] = useState<string | null>(null);
// Organize transcripts by clip
const clipTranscripts: ClipTranscript[] = useMemo(() => {
console.log('[TranscriptPanel] clips:', clips.map(c => ({
id: c.id,
name: c.name,
hasTranscript: !!c.transcript,
wordCount: c.transcript?.words?.length || 0,
})));
const filtered = clips
.filter((clip) => clip.transcript?.words && clip.transcript.words.length > 0)
.map((clip) => ({
clip,
words: clip.transcript?.words || [],
}))
.sort((a, b) => a.clip.order - b.clip.order);
console.log('[TranscriptPanel] clipTranscripts after filter:', filtered.length, 'clips with transcripts');
returnDialog function · typescript · L18-L62 (45 LOC)src/components/ui/dialog.tsx
export function Dialog({ open, onOpenChange, children }: DialogProps) {
const [internalOpen, setInternalOpen] = React.useState(open ?? false);
const isControlled = open !== undefined;
const isOpen = isControlled ? open : internalOpen;
const handleOpenChange = React.useCallback(
(newOpen: boolean) => {
if (!isControlled) {
setInternalOpen(newOpen);
}
onOpenChange?.(newOpen);
},
[isControlled, onOpenChange]
);
// Handle escape key
React.useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && isOpen) {
handleOpenChange(false);
}
};
if (isOpen) {
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}
}, [isOpen, handleOpenChange]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
cDropdownMenu function · typescript · L130-L160 (31 LOC)src/components/ui/dropdown-menu.tsx
export function DropdownMenu({ trigger, children, align = "end" }: DropdownMenuComponentProps) {
const [isOpen, setIsOpen] = React.useState(false);
const containerRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen]);
return (
<div ref={containerRef} className="relative inline-block">
<div onClick={() => setIsOpen(!isOpen)}>{trigger}</div>
{isOpen && (
<DropdownMenuContent align={align}>
{children}
</DropdownMenuContent>
)}
</div>
);
}DropdownMenuSub function · typescript · L168-L218 (51 LOC)src/components/ui/dropdown-menu.tsx
export function DropdownMenuSub({ children, onOpenChange }: DropdownMenuSubProps) {
const [isOpen, setIsOpen] = React.useState(false);
const containerRef = React.useRef<HTMLDivElement>(null);
const contentRef = React.useRef<HTMLDivElement>(null);
const handleOpenChange = React.useCallback(
(open: boolean) => {
setIsOpen(open);
onOpenChange?.(open);
},
[onOpenChange]
);
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
handleOpenChange(false);
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}
}, [isOpen, handleOpenChange]);
return (
<div ref={containerRef} className="relative">
{React.Children.map(children, (child) => {
if (React.isValidElement(child)) {
iChevronRightIcon function · typescript · L270-L287 (18 LOC)src/components/ui/dropdown-menu.tsx
function ChevronRightIcon({ className }: { className?: string }) {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="9 18 15 12 9 6" />
</svg>
);
}formatTime function · typescript · L28-L32 (5 LOC)src/components/ui/segment-header.tsx
function formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, "0")}`;
}getPreviewText function · typescript · L37-L39 (3 LOC)src/components/ui/segment-header.tsx
function getPreviewText(text: string): string {
return text.length > 50 ? text.slice(0, 50) + "..." : text;
}Repobility analyzer · published findings · https://repobility.com
SegmentHeader function · typescript · L62-L148 (87 LOC)src/components/ui/segment-header.tsx
export function SegmentHeader({
paragraph,
isDeleted,
onDelete,
onRestore,
}: SegmentHeaderProps) {
const wordCount = paragraph.wordIds.length;
const duration = `${formatTime(paragraph.startTime)} - ${formatTime(
paragraph.endTime
)}`;
return (
<div
className={cn(
"flex items-center gap-2 py-2 px-3 border-b border-border/50",
"hover:bg-muted/50 transition-colors",
isDeleted && "opacity-60"
)}
>
{/* Paragraph indicator */}
<FileText
className={cn(
"w-4 h-4 shrink-0",
isDeleted ? "text-muted-foreground" : "text-primary"
)}
/>
{/* Paragraph preview */}
<span
className={cn(
"text-sm flex-1 truncate",
isDeleted
? "text-muted-foreground line-through"
: "text-foreground"
)}
>
{getPreviewText(paragraph.text)}
</span>
{/* Word count badge */}
<Badge
variant="outlineToaster function · typescript · L28-L57 (30 LOC)src/components/ui/toaster.tsx
export function Toaster() {
return (
<SonnerToaster
position="bottom-right"
toastOptions={{
duration: 4000,
style: {
background: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
color: "hsl(var(--foreground))",
borderRadius: "calc(var(--radius) - 2px)",
padding: "0.75rem 1rem",
fontSize: "0.875rem",
},
classNames: {
toast: "border-border bg-card text-foreground",
title: "font-medium text-sm",
description: "text-sm text-muted-foreground",
actionButton: "bg-primary text-primary-foreground hover:bg-primary/90",
cancelButton: "bg-muted text-muted-foreground hover:bg-muted/80 border border-border",
closeButton: "bg-transparent text-muted-foreground hover:bg-muted hover:text-foreground",
success: "border-success/30 bg-success/10",
error: "border-destructive/30 bg-destructive/10",
warnTooltip function · typescript · L20-L100 (81 LOC)src/components/ui/tooltip.tsx
export function Tooltip({ content, children, side = "top" }: TooltipProps) {
const [isVisible, setIsVisible] = React.useState(false);
const [position, setPosition] = React.useState({ top: 0, left: 0 });
const triggerRef = React.useRef<HTMLDivElement>(null);
const handleMouseEnter = () => {
if (triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect();
let top = 0;
let left = rect.left + rect.width / 2;
switch (side) {
case "top":
top = rect.bottom + 8;
break;
case "bottom":
top = rect.bottom + 8;
break;
case "left":
top = rect.top + rect.height / 2;
left = rect.left - 8;
break;
case "right":
top = rect.top + rect.height / 2;
left = rect.right + 8;
break;
}
setPosition({ top, left });
setIsVisible(true);
}
};
const handleMouseLeave = () => {
setIsVisible(false);
};
VideoPlayer function · typescript · L24-L421 (398 LOC)src/components/VideoPlayer.tsx
export function VideoPlayer({
videoSrc,
duration,
currentTime,
isPlaying,
playbackSpeed,
removedSegments = [],
onTimeUpdate,
onPlayPause,
onSeek,
onSpeedChange,
}: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const playIntentRef = useRef(false);
const currentTimeRef = useRef(currentTime);
const onTimeUpdateRef = useRef(onTimeUpdate);
const onSeekRef = useRef(onSeek);
const scrubberContainerRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [showControls, setShowControls] = useState(false);
const [videoDuration, setVideoDuration] = useState(0);
// Sync refs with props
useEffect(() => {
currentTimeRef.current = currentTime;
onTimeUpdateRef.current = onTimeUpdate;
}, [currentTime, onTimeUpdate]);
useEffect(() => {
onSeekRef.current = onSeek;
}, [onSeek]);
// Effective duration (excluding removed segments)
coWordSpan function · typescript · L37-L87 (51 LOC)src/components/WordSpan.tsx
export function WordSpan({
word,
isDeleted,
isSelected,
isPlaying = false,
onClick,
onDoubleClick,
onSeek,
}: WordSpanProps) {
const handleClick = (e: React.MouseEvent) => {
// If seek is available and not selecting, seek on click
if (onSeek && !e.shiftKey && !isDeleted) {
onSeek(word.startTime);
}
// Always trigger selection for multi-select (pass event for Shift detection)
onClick(word, e);
};
const handleDoubleClick = () => {
onDoubleClick(word);
};
return (
<span
data-word-id={word.id}
data-start-time={word.startTime}
data-end-time={word.endTime}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
className={cn(
// Base styles
"inline transition-colors cursor-pointer rounded px-0.5 break-words",
// Deleted state
isDeleted && "line-through text-muted-foreground opacity-50",
// Playing state (highest priority visual feedback)
!isDeleted &ThemeProvider function · typescript · L20-L71 (52 LOC)src/contexts/ThemeContext.tsx
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<ThemeMode>("dark");
const [resolvedTheme, setResolvedTheme] = useState<"light" | "dark">("dark");
// Load theme from Rust backend on mount
useEffect(() => {
invoke<string>("get_theme")
.then((savedTheme) => {
setThemeState(savedTheme as ThemeMode);
})
.catch(() => setThemeState("dark"));
}, []);
// Resolve system theme and apply to document
useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const updateResolvedTheme = () => {
const isDark =
theme === "dark" || (theme === "system" && mediaQuery.matches);
setResolvedTheme(isDark ? "dark" : "light");
// Apply class to document
const root = document.documentElement;
root.classList.remove("light", "dark");
root.classList.add(isDark ? "dark" : "light");
};
updateResolvedTheme();
useTheme function · typescript · L73-L79 (7 LOC)src/contexts/ThemeContext.tsx
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}useAnalysis function · typescript · L46-L145 (100 LOC)src/hooks/useAnalysis.ts
export function useAnalysis(options?: UseAnalysisOptions): UseAnalysisResult {
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [progress, setProgress] = useState(0);
const [stage, setStage] = useState("");
const [segments, setSegments] = useState<Segment[]>([]);
const [statistics, setStatistics] = useState<AnalysisStatistics | null>(null);
const [error, setError] = useState<string | null>(null);
// Listen for progress updates
useTauriEvent<AnalysisProgress>("analysis-progress", {
onEvent: (data) => {
setProgress(data.progress);
setStage(data.stage);
},
});
// Listen for completion
useTauriEvent<{
clip_id: string;
segments: Array<{
id: string;
clip_id: string;
start_time: number;
end_time: number;
duration: number;
segment_type: "filler_word" | "silence" | "speech";
is_active: boolean;
confidence: number;
manually_edited: boolean;
content: string;
}>;
statRepobility · code-quality intelligence platform · https://repobility.com
classifyError function · typescript · L114-L174 (61 LOC)src/hooks/useExport.ts
function classifyError(error: string): ExportError {
const lower = error.toLowerCase();
// No active content
if (lower.includes("no segments") || lower.includes("empty") || lower.includes("no active content")) {
return {
code: "NO_ACTIVE_CONTENT",
message: "No active content to export",
details: "All words in the transcript are marked for deletion. Restore some words before exporting.",
recoverable: true,
};
}
// Invalid path
if (lower.includes("invalid path") || lower.includes("not found") || lower.includes("no such file")) {
return {
code: "INVALID_OUTPUT_PATH",
message: "Invalid output location",
details: "The selected output path is invalid or doesn't exist. Choose a different location.",
recoverable: true,
};
}
// Disk full
if (lower.includes("no space") || lower.includes("disk full") || lower.includes("no space left")) {
return {
code: "DISK_FULL",
message: "Not enough disk space"serializeError function · typescript · L179-L181 (3 LOC)src/hooks/useExport.ts
function serializeError(error: ExportError): string {
return JSON.stringify(error);
}deserializeError function · typescript · L186-L199 (14 LOC)src/hooks/useExport.ts
function deserializeError(errorStr: string | null): ExportError | null {
if (!errorStr) return null;
try {
return JSON.parse(errorStr) as ExportError;
} catch {
// If parsing fails, create a generic error
return {
code: "UNKNOWN_ERROR",
message: "Export failed",
details: errorStr,
recoverable: true,
};
}
}page 1 / 9next ›