1
- import React , { useState } from 'react' ;
1
+ import React , { useState , useRef , useEffect } from 'react' ;
2
2
import { FileInfo , deleteFile , deleteDirectory } from '../../lib/api' ;
3
3
import useFileExplorerStore from '../../store/fileExplorer' ;
4
4
import ContextMenu from '../ContextMenu/ContextMenu' ;
@@ -27,6 +27,11 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
27
27
const [ childrenLoaded , setChildrenLoaded ] = useState ( false ) ;
28
28
const [ loadedChildren , setLoadedChildren ] = useState < ( FileInfo & { children ?: FileInfo [ ] } ) [ ] > ( [ ] ) ;
29
29
30
+ // Touch handling for iOS long-press
31
+ const touchTimerRef = useRef < number | null > ( null ) ;
32
+ const touchStartPos = useRef < { x : number ; y : number } | null > ( null ) ;
33
+ const longPressTriggered = useRef ( false ) ;
34
+
30
35
const handleClick = ( e : React . MouseEvent ) => {
31
36
if ( e . ctrlKey || e . metaKey ) {
32
37
toggleFileSelection ( file . path ) ;
@@ -40,17 +45,17 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
40
45
const buildTreeFromFlatList = ( flatList : FileInfo [ ] , parentPath : string ) : ( FileInfo & { children ?: FileInfo [ ] } ) [ ] => {
41
46
const fileMap = new Map < string , FileInfo & { children ?: FileInfo [ ] } > ( ) ;
42
47
const topLevelFiles : ( FileInfo & { children ?: FileInfo [ ] } ) [ ] = [ ] ;
43
-
48
+
44
49
// First pass: create map of all files
45
50
flatList . forEach ( file => {
46
51
fileMap . set ( file . path , { ...file , children : [ ] } ) ;
47
52
} ) ;
48
-
53
+
49
54
// Second pass: build parent-child relationships
50
55
flatList . forEach ( file => {
51
56
const fileWithChildren = fileMap . get ( file . path ) ! ;
52
57
const fileParentPath = file . path . substring ( 0 , file . path . lastIndexOf ( '/' ) ) ;
53
-
58
+
54
59
if ( fileMap . has ( fileParentPath ) ) {
55
60
// This file has a parent in our list
56
61
const parent = fileMap . get ( fileParentPath ) ! ;
@@ -61,7 +66,7 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
61
66
topLevelFiles . push ( fileWithChildren ) ;
62
67
}
63
68
} ) ;
64
-
69
+
65
70
// Sort files: directories first, then by name
66
71
const sortFiles = ( files : ( FileInfo & { children ?: FileInfo [ ] } ) [ ] ) => {
67
72
return [ ...files ] . sort ( ( a , b ) => {
@@ -70,7 +75,7 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
70
75
return a . name . localeCompare ( b . name ) ;
71
76
} ) ;
72
77
} ;
73
-
78
+
74
79
// Recursively sort all children
75
80
const sortRecursive = ( files : ( FileInfo & { children ?: FileInfo [ ] } ) [ ] ) => {
76
81
const sorted = sortFiles ( files ) ;
@@ -81,21 +86,21 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
81
86
} ) ;
82
87
return sorted ;
83
88
} ;
84
-
89
+
85
90
return sortRecursive ( topLevelFiles ) ;
86
91
} ;
87
92
88
93
const handleExpandToggle = async ( e : React . MouseEvent ) => {
89
94
e . stopPropagation ( ) ;
90
-
95
+
91
96
// If expanding and we haven't loaded children yet, load them
92
97
if ( ! isExpanded && file . isDirectory && ! childrenLoaded && onLoadSubdirectory ) {
93
98
const flatChildren = await onLoadSubdirectory ( file . path ) ;
94
99
const treeChildren = buildTreeFromFlatList ( flatChildren , file . path ) ;
95
100
setLoadedChildren ( treeChildren ) ;
96
101
setChildrenLoaded ( true ) ;
97
102
}
98
-
103
+
99
104
setIsExpanded ( ! isExpanded ) ;
100
105
} ;
101
106
@@ -105,9 +110,74 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
105
110
setContextMenuOpen ( true ) ;
106
111
} ;
107
112
113
+ // Touch event handlers for iOS compatibility
114
+ const handleTouchStart = ( e : React . TouchEvent ) => {
115
+ const touch = e . touches [ 0 ] ;
116
+ touchStartPos . current = { x : touch . clientX , y : touch . clientY } ;
117
+ longPressTriggered . current = false ;
118
+
119
+ // Start long press timer (500ms)
120
+ touchTimerRef . current = window . setTimeout ( ( ) => {
121
+ if ( touchStartPos . current ) {
122
+ longPressTriggered . current = true ;
123
+ // Trigger context menu
124
+ setContextMenuPosition ( { x : touchStartPos . current . x , y : touchStartPos . current . y } ) ;
125
+ setContextMenuOpen ( true ) ;
126
+ // Prevent default touch behavior
127
+ e . preventDefault ( ) ;
128
+ }
129
+ } , 500 ) ;
130
+ } ;
131
+
132
+ const handleTouchMove = ( e : React . TouchEvent ) => {
133
+ // If the touch moves more than 10px, cancel the long press
134
+ if ( touchStartPos . current && touchTimerRef . current ) {
135
+ const touch = e . touches [ 0 ] ;
136
+ const deltaX = Math . abs ( touch . clientX - touchStartPos . current . x ) ;
137
+ const deltaY = Math . abs ( touch . clientY - touchStartPos . current . y ) ;
138
+
139
+ if ( deltaX > 10 || deltaY > 10 ) {
140
+ if ( touchTimerRef . current ) {
141
+ clearTimeout ( touchTimerRef . current ) ;
142
+ touchTimerRef . current = null ;
143
+ }
144
+ }
145
+ }
146
+ } ;
147
+
148
+ const handleTouchEnd = ( e : React . TouchEvent ) => {
149
+ // Clear the timer
150
+ if ( touchTimerRef . current ) {
151
+ clearTimeout ( touchTimerRef . current ) ;
152
+ touchTimerRef . current = null ;
153
+ }
154
+
155
+ // If long press was triggered, prevent default click behavior
156
+ if ( longPressTriggered . current ) {
157
+ e . preventDefault ( ) ;
158
+ longPressTriggered . current = false ;
159
+ } else if ( ! e . defaultPrevented ) {
160
+ // Normal tap - handle as click
161
+ if ( file . isDirectory ) {
162
+ onNavigate ( file . path ) ;
163
+ }
164
+ }
165
+
166
+ touchStartPos . current = null ;
167
+ } ;
168
+
169
+ // Clean up timer on unmount
170
+ useEffect ( ( ) => {
171
+ return ( ) => {
172
+ if ( touchTimerRef . current ) {
173
+ clearTimeout ( touchTimerRef . current ) ;
174
+ }
175
+ } ;
176
+ } , [ ] ) ;
177
+
108
178
const handleDelete = async ( ) => {
109
179
if ( ! confirm ( `Delete ${ file . name } ?` ) ) return ;
110
-
180
+
111
181
try {
112
182
if ( file . isDirectory ) {
113
183
await deleteDirectory ( file . path ) ;
@@ -164,9 +234,12 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
164
234
className = { `file-item file-item-${ viewMode } ${ isSelected ? 'selected' : '' } ` }
165
235
onClick = { handleClick }
166
236
onContextMenu = { handleContextMenu }
237
+ onTouchStart = { handleTouchStart }
238
+ onTouchMove = { handleTouchMove }
239
+ onTouchEnd = { handleTouchEnd }
167
240
style = { { paddingLeft : `${ depth * 20 + 10 } px` } }
168
241
>
169
- < span
242
+ < span
170
243
className = { `file-icon ${ file . isDirectory && viewMode === 'list' ? 'clickable-folder' : '' } ` }
171
244
onClick = { file . isDirectory && viewMode === 'list' ? handleExpandToggle : undefined }
172
245
>
@@ -189,7 +262,7 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
189
262
</ >
190
263
) }
191
264
</ div >
192
-
265
+
193
266
{ /* Render children when expanded */ }
194
267
{ isExpanded && viewMode === 'list' && childrenToRender . length > 0 && (
195
268
< div className = "file-children" >
@@ -230,4 +303,4 @@ const FileItem: React.FC<FileItemProps> = ({ file, viewMode, onNavigate, depth =
230
303
) ;
231
304
} ;
232
305
233
- export default FileItem ;
306
+ export default FileItem ;
0 commit comments