This commit is contained in:
2024-04-21 14:42:52 +02:00
parent 4b69674ede
commit 8a25f53c99
10700 changed files with 55767 additions and 14201 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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