all
This commit is contained in:
@ -0,0 +1,32 @@
|
||||
import { useContext } from 'react'
|
||||
import cx from 'classnames'
|
||||
import { File } from '@/client/api/file'
|
||||
import { DrawerContext, SwitchCard, IconInfo } from '@/lib'
|
||||
import DrawerDownloadButton from './drawer-download-button'
|
||||
import DrawerOpenNewTabButton from './drawer-open-new-tab-button'
|
||||
import DrawerFileInfo from './file-info'
|
||||
|
||||
export type DrawerContentProps = {
|
||||
file: File
|
||||
}
|
||||
|
||||
const DrawerContent = ({ file }: DrawerContentProps) => {
|
||||
const { isCollapsed } = useContext(DrawerContext)
|
||||
return (
|
||||
<div className={cx('flex', 'flex-col', 'gap-1')}>
|
||||
<DrawerDownloadButton file={file} isCollapsed={isCollapsed} />
|
||||
<DrawerOpenNewTabButton file={file} isCollapsed={isCollapsed} />
|
||||
<SwitchCard
|
||||
icon={<IconInfo />}
|
||||
label="Show info"
|
||||
isCollapsed={isCollapsed}
|
||||
localStorageNamespace="file_info"
|
||||
expandedMinWidth="200px"
|
||||
>
|
||||
<DrawerFileInfo file={file} />
|
||||
</SwitchCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DrawerContent
|
@ -0,0 +1,43 @@
|
||||
import { Button, IconButton } from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { File } from '@/client/api/file'
|
||||
import downloadFile from '@/helpers/download-file'
|
||||
import { IconDownload } from '@/lib'
|
||||
|
||||
export type DrawerDownloadButtonProps = {
|
||||
file: File
|
||||
isCollapsed?: boolean
|
||||
}
|
||||
|
||||
const DrawerDownloadButton = ({
|
||||
file,
|
||||
isCollapsed,
|
||||
}: DrawerDownloadButtonProps) => {
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<IconButton
|
||||
icon={<IconDownload />}
|
||||
variant="solid"
|
||||
colorScheme="blue"
|
||||
aria-label="Download"
|
||||
title="Download"
|
||||
className={cx('h-[50px]', 'w-[50px]', 'p-1.5', 'rounded-md')}
|
||||
onClick={() => downloadFile(file)}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
leftIcon={<IconDownload />}
|
||||
variant="solid"
|
||||
colorScheme="blue"
|
||||
className={cx('h-[50px]', 'w-full', 'p-1.5', 'rounded-md')}
|
||||
onClick={() => downloadFile(file)}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DrawerDownloadButton
|
@ -0,0 +1,60 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Button, IconButton } from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { File } from '@/client/api/file'
|
||||
import { IconOpenInNew } from '@/lib'
|
||||
|
||||
export type DrawerOpenNewTabButtonProps = {
|
||||
file: File
|
||||
isCollapsed?: boolean
|
||||
}
|
||||
|
||||
const LABEL = 'Open file'
|
||||
|
||||
const DrawerOpenNewTabButton = ({
|
||||
file,
|
||||
isCollapsed,
|
||||
}: DrawerOpenNewTabButtonProps) => {
|
||||
const download = useMemo(() => file.preview ?? file.original, [file])
|
||||
const path = useMemo(() => (file.preview ? 'preview' : 'original'), [file])
|
||||
const url = useMemo(() => {
|
||||
if (!download?.extension) {
|
||||
return ''
|
||||
}
|
||||
if (file.original?.extension) {
|
||||
return `/proxy/api/v1/files/${file.id}/${path}${download.extension}`
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}, [file, download, path])
|
||||
if (!file.original) {
|
||||
return null
|
||||
}
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<IconButton
|
||||
icon={<IconOpenInNew />}
|
||||
as="a"
|
||||
className={cx('h-[50px]', 'w-[50px]', 'p-1.5', 'rounded-md')}
|
||||
href={url}
|
||||
target="_blank"
|
||||
title={LABEL}
|
||||
aria-label={LABEL}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
leftIcon={<IconOpenInNew />}
|
||||
as="a"
|
||||
className={cx('h-[50px]', 'w-full', 'p-1.5', 'rounded-md')}
|
||||
href={url}
|
||||
target="_blank"
|
||||
>
|
||||
{LABEL}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DrawerOpenNewTabButton
|
@ -0,0 +1,19 @@
|
||||
import { Stat, StatLabel, StatNumber } from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { File } from '@/client/api/file'
|
||||
import prettyDate from '@/helpers/pretty-date'
|
||||
|
||||
export type FileInfoCreateTimeProps = {
|
||||
file: File
|
||||
}
|
||||
|
||||
const FileInfoCreateTime = ({ file }: FileInfoCreateTimeProps) => (
|
||||
<Stat>
|
||||
<StatLabel>Create time</StatLabel>
|
||||
<StatNumber className={cx('text-base')}>
|
||||
{prettyDate(file.createTime)}
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
)
|
||||
|
||||
export default FileInfoCreateTime
|
@ -0,0 +1,23 @@
|
||||
import { Badge, Stat, StatLabel, StatNumber } from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { File } from '@/client/api/file'
|
||||
|
||||
export type FileInfoExtensionProps = {
|
||||
file: File
|
||||
}
|
||||
|
||||
const FileInfoExtension = ({ file }: FileInfoExtensionProps) => {
|
||||
if (!file.original) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Stat>
|
||||
<StatLabel>File type</StatLabel>
|
||||
<StatNumber className={cx('text-base')}>
|
||||
<Badge>{file.original.extension}</Badge>
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileInfoExtension
|
@ -0,0 +1,23 @@
|
||||
import { Stat, StatLabel, StatNumber } from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { File } from '@/client/api/file'
|
||||
|
||||
export type FileInfoImageProps = {
|
||||
file: File
|
||||
}
|
||||
|
||||
const FileInfoImage = ({ file }: FileInfoImageProps) => {
|
||||
if (!file.original?.image) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Stat>
|
||||
<StatLabel>Image dimensions</StatLabel>
|
||||
<StatNumber className={cx('text-base')}>
|
||||
{file.original.image.width}x{file.original.image.height}
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileInfoImage
|
@ -0,0 +1,18 @@
|
||||
import { Badge, Stat, StatLabel, StatNumber } from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { File } from '@/client/api/file'
|
||||
|
||||
export type FileInfoPermissionProps = {
|
||||
file: File
|
||||
}
|
||||
|
||||
const FileInfoPermission = ({ file }: FileInfoPermissionProps) => (
|
||||
<Stat>
|
||||
<StatLabel>Permission</StatLabel>
|
||||
<StatNumber className={cx('text-base')}>
|
||||
<Badge>{file.permission}</Badge>
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
)
|
||||
|
||||
export default FileInfoPermission
|
@ -0,0 +1,24 @@
|
||||
import { Stat, StatLabel, StatNumber } from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { File } from '@/client/api/file'
|
||||
import prettyBytes from '@/helpers/pretty-bytes'
|
||||
|
||||
export type FileInfoSizeProps = {
|
||||
file: File
|
||||
}
|
||||
|
||||
const FileInfoSize = ({ file }: FileInfoSizeProps) => {
|
||||
if (!file.original) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Stat>
|
||||
<StatLabel>File size</StatLabel>
|
||||
<StatNumber className={cx('text-base')}>
|
||||
{prettyBytes(file.original.size)}
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileInfoSize
|
@ -0,0 +1,39 @@
|
||||
import { Progress, Stat, StatLabel, StatNumber } from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { File } from '@/client/api/file'
|
||||
import StorageAPI from '@/client/api/storage'
|
||||
import prettyBytes from '@/helpers/pretty-bytes'
|
||||
|
||||
export type FileInfoStorageUsageProps = {
|
||||
file: File
|
||||
}
|
||||
|
||||
const FileInfoStorageUsage = ({ file }: FileInfoStorageUsageProps) => {
|
||||
const { data, error } = StorageAPI.useGetFileUsage(file.id)
|
||||
return (
|
||||
<Stat>
|
||||
<StatLabel>Storage usage</StatLabel>
|
||||
<StatNumber className={cx('text-base')}>
|
||||
<div className={cx('flex', 'flex-col', 'gap-0.5')}>
|
||||
{error && <span className={cx('text-red-500')}>Failed to load.</span>}
|
||||
{data && !error && (
|
||||
<>
|
||||
<span>
|
||||
{prettyBytes(data.bytes)} of {prettyBytes(data.maxBytes)} used
|
||||
</span>
|
||||
<Progress size="sm" value={data.percentage} hasStripe />
|
||||
</>
|
||||
)}
|
||||
{!data && !error && (
|
||||
<>
|
||||
<span>Calculating…</span>
|
||||
<Progress size="sm" value={0} hasStripe />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileInfoStorageUsage
|
@ -0,0 +1,28 @@
|
||||
import { Stat, StatLabel, StatNumber } from '@chakra-ui/react'
|
||||
import cx from 'classnames'
|
||||
import { File } from '@/client/api/file'
|
||||
import prettyDate from '@/helpers/pretty-date'
|
||||
|
||||
export type FileInfoUpdateTimeProps = {
|
||||
file: File
|
||||
}
|
||||
|
||||
const FileInfoUpdateTime = ({ file }: FileInfoUpdateTimeProps) => {
|
||||
if (
|
||||
!file.updateTime ||
|
||||
(file.updateTime &&
|
||||
file.createTime.includes(file.updateTime.replaceAll('Z', '')))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Stat>
|
||||
<StatLabel>Update time</StatLabel>
|
||||
<StatNumber className={cx('text-base')}>
|
||||
{prettyDate(file.updateTime)}
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileInfoUpdateTime
|
@ -0,0 +1,32 @@
|
||||
import cx from 'classnames'
|
||||
import { File } from '@/client/api/file'
|
||||
import FileInfoCreateTime from './file-info-create-time'
|
||||
import FileInfoExtension from './file-info-extension'
|
||||
import FileInfoImage from './file-info-image'
|
||||
import FileInfoPermission from './file-info-permission'
|
||||
import FileInfoSize from './file-info-size'
|
||||
import FileInfoStorageUsage from './file-info-storage-usage'
|
||||
import FileInfoUpdateTime from './file-info-update-time'
|
||||
|
||||
export type DrawerFileInfoProps = {
|
||||
file: File
|
||||
}
|
||||
|
||||
const DrawerFileInfo = ({ file }: DrawerFileInfoProps) => {
|
||||
if (!file.original) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div className={cx('flex', 'flex-col', 'gap-1')}>
|
||||
<FileInfoImage file={file} />
|
||||
<FileInfoSize file={file} />
|
||||
<FileInfoExtension file={file} />
|
||||
<FileInfoStorageUsage file={file} />
|
||||
<FileInfoPermission file={file} />
|
||||
<FileInfoCreateTime file={file} />
|
||||
<FileInfoUpdateTime file={file} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DrawerFileInfo
|
@ -0,0 +1,33 @@
|
||||
import { useMemo } from 'react'
|
||||
import { File } from '@/client/api/file'
|
||||
import { getAccessTokenOrRedirect } from '@/infra/token'
|
||||
|
||||
export type ViewerAudioProps = {
|
||||
file: File
|
||||
}
|
||||
|
||||
const ViewerAudio = ({ file }: ViewerAudioProps) => {
|
||||
const download = useMemo(() => file.original, [file])
|
||||
const url = useMemo(() => {
|
||||
if (!download || !download.extension) {
|
||||
return ''
|
||||
}
|
||||
return `/proxy/api/v1/files/${file.id}/original${
|
||||
download.extension
|
||||
}?${new URLSearchParams({
|
||||
access_token: getAccessTokenOrRedirect(),
|
||||
})}`
|
||||
}, [file, download])
|
||||
|
||||
if (!download) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<audio controls>
|
||||
<source src={url} />
|
||||
</audio>
|
||||
)
|
||||
}
|
||||
|
||||
export default ViewerAudio
|
@ -0,0 +1,61 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import cx from 'classnames'
|
||||
import { File } from '@/client/api/file'
|
||||
import { getAccessTokenOrRedirect } from '@/infra/token'
|
||||
import { SectionSpinner } from '@/lib'
|
||||
|
||||
export type ViewerImageProps = {
|
||||
file: File
|
||||
}
|
||||
|
||||
const ViewerImage = ({ file }: ViewerImageProps) => {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const download = useMemo(() => file.preview ?? file.original, [file])
|
||||
const path = useMemo(() => (file.preview ? 'preview' : 'original'), [file])
|
||||
const url = useMemo(() => {
|
||||
if (!download?.extension) {
|
||||
return ''
|
||||
}
|
||||
return `/proxy/api/v1/files/${file.id}/${path}${
|
||||
download.extension
|
||||
}?${new URLSearchParams({
|
||||
access_token: getAccessTokenOrRedirect(),
|
||||
})}`
|
||||
}, [file, download, path])
|
||||
|
||||
if (!download) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx('flex', 'flex-col', 'w-full', 'h-full', 'gap-1.5')}>
|
||||
<div
|
||||
className={cx(
|
||||
'relative',
|
||||
'flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'grow',
|
||||
'w-full',
|
||||
'h-full',
|
||||
'overflow-scroll',
|
||||
)}
|
||||
>
|
||||
{isLoading && <SectionSpinner />}
|
||||
<img
|
||||
src={url}
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
width: isLoading ? 0 : 'auto',
|
||||
height: isLoading ? 0 : '100%',
|
||||
visibility: isLoading ? 'hidden' : 'visible',
|
||||
}}
|
||||
onLoad={() => setIsLoading(false)}
|
||||
alt={file.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ViewerImage
|
33
Downloads/Voltaserve/ui/src/components/viewer/viewer-pdf.tsx
Normal file
33
Downloads/Voltaserve/ui/src/components/viewer/viewer-pdf.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { useMemo } from 'react'
|
||||
import cx from 'classnames'
|
||||
import { File } from '@/client/api/file'
|
||||
import { getAccessTokenOrRedirect } from '@/infra/token'
|
||||
|
||||
export type ViewerPDFProps = {
|
||||
file: File
|
||||
}
|
||||
|
||||
const ViewerPDF = ({ file }: ViewerPDFProps) => {
|
||||
const download = useMemo(() => file.preview || file.original, [file])
|
||||
const urlPath = useMemo(() => (file.preview ? 'preview' : 'original'), [file])
|
||||
const url = useMemo(() => {
|
||||
if (!download || !download.extension) {
|
||||
return ''
|
||||
}
|
||||
return `/proxy/api/v1/files/${file.id}/${urlPath}${
|
||||
download.extension
|
||||
}?${new URLSearchParams({
|
||||
access_token: getAccessTokenOrRedirect(),
|
||||
})}`
|
||||
}, [file, download, urlPath])
|
||||
|
||||
if (!download) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<iframe className={cx('w-full', 'h-full')} src={url} title={file.name} />
|
||||
)
|
||||
}
|
||||
|
||||
export default ViewerPDF
|
@ -0,0 +1,33 @@
|
||||
import { useMemo } from 'react'
|
||||
import { File } from '@/client/api/file'
|
||||
import { getAccessTokenOrRedirect } from '@/infra/token'
|
||||
|
||||
export type ViewerVideoProps = {
|
||||
file: File
|
||||
}
|
||||
|
||||
const ViewerVideo = ({ file }: ViewerVideoProps) => {
|
||||
const download = useMemo(() => file.original, [file])
|
||||
const url = useMemo(() => {
|
||||
if (!download || !download.extension) {
|
||||
return ''
|
||||
}
|
||||
return `/proxy/api/v1/files/${file.id}/original${
|
||||
download.extension
|
||||
}?${new URLSearchParams({
|
||||
access_token: getAccessTokenOrRedirect(),
|
||||
})}`
|
||||
}, [file, download])
|
||||
|
||||
if (!download) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<video controls autoPlay style={{ maxWidth: '100%', maxHeight: '100%' }}>
|
||||
<source src={url} />
|
||||
</video>
|
||||
)
|
||||
}
|
||||
|
||||
export default ViewerVideo
|
Reference in New Issue
Block a user