|
import { |
|
useEffect, |
|
useState, |
|
useCallback, |
|
ChangeEvent, |
|
ClipboardEvent, |
|
MouseEventHandler, |
|
FormEvent, |
|
useRef |
|
} from "react" |
|
import Image from 'next/image' |
|
import PasteIcon from '@/assets/images/paste.svg' |
|
import UploadIcon from '@/assets/images/upload.svg' |
|
import CameraIcon from '@/assets/images/camera.svg' |
|
import { useBing } from '@/lib/hooks/use-bing' |
|
import { cn } from '@/lib/utils' |
|
|
|
interface ChatImageProps extends Pick<ReturnType<typeof useBing>, 'uploadImage'> {} |
|
|
|
const preventDefault: MouseEventHandler<HTMLDivElement> = (event) => { |
|
event.nativeEvent.stopImmediatePropagation() |
|
} |
|
|
|
const toBase64 = (file: File): Promise<string> => new Promise((resolve, reject) => { |
|
const reader = new FileReader() |
|
reader.readAsDataURL(file) |
|
reader.onload = () => resolve(reader.result as string) |
|
reader.onerror = reject |
|
}) |
|
|
|
export function ChatImage({ children, uploadImage }: React.PropsWithChildren<ChatImageProps>) { |
|
const videoRef = useRef<HTMLVideoElement>(null) |
|
const canvasRef = useRef<HTMLCanvasElement>(null) |
|
const mediaStream = useRef<MediaStream>() |
|
const [panel, setPanel] = useState('none') |
|
|
|
const upload = useCallback((url: string) => { |
|
if (url) { |
|
uploadImage(url) |
|
} |
|
setPanel('none') |
|
}, [panel]) |
|
|
|
const onUpload = useCallback(async (event: ChangeEvent<HTMLInputElement>) => { |
|
const file = event.target.files?.[0] |
|
if (file) { |
|
const fileDataUrl = await toBase64(file) |
|
if (fileDataUrl) { |
|
upload(fileDataUrl) |
|
} |
|
} |
|
}, []) |
|
|
|
const onPaste = useCallback((event: ClipboardEvent<HTMLInputElement>) => { |
|
const pasteUrl = event.clipboardData.getData('text') ?? '' |
|
upload(pasteUrl) |
|
}, []) |
|
|
|
const onEnter = useCallback((event: FormEvent<HTMLFormElement>) => { |
|
event.preventDefault() |
|
event.stopPropagation() |
|
|
|
const inputUrl = event.target.elements.image.value |
|
if (inputUrl) { |
|
upload(inputUrl) |
|
} |
|
}, []) |
|
|
|
const openVideo: MouseEventHandler<HTMLButtonElement> = async (event) => { |
|
event.stopPropagation() |
|
setPanel('camera-mode') |
|
} |
|
|
|
const onCapture = () => { |
|
if (canvasRef.current && videoRef.current) { |
|
const canvas = canvasRef.current |
|
canvas.width = videoRef.current!.videoWidth |
|
canvas.height = videoRef.current!.videoHeight |
|
canvas.getContext('2d')?.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height) |
|
const cameraUrl = canvas.toDataURL('image/jpeg') |
|
upload(cameraUrl) |
|
} |
|
} |
|
|
|
useEffect(() => { |
|
const handleBlur = () => { |
|
if (panel !== 'none') { |
|
setPanel('none') |
|
} |
|
} |
|
document.addEventListener('click', handleBlur) |
|
return () => { |
|
document.removeEventListener('click', handleBlur) |
|
} |
|
}, [panel]) |
|
|
|
useEffect(() => { |
|
if (panel === 'camera-mode') { |
|
navigator.mediaDevices.getUserMedia({ video: true, audio: false }) |
|
.then(videoStream => { |
|
mediaStream.current = videoStream |
|
if (videoRef.current) { |
|
videoRef.current.srcObject = videoStream |
|
} |
|
}) |
|
} else { |
|
if (mediaStream.current) { |
|
mediaStream.current.getTracks().forEach(function(track) { |
|
track.stop() |
|
}) |
|
mediaStream.current = undefined |
|
} |
|
} |
|
}, [panel]) |
|
|
|
return ( |
|
<div className="visual-search-container"> |
|
<div onClick={() => panel === 'none' ? setPanel('normal') : setPanel('none')}>{children}</div> |
|
<div className={cn('visual-search', panel)} onClick={preventDefault}> |
|
<div className="normal-content"> |
|
<div className="header"> |
|
<h4>添加图像</h4> |
|
</div> |
|
<div className="paste"> |
|
<Image alt="paste" src={PasteIcon} width={24} /> |
|
<form onSubmitCapture={onEnter}> |
|
<input |
|
className="paste-input" |
|
id="sb_imgpst" |
|
type="text" |
|
name="image" |
|
placeholder="粘贴图像 URL" |
|
aria-label="粘贴图像 URL" |
|
onPaste={onPaste} |
|
onClickCapture={(e) => e.stopPropagation()} |
|
/> |
|
</form> |
|
</div> |
|
<div className="buttons"> |
|
<button type="button" aria-label="从此设备上传"> |
|
<input |
|
id="vs_fileinput" |
|
className="fileinput" |
|
type="file" |
|
accept="image/gif, image/jpeg, image/png, image/webp" |
|
onChange={onUpload} |
|
/> |
|
<Image alt="uplaod" src={UploadIcon} width={20} /> |
|
从此设备上传 |
|
</button> |
|
<button type="button" aria-label="拍照" onClick={openVideo}> |
|
<Image alt="camera" src={CameraIcon} width={20} /> |
|
拍照 |
|
</button> |
|
</div> |
|
</div> |
|
{panel === 'camera-mode' && <div className="cam-content"> |
|
<div className="webvideo-container"> |
|
<video className="webvideo" autoPlay muted playsInline ref={videoRef} /> |
|
<canvas className="webcanvas" ref={canvasRef} /> |
|
</div> |
|
<div className="cambtn" role="button" aria-label="拍照" onClick={onCapture}> |
|
<div className="cam-btn-circle-large"></div> |
|
<div className="cam-btn-circle-small"></div> |
|
</div> |
|
</div>} |
|
</div> |
|
</div> |
|
) |
|
} |
|
|