redesign fileinfo completed
This commit is contained in:
parent
2edeeb0371
commit
7ea4a26e45
10
src/components/Chip.tsx
Normal file
10
src/components/Chip.tsx
Normal file
|
@ -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',
|
||||||
|
}));
|
11
src/components/EnteDrawer.tsx
Normal file
11
src/components/EnteDrawer.tsx
Normal file
|
@ -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),
|
||||||
|
},
|
||||||
|
}));
|
|
@ -31,6 +31,7 @@ import { CustomError } from 'utils/error';
|
||||||
import { User } from 'types/user';
|
import { User } from 'types/user';
|
||||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { Collection } from 'types/collection';
|
||||||
|
|
||||||
const Container = styled('div')`
|
const Container = styled('div')`
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -49,6 +50,7 @@ const PHOTOSWIPE_HASH_SUFFIX = '&opened';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
files: EnteFile[];
|
files: EnteFile[];
|
||||||
|
collections?: Collection[];
|
||||||
syncWithRemote: () => Promise<void>;
|
syncWithRemote: () => Promise<void>;
|
||||||
favItemIds?: Set<number>;
|
favItemIds?: Set<number>;
|
||||||
archivedCollections?: Set<number>;
|
archivedCollections?: Set<number>;
|
||||||
|
@ -76,6 +78,7 @@ type SourceURL = {
|
||||||
|
|
||||||
const PhotoFrame = ({
|
const PhotoFrame = ({
|
||||||
files,
|
files,
|
||||||
|
collections,
|
||||||
syncWithRemote,
|
syncWithRemote,
|
||||||
favItemIds,
|
favItemIds,
|
||||||
archivedCollections,
|
archivedCollections,
|
||||||
|
@ -184,6 +187,23 @@ const PhotoFrame = ({
|
||||||
});
|
});
|
||||||
}, [files, deletedFileIds, search, activeCollection]);
|
}, [files, deletedFileIds, search, activeCollection]);
|
||||||
|
|
||||||
|
const fileToCollectionsMap = useMemo(() => {
|
||||||
|
const fileToCollectionsMap = new Map<number, number[]>();
|
||||||
|
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<number, string>(
|
||||||
|
collections.map((collection) => [collection.id, collection.name])
|
||||||
|
);
|
||||||
|
}, [collections]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentURL = new URL(window.location.href);
|
const currentURL = new URL(window.location.href);
|
||||||
const end = currentURL.hash.lastIndexOf('&');
|
const end = currentURL.hash.lastIndexOf('&');
|
||||||
|
@ -600,6 +620,8 @@ const PhotoFrame = ({
|
||||||
isTrashCollection={activeCollection === TRASH_SECTION}
|
isTrashCollection={activeCollection === TRASH_SECTION}
|
||||||
enableDownload={enableDownload}
|
enableDownload={enableDownload}
|
||||||
isSourceLoaded={isSourceLoaded}
|
isSourceLoaded={isSourceLoaded}
|
||||||
|
fileToCollectionsMap={fileToCollectionsMap}
|
||||||
|
collectionNameMap={collectionNameMap}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,73 +1,70 @@
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
|
||||||
import { RenderInfoItem } from './RenderInfoItem';
|
import { Stack, styled, Typography } from '@mui/material';
|
||||||
import { LegendContainer } from '../styledComponents/LegendContainer';
|
import { FileInfoSidebar } from '.';
|
||||||
import { Pre } from '../styledComponents/Pre';
|
import Titlebar from 'components/Titlebar';
|
||||||
import {
|
import { Box } from '@mui/system';
|
||||||
Checkbox,
|
import CopyButton from 'components/CodeBlock/CopyButton';
|
||||||
FormControlLabel,
|
|
||||||
FormGroup,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
|
||||||
|
|
||||||
export function ExifData(props: { exif: any }) {
|
const ExifItem = styled(Box)`
|
||||||
const { exif } = props;
|
padding-left: 8px;
|
||||||
const [showAll, setShowAll] = useState(false);
|
padding-right: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
`;
|
||||||
|
|
||||||
const changeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
function parseExifValue(value: any) {
|
||||||
setShowAll(e.target.checked);
|
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 = () => <Pre>{exif.raw}</Pre>;
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<>
|
<FileInfoSidebar open={open} onClose={onClose}>
|
||||||
<LegendContainer>
|
<Titlebar
|
||||||
<Typography variant="subtitle" mb={1}>
|
onClose={onClose}
|
||||||
{constants.EXIF}
|
title={constants.EXIF}
|
||||||
</Typography>
|
caption={filename}
|
||||||
<FormGroup>
|
onRootClose={handleRootClose}
|
||||||
<FormControlLabel
|
actionButton={<CopyButton code={exif} color={'secondary'} />}
|
||||||
control={
|
/>
|
||||||
<Checkbox
|
<Stack py={3} px={1} spacing={2}>
|
||||||
size="small"
|
{[...Object.entries(exif)].map(([key, value]) => (
|
||||||
onChange={changeHandler}
|
<ExifItem key={key}>
|
||||||
color="accent"
|
<Typography variant="body2" color={'text.secondary'}>
|
||||||
/>
|
{key}
|
||||||
}
|
</Typography>
|
||||||
label={constants.SHOW_ALL}
|
<Typography>{parseExifValue(value)}</Typography>
|
||||||
/>
|
</ExifItem>
|
||||||
</FormGroup>
|
))}
|
||||||
</LegendContainer>
|
</Stack>
|
||||||
{showAll ? renderAllValues() : renderSelectedValues()}
|
</FileInfoSidebar>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,27 +26,29 @@ export default function InfoItem({
|
||||||
children,
|
children,
|
||||||
}: Iprops): JSX.Element {
|
}: Iprops): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<FlexWrapper height={48} justifyContent="space-between">
|
<FlexWrapper justifyContent="space-between">
|
||||||
<FlexWrapper gap={0.5} pr={1}>
|
<Box display={'flex'} alignItems="flex-start" gap={0.5} pr={1}>
|
||||||
<IconButton
|
<IconButton
|
||||||
color="secondary"
|
color="secondary"
|
||||||
sx={{ '&&': { cursor: 'default' } }}
|
sx={{ '&&': { cursor: 'default', m: 0.5 } }}
|
||||||
disableRipple>
|
disableRipple>
|
||||||
{icon}
|
{icon}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Box>
|
<Box py={0.5}>
|
||||||
{children ? (
|
{children ? (
|
||||||
children
|
children
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Typography>{title}</Typography>
|
<Typography sx={{ wordBreak: 'break-all' }}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{caption}
|
{caption}
|
||||||
</Typography>
|
</Typography>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</FlexWrapper>
|
</Box>
|
||||||
{customEndButton
|
{customEndButton
|
||||||
? customEndButton
|
? customEndButton
|
||||||
: !hideEditOption && (
|
: !hideEditOption && (
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
import { changeCaption, updateExistingFilePubMetadata } from 'utils/file';
|
import { changeCaption, updateExistingFilePubMetadata } from 'utils/file';
|
||||||
|
@ -35,9 +35,6 @@ export function RenderCaption({
|
||||||
setIsInEditMode(false);
|
setIsInEditMode(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(isInEditMode);
|
|
||||||
}, [isInEditMode]);
|
|
||||||
const saveEdits = async (newCaption: string) => {
|
const saveEdits = async (newCaption: string) => {
|
||||||
try {
|
try {
|
||||||
if (file) {
|
if (file) {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { FILE_TYPE } from 'constants/file';
|
||||||
import { PhotoOutlined, VideoFileOutlined } from '@mui/icons-material';
|
import { PhotoOutlined, VideoFileOutlined } from '@mui/icons-material';
|
||||||
import InfoItem from './InfoItem';
|
import InfoItem from './InfoItem';
|
||||||
import { makeHumanReadableStorage } from 'utils/billing';
|
import { makeHumanReadableStorage } from 'utils/billing';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
|
||||||
const getFileTitle = (filename, extension) => {
|
const getFileTitle = (filename, extension) => {
|
||||||
if (extension) {
|
if (extension) {
|
||||||
|
@ -22,14 +23,14 @@ const getFileTitle = (filename, extension) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCaption = (file: EnteFile, exif) => {
|
const getCaption = (file: EnteFile, parsedExifData) => {
|
||||||
const cameraMP = exif?.['megaPixels'];
|
const megaPixels = parsedExifData?.['megaPixels'];
|
||||||
const resolution = exif?.['resolution'];
|
const resolution = parsedExifData?.['resolution'];
|
||||||
const fileSize = file.info?.fileSize;
|
const fileSize = file.info?.fileSize;
|
||||||
|
|
||||||
const captionParts = [];
|
const captionParts = [];
|
||||||
if (cameraMP) {
|
if (megaPixels) {
|
||||||
captionParts.push(`${cameraMP} MP`);
|
captionParts.push(megaPixels);
|
||||||
}
|
}
|
||||||
if (resolution) {
|
if (resolution) {
|
||||||
captionParts.push(resolution);
|
captionParts.push(resolution);
|
||||||
|
@ -37,16 +38,22 @@ const getCaption = (file: EnteFile, exif) => {
|
||||||
if (fileSize) {
|
if (fileSize) {
|
||||||
captionParts.push(makeHumanReadableStorage(fileSize));
|
captionParts.push(makeHumanReadableStorage(fileSize));
|
||||||
}
|
}
|
||||||
return captionParts.join(' ');
|
return (
|
||||||
|
<FlexWrapper gap={1}>
|
||||||
|
{captionParts.map((caption) => (
|
||||||
|
<Box key={caption}> {caption}</Box>
|
||||||
|
))}
|
||||||
|
</FlexWrapper>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RenderFileName({
|
export function RenderFileName({
|
||||||
exif,
|
parsedExifData,
|
||||||
shouldDisableEdits,
|
shouldDisableEdits,
|
||||||
file,
|
file,
|
||||||
scheduleUpdate,
|
scheduleUpdate,
|
||||||
}: {
|
}: {
|
||||||
exif: Record<string, any>;
|
parsedExifData: Record<string, any>;
|
||||||
shouldDisableEdits: boolean;
|
shouldDisableEdits: boolean;
|
||||||
file: EnteFile;
|
file: EnteFile;
|
||||||
scheduleUpdate: () => void;
|
scheduleUpdate: () => void;
|
||||||
|
@ -93,9 +100,8 @@ export function RenderFileName({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
title={getFileTitle(filename, extension)}
|
title={getFileTitle(filename, extension)}
|
||||||
caption={getCaption(file, exif)}
|
caption={getCaption(file, parsedExifData)}
|
||||||
openEditor={openEditMode}
|
openEditor={openEditMode}
|
||||||
loading={false}
|
|
||||||
hideEditOption={shouldDisableEdits || isInEditMode}
|
hideEditOption={shouldDisableEdits || isInEditMode}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,72 +1,100 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { RenderFileName } from './RenderFileName';
|
import { RenderFileName } from './RenderFileName';
|
||||||
// import { ExifData } from './ExifData';
|
|
||||||
import { RenderCreationTime } from './RenderCreationTime';
|
import { RenderCreationTime } from './RenderCreationTime';
|
||||||
import { DialogProps, Drawer, Link, Stack, styled } from '@mui/material';
|
import { Box, DialogProps, Link, Stack, styled } from '@mui/material';
|
||||||
import { Location, Metadata } from 'types/upload';
|
import { Location } from 'types/upload';
|
||||||
import Photoswipe from 'photoswipe';
|
|
||||||
import { getEXIFLocation } from 'services/upload/exifService';
|
import { getEXIFLocation } from 'services/upload/exifService';
|
||||||
import { RenderCaption } from './RenderCaption';
|
import { RenderCaption } from './RenderCaption';
|
||||||
import {
|
import {
|
||||||
BackupOutlined,
|
BackupOutlined,
|
||||||
|
CameraOutlined,
|
||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
LocationOnOutlined,
|
LocationOnOutlined,
|
||||||
TextSnippetOutlined,
|
TextSnippetOutlined,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import CopyButton from 'components/CodeBlock/CopyButton';
|
import CopyButton from 'components/CodeBlock/CopyButton';
|
||||||
import { formatDateTime } from 'utils/time';
|
import { formatDateMedium, formatTime } from 'utils/time';
|
||||||
import { Badge } from 'components/Badge';
|
|
||||||
import Titlebar from 'components/Titlebar';
|
import Titlebar from 'components/Titlebar';
|
||||||
import InfoItem from './InfoItem';
|
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) => (
|
export const FileInfoSidebar = styled((props: DialogProps) => (
|
||||||
<Drawer {...props} anchor="right" />
|
<EnteDrawer {...props} anchor="right" />
|
||||||
))(({ theme }) => ({
|
))({
|
||||||
zIndex: 1501,
|
zIndex: 1501,
|
||||||
'& .MuiPaper-root': {
|
'& .MuiPaper-root': {
|
||||||
maxWidth: '375px',
|
padding: 8,
|
||||||
width: '100%',
|
|
||||||
scrollbarWidth: 'thin',
|
|
||||||
padding: theme.spacing(1),
|
|
||||||
},
|
},
|
||||||
}));
|
});
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
shouldDisableEdits: boolean;
|
shouldDisableEdits: boolean;
|
||||||
showInfo: boolean;
|
showInfo: boolean;
|
||||||
handleCloseInfo: () => void;
|
handleCloseInfo: () => void;
|
||||||
items: any[];
|
file: EnteFile;
|
||||||
photoSwipe: Photoswipe<Photoswipe.Options>;
|
|
||||||
metadata: Metadata;
|
|
||||||
exif: any;
|
exif: any;
|
||||||
scheduleUpdate: () => void;
|
scheduleUpdate: () => void;
|
||||||
refreshPhotoswipe: () => void;
|
refreshPhotoswipe: () => void;
|
||||||
|
fileToCollectionsMap: Map<number, number[]>;
|
||||||
|
collectionNameMap: Map<number, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BasicDeviceCamera({
|
||||||
|
parsedExifData,
|
||||||
|
}: {
|
||||||
|
parsedExifData: Record<string, any>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FlexWrapper gap={1}>
|
||||||
|
<Box>{parsedExifData['fNumber']}</Box>
|
||||||
|
<Box>{parsedExifData['exposureTime']}</Box>
|
||||||
|
<Box>{parsedExifData['ISO']}</Box>
|
||||||
|
</FlexWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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({
|
export function FileInfo({
|
||||||
shouldDisableEdits,
|
shouldDisableEdits,
|
||||||
showInfo,
|
showInfo,
|
||||||
handleCloseInfo,
|
handleCloseInfo,
|
||||||
items,
|
file,
|
||||||
photoSwipe,
|
|
||||||
metadata,
|
|
||||||
exif,
|
exif,
|
||||||
scheduleUpdate,
|
scheduleUpdate,
|
||||||
refreshPhotoswipe,
|
refreshPhotoswipe,
|
||||||
|
fileToCollectionsMap,
|
||||||
|
collectionNameMap,
|
||||||
}: Iprops) {
|
}: Iprops) {
|
||||||
const [location, setLocation] = useState<Location>(null);
|
const [location, setLocation] = useState<Location>(null);
|
||||||
|
const [parsedExifData, setParsedExifData] = useState<Record<string, any>>();
|
||||||
|
const [showExif, setShowExif] = useState(false);
|
||||||
|
|
||||||
|
const openExif = () => setShowExif(true);
|
||||||
|
const closeExif = () => setShowExif(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!location && metadata) {
|
if (!location && file && file.metadata) {
|
||||||
if (metadata.longitude || metadata.longitude === 0) {
|
if (file.metadata.longitude || file.metadata.longitude === 0) {
|
||||||
setLocation({
|
setLocation({
|
||||||
latitude: metadata.latitude,
|
latitude: file.metadata.latitude,
|
||||||
longitude: metadata.longitude,
|
longitude: file.metadata.longitude,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [metadata]);
|
}, [file]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!location && exif) {
|
if (!location && exif) {
|
||||||
|
@ -77,80 +105,175 @@ export function FileInfo({
|
||||||
}
|
}
|
||||||
}, [exif]);
|
}, [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 <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileInfoSidebar open={showInfo} onClose={handleCloseInfo}>
|
<FileInfoSidebar open={showInfo} onClose={handleCloseInfo}>
|
||||||
<Titlebar onClose={handleCloseInfo} title={constants.INFO} />
|
<Titlebar
|
||||||
|
onClose={handleCloseInfo}
|
||||||
|
title={constants.INFO}
|
||||||
|
backIsClose
|
||||||
|
/>
|
||||||
<Stack pt={1} pb={3} spacing={'20px'}>
|
<Stack pt={1} pb={3} spacing={'20px'}>
|
||||||
<RenderCaption
|
<RenderCaption
|
||||||
shouldDisableEdits={shouldDisableEdits}
|
shouldDisableEdits={shouldDisableEdits}
|
||||||
file={items[photoSwipe?.getCurrentIndex()]}
|
file={file}
|
||||||
scheduleUpdate={scheduleUpdate}
|
scheduleUpdate={scheduleUpdate}
|
||||||
refreshPhotoswipe={refreshPhotoswipe}
|
refreshPhotoswipe={refreshPhotoswipe}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RenderCreationTime
|
<RenderCreationTime
|
||||||
shouldDisableEdits={shouldDisableEdits}
|
shouldDisableEdits={shouldDisableEdits}
|
||||||
file={items[photoSwipe?.getCurrentIndex()]}
|
file={file}
|
||||||
scheduleUpdate={scheduleUpdate}
|
scheduleUpdate={scheduleUpdate}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RenderFileName
|
<RenderFileName
|
||||||
exif={exif}
|
parsedExifData={parsedExifData}
|
||||||
shouldDisableEdits={shouldDisableEdits}
|
shouldDisableEdits={shouldDisableEdits}
|
||||||
file={items[photoSwipe?.getCurrentIndex()]}
|
file={file}
|
||||||
scheduleUpdate={scheduleUpdate}
|
scheduleUpdate={scheduleUpdate}
|
||||||
/>
|
/>
|
||||||
|
{parsedExifData && parsedExifData['takenOnDevice'] && (
|
||||||
{location && (
|
|
||||||
<InfoItem
|
<InfoItem
|
||||||
icon={<LocationOnOutlined />}
|
icon={<CameraOutlined />}
|
||||||
title={constants.LOCATION}
|
title={parsedExifData['takenOnDevice']}
|
||||||
caption={
|
caption={
|
||||||
<Link
|
<BasicDeviceCamera
|
||||||
href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}>
|
parsedExifData={parsedExifData}
|
||||||
{constants.SHOW_ON_MAP}
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
customEndButton={
|
|
||||||
<CopyButton
|
|
||||||
code={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
|
|
||||||
color="secondary"
|
|
||||||
size="medium"
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
hideEditOption
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* {location && ( */}
|
||||||
|
<InfoItem
|
||||||
|
icon={<LocationOnOutlined />}
|
||||||
|
title={constants.LOCATION}
|
||||||
|
caption={
|
||||||
|
<Link
|
||||||
|
href={getOpenStreetMapLink({
|
||||||
|
latitude: file.metadata.latitude,
|
||||||
|
longitude: file.metadata.longitude,
|
||||||
|
})}
|
||||||
|
target="_blank">
|
||||||
|
{constants.SHOW_ON_MAP}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
customEndButton={
|
||||||
|
<CopyButton
|
||||||
|
code={getOpenStreetMapLink({
|
||||||
|
latitude: file.metadata.latitude,
|
||||||
|
longitude: file.metadata.longitude,
|
||||||
|
})}
|
||||||
|
color="secondary"
|
||||||
|
size="medium"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/* )} */}
|
||||||
<InfoItem
|
<InfoItem
|
||||||
icon={<TextSnippetOutlined />}
|
icon={<TextSnippetOutlined />}
|
||||||
title={constants.DETAILS}
|
title={constants.DETAILS}
|
||||||
caption={constants.VIEW_EXIF}
|
caption={
|
||||||
|
typeof exif === 'undefined' ? (
|
||||||
|
<EnteSpinner size={11.33} />
|
||||||
|
) : exif !== null ? (
|
||||||
|
<LinkButton
|
||||||
|
onClick={openExif}
|
||||||
|
sx={{
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'text.secondary',
|
||||||
|
}}>
|
||||||
|
{constants.VIEW_EXIF}
|
||||||
|
</LinkButton>
|
||||||
|
) : (
|
||||||
|
constants.NO_EXIF
|
||||||
|
)
|
||||||
|
}
|
||||||
hideEditOption
|
hideEditOption
|
||||||
/>
|
/>
|
||||||
<InfoItem
|
<InfoItem
|
||||||
icon={<BackupOutlined />}
|
icon={<BackupOutlined />}
|
||||||
title={formatDateTime(metadata.modificationTime / 1000)}
|
title={formatDateMedium(
|
||||||
caption={formatDateTime(metadata.modificationTime / 1000)}
|
file.metadata.modificationTime / 1000
|
||||||
|
)}
|
||||||
|
caption={formatTime(file.metadata.modificationTime / 1000)}
|
||||||
hideEditOption
|
hideEditOption
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InfoItem icon={<FolderOutlined />} hideEditOption>
|
<InfoItem icon={<FolderOutlined />} hideEditOption>
|
||||||
<Stack spacing={1} direction="row">
|
<Box
|
||||||
<Badge>abc</Badge>
|
display={'flex'}
|
||||||
<Badge>DEF</Badge>
|
gap={1}
|
||||||
<Badge>GHI</Badge>
|
flexWrap="wrap"
|
||||||
</Stack>
|
justifyContent={'flex-start'}
|
||||||
|
alignItems={'flex-start'}>
|
||||||
|
{fileToCollectionsMap
|
||||||
|
.get(file.id)
|
||||||
|
.map((collectionID) => (
|
||||||
|
<>
|
||||||
|
<Chip key={collectionID}>
|
||||||
|
{collectionNameMap.get(collectionID)}
|
||||||
|
</Chip>
|
||||||
|
<Chip key={collectionID}>
|
||||||
|
{collectionNameMap.get(collectionID)}
|
||||||
|
</Chip>
|
||||||
|
<Chip key={collectionID}>
|
||||||
|
{collectionNameMap.get(collectionID)}
|
||||||
|
</Chip>
|
||||||
|
<Chip key={collectionID}>
|
||||||
|
{collectionNameMap.get(collectionID)}
|
||||||
|
</Chip>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
</InfoItem>
|
</InfoItem>
|
||||||
|
|
||||||
{/* {exif && (
|
|
||||||
<>
|
|
||||||
<ExifData exif={exif} />
|
|
||||||
</>
|
|
||||||
)} */}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<ExifData
|
||||||
|
exif={exif}
|
||||||
|
open={showExif}
|
||||||
|
onClose={closeExif}
|
||||||
|
onInfoClose={handleCloseInfo}
|
||||||
|
filename={file.metadata.title}
|
||||||
|
/>
|
||||||
</FileInfoSidebar>
|
</FileInfoSidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { EnteFile } from 'types/file';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import exifr from 'exifr';
|
import exifr from 'exifr';
|
||||||
import { downloadFile } from 'utils/file';
|
import { downloadFile } from 'utils/file';
|
||||||
import { prettyPrintExif } from 'utils/exif';
|
|
||||||
import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
|
import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
|
@ -61,6 +60,8 @@ interface Iprops {
|
||||||
isTrashCollection: boolean;
|
isTrashCollection: boolean;
|
||||||
enableDownload: boolean;
|
enableDownload: boolean;
|
||||||
isSourceLoaded: boolean;
|
isSourceLoaded: boolean;
|
||||||
|
fileToCollectionsMap: Map<number, number[]>;
|
||||||
|
collectionNameMap: Map<number, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PhotoViewer(props: Iprops) {
|
function PhotoViewer(props: Iprops) {
|
||||||
|
@ -71,7 +72,6 @@ function PhotoViewer(props: Iprops) {
|
||||||
const { isOpen, items, isSourceLoaded } = props;
|
const { isOpen, items, isSourceLoaded } = props;
|
||||||
const [isFav, setIsFav] = useState(false);
|
const [isFav, setIsFav] = useState(false);
|
||||||
const [showInfo, setShowInfo] = useState(false);
|
const [showInfo, setShowInfo] = useState(false);
|
||||||
const [metadata, setMetaData] = useState<EnteFile['metadata']>(null);
|
|
||||||
const [exif, setExif] = useState<any>(null);
|
const [exif, setExif] = useState<any>(null);
|
||||||
const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState(
|
const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState(
|
||||||
defaultLivePhotoDefaultOptions
|
defaultLivePhotoDefaultOptions
|
||||||
|
@ -318,8 +318,10 @@ function PhotoViewer(props: Iprops) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkExifAvailable = async () => {
|
const checkExifAvailable = async (force?: boolean) => {
|
||||||
setExif(null);
|
if (exif || !force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
try {
|
try {
|
||||||
const img: HTMLImageElement = document.querySelector(
|
const img: HTMLImageElement = document.querySelector(
|
||||||
|
@ -327,11 +329,11 @@ function PhotoViewer(props: Iprops) {
|
||||||
);
|
);
|
||||||
if (img) {
|
if (img) {
|
||||||
const exifData = await exifr.parse(img);
|
const exifData = await exifr.parse(img);
|
||||||
if (!exifData) {
|
if (exifData) {
|
||||||
return;
|
setExif(exifData);
|
||||||
|
} else {
|
||||||
|
setExif(null);
|
||||||
}
|
}
|
||||||
exifData.raw = prettyPrintExif(exifData);
|
|
||||||
setExif(exifData);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'exifr parsing failed');
|
logError(e, 'exifr parsing failed');
|
||||||
|
@ -340,10 +342,9 @@ function PhotoViewer(props: Iprops) {
|
||||||
|
|
||||||
function updateInfo() {
|
function updateInfo() {
|
||||||
const file: EnteFile = this?.currItem;
|
const file: EnteFile = this?.currItem;
|
||||||
if (file?.metadata) {
|
if (file) {
|
||||||
setMetaData(file.metadata);
|
setExif(undefined);
|
||||||
setExif(null);
|
checkExifAvailable(true);
|
||||||
checkExifAvailable();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -493,12 +494,12 @@ function PhotoViewer(props: Iprops) {
|
||||||
shouldDisableEdits={props.isSharedCollection}
|
shouldDisableEdits={props.isSharedCollection}
|
||||||
showInfo={showInfo}
|
showInfo={showInfo}
|
||||||
handleCloseInfo={handleCloseInfo}
|
handleCloseInfo={handleCloseInfo}
|
||||||
items={items}
|
file={photoSwipe?.currItem as EnteFile}
|
||||||
photoSwipe={photoSwipe}
|
|
||||||
metadata={metadata}
|
|
||||||
exif={exif}
|
exif={exif}
|
||||||
scheduleUpdate={scheduleUpdate}
|
scheduleUpdate={scheduleUpdate}
|
||||||
refreshPhotoswipe={refreshPhotoswipe}
|
refreshPhotoswipe={refreshPhotoswipe}
|
||||||
|
fileToCollectionsMap={props.fileToCollectionsMap}
|
||||||
|
collectionNameMap={props.collectionNameMap}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { Drawer, styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import CircleIcon from '@mui/icons-material/Circle';
|
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': {
|
'& .MuiPaper-root': {
|
||||||
maxWidth: '375px',
|
|
||||||
width: '100%',
|
|
||||||
scrollbarWidth: 'thin',
|
|
||||||
padding: theme.spacing(1.5),
|
padding: theme.spacing(1.5),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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 { Box, IconButton, Typography } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { FlexWrapper } from './Container';
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
title: string;
|
title: string;
|
||||||
caption?: string;
|
caption?: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
backIsClose?: boolean;
|
||||||
|
onRootClose?: () => void;
|
||||||
|
actionButton?: JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Titlebar({
|
export default function Titlebar({
|
||||||
title,
|
title,
|
||||||
caption,
|
caption,
|
||||||
onClose,
|
onClose,
|
||||||
|
backIsClose,
|
||||||
|
actionButton,
|
||||||
|
onRootClose,
|
||||||
}: Iprops): JSX.Element {
|
}: Iprops): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box display={'flex'} height={48} alignItems={'center'}>
|
<FlexWrapper
|
||||||
<IconButton onClick={onClose} color="secondary">
|
height={48}
|
||||||
<Close />
|
alignItems={'center'}
|
||||||
|
justifyContent="space-between">
|
||||||
|
<IconButton
|
||||||
|
onClick={onClose}
|
||||||
|
color={backIsClose ? 'secondary' : 'primary'}>
|
||||||
|
{backIsClose ? <Close /> : <ArrowBack />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
<Box display={'flex'} gap="4px">
|
||||||
|
{actionButton && actionButton}
|
||||||
|
{!backIsClose && (
|
||||||
|
<IconButton onClick={onRootClose} color={'secondary'}>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</FlexWrapper>
|
||||||
<Box py={0.5} px={2} height={54}>
|
<Box py={0.5} px={2} height={54}>
|
||||||
<Typography variant="h3" fontWeight={'bold'}>
|
<Typography variant="h3" fontWeight={'bold'}>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2">{caption}</Typography>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ wordBreak: 'break-all' }}>
|
||||||
|
{caption}
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -666,6 +666,7 @@ export default function Gallery() {
|
||||||
/>
|
/>
|
||||||
<PhotoFrame
|
<PhotoFrame
|
||||||
files={files}
|
files={files}
|
||||||
|
collections={collections}
|
||||||
syncWithRemote={syncWithRemote}
|
syncWithRemote={syncWithRemote}
|
||||||
favItemIds={favItemIds}
|
favItemIds={favItemIds}
|
||||||
archivedCollections={archivedCollections}
|
archivedCollections={archivedCollections}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
export function prettyPrintExif(exifData: Object) {
|
|
||||||
let strPretty = '';
|
|
||||||
for (const [tagName, tagValue] of Object.entries(exifData)) {
|
|
||||||
if (tagValue instanceof Uint8Array) {
|
|
||||||
strPretty += tagName + ' : ' + '[' + tagValue + ']' + '\r\n';
|
|
||||||
} else if (tagValue instanceof Date) {
|
|
||||||
strPretty += tagName + ' : ' + tagValue.toDateString() + '\r\n';
|
|
||||||
} else {
|
|
||||||
strPretty += tagName + ' : ' + tagValue + '\r\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strPretty;
|
|
||||||
}
|
|
|
@ -443,6 +443,7 @@ const englishConstants = {
|
||||||
SHOW_ON_MAP: 'View on OpenStreetMap',
|
SHOW_ON_MAP: 'View on OpenStreetMap',
|
||||||
DETAILS: 'Details',
|
DETAILS: 'Details',
|
||||||
VIEW_EXIF: 'View all EXIF data',
|
VIEW_EXIF: 'View all EXIF data',
|
||||||
|
NO_EXIF: 'No EXIF data',
|
||||||
EXIF: 'Exif',
|
EXIF: 'Exif',
|
||||||
DEVICE: 'Device',
|
DEVICE: 'Device',
|
||||||
IMAGE_SIZE: 'Image size',
|
IMAGE_SIZE: 'Image size',
|
||||||
|
|
Loading…
Reference in a new issue