Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Msg file preview #11466

Merged
merged 16 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions web/app/components/base/file-uploader/audio-preview.tsx
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const FileImageItem = ({
<>
<div
className='group/file-image relative cursor-pointer'
onClick={() => canPreview && setImagePreviewUrl(url || '')}
onClick={() => canPreview && setImagePreviewUrl(base64Url || url || '')}
>
{
showDeleteAction && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
RiCloseLine,
RiDownloadLine,
} from '@remixicon/react'
import { useState } from 'react'
import {
downloadFile,
fileIsUploaded,
Expand All @@ -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
}
Expand All @@ -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>
{
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>
)
}
{
ext && (
<>
{ext}
<div className='mx-1'>·</div>
</>
progress >= 0 && !fileIsUploaded(file) && (
<ProgressCircle
percentage={progress}
size={12}
className='shrink-0'
/>
)
}
{
!!file.size && formatFileSize(file.size)
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('') }} />
)
}
</>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)}>
Expand Down Expand Up @@ -51,6 +51,7 @@ export const FileList = ({
showDownloadAction={showDownloadAction}
onRemove={onRemove}
onReUpload={onReUpload}
canPreview={canPreview}
/>
)
})
Expand Down
101 changes: 101 additions & 0 deletions web/app/components/base/file-uploader/pdf-preview.tsx
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading