11import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' ;
2- import { faFileAlt , faFileArchive , faFileImport , faFolder } from '@fortawesome/free-solid-svg-icons' ;
2+ import { faFileAlt , faFileArchive , faFileImport , faFolder , faImage } from '@fortawesome/free-solid-svg-icons' ;
33import { encodePathSegments } from '@/helpers' ;
44import { differenceInHours , format , formatDistanceToNow } from 'date-fns' ;
5- import React , { memo } from 'react' ;
5+ import React , { memo , useState } from 'react' ;
66import { FileObject } from '@/api/server/files/loadDirectory' ;
77import FileDropdownMenu from '@/components/server/files/FileDropdownMenu' ;
88import { ServerContext } from '@/state/server' ;
@@ -14,57 +14,97 @@ import { usePermissions } from '@/plugins/usePermissions';
1414import { join } from 'pathe' ;
1515import { bytesToString } from '@/lib/formatters' ;
1616import styles from './style.module.css' ;
17+ import ImagePreviewModal from '@/components/server/files/ImagePreviewModal' ;
1718
18- const Clickable : React . FC < { file : FileObject } > = memo ( ( { file, children } ) => {
19+ const IMAGE_EXTENSIONS = [ 'png' , 'jpg' , 'jpeg' , 'gif' , 'webp' , 'svg' , 'bmp' , 'ico' ] ;
20+
21+ const isImageFile = ( filename : string ) : boolean => {
22+ const ext = filename . split ( '.' ) . pop ( ) ?. toLowerCase ( ) ;
23+ return ext ? IMAGE_EXTENSIONS . includes ( ext ) : false ;
24+ } ;
25+
26+ const Clickable : React . FC < { file : FileObject ; onImageClick ?: ( ) => void } > = memo ( ( { file, children, onImageClick } ) => {
1927 const [ canRead ] = usePermissions ( [ 'file.read' ] ) ;
2028 const [ canReadContents ] = usePermissions ( [ 'file.read-content' ] ) ;
2129 const directory = ServerContext . useStoreState ( ( state ) => state . files . directory ) ;
2230
2331 const match = useRouteMatch ( ) ;
2432
25- return ( file . isFile && ( ! file . isEditable ( ) || ! canReadContents ) ) || ( ! file . isFile && ! canRead ) ? (
26- < div className = { styles . details } > { children } </ div >
27- ) : (
33+ const isImage = file . isFile && isImageFile ( file . name ) && canReadContents && onImageClick ;
34+ const canClick = ( file . isFile && file . isEditable ( ) && canReadContents ) || ( ! file . isFile && canRead ) || isImage ;
35+
36+ return canClick ? (
2837 < NavLink
2938 className = { styles . details }
3039 to = { `${ match . url } ${ file . isFile ? '/edit' : '' } #${ encodePathSegments ( join ( directory , file . name ) ) } ` }
40+ onClick = { isImage ? ( e ) => {
41+ e . preventDefault ( ) ;
42+ onImageClick ( ) ;
43+ } : undefined }
3144 >
3245 { children }
3346 </ NavLink >
47+ ) : (
48+ < div className = { styles . details } > { children } </ div >
3449 ) ;
3550} , isEqual ) ;
3651
37- const FileObjectRow = ( { file } : { file : FileObject } ) => (
38- < div
39- className = { styles . file_row }
40- key = { file . name }
41- onContextMenu = { ( e ) => {
42- e . preventDefault ( ) ;
43- window . dispatchEvent ( new CustomEvent ( `pterodactyl:files:ctx:${ file . key } ` , { detail : e . clientX } ) ) ;
44- } }
45- >
46- < SelectFileCheckbox name = { file . name } />
47- < Clickable file = { file } >
48- < div css = { tw `flex-none text-neutral-400 ml-6 mr-4 text-lg pl-3` } >
49- { file . isFile ? (
50- < FontAwesomeIcon
51- icon = { file . isSymlink ? faFileImport : file . isArchiveType ( ) ? faFileArchive : faFileAlt }
52- />
53- ) : (
54- < FontAwesomeIcon icon = { faFolder } />
55- ) }
56- </ div >
57- < div css = { tw `flex-1 truncate` } > { file . name } </ div >
58- { file . isFile && < div css = { tw `w-1/6 text-right mr-4 hidden sm:block` } > { bytesToString ( file . size ) } </ div > }
59- < div css = { tw `w-1/5 text-right mr-4 hidden md:block` } title = { file . modifiedAt . toString ( ) } >
60- { Math . abs ( differenceInHours ( file . modifiedAt , new Date ( ) ) ) > 48
61- ? format ( file . modifiedAt , 'MMM do, yyyy h:mma' )
62- : formatDistanceToNow ( file . modifiedAt , { addSuffix : true } ) }
52+ const FileObjectRow = ( { file } : { file : FileObject } ) => {
53+ const [ showImageModal , setShowImageModal ] = useState ( false ) ;
54+ const directory = ServerContext . useStoreState ( ( state ) => state . files . directory ) ;
55+ const fullPath = join ( directory , file . name ) ;
56+ const isImage = file . isFile && isImageFile ( file . name ) ;
57+
58+ return (
59+ < >
60+ < div
61+ className = { styles . file_row }
62+ key = { file . name }
63+ onContextMenu = { ( e ) => {
64+ e . preventDefault ( ) ;
65+ window . dispatchEvent ( new CustomEvent ( `pterodactyl:files:ctx:${ file . key } ` , { detail : e . clientX } ) ) ;
66+ } }
67+ >
68+ < SelectFileCheckbox name = { file . name } />
69+ < Clickable file = { file } onImageClick = { isImage ? ( ) => setShowImageModal ( true ) : undefined } >
70+ < div css = { tw `flex-none text-neutral-400 ml-6 mr-4 text-lg pl-3` } >
71+ { file . isFile ? (
72+ < FontAwesomeIcon
73+ icon = {
74+ isImage
75+ ? faImage
76+ : file . isSymlink
77+ ? faFileImport
78+ : file . isArchiveType ( )
79+ ? faFileArchive
80+ : faFileAlt
81+ }
82+ />
83+ ) : (
84+ < FontAwesomeIcon icon = { faFolder } />
85+ ) }
86+ </ div >
87+ < div css = { tw `flex-1 truncate` } > { file . name } </ div >
88+ { file . isFile && < div css = { tw `w-1/6 text-right mr-4 hidden sm:block` } > { bytesToString ( file . size ) } </ div > }
89+ < div css = { tw `w-1/5 text-right mr-4 hidden md:block` } title = { file . modifiedAt . toString ( ) } >
90+ { Math . abs ( differenceInHours ( file . modifiedAt , new Date ( ) ) ) > 48
91+ ? format ( file . modifiedAt , 'MMM do, yyyy h:mma' )
92+ : formatDistanceToNow ( file . modifiedAt , { addSuffix : true } ) }
93+ </ div >
94+ </ Clickable >
95+ < FileDropdownMenu file = { file } />
6396 </ div >
64- </ Clickable >
65- < FileDropdownMenu file = { file } />
66- </ div >
67- ) ;
97+
98+ { isImage && (
99+ < ImagePreviewModal
100+ open = { showImageModal }
101+ onClose = { ( ) => setShowImageModal ( false ) }
102+ file = { fullPath }
103+ />
104+ ) }
105+ </ >
106+ ) ;
107+ } ;
68108
69109export default memo ( FileObjectRow , ( prevProps , nextProps ) => {
70110 /* eslint-disable @typescript-eslint/no-unused-vars */
0 commit comments