search_vid
Version:
CLI tool for searching subtitles and playing videos at specific timestamps
170 lines (169 loc) • 7.35 kB
JavaScript
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"))));
}