Msg file preview (#11466)

Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
This commit is contained in:
Charlie.Wei
2024-12-10 10:53:37 +08:00
committed by GitHub
parent fc1415d705
commit bdd5869244
10 changed files with 2915 additions and 3814 deletions

View File

@@ -0,0 +1,47 @@
import type { FC } from 'react'
import { createPortal } from 'react-dom'
import { RiCloseLine } from '@remixicon/react'
import React from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
type AudioPreviewProps = {
url: string
title: string
onCancel: () => void
}
const AudioPreview: FC<AudioPreviewProps> = ({
url,
title,
onCancel,
}) => {
useHotkeys('esc', onCancel)
return createPortal(
<div
className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000]'
onClick={e => e.stopPropagation()}
tabIndex={-1}
>
<div>
<audio controls title={title} autoPlay={false} preload="metadata">
<source
type="audio/mpeg"
src={url}
className='max-w-full max-h-full'
/>
</audio>
</div>
<div
className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/[0.08] rounded-lg backdrop-blur-[2px] cursor-pointer'
onClick={onCancel}
>
<RiCloseLine className='w-4 h-4 text-gray-500'/>
</div>
</div>
,
document.body,
)
}
export default AudioPreview

View File

@@ -20,7 +20,7 @@ const FileImageRender = ({
<div className={cn('border-[2px] border-effects-image-frame shadow-xs', className)}>
<img
className={cn('w-full h-full object-cover', showDownloadAction && 'cursor-pointer')}
alt={alt}
alt={alt || 'Preview'}
onLoad={onLoad}
onError={onError}
src={imageUrl}

View File

@@ -37,7 +37,7 @@ const FileImageItem = ({
<>
<div
className='group/file-image relative cursor-pointer'
onClick={() => canPreview && setImagePreviewUrl(url || '')}
onClick={() => canPreview && setImagePreviewUrl(base64Url || url || '')}
>
{
showDeleteAction && (

View File

@@ -2,6 +2,7 @@ import {
RiCloseLine,
RiDownloadLine,
} from '@remixicon/react'
import { useState } from 'react'
import {
downloadFile,
fileIsUploaded,
@@ -16,11 +17,15 @@ import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
import PdfPreview from '@/app/components/base/file-uploader/pdf-preview'
import AudioPreview from '@/app/components/base/file-uploader/audio-preview'
import VideoPreview from '@/app/components/base/file-uploader/video-preview'
type FileItemProps = {
file: FileEntity
showDeleteAction?: boolean
showDownloadAction?: boolean
canPreview?: boolean
onRemove?: (fileId: string) => void
onReUpload?: (fileId: string) => void
}
@@ -30,88 +35,120 @@ const FileItem = ({
showDownloadAction = true,
onRemove,
onReUpload,
canPreview,
}: FileItemProps) => {
const { id, name, type, progress, url, base64Url, isRemote } = file
const [previewUrl, setPreviewUrl] = useState('')
const ext = getFileExtension(name, type, isRemote)
const uploadError = progress === -1
let tmp_preview_url = url || base64Url
if (!tmp_preview_url && file?.originalFile)
tmp_preview_url = URL.createObjectURL(file.originalFile.slice()).toString()
return (
<div
className={cn(
'group/file-item relative p-2 w-[144px] h-[68px] rounded-lg border-[0.5px] border-components-panel-border bg-components-card-bg shadow-xs',
!uploadError && 'hover:bg-components-card-bg-alt',
uploadError && 'border border-state-destructive-border bg-state-destructive-hover',
uploadError && 'hover:border-[0.5px] hover:border-state-destructive-border bg-state-destructive-hover-alt',
)}
>
{
showDeleteAction && (
<Button
className='hidden group-hover/file-item:flex absolute -right-1.5 -top-1.5 p-0 w-5 h-5 rounded-full z-[11]'
onClick={() => onRemove?.(id)}
>
<RiCloseLine className='w-4 h-4 text-components-button-secondary-text' />
</Button>
)
}
<>
<div
className='mb-1 h-8 line-clamp-2 system-xs-medium text-text-tertiary break-all'
title={name}
className={cn(
'group/file-item relative p-2 w-[144px] h-[68px] rounded-lg border-[0.5px] border-components-panel-border bg-components-card-bg shadow-xs',
!uploadError && 'hover:bg-components-card-bg-alt',
uploadError && 'border border-state-destructive-border bg-state-destructive-hover',
uploadError && 'hover:border-[0.5px] hover:border-state-destructive-border bg-state-destructive-hover-alt',
)}
>
{name}
</div>
<div className='relative flex items-center justify-between'>
<div className='flex items-center system-2xs-medium-uppercase text-text-tertiary'>
<FileTypeIcon
size='sm'
type={getFileAppearanceType(name, type)}
className='mr-1'
/>
{
showDeleteAction && (
<Button
className='hidden group-hover/file-item:flex absolute -right-1.5 -top-1.5 p-0 w-5 h-5 rounded-full z-[11]'
onClick={() => onRemove?.(id)}
>
<RiCloseLine className='w-4 h-4 text-components-button-secondary-text' />
</Button>
)
}
<div
className='mb-1 h-8 line-clamp-2 system-xs-medium text-text-tertiary break-all cursor-pointer'
title={name}
onClick={() => canPreview && setPreviewUrl(tmp_preview_url || '')}
>
{name}
</div>
<div className='relative flex items-center justify-between'>
<div className='flex items-center system-2xs-medium-uppercase text-text-tertiary'>
<FileTypeIcon
size='sm'
type={getFileAppearanceType(name, type)}
className='mr-1'
/>
{
ext && (
<>
{ext}
<div className='mx-1'>·</div>
</>
)
}
{
!!file.size && formatFileSize(file.size)
}
</div>
{
ext && (
<>
{ext}
<div className='mx-1'>·</div>
</>
showDownloadAction && tmp_preview_url && (
<ActionButton
size='m'
className='hidden group-hover/file-item:flex absolute -right-1 -top-1'
onClick={(e) => {
e.stopPropagation()
downloadFile(tmp_preview_url || '', name)
}}
>
<RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' />
</ActionButton>
)
}
{
!!file.size && formatFileSize(file.size)
progress >= 0 && !fileIsUploaded(file) && (
<ProgressCircle
percentage={progress}
size={12}
className='shrink-0'
/>
)
}
{
uploadError && (
<ReplayLine
className='w-4 h-4 text-text-tertiary'
onClick={() => onReUpload?.(id)}
/>
)
}
</div>
{
showDownloadAction && url && (
<ActionButton
size='m'
className='hidden group-hover/file-item:flex absolute -right-1 -top-1'
onClick={(e) => {
e.stopPropagation()
downloadFile(url || base64Url || '', name)
}}
>
<RiDownloadLine className='w-3.5 h-3.5 text-text-tertiary' />
</ActionButton>
)
}
{
progress >= 0 && !fileIsUploaded(file) && (
<ProgressCircle
percentage={progress}
size={12}
className='shrink-0'
/>
)
}
{
uploadError && (
<ReplayLine
className='w-4 h-4 text-text-tertiary'
onClick={() => onReUpload?.(id)}
/>
)
}
</div>
</div>
{
type.split('/')[0] === 'audio' && canPreview && previewUrl && (
<AudioPreview
title={name}
url={previewUrl}
onCancel={() => setPreviewUrl('')}
/>
)
}
{
type.split('/')[0] === 'video' && canPreview && previewUrl && (
<VideoPreview
title={name}
url={previewUrl}
onCancel={() => setPreviewUrl('')}
/>
)
}
{
type.split('/')[1] === 'pdf' && canPreview && previewUrl && (
<PdfPreview url={previewUrl} onCancel={() => { setPreviewUrl('') }} />
)
}
</>
)
}

View File

@@ -23,7 +23,7 @@ export const FileList = ({
onRemove,
showDeleteAction = true,
showDownloadAction = false,
canPreview,
canPreview = true,
}: FileListProps) => {
return (
<div className={cn('flex flex-wrap gap-2', className)}>
@@ -51,6 +51,7 @@ export const FileList = ({
showDownloadAction={showDownloadAction}
onRemove={onRemove}
onReUpload={onReUpload}
canPreview={canPreview}
/>
)
})

View File

@@ -0,0 +1,101 @@
import type { FC } from 'react'
import { createPortal } from 'react-dom'
import 'react-pdf-highlighter/dist/style.css'
import { PdfHighlighter, PdfLoader } from 'react-pdf-highlighter'
import { t } from 'i18next'
import { RiCloseLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react'
import React, { useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import Tooltip from '@/app/components/base/tooltip'
type PdfPreviewProps = {
url: string
onCancel: () => void
}
const PdfPreview: FC<PdfPreviewProps> = ({
url,
onCancel,
}) => {
const media = useBreakpoints()
const [scale, setScale] = useState(1)
const [position, setPosition] = useState({ x: 0, y: 0 })
const isMobile = media === MediaType.mobile
const zoomIn = () => {
setScale(prevScale => Math.min(prevScale * 1.2, 15))
setPosition({ x: position.x - 50, y: position.y - 50 })
}
const zoomOut = () => {
setScale((prevScale) => {
const newScale = Math.max(prevScale / 1.2, 0.5)
if (newScale === 1)
setPosition({ x: 0, y: 0 })
else
setPosition({ x: position.x + 50, y: position.y + 50 })
return newScale
})
}
useHotkeys('esc', onCancel)
useHotkeys('up', zoomIn)
useHotkeys('down', zoomOut)
return createPortal(
<div
className={`fixed inset-0 flex items-center justify-center bg-black/80 z-[1000] ${!isMobile && 'p-8'}`}
onClick={e => e.stopPropagation()}
tabIndex={-1}
>
<div
className='h-[95vh] w-[100vw] max-w-full max-h-full overflow-hidden'
style={{ transform: `scale(${scale})`, transformOrigin: 'center', scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
<PdfLoader
url={url}
beforeLoad={<div className='flex justify-center items-center h-64'><Loading type='app' /></div>}
>
{(pdfDocument) => {
return (
<PdfHighlighter
pdfDocument={pdfDocument}
enableAreaSelection={event => event.altKey}
scrollRef={() => { }}
onScrollChange={() => { }}
onSelectionFinished={() => null}
highlightTransform={() => { return <div/> }}
highlights={[]}
/>
)
}}
</PdfLoader>
</div>
<Tooltip popupContent={t('common.operation.zoomOut')}>
<div className='absolute top-6 right-24 flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer'
onClick={zoomOut}>
<RiZoomOutLine className='w-4 h-4 text-gray-500'/>
</div>
</Tooltip>
<Tooltip popupContent={t('common.operation.zoomIn')}>
<div className='absolute top-6 right-16 flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer'
onClick={zoomIn}>
<RiZoomInLine className='w-4 h-4 text-gray-500'/>
</div>
</Tooltip>
<Tooltip popupContent={t('common.operation.cancel')}>
<div
className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/8 rounded-lg backdrop-blur-[2px] cursor-pointer'
onClick={onCancel}>
<RiCloseLine className='w-4 h-4 text-gray-500'/>
</div>
</Tooltip>
</div>,
document.body,
)
}
export default PdfPreview

View File

@@ -0,0 +1,45 @@
import type { FC } from 'react'
import { createPortal } from 'react-dom'
import { RiCloseLine } from '@remixicon/react'
import React from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
type VideoPreviewProps = {
url: string
title: string
onCancel: () => void
}
const VideoPreview: FC<VideoPreviewProps> = ({
url,
title,
onCancel,
}) => {
useHotkeys('esc', onCancel)
return createPortal(
<div
className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000]'
onClick={e => e.stopPropagation()}
tabIndex={-1}
>
<div>
<video controls title={title} autoPlay={false} preload="metadata">
<source
type="video/mp4"
src={url}
className='max-w-full max-h-full'
/>
</video>
</div>
<div
className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/[0.08] rounded-lg backdrop-blur-[2px] cursor-pointer'
onClick={onCancel}
>
<RiCloseLine className='w-4 h-4 text-gray-500'/>
</div>
</div>
, document.body,
)
}
export default VideoPreview