import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Box, Text, useInput } from 'ink'; import { homedir } from 'os'; import { join } from 'path'; import { colors } from '../theme/index.js'; import { Modal } from './Modal.js'; import { TextInput } from './TextInput.js'; import { DirectoryBrowser } from './DirectoryBrowser.js'; import { parsePastedPaths, filterTorrentFiles } from '../hooks/usePaste.js'; function getDefaultDownloadsPath(): string { return join(homedir(), 'Downloads'); } export interface AddTorrentModalProps { visible: boolean; onAdd: (source: string, downloadPath: string) => void; onClose: () => void; /** Default download path from settings */ defaultDownloadPath?: string; /** Called when multiple .torrent files are detected (drag-and-drop) */ onBatchFiles?: (files: string[]) => void; } function isValidTorrentSource(value: string): boolean { const trimmed = value.trim(); if (trimmed.length === 0) return false; if (trimmed.startsWith('magnet:?')) return true; if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) return true; if (trimmed.endsWith('.torrent')) return true; return false; } /** * Expand ~ to home directory in paths */ function expandPath(path: string): string { if (path.startsWith('~/')) { const home = process.env.HOME || process.env.USERPROFILE || ''; return home + path.slice(1); } return path; } export const AddTorrentModal: React.FC = ({ visible, onAdd, onClose, defaultDownloadPath, onBatchFiles, }) => { const [source, setSource] = useState(''); const [downloadPath, setDownloadPath] = useState(''); const [activeField, setActiveField] = useState<'source' | 'path'>('source'); const wasVisible = useRef(false); // Get the effective default path const effectiveDefaultPath = defaultDownloadPath || getDefaultDownloadsPath(); // Reset when modal opens, restore defaults when it closes useEffect(() => { if (visible && !wasVisible.current) { // Modal just opened - set to defaults setSource(''); setDownloadPath(effectiveDefaultPath); setActiveField('source'); } else if (!visible && wasVisible.current) { // Modal just closed - reset state setSource(''); setDownloadPath(''); setActiveField('source'); } wasVisible.current = visible; }, [visible, effectiveDefaultPath]); // Detect .torrent files in source (drag-and-drop detection) const handleSourceChange = useCallback( (value: string) => { // Check if this looks like .torrent file(s) were dropped const paths = parsePastedPaths(value); const expandedPaths = paths.map(expandPath); const torrentFiles = filterTorrentFiles(expandedPaths); if (torrentFiles.length >= 1 && onBatchFiles) { // .torrent file(s) detected - switch to batch mode for visual feedback onBatchFiles(torrentFiles); return; } // Other input (magnet link, URL, etc.) - handle normally setSource(value); }, [onBatchFiles] ); const isValid = isValidTorrentSource(source); const isEmpty = source.trim().length === 0; const hasPath = downloadPath.trim().length > 0; const canSubmit = isValid && hasPath; // Handle keyboard when source field is active useInput( (input, key) => { // Enter to submit when valid if (key.return && canSubmit) { onAdd(source.trim(), downloadPath.trim()); return; } // Down arrow to go to path field if (key.downArrow) { setActiveField('path'); return; } }, { isActive: visible && activeField === 'source' } ); // Handle Esc from path field to go back to source const handlePathEscape = () => { setActiveField('source'); }; // Handle Enter from path field to submit const handlePathSubmit = () => { if (canSubmit) { onAdd(source.trim(), downloadPath.trim()); } }; const modalWidth = 72; const contentWidth = modalWidth - 8; return ( {/* Source input */} Magnet link, URL, or file path: {/* Validation hint */} {isEmpty ? 'Paste magnet/URL, or drag .torrent files here' : isValid ? 'Valid torrent source' : 'Enter a magnet link, URL, or .torrent file path'} {/* Download path with DirectoryBrowser */} Download to: {/* Contextual help text */} {activeField === 'source' ? ( [Enter] Add [↓] Edit path [Esc] Cancel ) : ( ↑↓: select Tab/→: complete Enter: add Esc: back )} ); }; export default AddTorrentModal;