import bookIcon from '@tabler/icons/outline/book.svg'; import fileCodeIcon from '@tabler/icons/outline/file-code.svg'; import fileSpreadsheetIcon from '@tabler/icons/outline/file-spreadsheet.svg'; import fileTextIcon from '@tabler/icons/outline/file-text.svg'; import fileZipIcon from '@tabler/icons/outline/file-zip.svg'; import defaultIcon from '@tabler/icons/outline/paperclip.svg'; import presentationIcon from '@tabler/icons/outline/presentation.svg'; import xIcon from '@tabler/icons/outline/x.svg'; import zoomInIcon from '@tabler/icons/outline/zoom-in.svg'; import clsx from 'clsx'; import React, { useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { spring } from 'react-motion'; import { openModal } from 'pl-fe/actions/modals'; import AltIndicator from 'pl-fe/components/alt-indicator'; import Blurhash from 'pl-fe/components/blurhash'; import { HStack, Icon, IconButton } from 'pl-fe/components/ui'; import Motion from 'pl-fe/features/ui/util/optional-motion'; import { useAppDispatch, useSettings } from 'pl-fe/hooks'; import type { MediaAttachment } from 'pl-api'; const MIMETYPE_ICONS: Record = { 'application/x-freearc': fileZipIcon, 'application/x-bzip': fileZipIcon, 'application/x-bzip2': fileZipIcon, 'application/gzip': fileZipIcon, 'application/vnd.rar': fileZipIcon, 'application/x-tar': fileZipIcon, 'application/zip': fileZipIcon, 'application/x-7z-compressed': fileZipIcon, 'application/x-csh': fileCodeIcon, 'application/html': fileCodeIcon, 'text/javascript': fileCodeIcon, 'application/json': fileCodeIcon, 'application/ld+json': fileCodeIcon, 'application/x-httpd-php': fileCodeIcon, 'application/x-sh': fileCodeIcon, 'application/xhtml+xml': fileCodeIcon, 'application/xml': fileCodeIcon, 'application/epub+zip': bookIcon, 'application/vnd.oasis.opendocument.spreadsheet': fileSpreadsheetIcon, 'application/vnd.ms-excel': fileSpreadsheetIcon, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': fileSpreadsheetIcon, 'application/pdf': fileTextIcon, 'application/vnd.oasis.opendocument.presentation': presentationIcon, 'application/vnd.ms-powerpoint': presentationIcon, 'application/vnd.openxmlformats-officedocument.presentationml.presentation': presentationIcon, 'text/plain': fileTextIcon, 'application/rtf': fileTextIcon, 'application/msword': fileTextIcon, 'application/x-abiword': fileTextIcon, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': fileTextIcon, 'application/vnd.oasis.opendocument.text': fileTextIcon, }; const messages = defineMessages({ description: { id: 'upload_form.description', defaultMessage: 'Describe for the visually impaired' }, delete: { id: 'upload_form.undo', defaultMessage: 'Delete' }, preview: { id: 'upload_form.preview', defaultMessage: 'Preview' }, descriptionMissingTitle: { id: 'upload_form.description_missing.title', defaultMessage: 'This attachment doesn\'t have a description' }, }); interface IUpload extends Pick, 'onDragStart' | 'onDragEnter' | 'onDragEnd'> { media: MediaAttachment; onSubmit?(): void; onDelete?(): void; onDescriptionChange?(description: string): void; descriptionLimit?: number; withPreview?: boolean; } const Upload: React.FC = ({ media, onSubmit, onDelete, onDescriptionChange, onDragStart, onDragEnter, onDragEnd, descriptionLimit, withPreview = true, }) => { const intl = useIntl(); const dispatch = useAppDispatch(); const { missingDescriptionModal } = useSettings(); const [hovered, setHovered] = useState(false); const [focused, setFocused] = useState(false); const [dirtyDescription, setDirtyDescription] = useState(null); const handleKeyDown: React.KeyboardEventHandler = (e) => { if (onSubmit && e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { handleInputBlur(); onSubmit(); } }; const handleUndoClick: React.MouseEventHandler = e => { if (onDelete) { e.stopPropagation(); onDelete(); } }; const handleInputChange: React.ChangeEventHandler = e => { setDirtyDescription(e.target.value); }; const handleMouseEnter = () => { setHovered(true); }; const handleMouseLeave = () => { setHovered(false); }; const handleInputFocus = () => { setFocused(true); }; const handleClick = () => { setFocused(true); }; const handleInputBlur = () => { setFocused(false); setDirtyDescription(null); if (dirtyDescription !== null && onDescriptionChange) { onDescriptionChange(dirtyDescription); } }; const handleOpenModal = () => { dispatch(openModal('MEDIA', { media: [media], index: 0 })); }; const active = hovered || focused; const description = dirtyDescription || (dirtyDescription !== '' && media.description) || ''; const focusX = media.type === 'image' && media.meta?.focus?.x || undefined; const focusY = media.type === 'image' && media.meta?.focus?.y || undefined; const x = focusX ? ((focusX / 2) + .5) * 100 : undefined; const y = focusY ? ((focusY / -2) + .5) * 100 : undefined; const mediaType = media.type; const mimeType = media.mime_type as string | undefined; const uploadIcon = mediaType === 'unknown' && ( ); return (
{({ scale }) => (
{(withPreview && mediaType !== 'unknown' && Boolean(media.url)) && ( )} {onDelete && ( )} {onDescriptionChange && (