Spaces:
Paused
Paused
import { | |
ArrowRightOutlined, | |
AudioOutlined, | |
BranchesOutlined, | |
CopyOutlined, | |
DeleteOutlined, DownloadOutlined, CloudDownloadOutlined, | |
EditOutlined, | |
FileImageOutlined, | |
FileOutlined, | |
FilePdfOutlined, | |
FolderOpenOutlined, | |
GlobalOutlined, ProfileOutlined, ScissorOutlined, | |
ShareAltOutlined, | |
SnippetsOutlined, | |
TeamOutlined, | |
VideoCameraOutlined | |
} from '@ant-design/icons' | |
import { Descriptions, Menu, Modal, Table, Tag, Typography } from 'antd' | |
import { SorterResult } from 'antd/lib/table/interface' | |
import moment from 'moment' | |
import prettyBytes from 'pretty-bytes' | |
import React, { useCallback, useEffect, useRef, useState } from 'react' | |
import { DndProvider, useDrag, useDrop } from 'react-dnd' | |
import { HTML5Backend } from 'react-dnd-html5-backend' | |
import useSWR from 'swr' | |
import { directDownload } from '../../../utils/Download' | |
import { apiUrl, fetcher } from '../../../utils/Fetcher' | |
interface Props { | |
files?: any, | |
tab: string, | |
me?: any, | |
onChange: (...args: any[]) => void, | |
onDelete: (row: any) => void, | |
onRename: (row: any) => void, | |
onShare: (row: any, action: string) => void, | |
onRowClick: (row: any) => void, | |
onCut?: (row: any) => void, | |
onCopy?: (row: any) => void, | |
onPaste?: (rows: any[]) => void, | |
onCutAndPaste?: (dragRow: any, hoverRow: any) => void, | |
loading?: boolean, | |
sorterData?: SorterResult<any>, | |
dataSource: any[], | |
action?: string, | |
dataSelect: [any[], (data: any[]) => void] | |
} | |
const TableFiles: React.FC<Props> = ({ | |
files, | |
tab, | |
me, | |
onChange, | |
onDelete, | |
onRename, | |
onShare, | |
onRowClick, | |
onCut, | |
onCopy, | |
onPaste, | |
onCutAndPaste, | |
loading, | |
sorterData, | |
dataSource, | |
action, | |
dataSelect: [selected, setSelected] }) => { | |
const [popup, setPopup] = useState<{ visible: boolean, x?: number, y?: number, row?: any }>() | |
const [showDetails, setShowDetails] = useState<any>() | |
const { data: user } = useSWR(showDetails ? `/users/${showDetails.user_id}` : null, fetcher) | |
const { data: filesParts } = useSWR(showDetails ? `/files?name.like=${showDetails.name.replace(/\.part0*\d+$/, '')}&user_id=${showDetails.user_id}&parent_id${showDetails.parent_id ? `=${showDetails.parent_id}` : '=null'}${tab === 'shared' ? '&shared=1' : ''}` : null, fetcher) | |
const pasteEnabled = useRef<boolean | null>(null) | |
useEffect(() => { | |
pasteEnabled.current = Boolean(selected?.length && action) | |
const context = document.querySelector('.App') | |
context?.addEventListener('contextmenu', function rightClick(e) { | |
if (pasteEnabled.current && !(e.target as any)?.outerHTML.match(/^\<td\ /gi)) { | |
e.preventDefault() | |
document.addEventListener('click', function onClickOutside() { | |
setPopup({ visible: false }) | |
document.removeEventListener('click', onClickOutside) | |
}) | |
const parent = document.querySelector('.ant-col-24.ant-col-md-20.ant-col-md-offset-2') | |
setPopup({ | |
row: null, | |
visible: true, | |
x: (e as any).clientX - (parent?.getBoundingClientRect().left || 0), | |
y: (e as any).clientY - (parent?.getBoundingClientRect().top || 0) | |
}) | |
} else if (!pasteEnabled.current) { | |
context?.removeEventListener('contextmenu', rightClick) | |
} | |
}) | |
}, [selected, action]) | |
const Icon = ({ type }: { type: string }) => { | |
if (type === 'image') { | |
return <FileImageOutlined /> | |
} else if (type === 'video') { | |
return <VideoCameraOutlined /> | |
} else if (type === 'document') { | |
return <FilePdfOutlined /> | |
} else if (type === 'folder') { | |
return <FolderOpenOutlined /> | |
} else if (type === 'audio') { | |
return <AudioOutlined /> | |
} else { | |
return <FileOutlined /> | |
} | |
} | |
const ContextMenu = () => { | |
const baseProps = { | |
style: { margin: 0 } | |
} | |
if (!popup?.visible) return <></> | |
if (popup?.row) { | |
return <Menu style={{ zIndex: 1, position: 'absolute', left: `${popup?.x}px`, top: `${popup?.y}px`, boxShadow: '0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05)' }}> | |
<Menu.Item {...baseProps} | |
icon={<ProfileOutlined />} | |
key="details" | |
onClick={() => setShowDetails(popup?.row)}>Details</Menu.Item> | |
{tab === 'mine' && <> | |
<Menu.Item {...baseProps} | |
icon={<EditOutlined />} | |
key="rename" | |
onClick={() => onRename(popup?.row)}>Rename</Menu.Item> | |
{!popup?.row.link_id ? <Menu.Item {...baseProps} | |
icon={<CopyOutlined />} | |
key="copy" | |
onClick={() => onCopy?.(popup?.row)}>Copy</Menu.Item> : ''} | |
<Menu.Item {...baseProps} | |
icon={<ScissorOutlined />} | |
key="cut" | |
onClick={() => onCut?.(popup?.row)}>Cut</Menu.Item> | |
<Menu.Item {...baseProps} | |
icon={<ShareAltOutlined />} | |
key="share" | |
onClick={() => onShare(popup?.row, 'share')}>Share</Menu.Item> | |
{popup?.row.type !== 'folder' ? <Menu.Item {...baseProps} | |
icon={<ArrowRightOutlined />} | |
key="send" | |
onClick={() => onShare(popup?.row, 'forward')}>Send to</Menu.Item> : ''} | |
{popup?.row.type !== 'folder' ? <Menu.Item {...baseProps} | |
icon={<DownloadOutlined />} | |
key="download" | |
onClick={async () => { | |
location.replace(`${apiUrl}/files/${popup?.row.id}?raw=1&dl=1`) | |
}}>Download</Menu.Item> : ''} | |
{popup?.row.type !== 'folder' ? <Menu.Item {...baseProps} | |
icon={<CloudDownloadOutlined />} | |
key="fastdownload" | |
onClick={async () => { | |
popup?.row && await directDownload(popup?.row.id, popup?.row.name.replace(/\.part0*\d+$/, '')) | |
}}>Fast Download <Tag color="green">beta</Tag></Menu.Item> : ''} | |
<Menu.Item {...baseProps} | |
icon={<DeleteOutlined />} | |
key="delete" | |
danger | |
onClick={() => onDelete(popup?.row)}>Delete</Menu.Item> | |
</>} | |
</Menu> | |
} | |
if (selected?.length && action) { | |
return <Menu defaultSelectedKeys={['download']} style={{ position: 'absolute', left: `${popup?.x}px`, top: `${popup?.y}px`, boxShadow: '0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05)' }}> | |
<Menu.Item style={{ margin: 0 }} icon={<SnippetsOutlined />} key="paste" onClick={() => onPaste?.(selected)}>Paste</Menu.Item> | |
</Menu> | |
} | |
return <></> | |
} | |
const columns = [ | |
{ | |
title: 'File', | |
dataIndex: 'name', | |
key: 'type', | |
sorter: true, | |
sortOrder: sorterData?.column?.dataIndex === 'name' ? sorterData.order : undefined, | |
filters: [ | |
{ | |
text: 'Folder', | |
value: 'folder' | |
}, | |
{ | |
text: 'Image', | |
value: 'image' | |
}, | |
{ | |
text: 'Video', | |
value: 'video' | |
}, | |
{ | |
text: 'Audio', | |
value: 'audio' | |
}, | |
{ | |
text: 'Document', | |
value: 'document' | |
}, | |
{ | |
text: 'Unknown', | |
value: 'unknown' | |
} | |
], | |
ellipsis: true, | |
onCell: (row: any) => ({ | |
onClick: () => onRowClick(row) | |
}), | |
render: (_: any, row: any) => { | |
let type: any | |
if (row.sharing_options?.includes('*')) { | |
type = <GlobalOutlined /> | |
} else if (row.sharing_options?.length) { | |
type = <TeamOutlined /> | |
} | |
return <> | |
{row.link_id ? <BranchesOutlined /> : '' } {type} <Icon type={row.type} /> {row.name?.replace(/\.part0*\d+$/, '')} | |
</> | |
} | |
}, | |
{ | |
title: 'Size', | |
dataIndex: 'size', | |
key: 'size', | |
sorter: true, | |
sortOrder: sorterData?.column?.key === 'size' ? sorterData.order : undefined, | |
responsive: ['md'], | |
width: 100, | |
align: 'center', | |
render: (value: any) => { | |
if (Number(value) === 2_000_000_000) { | |
return '> 2 GB' | |
} | |
return value ? prettyBytes(Number(value)) : '-' | |
} | |
}, | |
{ | |
title: 'Uploaded At', | |
dataIndex: 'uploaded_at', | |
key: 'uploaded_at', | |
sorter: true, | |
sortOrder: sorterData?.column?.key === 'uploaded_at' ? sorterData.order : undefined, | |
responsive: ['md'], | |
width: 250, | |
align: 'center', | |
render: (value: any, row: any) => row.upload_progress !== null ? <>Uploading...</> : moment(value).local().format('llll') | |
} | |
] | |
const DraggableBodyRow = ({ index, moveRow, className, style, ...restProps }) => { | |
const ref = useRef() | |
const [{ isOver, dropClassName }, drop] = useDrop({ | |
accept: 'DraggableBodyRow', | |
collect: (monitor: any) => { | |
const { index: dragIndex } = monitor.getItem() || {} | |
if (dragIndex === index) { | |
return {} | |
} | |
return { | |
isOver: monitor.isOver(), | |
dropClassName: dragIndex < index ? ' drop-over-downward' : ' drop-over-upward', | |
} | |
}, | |
drop: (item: any) => { | |
moveRow(item.index, index) | |
}, | |
}) | |
const [, drag] = useDrag({ | |
type: 'DraggableBodyRow', | |
item: { index }, | |
collect: monitor => ({ | |
isDragging: monitor.isDragging(), | |
}), | |
}) | |
drop(drag(ref)) | |
return ( | |
<tr | |
ref={ref as any} | |
className={`${className}${isOver ? dropClassName : ''}`} | |
style={{ cursor: 'move', ...style }} | |
{...restProps} | |
/> | |
) | |
} | |
return <> | |
<DndProvider backend={HTML5Backend}> | |
<Table | |
className="tableFiles" | |
loading={!files || loading} | |
showSorterTooltip={false} | |
rowSelection={{ type: 'checkbox', selectedRowKeys: selected.map(row => row.key), onChange: (_: React.Key[], rows: any[]) => setSelected(rows) }} | |
dataSource={dataSource} | |
columns={columns as any} | |
components={{ | |
body: { | |
row: DraggableBodyRow | |
} | |
}} | |
onChange={onChange} | |
pagination={false} | |
scroll={{ x: 330 }} | |
onRow={(row, index) => ({ | |
index, | |
moveRow: useCallback((dragIndex, hoverIndex) => { | |
const hoverRow = dataSource[hoverIndex] | |
const dragRow = dataSource[dragIndex] | |
if (hoverRow.type === 'folder') { | |
onCutAndPaste?.(dragRow, hoverRow) | |
} | |
}, [dataSource, selected]), | |
onContextMenu: e => { | |
// if (tab !== 'mine') return | |
e.preventDefault() | |
if (!popup?.visible) { | |
document.addEventListener('click', function onClickOutside() { | |
setPopup({ visible: false }) | |
document.removeEventListener('click', onClickOutside) | |
}) | |
} | |
const parent = document.querySelector('.ant-col-24.ant-col-md-20.ant-col-md-offset-2') | |
setPopup({ | |
row, | |
visible: true, | |
x: e.clientX - (parent?.getBoundingClientRect().left || 0), | |
y: e.clientY - (parent?.getBoundingClientRect().top || 0) | |
}) | |
} | |
})} | |
expandable={me?.settings?.expandable_rows && window.innerWidth < 752 ? { | |
expandedRowRender: (row: any) => <Descriptions labelStyle={{ fontWeight: 'bold' }} column={1}> | |
<Descriptions.Item label="Size">{row.size ? prettyBytes(Number(row.size)) : '-'}</Descriptions.Item> | |
<Descriptions.Item label="Uploaded At">{row.upload_progress !== null ? <>Uploading {Number((row.upload_progress * 100).toFixed(2))}%</> : moment(row.uploaded_at).local().format('lll')}</Descriptions.Item> | |
</Descriptions>, | |
rowExpandable: (_: any) => window.innerWidth < 752, | |
} : undefined} /> | |
</DndProvider> | |
<ContextMenu /> | |
<Modal title={<Typography.Text ellipsis><Icon type={showDetails?.type} /> {showDetails?.name.replace(/\.part0*\d+$/, '')}</Typography.Text>} | |
visible={Boolean(showDetails)} | |
onCancel={() => setShowDetails(undefined)} | |
okText="View" | |
onOk={() => { | |
setShowDetails(undefined) | |
onRowClick(showDetails) | |
}} | |
cancelButtonProps={{ shape: 'round' }} | |
okButtonProps={{ shape: 'round' }}> | |
<Descriptions column={1}> | |
<Descriptions.Item label="Size"> | |
{filesParts?.length ? prettyBytes(filesParts?.files.reduce((res: number, file: any) => res + Number(file.size), 0)) + ` (${filesParts?.length} parts)` : showDetails?.size && prettyBytes(Number(showDetails?.size || 0))} | |
</Descriptions.Item> | |
<Descriptions.Item label="Uploaded At">{moment(showDetails?.uploaded_at).local().format('lll')}</Descriptions.Item> | |
<Descriptions.Item label="Uploaded By"> | |
<a href={`https://t.me/${user?.user.username}`} target="_blank">@{user?.user.username}</a> | |
</Descriptions.Item> | |
</Descriptions> | |
</Modal> | |
</> | |
} | |
export default TableFiles | |