From 7ea4a26e45bd2fc4e6e51a57aa11f41aacf565d0 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Thu, 24 Nov 2022 18:29:57 +0530 Subject: [PATCH] redesign fileinfo completed --- src/components/Chip.tsx | 10 + src/components/EnteDrawer.tsx | 11 + src/components/PhotoFrame.tsx | 22 ++ .../PhotoViewer/FileInfo/ExifData.tsx | 125 +++++---- .../PhotoViewer/FileInfo/InfoItem.tsx | 14 +- .../PhotoViewer/FileInfo/RenderCaption.tsx | 5 +- .../PhotoViewer/FileInfo/RenderFileName.tsx | 26 +- src/components/PhotoViewer/FileInfo/index.tsx | 241 +++++++++++++----- src/components/PhotoViewer/index.tsx | 31 +-- src/components/Sidebar/styledComponents.tsx | 8 +- src/components/Titlebar.tsx | 37 ++- src/pages/gallery/index.tsx | 1 + src/utils/exif/index.ts | 13 - src/utils/strings/englishConstants.tsx | 1 + 14 files changed, 363 insertions(+), 182 deletions(-) create mode 100644 src/components/Chip.tsx create mode 100644 src/components/EnteDrawer.tsx delete mode 100644 src/utils/exif/index.ts diff --git a/src/components/Chip.tsx b/src/components/Chip.tsx new file mode 100644 index 000000000..bce7429e1 --- /dev/null +++ b/src/components/Chip.tsx @@ -0,0 +1,10 @@ +import { Box, styled } from '@mui/material'; +import { CSSProperties } from 'react'; + +export const Chip = styled(Box)(({ theme }) => ({ + ...(theme.typography.body2 as CSSProperties), + padding: '8px 12px', + borderRadius: '4px', + backgroundColor: theme.palette.fill.dark, + fontWeight: 'bold', +})); diff --git a/src/components/EnteDrawer.tsx b/src/components/EnteDrawer.tsx new file mode 100644 index 000000000..5c860e9d6 --- /dev/null +++ b/src/components/EnteDrawer.tsx @@ -0,0 +1,11 @@ +import { Drawer } from '@mui/material'; +import styled from 'styled-components'; + +export const EnteDrawer = styled(Drawer)(({ theme }) => ({ + '& .MuiPaper-root': { + maxWidth: '375px', + width: '100%', + scrollbarWidth: 'thin', + padding: theme.spacing(1), + }, +})); diff --git a/src/components/PhotoFrame.tsx b/src/components/PhotoFrame.tsx index da588e654..6c719101d 100644 --- a/src/components/PhotoFrame.tsx +++ b/src/components/PhotoFrame.tsx @@ -31,6 +31,7 @@ import { CustomError } from 'utils/error'; import { User } from 'types/user'; import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { useMemo } from 'react'; +import { Collection } from 'types/collection'; const Container = styled('div')` display: block; @@ -49,6 +50,7 @@ const PHOTOSWIPE_HASH_SUFFIX = '&opened'; interface Props { files: EnteFile[]; + collections?: Collection[]; syncWithRemote: () => Promise; favItemIds?: Set; archivedCollections?: Set; @@ -76,6 +78,7 @@ type SourceURL = { const PhotoFrame = ({ files, + collections, syncWithRemote, favItemIds, archivedCollections, @@ -184,6 +187,23 @@ const PhotoFrame = ({ }); }, [files, deletedFileIds, search, activeCollection]); + const fileToCollectionsMap = useMemo(() => { + const fileToCollectionsMap = new Map(); + files.forEach((file) => { + if (!fileToCollectionsMap.get(file.id)) { + fileToCollectionsMap.set(file.id, []); + } + fileToCollectionsMap.get(file.id).push(file.collectionID); + }); + return fileToCollectionsMap; + }, [files]); + + const collectionNameMap = useMemo(() => { + return new Map( + collections.map((collection) => [collection.id, collection.name]) + ); + }, [collections]); + useEffect(() => { const currentURL = new URL(window.location.href); const end = currentURL.hash.lastIndexOf('&'); @@ -600,6 +620,8 @@ const PhotoFrame = ({ isTrashCollection={activeCollection === TRASH_SECTION} enableDownload={enableDownload} isSourceLoaded={isSourceLoaded} + fileToCollectionsMap={fileToCollectionsMap} + collectionNameMap={collectionNameMap} /> )} diff --git a/src/components/PhotoViewer/FileInfo/ExifData.tsx b/src/components/PhotoViewer/FileInfo/ExifData.tsx index 741ee2086..338fe1789 100644 --- a/src/components/PhotoViewer/FileInfo/ExifData.tsx +++ b/src/components/PhotoViewer/FileInfo/ExifData.tsx @@ -1,73 +1,70 @@ -import React, { useState } from 'react'; +import React from 'react'; import constants from 'utils/strings/constants'; -import { RenderInfoItem } from './RenderInfoItem'; -import { LegendContainer } from '../styledComponents/LegendContainer'; -import { Pre } from '../styledComponents/Pre'; -import { - Checkbox, - FormControlLabel, - FormGroup, - Typography, -} from '@mui/material'; +import { Stack, styled, Typography } from '@mui/material'; +import { FileInfoSidebar } from '.'; +import Titlebar from 'components/Titlebar'; +import { Box } from '@mui/system'; +import CopyButton from 'components/CodeBlock/CopyButton'; -export function ExifData(props: { exif: any }) { - const { exif } = props; - const [showAll, setShowAll] = useState(false); +const ExifItem = styled(Box)` + padding-left: 8px; + padding-right: 8px; + display: flex; + flex-direction: column; + gap: 4px; +`; - const changeHandler = (e: React.ChangeEvent) => { - setShowAll(e.target.checked); +function parseExifValue(value: any) { + switch (typeof value) { + case 'string': + case 'number': + return value; + case 'object': + if (value instanceof Date) { + return value.toString(); + } + break; + default: + return JSON.stringify(value); + } +} +export function ExifData(props: { + exif: any; + open: boolean; + onClose: () => void; + filename: string; + onInfoClose: () => void; +}) { + const { exif, open, onClose, filename, onInfoClose } = props; + + if (!exif) { + return <>; + } + const handleRootClose = () => { + onClose(); + onInfoClose(); }; - const renderAllValues = () =>
{exif.raw}
; - - const renderSelectedValues = () => ( - <> - {exif?.Make && - exif?.Model && - RenderInfoItem(constants.DEVICE, `${exif.Make} ${exif.Model}`)} - {exif?.ImageWidth && - exif?.ImageHeight && - RenderInfoItem( - constants.IMAGE_SIZE, - `${exif.ImageWidth} x ${exif.ImageHeight}` - )} - {exif?.Flash && RenderInfoItem(constants.FLASH, exif.Flash)} - {exif?.FocalLength && - RenderInfoItem( - constants.FOCAL_LENGTH, - exif.FocalLength.toString() - )} - {exif?.ApertureValue && - RenderInfoItem( - constants.APERTURE, - exif.ApertureValue.toString() - )} - {exif?.ISOSpeedRatings && - RenderInfoItem(constants.ISO, exif.ISOSpeedRatings.toString())} - - ); - return ( - <> - - - {constants.EXIF} - - - - } - label={constants.SHOW_ALL} - /> - - - {showAll ? renderAllValues() : renderSelectedValues()} - + + } + /> + + {[...Object.entries(exif)].map(([key, value]) => ( + + + {key} + + {parseExifValue(value)} + + ))} + + ); } diff --git a/src/components/PhotoViewer/FileInfo/InfoItem.tsx b/src/components/PhotoViewer/FileInfo/InfoItem.tsx index 8ebc1cc01..566316689 100644 --- a/src/components/PhotoViewer/FileInfo/InfoItem.tsx +++ b/src/components/PhotoViewer/FileInfo/InfoItem.tsx @@ -26,27 +26,29 @@ export default function InfoItem({ children, }: Iprops): JSX.Element { return ( - - + + {icon} - + {children ? ( children ) : ( <> - {title} + + {title} + {caption} )} - + {customEndButton ? customEndButton : !hideEditOption && ( diff --git a/src/components/PhotoViewer/FileInfo/RenderCaption.tsx b/src/components/PhotoViewer/FileInfo/RenderCaption.tsx index 5ab45e19b..cf4f6e56c 100644 --- a/src/components/PhotoViewer/FileInfo/RenderCaption.tsx +++ b/src/components/PhotoViewer/FileInfo/RenderCaption.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { updateFilePublicMagicMetadata } from 'services/fileService'; import { EnteFile } from 'types/file'; import { changeCaption, updateExistingFilePubMetadata } from 'utils/file'; @@ -35,9 +35,6 @@ export function RenderCaption({ setIsInEditMode(false); }; - useEffect(() => { - console.log(isInEditMode); - }, [isInEditMode]); const saveEdits = async (newCaption: string) => { try { if (file) { diff --git a/src/components/PhotoViewer/FileInfo/RenderFileName.tsx b/src/components/PhotoViewer/FileInfo/RenderFileName.tsx index 07a4e6f1d..6c31dfdee 100644 --- a/src/components/PhotoViewer/FileInfo/RenderFileName.tsx +++ b/src/components/PhotoViewer/FileInfo/RenderFileName.tsx @@ -13,6 +13,7 @@ import { FILE_TYPE } from 'constants/file'; import { PhotoOutlined, VideoFileOutlined } from '@mui/icons-material'; import InfoItem from './InfoItem'; import { makeHumanReadableStorage } from 'utils/billing'; +import Box from '@mui/material/Box'; const getFileTitle = (filename, extension) => { if (extension) { @@ -22,14 +23,14 @@ const getFileTitle = (filename, extension) => { } }; -const getCaption = (file: EnteFile, exif) => { - const cameraMP = exif?.['megaPixels']; - const resolution = exif?.['resolution']; +const getCaption = (file: EnteFile, parsedExifData) => { + const megaPixels = parsedExifData?.['megaPixels']; + const resolution = parsedExifData?.['resolution']; const fileSize = file.info?.fileSize; const captionParts = []; - if (cameraMP) { - captionParts.push(`${cameraMP} MP`); + if (megaPixels) { + captionParts.push(megaPixels); } if (resolution) { captionParts.push(resolution); @@ -37,16 +38,22 @@ const getCaption = (file: EnteFile, exif) => { if (fileSize) { captionParts.push(makeHumanReadableStorage(fileSize)); } - return captionParts.join(' '); + return ( + + {captionParts.map((caption) => ( + {caption} + ))} + + ); }; export function RenderFileName({ - exif, + parsedExifData, shouldDisableEdits, file, scheduleUpdate, }: { - exif: Record; + parsedExifData: Record; shouldDisableEdits: boolean; file: EnteFile; scheduleUpdate: () => void; @@ -93,9 +100,8 @@ export function RenderFileName({ ) } title={getFileTitle(filename, extension)} - caption={getCaption(file, exif)} + caption={getCaption(file, parsedExifData)} openEditor={openEditMode} - loading={false} hideEditOption={shouldDisableEdits || isInEditMode} /> ) : ( diff --git a/src/components/PhotoViewer/FileInfo/index.tsx b/src/components/PhotoViewer/FileInfo/index.tsx index 13adc6c85..f61960fa6 100644 --- a/src/components/PhotoViewer/FileInfo/index.tsx +++ b/src/components/PhotoViewer/FileInfo/index.tsx @@ -1,72 +1,100 @@ import React, { useEffect, useState } from 'react'; import constants from 'utils/strings/constants'; import { RenderFileName } from './RenderFileName'; -// import { ExifData } from './ExifData'; import { RenderCreationTime } from './RenderCreationTime'; -import { DialogProps, Drawer, Link, Stack, styled } from '@mui/material'; -import { Location, Metadata } from 'types/upload'; -import Photoswipe from 'photoswipe'; +import { Box, DialogProps, Link, Stack, styled } from '@mui/material'; +import { Location } from 'types/upload'; import { getEXIFLocation } from 'services/upload/exifService'; import { RenderCaption } from './RenderCaption'; import { BackupOutlined, + CameraOutlined, FolderOutlined, LocationOnOutlined, TextSnippetOutlined, } from '@mui/icons-material'; import CopyButton from 'components/CodeBlock/CopyButton'; -import { formatDateTime } from 'utils/time'; -import { Badge } from 'components/Badge'; +import { formatDateMedium, formatTime } from 'utils/time'; import Titlebar from 'components/Titlebar'; import InfoItem from './InfoItem'; +import { FlexWrapper } from 'components/Container'; +import EnteSpinner from 'components/EnteSpinner'; +import { EnteFile } from 'types/file'; +import { Chip } from 'components/Chip'; +import LinkButton from 'components/pages/gallery/LinkButton'; +import { ExifData } from './ExifData'; +import { EnteDrawer } from 'components/EnteDrawer'; -const FileInfoSidebar = styled((props: DialogProps) => ( - -))(({ theme }) => ({ +export const FileInfoSidebar = styled((props: DialogProps) => ( + +))({ zIndex: 1501, '& .MuiPaper-root': { - maxWidth: '375px', - width: '100%', - scrollbarWidth: 'thin', - padding: theme.spacing(1), + padding: 8, }, -})); +}); interface Iprops { shouldDisableEdits: boolean; showInfo: boolean; handleCloseInfo: () => void; - items: any[]; - photoSwipe: Photoswipe; - metadata: Metadata; + file: EnteFile; exif: any; scheduleUpdate: () => void; refreshPhotoswipe: () => void; + fileToCollectionsMap: Map; + collectionNameMap: Map; +} + +function BasicDeviceCamera({ + parsedExifData, +}: { + parsedExifData: Record; +}) { + return ( + + {parsedExifData['fNumber']} + {parsedExifData['exposureTime']} + {parsedExifData['ISO']} + + ); +} + +function getOpenStreetMapLink(location: { + latitude: number; + longitude: number; +}) { + return `https://www.openstreetmap.org/?mlat=${location.latitude}&mlon=${location.longitude}#map=15/${location.latitude}/${location.longitude}`; } export function FileInfo({ shouldDisableEdits, showInfo, handleCloseInfo, - items, - photoSwipe, - metadata, + file, exif, scheduleUpdate, refreshPhotoswipe, + fileToCollectionsMap, + collectionNameMap, }: Iprops) { const [location, setLocation] = useState(null); + const [parsedExifData, setParsedExifData] = useState>(); + const [showExif, setShowExif] = useState(false); + + const openExif = () => setShowExif(true); + const closeExif = () => setShowExif(false); useEffect(() => { - if (!location && metadata) { - if (metadata.longitude || metadata.longitude === 0) { + if (!location && file && file.metadata) { + if (file.metadata.longitude || file.metadata.longitude === 0) { setLocation({ - latitude: metadata.latitude, - longitude: metadata.longitude, + latitude: file.metadata.latitude, + longitude: file.metadata.longitude, }); } } - }, [metadata]); + }, [file]); useEffect(() => { if (!location && exif) { @@ -77,80 +105,175 @@ export function FileInfo({ } }, [exif]); - if (!metadata) { + useEffect(() => { + if (!exif) { + return; + } + const parsedExifData = {}; + if (exif['fNumber']) { + parsedExifData['fNumber'] = `f/${Math.ceil(exif['FNumber'])}`; + } else if (exif['ApertureValue'] && exif['FocalLength']) { + parsedExifData['fNumber'] = `f/${Math.ceil( + exif['FocalLength'] / exif['ApertureValue'] + )}`; + } + const imageWidth = exif['ImageWidth'] ?? exif['ExifImageWidth']; + const imageHeight = exif['ImageHeight'] ?? exif['ExifImageHeight']; + if (imageWidth && imageHeight) { + parsedExifData['resolution'] = `${imageWidth} x ${imageHeight}`; + parsedExifData['megaPixels'] = `${Math.round( + (imageWidth * imageHeight) / 1000000 + )}MP`; + } + if (exif['Make'] && exif['Model']) { + parsedExifData[ + 'takenOnDevice' + ] = `${exif['Make']} ${exif['Model']}`; + } + if (exif['ExposureTime']) { + parsedExifData['exposureTime'] = exif['ExposureTime']; + } + if (exif['ISO']) { + parsedExifData['ISO'] = `ISO${exif['ISO']}`; + } + setParsedExifData(parsedExifData); + }, [exif]); + + if (!file) { return <>; } return ( - + - - {location && ( + {parsedExifData && parsedExifData['takenOnDevice'] && ( } - title={constants.LOCATION} + icon={} + title={parsedExifData['takenOnDevice']} caption={ - - {constants.SHOW_ON_MAP} - - } - customEndButton={ - } + hideEditOption /> )} + + {/* {location && ( */} + } + title={constants.LOCATION} + caption={ + + {constants.SHOW_ON_MAP} + + } + customEndButton={ + + } + /> + {/* )} */} } title={constants.DETAILS} - caption={constants.VIEW_EXIF} + caption={ + typeof exif === 'undefined' ? ( + + ) : exif !== null ? ( + + {constants.VIEW_EXIF} + + ) : ( + constants.NO_EXIF + ) + } hideEditOption /> } - title={formatDateTime(metadata.modificationTime / 1000)} - caption={formatDateTime(metadata.modificationTime / 1000)} + title={formatDateMedium( + file.metadata.modificationTime / 1000 + )} + caption={formatTime(file.metadata.modificationTime / 1000)} hideEditOption /> } hideEditOption> - - abc - DEF - GHI - + + {fileToCollectionsMap + .get(file.id) + .map((collectionID) => ( + <> + + {collectionNameMap.get(collectionID)} + + + {collectionNameMap.get(collectionID)} + + + {collectionNameMap.get(collectionID)} + + + {collectionNameMap.get(collectionID)} + + + ))} + - - {/* {exif && ( - <> - - - )} */} + ); } diff --git a/src/components/PhotoViewer/index.tsx b/src/components/PhotoViewer/index.tsx index 55139e6b0..a0e72a996 100644 --- a/src/components/PhotoViewer/index.tsx +++ b/src/components/PhotoViewer/index.tsx @@ -10,7 +10,6 @@ import { EnteFile } from 'types/file'; import constants from 'utils/strings/constants'; import exifr from 'exifr'; import { downloadFile } from 'utils/file'; -import { prettyPrintExif } from 'utils/exif'; import { livePhotoBtnHTML } from 'components/LivePhotoBtn'; import { logError } from 'utils/sentry'; @@ -61,6 +60,8 @@ interface Iprops { isTrashCollection: boolean; enableDownload: boolean; isSourceLoaded: boolean; + fileToCollectionsMap: Map; + collectionNameMap: Map; } function PhotoViewer(props: Iprops) { @@ -71,7 +72,6 @@ function PhotoViewer(props: Iprops) { const { isOpen, items, isSourceLoaded } = props; const [isFav, setIsFav] = useState(false); const [showInfo, setShowInfo] = useState(false); - const [metadata, setMetaData] = useState(null); const [exif, setExif] = useState(null); const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState( defaultLivePhotoDefaultOptions @@ -318,8 +318,10 @@ function PhotoViewer(props: Iprops) { } }; - const checkExifAvailable = async () => { - setExif(null); + const checkExifAvailable = async (force?: boolean) => { + if (exif || !force) { + return; + } await sleep(100); try { const img: HTMLImageElement = document.querySelector( @@ -327,11 +329,11 @@ function PhotoViewer(props: Iprops) { ); if (img) { const exifData = await exifr.parse(img); - if (!exifData) { - return; + if (exifData) { + setExif(exifData); + } else { + setExif(null); } - exifData.raw = prettyPrintExif(exifData); - setExif(exifData); } } catch (e) { logError(e, 'exifr parsing failed'); @@ -340,10 +342,9 @@ function PhotoViewer(props: Iprops) { function updateInfo() { const file: EnteFile = this?.currItem; - if (file?.metadata) { - setMetaData(file.metadata); - setExif(null); - checkExifAvailable(); + if (file) { + setExif(undefined); + checkExifAvailable(true); } } @@ -493,12 +494,12 @@ function PhotoViewer(props: Iprops) { shouldDisableEdits={props.isSharedCollection} showInfo={showInfo} handleCloseInfo={handleCloseInfo} - items={items} - photoSwipe={photoSwipe} - metadata={metadata} + file={photoSwipe?.currItem as EnteFile} exif={exif} scheduleUpdate={scheduleUpdate} refreshPhotoswipe={refreshPhotoswipe} + fileToCollectionsMap={props.fileToCollectionsMap} + collectionNameMap={props.collectionNameMap} /> ); diff --git a/src/components/Sidebar/styledComponents.tsx b/src/components/Sidebar/styledComponents.tsx index 433c9ab0a..15c11f83f 100644 --- a/src/components/Sidebar/styledComponents.tsx +++ b/src/components/Sidebar/styledComponents.tsx @@ -1,11 +1,9 @@ -import { Drawer, styled } from '@mui/material'; +import { styled } from '@mui/material'; import CircleIcon from '@mui/icons-material/Circle'; +import { EnteDrawer } from 'components/EnteDrawer'; -export const DrawerSidebar = styled(Drawer)(({ theme }) => ({ +export const DrawerSidebar = styled(EnteDrawer)(({ theme }) => ({ '& .MuiPaper-root': { - maxWidth: '375px', - width: '100%', - scrollbarWidth: 'thin', padding: theme.spacing(1.5), }, })); diff --git a/src/components/Titlebar.tsx b/src/components/Titlebar.tsx index f5f55b855..b379cd1d1 100644 --- a/src/components/Titlebar.tsx +++ b/src/components/Titlebar.tsx @@ -1,30 +1,55 @@ -import { Close } from '@mui/icons-material'; +import { ArrowBack, Close } from '@mui/icons-material'; import { Box, IconButton, Typography } from '@mui/material'; import React from 'react'; +import { FlexWrapper } from './Container'; interface Iprops { title: string; caption?: string; onClose: () => void; + backIsClose?: boolean; + onRootClose?: () => void; + actionButton?: JSX.Element; } export default function Titlebar({ title, caption, onClose, + backIsClose, + actionButton, + onRootClose, }: Iprops): JSX.Element { return ( <> - - - + + + {backIsClose ? : } - + + {actionButton && actionButton} + {!backIsClose && ( + + + + )} + + {title} - {caption} + + {caption} + ); diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index aab01b4a7..497551545 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -666,6 +666,7 @@ export default function Gallery() { />