mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-11 06:59:21 +08:00
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:
parent
fc1415d705
commit
bdd5869244
47
web/app/components/base/file-uploader/audio-preview.tsx
Normal file
47
web/app/components/base/file-uploader/audio-preview.tsx
Normal 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
|
@ -20,7 +20,7 @@ const FileImageRender = ({
|
|||||||
<div className={cn('border-[2px] border-effects-image-frame shadow-xs', className)}>
|
<div className={cn('border-[2px] border-effects-image-frame shadow-xs', className)}>
|
||||||
<img
|
<img
|
||||||
className={cn('w-full h-full object-cover', showDownloadAction && 'cursor-pointer')}
|
className={cn('w-full h-full object-cover', showDownloadAction && 'cursor-pointer')}
|
||||||
alt={alt}
|
alt={alt || 'Preview'}
|
||||||
onLoad={onLoad}
|
onLoad={onLoad}
|
||||||
onError={onError}
|
onError={onError}
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
|
@ -37,7 +37,7 @@ const FileImageItem = ({
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className='group/file-image relative cursor-pointer'
|
className='group/file-image relative cursor-pointer'
|
||||||
onClick={() => canPreview && setImagePreviewUrl(url || '')}
|
onClick={() => canPreview && setImagePreviewUrl(base64Url || url || '')}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
showDeleteAction && (
|
showDeleteAction && (
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
RiCloseLine,
|
RiCloseLine,
|
||||||
RiDownloadLine,
|
RiDownloadLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
|
import { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
downloadFile,
|
downloadFile,
|
||||||
fileIsUploaded,
|
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 { ReplayLine } from '@/app/components/base/icons/src/vender/other'
|
||||||
import ActionButton from '@/app/components/base/action-button'
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
import Button from '@/app/components/base/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 = {
|
type FileItemProps = {
|
||||||
file: FileEntity
|
file: FileEntity
|
||||||
showDeleteAction?: boolean
|
showDeleteAction?: boolean
|
||||||
showDownloadAction?: boolean
|
showDownloadAction?: boolean
|
||||||
|
canPreview?: boolean
|
||||||
onRemove?: (fileId: string) => void
|
onRemove?: (fileId: string) => void
|
||||||
onReUpload?: (fileId: string) => void
|
onReUpload?: (fileId: string) => void
|
||||||
}
|
}
|
||||||
@ -30,88 +35,120 @@ const FileItem = ({
|
|||||||
showDownloadAction = true,
|
showDownloadAction = true,
|
||||||
onRemove,
|
onRemove,
|
||||||
onReUpload,
|
onReUpload,
|
||||||
|
canPreview,
|
||||||
}: FileItemProps) => {
|
}: FileItemProps) => {
|
||||||
const { id, name, type, progress, url, base64Url, isRemote } = file
|
const { id, name, type, progress, url, base64Url, isRemote } = file
|
||||||
|
const [previewUrl, setPreviewUrl] = useState('')
|
||||||
const ext = getFileExtension(name, type, isRemote)
|
const ext = getFileExtension(name, type, isRemote)
|
||||||
const uploadError = progress === -1
|
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 (
|
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
|
<div
|
||||||
className='mb-1 h-8 line-clamp-2 system-xs-medium text-text-tertiary break-all'
|
className={cn(
|
||||||
title={name}
|
'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>
|
showDeleteAction && (
|
||||||
<div className='relative flex items-center justify-between'>
|
<Button
|
||||||
<div className='flex items-center system-2xs-medium-uppercase text-text-tertiary'>
|
className='hidden group-hover/file-item:flex absolute -right-1.5 -top-1.5 p-0 w-5 h-5 rounded-full z-[11]'
|
||||||
<FileTypeIcon
|
onClick={() => onRemove?.(id)}
|
||||||
size='sm'
|
>
|
||||||
type={getFileAppearanceType(name, type)}
|
<RiCloseLine className='w-4 h-4 text-components-button-secondary-text' />
|
||||||
className='mr-1'
|
</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 && (
|
showDownloadAction && tmp_preview_url && (
|
||||||
<>
|
<ActionButton
|
||||||
{ext}
|
size='m'
|
||||||
<div className='mx-1'>·</div>
|
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>
|
</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>
|
||||||
</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('') }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ export const FileList = ({
|
|||||||
onRemove,
|
onRemove,
|
||||||
showDeleteAction = true,
|
showDeleteAction = true,
|
||||||
showDownloadAction = false,
|
showDownloadAction = false,
|
||||||
canPreview,
|
canPreview = true,
|
||||||
}: FileListProps) => {
|
}: FileListProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-wrap gap-2', className)}>
|
<div className={cn('flex flex-wrap gap-2', className)}>
|
||||||
@ -51,6 +51,7 @@ export const FileList = ({
|
|||||||
showDownloadAction={showDownloadAction}
|
showDownloadAction={showDownloadAction}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
onReUpload={onReUpload}
|
onReUpload={onReUpload}
|
||||||
|
canPreview={canPreview}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
101
web/app/components/base/file-uploader/pdf-preview.tsx
Normal file
101
web/app/components/base/file-uploader/pdf-preview.tsx
Normal 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
|
45
web/app/components/base/file-uploader/video-preview.tsx
Normal file
45
web/app/components/base/file-uploader/video-preview.tsx
Normal 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
|
@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { RiAddBoxLine, RiCloseLine, RiDownloadCloud2Line, RiFileCopyLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react'
|
import { RiAddBoxLine, RiCloseLine, RiDownloadCloud2Line, RiFileCopyLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
|
|
||||||
@ -10,6 +11,8 @@ type ImagePreviewProps = {
|
|||||||
url: string
|
url: string
|
||||||
title: string
|
title: string
|
||||||
onCancel: () => void
|
onCancel: () => void
|
||||||
|
onPrev?: () => void
|
||||||
|
onNext?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBase64 = (str: string): boolean => {
|
const isBase64 = (str: string): boolean => {
|
||||||
@ -25,6 +28,8 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
|||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
onPrev,
|
||||||
|
onNext,
|
||||||
}) => {
|
}) => {
|
||||||
const [scale, setScale] = useState(1)
|
const [scale, setScale] = useState(1)
|
||||||
const [position, setPosition] = useState({ x: 0, y: 0 })
|
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||||||
@ -32,7 +37,6 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
|||||||
const imgRef = useRef<HTMLImageElement>(null)
|
const imgRef = useRef<HTMLImageElement>(null)
|
||||||
const dragStartRef = useRef({ x: 0, y: 0 })
|
const dragStartRef = useRef({ x: 0, y: 0 })
|
||||||
const [isCopied, setIsCopied] = useState(false)
|
const [isCopied, setIsCopied] = useState(false)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
const openInNewTab = () => {
|
const openInNewTab = () => {
|
||||||
// Open in a new window, considering the case when the page is inside an iframe
|
// Open in a new window, considering the case when the page is inside an iframe
|
||||||
@ -51,6 +55,7 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const downloadImage = () => {
|
const downloadImage = () => {
|
||||||
// Open in a new window, considering the case when the page is inside an iframe
|
// Open in a new window, considering the case when the page is inside an iframe
|
||||||
if (url.startsWith('http') || url.startsWith('https')) {
|
if (url.startsWith('http') || url.startsWith('https')) {
|
||||||
@ -188,23 +193,11 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
|||||||
}
|
}
|
||||||
}, [handleMouseUp])
|
}, [handleMouseUp])
|
||||||
|
|
||||||
useEffect(() => {
|
useHotkeys('esc', onCancel)
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
useHotkeys('up', zoomIn)
|
||||||
if (event.key === 'Escape')
|
useHotkeys('down', zoomOut)
|
||||||
onCancel()
|
useHotkeys('left', onPrev || (() => {}))
|
||||||
}
|
useHotkeys('right', onNext || (() => {}))
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
|
||||||
|
|
||||||
// Set focus to the container element
|
|
||||||
if (containerRef.current)
|
|
||||||
containerRef.current.focus()
|
|
||||||
|
|
||||||
// Cleanup function
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleKeyDown)
|
|
||||||
}
|
|
||||||
}, [onCancel])
|
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<div className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000] image-preview-container'
|
<div className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000] image-preview-container'
|
||||||
|
@ -79,11 +79,13 @@
|
|||||||
"react-easy-crop": "^5.0.8",
|
"react-easy-crop": "^5.0.8",
|
||||||
"react-error-boundary": "^4.0.2",
|
"react-error-boundary": "^4.0.2",
|
||||||
"react-hook-form": "^7.51.4",
|
"react-hook-form": "^7.51.4",
|
||||||
|
"react-hotkeys-hook": "^4.6.1",
|
||||||
"react-i18next": "^12.2.0",
|
"react-i18next": "^12.2.0",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-markdown": "^8.0.6",
|
"react-markdown": "^8.0.6",
|
||||||
"react-multi-email": "^1.0.14",
|
"react-multi-email": "^1.0.14",
|
||||||
"react-papaparse": "^4.1.0",
|
"react-papaparse": "^4.1.0",
|
||||||
|
"react-pdf-highlighter": "^8.0.0-rc.0",
|
||||||
"react-slider": "^2.0.4",
|
"react-slider": "^2.0.4",
|
||||||
"react-sortablejs": "^6.1.4",
|
"react-sortablejs": "^6.1.4",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
6327
web/yarn.lock
6327
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user