import React from 'react'; import { Box, Text } from 'ink'; import type { TorrentFile } from '../../engine/types.js'; import { colors, borders, progressChars } from '../theme/index.js'; import { formatBytes, formatProgress, truncateText } from '../utils/format.js'; export interface FileListProps { /** Array of files in the torrent */ files: TorrentFile[]; } /** * Column widths for file list display */ const COLUMN_WIDTHS = { priority: 3, name: 40, size: 10, progress: 12, } as const; /** * Priority display configuration */ const PRIORITY_DISPLAY: Record< number, { icon: string; color: string; label: string } > = { 0: { icon: '\u2717', color: colors.muted, label: 'Skip' }, // ✗ Skip 1: { icon: '\u2193', color: colors.muted, label: 'Low' }, // ↓ Low 2: { icon: '\u2022', color: colors.primary, label: 'Normal' }, // • Normal 3: { icon: '\u2191', color: colors.success, label: 'High' }, // ↑ High }; /** * Parse file path to extract directory structure */ function parseFilePath(path: string): { dirs: string[]; filename: string } { const parts = path.split('/'); const filename = parts.pop() || path; return { dirs: parts, filename }; } /** * Build a tree-style prefix for file display * * @param depth - Nesting depth (number of parent directories) * @param isLast - Whether this is the last item at this level */ function buildTreePrefix(depth: number, isLast: boolean = false): string { if (depth === 0) return ''; const indent = ' '.repeat(depth - 1); const connector = isLast ? '\u2514' : '\u251c'; // └ or ├ return `${indent}${connector}\u2500 `; // ─ } /** * Create a mini progress bar for file progress */ function createMiniProgressBar(progress: number, width: number = 6): string { const filled = Math.round(progress * width); const empty = width - filled; return ( progressChars.filled.repeat(filled) + progressChars.empty.repeat(empty) ); } /** * Header row for file list */ const FileListHeader: React.FC = () => { return ( Pri File Size Progress {borders.horizontal.repeat( COLUMN_WIDTHS.priority + COLUMN_WIDTHS.name + COLUMN_WIDTHS.size + COLUMN_WIDTHS.progress )} ); }; /** * Single file row component */ const FileRow: React.FC<{ file: TorrentFile; treePrefix: string }> = ({ file, treePrefix, }) => { const { filename } = parseFilePath(file.path); const progress = file.size > 0 ? file.downloaded / file.size : 0; const priorityInfo = PRIORITY_DISPLAY[file.priority] || PRIORITY_DISPLAY[2]; // Build display name with tree structure const displayName = treePrefix + truncateText(filename, COLUMN_WIDTHS.name - treePrefix.length); // Determine progress color const progressColor = progress >= 1 ? colors.success : progress > 0 ? colors.primary : colors.muted; return ( {priorityInfo.icon} {displayName.padEnd(COLUMN_WIDTHS.name)} {formatBytes(file.size).padStart(COLUMN_WIDTHS.size)} {createMiniProgressBar(progress)}{' '} {formatProgress(progress).padStart(4)} ); }; /** * Directory row component for multi-file torrents */ const DirectoryRow: React.FC<{ name: string; depth: number }> = ({ name, depth, }) => { const prefix = depth > 0 ? ' '.repeat(depth - 1) + '\u251c\u2500 ' : ''; const displayName = prefix + '\u{1F4C1} ' + name; // 📁 return ( {displayName} ); }; /** * Empty state when no files */ const EmptyState: React.FC = () => { return ( No files ); }; /** * Build file tree structure for display */ interface FileTreeNode { type: 'file' | 'directory'; name: string; file?: TorrentFile; depth: number; isLast: boolean; } function buildFileTree(files: TorrentFile[]): FileTreeNode[] { // Sort files by path for consistent tree structure const sortedFiles = [...files].sort((a, b) => a.path.localeCompare(b.path)); // Track seen directories to avoid duplicates const seenDirs = new Set(); const nodes: FileTreeNode[] = []; for (let i = 0; i < sortedFiles.length; i++) { const file = sortedFiles[i]; const { dirs, filename } = parseFilePath(file.path); // Add directory nodes let currentPath = ''; for (let d = 0; d < dirs.length; d++) { currentPath = currentPath ? `${currentPath}/${dirs[d]}` : dirs[d]; if (!seenDirs.has(currentPath)) { seenDirs.add(currentPath); nodes.push({ type: 'directory', name: dirs[d], depth: d, isLast: false, }); } } // Add file node const isLast = i === sortedFiles.length - 1; nodes.push({ type: 'file', name: filename, file, depth: dirs.length, isLast, }); } return nodes; } /** * FileList component for displaying torrent files * * Shows a tree-structured list of files within the torrent, * including file path, size, progress, and download priority. * * @example * * * // Output (single file): * // Pri File Size Progress * // ────────────────────────────────────────────────────────────── * // • ubuntu-24.04.iso 4.7 GB ██████ 100% * * // Output (multi-file): * // Pri File Size Progress * // ────────────────────────────────────────────────────────────── * // 📁 Album Name * // • ├─ 01 - Track One.mp3 8.2 MB ██████ 100% * // • └─ 02 - Track Two.mp3 7.1 MB ████░░ 67% */ export const FileList: React.FC = ({ files }) => { if (files.length === 0) { return ; } // For single file torrents, render simply if (files.length === 1) { return ( ); } // For multi-file torrents, build and render tree const tree = buildFileTree(files); return ( {tree.map((node) => { if (node.type === 'directory') { return ( ); } return ( ); })} ); }; export default FileList;