UNPKG

search_vid

Version:

CLI tool for searching subtitles and playing videos at specific timestamps

170 lines (169 loc) 7.35 kB
import React, { useState, useEffect } from 'react'; import { Box, Text, useInput } from 'ink'; import path from 'path'; function highlightText(text, query) { if (!query) return [React.createElement(Text, { key: "0" }, text)]; const parts = text.split(new RegExp(`(${query})`, 'gi')); return parts.map((part, i) => { if (part.toLowerCase() === query.toLowerCase()) { return (React.createElement(Text, { key: i, backgroundColor: "yellow", color: "black" }, part)); } return React.createElement(Text, { key: i }, part); }); } export function SearchResults({ results, query, options, onSelect, onNewSearch, onQuit, }) { const [selectedIndex, setSelectedIndex] = useState(0); const [currentPage, setCurrentPage] = useState(0); const [isSearchMode, setIsSearchMode] = useState(false); const [newQuery, setNewQuery] = useState(''); const [pageSize, setPageSize] = useState(options.pageSize ?? 10); // Update pageSize when options.pageSize changes useEffect(() => { // Only update if options.pageSize is explicitly set if (options.pageSize !== undefined) { setPageSize(Math.min(Math.max(options.pageSize, 5), 100)); } }, [options.pageSize]); const flattenedResults = React.useMemo(() => { return options.treeView ? results : results.flatMap(result => result.matches.map(match => ({ ...result, currentMatch: match, matches: [match], }))); }, [results, options.treeView]); const totalItems = options.treeView ? results.length : results.reduce((sum, result) => sum + result.matches.length, 0); const totalPages = Math.max(1, Math.ceil(flattenedResults.length / pageSize)); const startIndex = currentPage * pageSize; const endIndex = Math.min(startIndex + pageSize, flattenedResults.length); const currentResults = flattenedResults.slice(startIndex, endIndex); useEffect(() => { setSelectedIndex(0); setCurrentPage(prev => { const maxPage = Math.max(0, totalPages - 1); return Math.min(prev, maxPage); }); }, [results, pageSize, totalPages]); useInput((input, key) => { if (isSearchMode) { if (key.return) { const isExactSearch = options.exactMatch ?? true; const toggleSearch = isExactSearch ? newQuery.startsWith('?') : newQuery.startsWith('!'); const searchQuery = toggleSearch ? newQuery.slice(1) : newQuery; onNewSearch(searchQuery, toggleSearch ? !isExactSearch : isExactSearch); setIsSearchMode(false); setNewQuery(''); } else if (key.escape) { setIsSearchMode(false); setNewQuery(''); } else if (key.backspace || key.delete) { setNewQuery(prev => prev.slice(0, -1)); } else if (input && input.length === 1) { setNewQuery(prev => prev + input); } return; } const maxIndex = currentResults.length - 1; if (key.upArrow) { setSelectedIndex(prev => (prev > 0 ? prev - 1 : prev)); } else if (key.downArrow) { setSelectedIndex(prev => (prev < maxIndex ? prev + 1 : prev)); } else if (key.leftArrow) { if (currentPage > 0) { setCurrentPage(prev => prev - 1); setSelectedIndex(0); } } else if (key.rightArrow) { if (currentPage < totalPages - 1) { setCurrentPage(prev => prev + 1); setSelectedIndex(0); } } else if (key.return && currentResults[selectedIndex]) { const selectedItem = currentResults[selectedIndex]; onSelect(selectedItem); } else if (input === '/') { setIsSearchMode(true); } else if (input === 'q') { onQuit(); } else if (input === '+') { setPageSize(prev => { const newSize = prev + 5; return newSize <= 100 ? newSize : 100; }); } else if (input === '-') { setPageSize(prev => { const newSize = prev - 5; return newSize >= 5 ? newSize : 5; }); } }); const renderFlatView = () => { return (React.createElement(Box, { flexDirection: "column" }, currentResults.map((item, index) => (React.createElement(Box, { key: index }, React.createElement(Text, null, index === selectedIndex ? '>' : ' ', " "), React.createElement(Text, { backgroundColor: index === selectedIndex ? 'blue' : undefined, color: index === selectedIndex ? 'white' : undefined, bold: index === selectedIndex }, "[", startIndex + index + 1, "]", ' ', highlightText(item.currentMatch.subtitle.text, query), ' ', "- ", path.basename(item.filePath))))))); }; const renderTreeView = () => (React.createElement(Box, { flexDirection: "column" }, currentResults.map((result, index) => (React.createElement(Box, { key: index, flexDirection: "column" }, React.createElement(Box, null, React.createElement(Text, null, index === selectedIndex ? '>' : ' ', " "), React.createElement(Text, { backgroundColor: index === selectedIndex ? 'blue' : undefined, color: index === selectedIndex ? 'white' : undefined, bold: index === selectedIndex }, "[", startIndex + index + 1, "] ", path.basename(result.filePath))), result.matches.map((match, matchIndex) => (React.createElement(Box, { key: matchIndex, marginLeft: 2 }, React.createElement(Text, null, "\u2514\u2500 [", matchIndex + 1, "]", ' ', highlightText(match.subtitle.text, query)))))))))); return (React.createElement(Box, { flexDirection: "column" }, React.createElement(Box, { marginY: 1, borderStyle: "round", borderColor: "blue" }, React.createElement(Text, { bold: true, color: "blue" }, "Results for \"", query, "\" (", totalItems, " matches)")), options.treeView ? renderTreeView() : renderFlatView(), isSearchMode && (React.createElement(Box, { marginY: 1 }, React.createElement(Text, null, newQuery || (options.exactMatch ?? true ? 'Type to search (? for fuzzy search)' : 'Type to search (! for exact search)')))), React.createElement(Box, { marginTop: 2 }, React.createElement(Text, { dimColor: true }, currentPage + 1, "/", totalPages, " | \u2191\u2193:nav | \u2190/\u2192:pages | /:search | +/-:size | esc:select | q:quit")))); }