@@ -8,13 +8,53 @@ import { useAppLogic } from '../hooks/useAppLogic';
88import ScrollSlider from './ScrollSlider' ;
99import StructureView from './StructureView' ;
1010import ScrollToTopButton from './ScrollToTopButton' ;
11+ import { FileNode } from '../types' ;
1112
1213interface MainContentProps {
1314 logic : ReturnType < typeof useAppLogic > ;
1415 codeViewRef : React . RefObject < HTMLDivElement > ;
1516 leftPanelRef : React . RefObject < HTMLDivElement > ;
1617}
1718
19+ /** Recursively collect unique file extensions from a tree */
20+ function collectExtensions ( nodes : FileNode [ ] ) : string [ ] {
21+ const exts = new Set < string > ( ) ;
22+ const walk = ( items : FileNode [ ] ) => {
23+ for ( const node of items ) {
24+ if ( node . isDirectory ) {
25+ walk ( node . children ) ;
26+ } else {
27+ const dot = node . name . lastIndexOf ( '.' ) ;
28+ if ( dot > 0 && dot < node . name . length - 1 ) {
29+ exts . add ( node . name . slice ( dot ) . toLowerCase ( ) ) ;
30+ }
31+ }
32+ }
33+ } ;
34+ walk ( nodes ) ;
35+ return Array . from ( exts ) . sort ( ) ;
36+ }
37+
38+ /** Recursively filter tree to files matching `ext`, keeping directories that have matching children */
39+ function filterTreeByExtension ( nodes : FileNode [ ] , ext : string | null ) : FileNode [ ] {
40+ if ( ! ext ) return nodes ;
41+ return nodes . reduce < FileNode [ ] > ( ( acc , node ) => {
42+ if ( node . isDirectory ) {
43+ const filteredChildren = filterTreeByExtension ( node . children , ext ) ;
44+ if ( filteredChildren . length > 0 ) {
45+ acc . push ( { ...node , children : filteredChildren } ) ;
46+ }
47+ } else {
48+ const dot = node . name . lastIndexOf ( '.' ) ;
49+ const fileExt = dot > 0 && dot < node . name . length - 1 ? node . name . slice ( dot ) . toLowerCase ( ) : '' ;
50+ if ( fileExt === ext ) {
51+ acc . push ( node ) ;
52+ }
53+ }
54+ return acc ;
55+ } , [ ] ) ;
56+ }
57+
1858const LoadingIndicator : React . FC < { message : string } > = ( { message} ) => (
1959 < div className = "flex flex-col items-center justify-center h-full text-center p-4" >
2060 < i className = "fa-solid fa-spinner fa-spin text-4xl text-primary mb-4" > </ i >
@@ -27,6 +67,11 @@ const MainContent: React.FC<MainContentProps> = ({ logic, codeViewRef, leftPanel
2767 const { state, handlers } = logic ;
2868 const { isMobile } = state ;
2969 const fileTreeScrollRef = React . useRef < HTMLDivElement > ( null ) ;
70+ const [ filterExt , setFilterExt ] = React . useState < string | null > ( null ) ;
71+
72+ const treeData = state . processedData ?. treeData || [ ] ;
73+ const extensions = React . useMemo ( ( ) => collectExtensions ( treeData ) , [ treeData ] ) ;
74+ const filteredNodes = React . useMemo ( ( ) => filterTreeByExtension ( treeData , filterExt ) , [ treeData , filterExt ] ) ;
3075
3176 const mobileFabIcon = ( ) => {
3277 if ( ! state . processedData ) return 'fa-list-ul' ;
@@ -41,8 +86,21 @@ const MainContent: React.FC<MainContentProps> = ({ logic, codeViewRef, leftPanel
4186 < main className = "flex-1 flex overflow-hidden relative" >
4287 < AnimatePresence >
4388 { state . isDragging && (
44- < motion . div initial = { { opacity : 0 } } animate = { { opacity : 1 } } exit = { { opacity : 0 } } className = "absolute inset-0 bg-primary/50 backdrop-blur-sm flex items-center justify-center z-30 pointer-events-none" >
45- < div className = "text-center text-white bg-primary/80 p-8 rounded-lg" > < i className = "fa-solid fa-upload fa-3x mb-4" > </ i > < p className = "text-xl font-bold" > 拖放文件夹以进行分析</ p > </ div >
89+ < motion . div
90+ initial = { { opacity : 0 } }
91+ animate = { { opacity : 1 } }
92+ exit = { { opacity : 0 } }
93+ className = "absolute inset-0 z-30 pointer-events-none flex items-center justify-center bg-white/70 dark:bg-dark-bg/70 backdrop-blur-sm"
94+ >
95+ < div className = "border-4 border-dashed border-primary/60 rounded-2xl p-12 flex flex-col items-center gap-4 max-w-md mx-4" >
96+ < motion . i
97+ className = "fa-solid fa-cloud-arrow-up text-5xl text-primary"
98+ animate = { { scale : [ 1 , 1.15 , 1 ] } }
99+ transition = { { duration : 1.5 , repeat : Infinity , ease : 'easeInOut' } }
100+ />
101+ < p className = "text-xl font-bold text-light-text dark:text-dark-text" > 拖放文件夹或 .zip 文件</ p >
102+ < p className = "text-sm text-light-subtle-text dark:text-dark-subtle-text" > 支持任意代码项目</ p >
103+ </ div >
46104 </ motion . div >
47105 ) }
48106 </ AnimatePresence >
@@ -52,14 +110,31 @@ const MainContent: React.FC<MainContentProps> = ({ logic, codeViewRef, leftPanel
52110 < AnimatePresence initial = { false } >
53111 { state . mobileView === 'tree' && state . processedData && (
54112 < motion . div key = "tree" initial = { { x : '-100%' } } animate = { { x : '0%' } } exit = { { x : '-100%' } } transition = { { duration : 0.3 , ease : 'easeInOut' } } className = "absolute inset-0 h-full overflow-y-auto bg-light-panel dark:bg-dark-panel" >
55- < FileTree
56- nodes = { state . processedData . treeData || [ ] }
57- onFileSelect = { handlers . handleFileTreeSelect }
58- onDeleteFile = { handlers . handleDeleteFile }
59- onCopyPath = { handlers . handleCopyPath }
113+ { extensions . length > 0 && (
114+ < div className = "flex flex-wrap gap-1.5 px-3 pt-2 pb-1 border-b border-light-border dark:border-dark-border" >
115+ { extensions . map ( ext => (
116+ < button
117+ key = { ext }
118+ onClick = { ( ) => setFilterExt ( filterExt === ext ? null : ext ) }
119+ className = { `px-2.5 py-0.5 rounded-full text-xs font-medium transition-colors ${
120+ filterExt === ext
121+ ? 'bg-primary text-white'
122+ : 'bg-light-hover dark:bg-dark-hover text-light-subtle-text dark:text-dark-subtle-text hover:bg-primary/20 dark:hover:bg-primary/20'
123+ } `}
124+ >
125+ { ext }
126+ </ button >
127+ ) ) }
128+ </ div >
129+ ) }
130+ < FileTree
131+ nodes = { filteredNodes }
132+ onFileSelect = { handlers . handleFileTreeSelect }
133+ onDeleteFile = { handlers . handleDeleteFile }
134+ onCopyPath = { handlers . handleCopyPath }
60135 onToggleExclude = { handlers . handleToggleExclude }
61- selectedFilePath = { state . selectedFilePath }
62- showCharCount = { state . showCharCount }
136+ selectedFilePath = { state . selectedFilePath }
137+ showCharCount = { state . showCharCount }
63138 />
64139 </ motion . div >
65140 ) }
@@ -105,15 +180,34 @@ const MainContent: React.FC<MainContentProps> = ({ logic, codeViewRef, leftPanel
105180 < div ref = { leftPanelRef } className = "relative h-full bg-light-panel dark:bg-dark-panel" style = { { width : `${ state . panelWidth } %` } } >
106181 < div ref = { fileTreeScrollRef } className = "h-full overflow-y-auto no-scrollbar" >
107182 { state . processedData && (
108- < FileTree
109- nodes = { state . processedData . treeData || [ ] }
110- onFileSelect = { handlers . handleFileTreeSelect }
111- onDeleteFile = { handlers . handleDeleteFile }
112- onCopyPath = { handlers . handleCopyPath }
113- onToggleExclude = { handlers . handleToggleExclude }
114- selectedFilePath = { state . selectedFilePath }
115- showCharCount = { state . showCharCount }
116- />
183+ < >
184+ { extensions . length > 0 && (
185+ < div className = "flex flex-wrap gap-1.5 px-3 pt-2 pb-1 border-b border-light-border dark:border-dark-border" >
186+ { extensions . map ( ext => (
187+ < button
188+ key = { ext }
189+ onClick = { ( ) => setFilterExt ( filterExt === ext ? null : ext ) }
190+ className = { `px-2.5 py-0.5 rounded-full text-xs font-medium transition-colors ${
191+ filterExt === ext
192+ ? 'bg-primary text-white'
193+ : 'bg-light-hover dark:bg-dark-hover text-light-subtle-text dark:text-dark-subtle-text hover:bg-primary/20 dark:hover:bg-primary/20'
194+ } `}
195+ >
196+ { ext }
197+ </ button >
198+ ) ) }
199+ </ div >
200+ ) }
201+ < FileTree
202+ nodes = { filteredNodes }
203+ onFileSelect = { handlers . handleFileTreeSelect }
204+ onDeleteFile = { handlers . handleDeleteFile }
205+ onCopyPath = { handlers . handleCopyPath }
206+ onToggleExclude = { handlers . handleToggleExclude }
207+ selectedFilePath = { state . selectedFilePath }
208+ showCharCount = { state . showCharCount }
209+ />
210+ </ >
117211 ) }
118212 </ div >
119213 { state . processedData && < ScrollSlider scrollRef = { fileTreeScrollRef } /> }
0 commit comments