redesign fileinfo completed

This commit is contained in:
Abhinav 2022-11-24 18:29:57 +05:30
parent 2edeeb0371
commit 7ea4a26e45
14 changed files with 363 additions and 182 deletions

10
src/components/Chip.tsx Normal file
View 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',
}));

View 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),
},
}));

View file

@ -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<void>;
favItemIds?: Set<number>;
archivedCollections?: Set<number>;
@ -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<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(() => {
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}
/>
</Container>
)}

View file

@ -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<HTMLInputElement>) => {
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 = () => <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 (
<>
<LegendContainer>
<Typography variant="subtitle" mb={1}>
{constants.EXIF}
<FileInfoSidebar open={open} onClose={onClose}>
<Titlebar
onClose={onClose}
title={constants.EXIF}
caption={filename}
onRootClose={handleRootClose}
actionButton={<CopyButton code={exif} color={'secondary'} />}
/>
<Stack py={3} px={1} spacing={2}>
{[...Object.entries(exif)].map(([key, value]) => (
<ExifItem key={key}>
<Typography variant="body2" color={'text.secondary'}>
{key}
</Typography>
<FormGroup>
<FormControlLabel
control={
<Checkbox
size="small"
onChange={changeHandler}
color="accent"
/>
}
label={constants.SHOW_ALL}
/>
</FormGroup>
</LegendContainer>
{showAll ? renderAllValues() : renderSelectedValues()}
</>
<Typography>{parseExifValue(value)}</Typography>
</ExifItem>
))}
</Stack>
</FileInfoSidebar>
);
}

View file

@ -26,27 +26,29 @@ export default function InfoItem({
children,
}: Iprops): JSX.Element {
return (
<FlexWrapper height={48} justifyContent="space-between">
<FlexWrapper gap={0.5} pr={1}>
<FlexWrapper justifyContent="space-between">
<Box display={'flex'} alignItems="flex-start" gap={0.5} pr={1}>
<IconButton
color="secondary"
sx={{ '&&': { cursor: 'default' } }}
sx={{ '&&': { cursor: 'default', m: 0.5 } }}
disableRipple>
{icon}
</IconButton>
<Box>
<Box py={0.5}>
{children ? (
children
) : (
<>
<Typography>{title}</Typography>
<Typography sx={{ wordBreak: 'break-all' }}>
{title}
</Typography>
<Typography variant="body2" color="text.secondary">
{caption}
</Typography>
</>
)}
</Box>
</FlexWrapper>
</Box>
{customEndButton
? customEndButton
: !hideEditOption && (

View file

@ -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) {

View file

@ -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 (
<FlexWrapper gap={1}>
{captionParts.map((caption) => (
<Box key={caption}> {caption}</Box>
))}
</FlexWrapper>
);
};
export function RenderFileName({
exif,
parsedExifData,
shouldDisableEdits,
file,
scheduleUpdate,
}: {
exif: Record<string, any>;
parsedExifData: Record<string, any>;
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}
/>
) : (

View file

@ -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) => (
<Drawer {...props} anchor="right" />
))(({ theme }) => ({
export const FileInfoSidebar = styled((props: DialogProps) => (
<EnteDrawer {...props} anchor="right" />
))({
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<Photoswipe.Options>;
metadata: Metadata;
file: EnteFile;
exif: any;
scheduleUpdate: () => 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({
shouldDisableEdits,
showInfo,
handleCloseInfo,
items,
photoSwipe,
metadata,
file,
exif,
scheduleUpdate,
refreshPhotoswipe,
fileToCollectionsMap,
collectionNameMap,
}: Iprops) {
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(() => {
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 (
<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'}>
<RenderCaption
shouldDisableEdits={shouldDisableEdits}
file={items[photoSwipe?.getCurrentIndex()]}
file={file}
scheduleUpdate={scheduleUpdate}
refreshPhotoswipe={refreshPhotoswipe}
/>
<RenderCreationTime
shouldDisableEdits={shouldDisableEdits}
file={items[photoSwipe?.getCurrentIndex()]}
file={file}
scheduleUpdate={scheduleUpdate}
/>
<RenderFileName
exif={exif}
parsedExifData={parsedExifData}
shouldDisableEdits={shouldDisableEdits}
file={items[photoSwipe?.getCurrentIndex()]}
file={file}
scheduleUpdate={scheduleUpdate}
/>
{parsedExifData && parsedExifData['takenOnDevice'] && (
<InfoItem
icon={<CameraOutlined />}
title={parsedExifData['takenOnDevice']}
caption={
<BasicDeviceCamera
parsedExifData={parsedExifData}
/>
}
hideEditOption
/>
)}
{location && (
{/* {location && ( */}
<InfoItem
icon={<LocationOnOutlined />}
title={constants.LOCATION}
caption={
<Link
href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}>
href={getOpenStreetMapLink({
latitude: file.metadata.latitude,
longitude: file.metadata.longitude,
})}
target="_blank">
{constants.SHOW_ON_MAP}
</Link>
}
customEndButton={
<CopyButton
code={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
code={getOpenStreetMapLink({
latitude: file.metadata.latitude,
longitude: file.metadata.longitude,
})}
color="secondary"
size="medium"
/>
}
/>
)}
{/* )} */}
<InfoItem
icon={<TextSnippetOutlined />}
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
/>
<InfoItem
icon={<BackupOutlined />}
title={formatDateTime(metadata.modificationTime / 1000)}
caption={formatDateTime(metadata.modificationTime / 1000)}
title={formatDateMedium(
file.metadata.modificationTime / 1000
)}
caption={formatTime(file.metadata.modificationTime / 1000)}
hideEditOption
/>
<InfoItem icon={<FolderOutlined />} hideEditOption>
<Stack spacing={1} direction="row">
<Badge>abc</Badge>
<Badge>DEF</Badge>
<Badge>GHI</Badge>
</Stack>
</InfoItem>
{/* {exif && (
<Box
display={'flex'}
gap={1}
flexWrap="wrap"
justifyContent={'flex-start'}
alignItems={'flex-start'}>
{fileToCollectionsMap
.get(file.id)
.map((collectionID) => (
<>
<ExifData exif={exif} />
<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>
</Stack>
<ExifData
exif={exif}
open={showExif}
onClose={closeExif}
onInfoClose={handleCloseInfo}
filename={file.metadata.title}
/>
</FileInfoSidebar>
);
}

View file

@ -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<number, number[]>;
collectionNameMap: Map<number, string>;
}
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<EnteFile['metadata']>(null);
const [exif, setExif] = useState<any>(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;
}
exifData.raw = prettyPrintExif(exifData);
if (exifData) {
setExif(exifData);
} else {
setExif(null);
}
}
} 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}
/>
</>
);

View file

@ -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),
},
}));

View file

@ -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 (
<>
<Box display={'flex'} height={48} alignItems={'center'}>
<IconButton onClick={onClose} color="secondary">
<FlexWrapper
height={48}
alignItems={'center'}
justifyContent="space-between">
<IconButton
onClick={onClose}
color={backIsClose ? 'secondary' : 'primary'}>
{backIsClose ? <Close /> : <ArrowBack />}
</IconButton>
<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}>
<Typography variant="h3" fontWeight={'bold'}>
{title}
</Typography>
<Typography variant="body2">{caption}</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ wordBreak: 'break-all' }}>
{caption}
</Typography>
</Box>
</>
);

View file

@ -666,6 +666,7 @@ export default function Gallery() {
/>
<PhotoFrame
files={files}
collections={collections}
syncWithRemote={syncWithRemote}
favItemIds={favItemIds}
archivedCollections={archivedCollections}

View file

@ -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;
}

View file

@ -443,6 +443,7 @@ const englishConstants = {
SHOW_ON_MAP: 'View on OpenStreetMap',
DETAILS: 'Details',
VIEW_EXIF: 'View all EXIF data',
NO_EXIF: 'No EXIF data',
EXIF: 'Exif',
DEVICE: 'Device',
IMAGE_SIZE: 'Image size',