120 lines
6.6 KiB
TypeScript
120 lines
6.6 KiB
TypeScript
import React from 'react';
|
|
import { ArrowUp } from 'lucide-react';
|
|
import type { INode } from './types';
|
|
import { FOCUS_BG, FOCUS_BORDER, SELECTED_BG, SELECTED_BORDER } from './types';
|
|
import { getMimeCategory, CATEGORY_STYLE, formatSize, formatDate } from './helpers';
|
|
import { NodeIcon } from './ThumbPreview';
|
|
|
|
// ── Props ────────────────────────────────────────────────────────
|
|
|
|
interface FileListViewProps {
|
|
listRef: React.RefObject<HTMLDivElement>;
|
|
sorted: INode[];
|
|
canGoUp: boolean;
|
|
goUp: () => void;
|
|
focusIdx: number;
|
|
setFocusIdx: (idx: number) => void;
|
|
selected: INode[];
|
|
onItemClick: (idx: number, e?: React.MouseEvent) => void;
|
|
onItemDoubleClick: (idx: number) => void;
|
|
fontSize: number;
|
|
mode: string;
|
|
/** Current type-ahead search buffer for visual highlight */
|
|
searchBuffer?: string;
|
|
isSearchMode?: boolean;
|
|
}
|
|
|
|
// ── Component ────────────────────────────────────────────────────
|
|
|
|
const FileListView: React.FC<FileListViewProps> = ({
|
|
listRef, sorted, canGoUp, goUp, focusIdx, setFocusIdx,
|
|
selected, onItemClick, onItemDoubleClick, fontSize, mode, searchBuffer, isSearchMode
|
|
}) => (
|
|
<div ref={listRef as any} data-testid="file-list-view" style={{ overflowY: 'auto', flex: 1, padding: 2 }}>
|
|
{canGoUp && !isSearchMode && (
|
|
<div data-fb-idx={0} onClick={() => setFocusIdx(0)} onDoubleClick={goUp}
|
|
data-testid="file-list-node-up"
|
|
className="fb-row" style={{
|
|
display: 'flex', alignItems: 'center', gap: 8, padding: '5px 10px',
|
|
cursor: 'pointer', fontSize, borderBottom: '1px solid rgba(255,255,255,0.06)',
|
|
background: focusIdx === 0 ? FOCUS_BG : 'transparent',
|
|
borderLeftWidth: 2, borderLeftColor: 'transparent',
|
|
borderLeftStyle: 'solid',
|
|
outline: focusIdx === 0 ? `2px solid ${FOCUS_BORDER}` : 'none',
|
|
outlineOffset: '-2px',
|
|
}}>
|
|
<ArrowUp size={14} style={{ color: CATEGORY_STYLE.dir.color }} />
|
|
<span style={{ fontWeight: 500 }}>..</span>
|
|
</div>
|
|
)}
|
|
{sorted.map((node, i) => {
|
|
// Note: in search mode, canGoUp is usually false, or we explicitly don't shift index by 1 since there is no ".." node rendered
|
|
const idx = ((canGoUp && !isSearchMode) ? i + 1 : i);
|
|
const isDir = getMimeCategory(node) === 'dir';
|
|
const isFocused = focusIdx === idx;
|
|
const isSelected = selected.some(sel => sel.path === node.path);
|
|
return (
|
|
<div key={node.path || node.name} data-fb-idx={idx}
|
|
data-testid="file-list-node"
|
|
data-node-id={node.path || node.name}
|
|
onClick={(e) => onItemClick(idx, e)}
|
|
onDoubleClick={() => onItemDoubleClick(idx)}
|
|
className="fb-row" style={{
|
|
display: 'flex', alignItems: 'center', gap: 8, padding: isSearchMode ? '8px 10px' : '5px 10px',
|
|
cursor: 'pointer', fontSize,
|
|
borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.06)', borderBottomStyle: 'solid',
|
|
background: isSelected ? SELECTED_BG : isFocused ? FOCUS_BG : 'transparent',
|
|
borderLeftWidth: 2, borderLeftColor: isSelected ? SELECTED_BORDER : 'transparent',
|
|
borderLeftStyle: isSelected ? 'outset' : 'solid',
|
|
outline: isFocused ? `2px solid ${FOCUS_BORDER}` : 'none',
|
|
outlineOffset: '-2px',
|
|
}}>
|
|
<NodeIcon node={node} />
|
|
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column', gap: isSearchMode ? 2 : 0 }}>
|
|
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
{(() => {
|
|
if (!isFocused || !searchBuffer) return node.name;
|
|
const lower = node.name.toLowerCase();
|
|
const pos = lower.startsWith(searchBuffer) ? 0 : lower.indexOf(searchBuffer);
|
|
if (pos < 0) return node.name;
|
|
const isDark = document.documentElement.classList.contains('dark');
|
|
const hlStyle = {
|
|
background: isDark ? 'rgba(14,165,233,0.3)' : 'rgba(245,158,11,0.25)',
|
|
color: isDark ? '#bae6fd' : '#92400e',
|
|
borderBottom: isDark ? '1px solid #38bdf8' : '1px solid #f59e0b',
|
|
};
|
|
return (
|
|
<>
|
|
{pos > 0 && node.name.slice(0, pos)}
|
|
<span style={hlStyle}>
|
|
{node.name.slice(pos, pos + searchBuffer.length)}
|
|
</span>
|
|
{node.name.slice(pos + searchBuffer.length)}
|
|
</>
|
|
);
|
|
})()}
|
|
</span>
|
|
{isSearchMode && (
|
|
<span style={{ fontSize: 10, color: 'var(--muted-foreground)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
{node.parent || '/'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{node.size !== undefined && (
|
|
<span style={{ color: 'var(--muted-foreground, #64748b)', fontSize: 10, flexShrink: 0 }}>
|
|
{formatSize(node.size)}
|
|
</span>
|
|
)}
|
|
{mode === 'advanced' && node.mtime && (
|
|
<span style={{ color: 'var(--muted-foreground, #64748b)', fontSize: 10, flexShrink: 0, width: 120, textAlign: 'right' }}>
|
|
{formatDate(node.mtime)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
|
|
export default FileListView;
|