3
3
4
4
import { ContextMenuModel } from "@/app/store/contextmenu" ;
5
5
import { atoms , createBlock , getApi } from "@/app/store/global" ;
6
+ import { FileService } from "@/app/store/services" ;
6
7
import type { PreviewModel } from "@/app/view/preview/preview" ;
7
- import * as services from "@/store/services" ;
8
- import * as keyutil from "@/util/keyutil" ;
9
- import * as util from "@/util/util" ;
8
+ import { checkKeyPressed , isCharacterKeyEvent } from "@/util/keyutil" ;
9
+ import { base64ToString , isBlank } from "@/util/util" ;
10
10
import {
11
11
Column ,
12
12
Row ,
@@ -19,14 +19,11 @@ import {
19
19
} from "@tanstack/react-table" ;
20
20
import clsx from "clsx" ;
21
21
import dayjs from "dayjs" ;
22
- import * as jotai from "jotai" ;
23
- import { OverlayScrollbarsComponent } from "overlayscrollbars-react" ;
22
+ import { useAtom , useAtomValue } from "jotai" ;
23
+ import { OverlayScrollbarsComponent , OverlayScrollbarsComponentRef } from "overlayscrollbars-react" ;
24
24
import React , { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
25
25
import { quote as shellQuote } from "shell-quote" ;
26
-
27
- import { OverlayScrollbars } from "overlayscrollbars" ;
28
-
29
- import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions" ;
26
+ import { debounce } from "throttle-debounce" ;
30
27
import "./directorypreview.less" ;
31
28
32
29
interface DirectoryTableProps {
@@ -95,7 +92,7 @@ function getLastModifiedTime(unixMillis: number, column: Column<FileInfo, number
95
92
const iconRegex = / ^ [ a - z 0 - 9 - ] + $ / ;
96
93
97
94
function isIconValid ( icon : string ) : boolean {
98
- if ( util . isBlank ( icon ) ) {
95
+ if ( isBlank ( icon ) ) {
99
96
return false ;
100
97
}
101
98
return icon . match ( iconRegex ) != null ;
@@ -134,11 +131,11 @@ function DirectoryTable({
134
131
setSelectedPath,
135
132
setRefreshVersion,
136
133
} : DirectoryTableProps ) {
137
- const fullConfig = jotai . useAtomValue ( atoms . fullConfigAtom ) ;
134
+ const fullConfig = useAtomValue ( atoms . fullConfigAtom ) ;
138
135
const getIconFromMimeType = useCallback (
139
136
( mimeType : string ) : string => {
140
137
while ( mimeType . length > 0 ) {
141
- let icon = fullConfig . mimetypes ?. [ mimeType ] ?. icon ?? null ;
138
+ const icon = fullConfig . mimetypes ?. [ mimeType ] ?. icon ?? null ;
142
139
if ( isIconValid ( icon ) ) {
143
140
return `fa fa-solid fa-${ icon } fa-fw` ;
144
141
}
@@ -149,10 +146,7 @@ function DirectoryTable({
149
146
[ fullConfig . mimetypes ]
150
147
) ;
151
148
const getIconColor = useCallback (
152
- ( mimeType : string ) : string => {
153
- let iconColor = fullConfig . mimetypes ?. [ mimeType ] ?. color ?? "inherit" ;
154
- return iconColor ;
155
- } ,
149
+ ( mimeType : string ) : string => fullConfig . mimetypes ?. [ mimeType ] ?. color ?? "inherit" ,
156
150
[ fullConfig . mimetypes ]
157
151
) ;
158
152
const columns = useMemo (
@@ -261,8 +255,25 @@ function DirectoryTable({
261
255
return colSizes ;
262
256
} , [ table . getState ( ) . columnSizingInfo ] ) ;
263
257
258
+ const osRef = useRef < OverlayScrollbarsComponentRef > ( ) ;
259
+ const bodyRef = useRef < HTMLDivElement > ( ) ;
260
+ const [ scrollHeight , setScrollHeight ] = useState ( 0 ) ;
261
+
262
+ const onScroll = useCallback (
263
+ debounce ( 2 , ( ) => {
264
+ setScrollHeight ( osRef . current . osInstance ( ) . elements ( ) . viewport . scrollTop ) ;
265
+ } ) ,
266
+ [ ]
267
+ ) ;
264
268
return (
265
- < div className = "dir-table" style = { { ...columnSizeVars } } >
269
+ < OverlayScrollbarsComponent
270
+ options = { { scrollbars : { autoHide : "leave" } } }
271
+ events = { { scroll : onScroll } }
272
+ className = "dir-table"
273
+ style = { { ...columnSizeVars } }
274
+ ref = { osRef }
275
+ data-scroll-height = { scrollHeight }
276
+ >
266
277
< div className = "dir-table-head" >
267
278
{ table . getHeaderGroups ( ) . map ( ( headerGroup ) => (
268
279
< div className = "dir-table-head-row" key = { headerGroup . id } >
@@ -295,6 +306,7 @@ function DirectoryTable({
295
306
</ div >
296
307
{ table . getState ( ) . columnSizingInfo . isResizingColumn ? (
297
308
< MemoizedTableBody
309
+ bodyRef = { bodyRef }
298
310
model = { model }
299
311
data = { data }
300
312
table = { table }
@@ -304,9 +316,11 @@ function DirectoryTable({
304
316
setSearch = { setSearch }
305
317
setSelectedPath = { setSelectedPath }
306
318
setRefreshVersion = { setRefreshVersion }
319
+ osRef = { osRef . current }
307
320
/>
308
321
) : (
309
322
< TableBody
323
+ bodyRef = { bodyRef }
310
324
model = { model }
311
325
data = { data }
312
326
table = { table }
@@ -316,13 +330,15 @@ function DirectoryTable({
316
330
setSearch = { setSearch }
317
331
setSelectedPath = { setSelectedPath }
318
332
setRefreshVersion = { setRefreshVersion }
333
+ osRef = { osRef . current }
319
334
/>
320
335
) }
321
- </ div >
336
+ </ OverlayScrollbarsComponent >
322
337
) ;
323
338
}
324
339
325
340
interface TableBodyProps {
341
+ bodyRef : React . RefObject < HTMLDivElement > ;
326
342
model : PreviewModel ;
327
343
data : Array < FileInfo > ;
328
344
table : Table < FileInfo > ;
@@ -332,48 +348,32 @@ interface TableBodyProps {
332
348
setSearch : ( _ : string ) => void ;
333
349
setSelectedPath : ( _ : string ) => void ;
334
350
setRefreshVersion : React . Dispatch < React . SetStateAction < number > > ;
351
+ osRef : OverlayScrollbarsComponentRef ;
335
352
}
336
353
337
354
function TableBody ( {
355
+ bodyRef,
338
356
model,
339
- data,
340
357
table,
341
358
search,
342
359
focusIndex,
343
360
setFocusIndex,
344
361
setSearch,
345
- setSelectedPath,
346
362
setRefreshVersion,
363
+ osRef,
347
364
} : TableBodyProps ) {
348
- const [ bodyHeight , setBodyHeight ] = useState ( 0 ) ;
349
-
350
- const dummyLineRef = useRef < HTMLDivElement > ( null ) ;
351
- const parentRef = useRef < HTMLDivElement > ( null ) ;
352
- const warningBoxRef = useRef < HTMLDivElement > ( null ) ;
353
- const osInstanceRef = useRef < OverlayScrollbars > ( null ) ;
365
+ const dummyLineRef = useRef < HTMLDivElement > ( ) ;
366
+ const warningBoxRef = useRef < HTMLDivElement > ( ) ;
354
367
const rowRefs = useRef < HTMLDivElement [ ] > ( [ ] ) ;
355
- const domRect = useDimensionsWithExistingRef ( parentRef , 30 ) ;
356
- const parentHeight = domRect ?. height ?? 0 ;
357
- const conn = jotai . useAtomValue ( model . connection ) ;
358
-
359
- useEffect ( ( ) => {
360
- if ( dummyLineRef . current && data && parentRef . current ) {
361
- const rowHeight = dummyLineRef . current . offsetHeight ;
362
- const fullTBodyHeight = rowHeight * data . length ;
363
- const warningBoxHeight = warningBoxRef . current ?. offsetHeight ?? 0 ;
364
- const maxHeightLessHeader = parentHeight - warningBoxHeight ;
365
- const tbodyHeight = Math . min ( maxHeightLessHeader , fullTBodyHeight ) ;
366
- setBodyHeight ( tbodyHeight ) ;
367
- }
368
- } , [ data , parentHeight ] ) ;
368
+ const conn = useAtomValue ( model . connection ) ;
369
369
370
370
useEffect ( ( ) => {
371
- if ( focusIndex !== null && rowRefs . current [ focusIndex ] && parentRef . current ) {
372
- const viewport = osInstanceRef . current . elements ( ) . viewport ;
371
+ if ( focusIndex !== null && rowRefs . current [ focusIndex ] && bodyRef . current && osRef ) {
372
+ const viewport = osRef . osInstance ( ) . elements ( ) . viewport ;
373
373
const viewportHeight = viewport . offsetHeight ;
374
374
const rowElement = rowRefs . current [ focusIndex ] ;
375
375
const rowRect = rowElement . getBoundingClientRect ( ) ;
376
- const parentRect = parentRef . current . getBoundingClientRect ( ) ;
376
+ const parentRect = bodyRef . current . getBoundingClientRect ( ) ;
377
377
const viewportScrollTop = viewport . scrollTop ;
378
378
379
379
const rowTopRelativeToViewport = rowRect . top - parentRect . top + viewportScrollTop ;
@@ -387,7 +387,7 @@ function TableBody({
387
387
viewport . scrollTo ( { top : rowBottomRelativeToViewport - viewportHeight } ) ;
388
388
}
389
389
}
390
- } , [ focusIndex , parentHeight ] ) ;
390
+ } , [ focusIndex ] ) ;
391
391
392
392
const handleFileContextMenu = useCallback (
393
393
( e : any , path : string , mimetype : string ) => {
@@ -455,7 +455,7 @@ function TableBody({
455
455
menu . push ( {
456
456
label : "Delete File" ,
457
457
click : async ( ) => {
458
- await services . FileService . DeleteFile ( conn , path ) . catch ( ( e ) => console . log ( e ) ) ;
458
+ await FileService . DeleteFile ( conn , path ) . catch ( ( e ) => console . log ( e ) ) ;
459
459
setRefreshVersion ( ( current ) => current + 1 ) ;
460
460
} ,
461
461
} ) ;
@@ -492,12 +492,8 @@ function TableBody({
492
492
[ setSearch , handleFileContextMenu , setFocusIndex , focusIndex ]
493
493
) ;
494
494
495
- const handleScrollbarInitialized = ( instance ) => {
496
- osInstanceRef . current = instance ;
497
- } ;
498
-
499
495
return (
500
- < div className = "dir-table-body" ref = { parentRef } >
496
+ < div className = "dir-table-body" ref = { bodyRef } >
501
497
{ search !== "" && (
502
498
< div className = "dir-table-body-search-display" ref = { warningBoxRef } >
503
499
< span > Searching for "{ search } "</ span >
@@ -507,18 +503,13 @@ function TableBody({
507
503
</ div >
508
504
</ div >
509
505
) }
510
- < OverlayScrollbarsComponent
511
- options = { { scrollbars : { autoHide : "leave" } } }
512
- events = { { initialized : handleScrollbarInitialized } }
513
- >
514
- < div className = "dir-table-body-scroll-box" style = { { height : bodyHeight } } >
515
- < div className = "dummy dir-table-body-row" ref = { dummyLineRef } >
516
- < div className = "dir-table-body-cell" > dummy-data</ div >
517
- </ div >
518
- { table . getTopRows ( ) . map ( displayRow ) }
519
- { table . getCenterRows ( ) . map ( ( row , idx ) => displayRow ( row , idx + table . getTopRows ( ) . length ) ) }
506
+ < div className = "dir-table-body-scroll-box" >
507
+ < div className = "dummy dir-table-body-row" ref = { dummyLineRef } >
508
+ < div className = "dir-table-body-cell" > dummy-data</ div >
520
509
</ div >
521
- </ OverlayScrollbarsComponent >
510
+ { table . getTopRows ( ) . map ( displayRow ) }
511
+ { table . getCenterRows ( ) . map ( ( row , idx ) => displayRow ( row , idx + table . getTopRows ( ) . length ) ) }
512
+ </ div >
522
513
</ div >
523
514
) ;
524
515
}
@@ -537,11 +528,11 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
537
528
const [ focusIndex , setFocusIndex ] = useState ( 0 ) ;
538
529
const [ unfilteredData , setUnfilteredData ] = useState < FileInfo [ ] > ( [ ] ) ;
539
530
const [ filteredData , setFilteredData ] = useState < FileInfo [ ] > ( [ ] ) ;
540
- const fileName = jotai . useAtomValue ( model . metaFilePath ) ;
541
- const showHiddenFiles = jotai . useAtomValue ( model . showHiddenFiles ) ;
531
+ const fileName = useAtomValue ( model . metaFilePath ) ;
532
+ const showHiddenFiles = useAtomValue ( model . showHiddenFiles ) ;
542
533
const [ selectedPath , setSelectedPath ] = useState ( "" ) ;
543
- const [ refreshVersion , setRefreshVersion ] = jotai . useAtom ( model . refreshVersion ) ;
544
- const conn = jotai . useAtomValue ( model . connection ) ;
534
+ const [ refreshVersion , setRefreshVersion ] = useAtom ( model . refreshVersion ) ;
535
+ const conn = useAtomValue ( model . connection ) ;
545
536
546
537
useEffect ( ( ) => {
547
538
model . refreshCallback = ( ) => {
@@ -554,8 +545,8 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
554
545
555
546
useEffect ( ( ) => {
556
547
const getContent = async ( ) => {
557
- const file = await services . FileService . ReadFile ( conn , fileName ) ;
558
- const serializedContent = util . base64ToString ( file ?. data64 ) ;
548
+ const file = await FileService . ReadFile ( conn , fileName ) ;
549
+ const serializedContent = base64ToString ( file ?. data64 ) ;
559
550
const content : FileInfo [ ] = JSON . parse ( serializedContent ) ;
560
551
setUnfilteredData ( content ) ;
561
552
} ;
@@ -574,34 +565,34 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
574
565
575
566
useEffect ( ( ) => {
576
567
model . directoryKeyDownHandler = ( waveEvent : WaveKeyboardEvent ) : boolean => {
577
- if ( keyutil . checkKeyPressed ( waveEvent , "Escape" ) ) {
568
+ if ( checkKeyPressed ( waveEvent , "Escape" ) ) {
578
569
setSearchText ( "" ) ;
579
570
return ;
580
571
}
581
- if ( keyutil . checkKeyPressed ( waveEvent , "ArrowUp" ) ) {
572
+ if ( checkKeyPressed ( waveEvent , "ArrowUp" ) ) {
582
573
setFocusIndex ( ( idx ) => Math . max ( idx - 1 , 0 ) ) ;
583
574
return true ;
584
575
}
585
- if ( keyutil . checkKeyPressed ( waveEvent , "ArrowDown" ) ) {
576
+ if ( checkKeyPressed ( waveEvent , "ArrowDown" ) ) {
586
577
setFocusIndex ( ( idx ) => Math . min ( idx + 1 , filteredData . length - 1 ) ) ;
587
578
return true ;
588
579
}
589
- if ( keyutil . checkKeyPressed ( waveEvent , "Enter" ) ) {
580
+ if ( checkKeyPressed ( waveEvent , "Enter" ) ) {
590
581
if ( filteredData . length == 0 ) {
591
582
return ;
592
583
}
593
584
model . goHistory ( selectedPath ) ;
594
585
setSearchText ( "" ) ;
595
586
return true ;
596
587
}
597
- if ( keyutil . checkKeyPressed ( waveEvent , "Backspace" ) ) {
588
+ if ( checkKeyPressed ( waveEvent , "Backspace" ) ) {
598
589
if ( searchText . length == 0 ) {
599
590
return true ;
600
591
}
601
592
setSearchText ( ( current ) => current . slice ( 0 , - 1 ) ) ;
602
593
return true ;
603
594
}
604
- if ( keyutil . isCharacterKeyEvent ( waveEvent ) ) {
595
+ if ( isCharacterKeyEvent ( waveEvent ) ) {
605
596
setSearchText ( ( current ) => current + waveEvent . key ) ;
606
597
return true ;
607
598
}
0 commit comments