Skip to content

Commit b548c4d

Browse files
Enhance FileObjectRow with image preview support
Added image preview functionality and updated file handling.
1 parent c455dfc commit b548c4d

File tree

1 file changed

+76
-36
lines changed

1 file changed

+76
-36
lines changed

resources/scripts/components/server/files/FileObjectRow.tsx

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { 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';
33
import { encodePathSegments } from '@/helpers';
44
import { differenceInHours, format, formatDistanceToNow } from 'date-fns';
5-
import React, { memo } from 'react';
5+
import React, { memo, useState } from 'react';
66
import { FileObject } from '@/api/server/files/loadDirectory';
77
import FileDropdownMenu from '@/components/server/files/FileDropdownMenu';
88
import { ServerContext } from '@/state/server';
@@ -14,57 +14,97 @@ import { usePermissions } from '@/plugins/usePermissions';
1414
import { join } from 'pathe';
1515
import { bytesToString } from '@/lib/formatters';
1616
import 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

69109
export default memo(FileObjectRow, (prevProps, nextProps) => {
70110
/* eslint-disable @typescript-eslint/no-unused-vars */

0 commit comments

Comments
 (0)