| 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, Image, 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) |
| const [visible, setVisible] = useState(false) |
| const [linkRaw, setLinkRaw] = useState<string>('') |
|
|
| 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' && <> |
| {popup?.row.type === 'image' ? <Menu.Item {...baseProps} |
| icon={<FileImageOutlined />} |
| key="viewImage" |
| onClick={() => { |
| setLinkRaw(`${process.env.REACT_APP_API_URL || window.location.origin}/api/v1/files/${popup?.row.id}?raw=1&password=${sessionStorage.getItem(`pass-${popup?.row.id}`)}`) |
| setVisible(true) |
| console.log('row', popup?.row) |
| }}>View Image</Menu.Item> : ''} |
| <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 /> |
| <Image |
| width={200} |
| style={{ display: 'none' }} |
| src={linkRaw} |
| preview={{ |
| visible, |
| src: linkRaw, |
| onVisibleChange: value => { |
| setLinkRaw('') |
| setVisible(value) |
| }, |
| }} |
| /> |
| <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 |
|
|