udpate photoswipe info dialog
This commit is contained in:
parent
5ee5b5f890
commit
ed60c290ce
|
@ -41,20 +41,19 @@ export const IconButton = styled.button`
|
|||
export const Row = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const Label = styled.div<{ width?: string }>`
|
||||
width: ${(props) => props.width ?? '70%'};
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
`;
|
||||
export const Value = styled.div<{ width?: string }>`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: ${(props) => props.width ?? '30%'};
|
||||
|
||||
color: #ddd;
|
||||
`;
|
||||
|
||||
export const FlexWrapper = styled(Box)`
|
||||
|
|
|
@ -6,7 +6,7 @@ import styled from 'styled-components';
|
|||
import DownloadManager from 'services/downloadManager';
|
||||
import constants from 'utils/strings/constants';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
|
||||
import PhotoSwipe from 'components/PhotoSwipe';
|
||||
import { formatDateRelative } from 'utils/file';
|
||||
import {
|
||||
ALL_SECTION,
|
||||
|
|
63
src/components/PhotoSwipe/InfoDialog/ExifData.tsx
Normal file
63
src/components/PhotoSwipe/InfoDialog/ExifData.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import React, { useState } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { FormCheck } from 'react-bootstrap';
|
||||
|
||||
import { RenderInfoItem } from './RenderInfoItem';
|
||||
import { LegendContainer } from '../styledComponents/LegendContainer';
|
||||
import { Pre } from '../styledComponents/Pre';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
export function ExifData(props: { exif: any }) {
|
||||
const { exif } = props;
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
const changeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setShowAll(e.target.checked);
|
||||
};
|
||||
|
||||
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}
|
||||
</Typography>
|
||||
<FormCheck>
|
||||
<FormCheck.Label>
|
||||
<FormCheck.Input onChange={changeHandler} />
|
||||
{constants.SHOW_ALL}
|
||||
</FormCheck.Label>
|
||||
</FormCheck>
|
||||
</LegendContainer>
|
||||
{showAll ? renderAllValues() : renderSelectedValues()}
|
||||
</>
|
||||
);
|
||||
}
|
99
src/components/PhotoSwipe/InfoDialog/FileNameEditForm.tsx
Normal file
99
src/components/PhotoSwipe/InfoDialog/FileNameEditForm.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
import React, { useState } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { Col, Form, FormControl } from 'react-bootstrap';
|
||||
import { FlexWrapper, IconButton, Value } from 'components/Container';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import TickIcon from '@mui/icons-material/Done';
|
||||
import { Formik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { MAX_EDITED_FILE_NAME_LENGTH } from 'constants/file';
|
||||
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
|
||||
|
||||
export interface formValues {
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export const FileNameEditForm = ({
|
||||
filename,
|
||||
saveEdits,
|
||||
discardEdits,
|
||||
extension,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onSubmit = async (values: formValues) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await saveEdits(values.filename);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Formik<formValues>
|
||||
initialValues={{ filename }}
|
||||
validationSchema={Yup.object().shape({
|
||||
filename: Yup.string()
|
||||
.required(constants.REQUIRED)
|
||||
.max(
|
||||
MAX_EDITED_FILE_NAME_LENGTH,
|
||||
constants.FILE_NAME_CHARACTER_LIMIT
|
||||
),
|
||||
})}
|
||||
validateOnBlur={false}
|
||||
onSubmit={onSubmit}>
|
||||
{({ values, errors, handleChange, handleSubmit }) => (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Row>
|
||||
<Form.Group
|
||||
bsPrefix="ente-form-group"
|
||||
as={Col}
|
||||
xs={extension ? 7 : 8}>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder={constants.FILE_NAME}
|
||||
value={values.filename}
|
||||
onChange={handleChange('filename')}
|
||||
isInvalid={Boolean(errors.filename)}
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
/>
|
||||
<FormControl.Feedback
|
||||
type="invalid"
|
||||
style={{ textAlign: 'center' }}>
|
||||
{errors.filename}
|
||||
</FormControl.Feedback>
|
||||
</Form.Group>
|
||||
{extension && (
|
||||
<Form.Group
|
||||
bsPrefix="ente-form-group"
|
||||
as={Col}
|
||||
xs={1}
|
||||
controlId="formHorizontalFileName">
|
||||
<FlexWrapper style={{ padding: '5px' }}>
|
||||
{`.${extension}`}
|
||||
</FlexWrapper>
|
||||
</Form.Group>
|
||||
)}
|
||||
<Form.Group bsPrefix="ente-form-group" as={Col} xs={2}>
|
||||
<Value width={'16.67%'}>
|
||||
<IconButton type="submit" disabled={loading}>
|
||||
{loading ? (
|
||||
<SmallLoadingSpinner />
|
||||
) : (
|
||||
<TickIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={discardEdits}
|
||||
disabled={loading}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Value>
|
||||
</Form.Group>
|
||||
</Form.Row>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
121
src/components/PhotoSwipe/InfoDialog/RenderCreationTime.tsx
Normal file
121
src/components/PhotoSwipe/InfoDialog/RenderCreationTime.tsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
import React, { useState } from 'react';
|
||||
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import constants from 'utils/strings/constants';
|
||||
import {
|
||||
changeFileCreationTime,
|
||||
formatDateTime,
|
||||
updateExistingFilePubMetadata,
|
||||
} from 'utils/file';
|
||||
import EditIcon from 'components/icons/EditIcon';
|
||||
import { IconButton, Label, Row, Value } from 'components/Container';
|
||||
import { logError } from 'utils/sentry';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import TickIcon from '@mui/icons-material/Done';
|
||||
import EnteDateTimePicker from 'components/EnteDateTimePicker';
|
||||
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
|
||||
|
||||
export function RenderCreationTime({
|
||||
shouldDisableEdits,
|
||||
file,
|
||||
scheduleUpdate,
|
||||
}: {
|
||||
shouldDisableEdits: boolean;
|
||||
file: EnteFile;
|
||||
scheduleUpdate: () => void;
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const originalCreationTime = new Date(file?.metadata.creationTime / 1000);
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
|
||||
const [pickedTime, setPickedTime] = useState(originalCreationTime);
|
||||
|
||||
const openEditMode = () => setIsInEditMode(true);
|
||||
const closeEditMode = () => setIsInEditMode(false);
|
||||
|
||||
const saveEdits = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (isInEditMode && file) {
|
||||
const unixTimeInMicroSec = pickedTime.getTime() * 1000;
|
||||
if (unixTimeInMicroSec === file?.metadata.creationTime) {
|
||||
closeEditMode();
|
||||
return;
|
||||
}
|
||||
let updatedFile = await changeFileCreationTime(
|
||||
file,
|
||||
unixTimeInMicroSec
|
||||
);
|
||||
updatedFile = (
|
||||
await updateFilePublicMagicMetadata([updatedFile])
|
||||
)[0];
|
||||
updateExistingFilePubMetadata(file, updatedFile);
|
||||
scheduleUpdate();
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'failed to update creationTime');
|
||||
} finally {
|
||||
closeEditMode();
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const discardEdits = () => {
|
||||
setPickedTime(originalCreationTime);
|
||||
closeEditMode();
|
||||
};
|
||||
const handleChange = (newDate: Date) => {
|
||||
if (newDate instanceof Date) {
|
||||
setPickedTime(newDate);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Label width="30%">{constants.CREATION_TIME}</Label>
|
||||
<Value
|
||||
width={
|
||||
!shouldDisableEdits
|
||||
? isInEditMode
|
||||
? '50%'
|
||||
: '60%'
|
||||
: '70%'
|
||||
}>
|
||||
{isInEditMode ? (
|
||||
<EnteDateTimePicker
|
||||
loading={loading}
|
||||
isInEditMode={isInEditMode}
|
||||
pickedTime={pickedTime}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
) : (
|
||||
formatDateTime(pickedTime)
|
||||
)}
|
||||
</Value>
|
||||
{!shouldDisableEdits && (
|
||||
<Value
|
||||
width={isInEditMode ? '20%' : '10%'}
|
||||
style={{ cursor: 'pointer', marginLeft: '10px' }}>
|
||||
{!isInEditMode ? (
|
||||
<IconButton onClick={openEditMode}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<>
|
||||
<IconButton onClick={saveEdits}>
|
||||
{loading ? (
|
||||
<SmallLoadingSpinner />
|
||||
) : (
|
||||
<TickIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton onClick={discardEdits}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
</Value>
|
||||
)}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
103
src/components/PhotoSwipe/InfoDialog/RenderFileName.tsx
Normal file
103
src/components/PhotoSwipe/InfoDialog/RenderFileName.tsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
import React, { useState } from 'react';
|
||||
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import constants from 'utils/strings/constants';
|
||||
import {
|
||||
changeFileName,
|
||||
splitFilenameAndExtension,
|
||||
updateExistingFilePubMetadata,
|
||||
} from 'utils/file';
|
||||
import EditIcon from 'components/icons/EditIcon';
|
||||
import {
|
||||
FreeFlowText,
|
||||
IconButton,
|
||||
Label,
|
||||
Row,
|
||||
Value,
|
||||
} from 'components/Container';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { FileNameEditForm } from './FileNameEditForm';
|
||||
|
||||
export const getFileTitle = (filename, extension) => {
|
||||
if (extension) {
|
||||
return filename + '.' + extension;
|
||||
} else {
|
||||
return filename;
|
||||
}
|
||||
};
|
||||
|
||||
export function RenderFileName({
|
||||
shouldDisableEdits,
|
||||
file,
|
||||
scheduleUpdate,
|
||||
}: {
|
||||
shouldDisableEdits: boolean;
|
||||
file: EnteFile;
|
||||
scheduleUpdate: () => void;
|
||||
}) {
|
||||
const originalTitle = file?.metadata.title;
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
const [originalFileName, extension] =
|
||||
splitFilenameAndExtension(originalTitle);
|
||||
const [filename, setFilename] = useState(originalFileName);
|
||||
const openEditMode = () => setIsInEditMode(true);
|
||||
const closeEditMode = () => setIsInEditMode(false);
|
||||
|
||||
const saveEdits = async (newFilename: string) => {
|
||||
try {
|
||||
if (file) {
|
||||
if (filename === newFilename) {
|
||||
closeEditMode();
|
||||
return;
|
||||
}
|
||||
setFilename(newFilename);
|
||||
const newTitle = getFileTitle(newFilename, extension);
|
||||
let updatedFile = await changeFileName(file, newTitle);
|
||||
updatedFile = (
|
||||
await updateFilePublicMagicMetadata([updatedFile])
|
||||
)[0];
|
||||
updateExistingFilePubMetadata(file, updatedFile);
|
||||
scheduleUpdate();
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'failed to update file name');
|
||||
} finally {
|
||||
closeEditMode();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Label width="30%">{constants.FILE_NAME}</Label>
|
||||
{!isInEditMode ? (
|
||||
<>
|
||||
<Value width={!shouldDisableEdits ? '60%' : '70%'}>
|
||||
<FreeFlowText>
|
||||
{getFileTitle(filename, extension)}
|
||||
</FreeFlowText>
|
||||
</Value>
|
||||
{!shouldDisableEdits && (
|
||||
<Value
|
||||
width="10%"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
marginLeft: '10px',
|
||||
}}>
|
||||
<IconButton onClick={openEditMode}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Value>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<FileNameEditForm
|
||||
extension={extension}
|
||||
filename={filename}
|
||||
saveEdits={saveEdits}
|
||||
discardEdits={closeEditMode}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
9
src/components/PhotoSwipe/InfoDialog/RenderInfoItem.tsx
Normal file
9
src/components/PhotoSwipe/InfoDialog/RenderInfoItem.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
import { Label, Row, Value } from 'components/Container';
|
||||
|
||||
export const RenderInfoItem = (label: string, value: string | JSX.Element) => (
|
||||
<Row>
|
||||
<Label width="30%">{label}</Label>
|
||||
<Value width="70%">{value}</Value>
|
||||
</Row>
|
||||
);
|
77
src/components/PhotoSwipe/InfoDialog/index.tsx
Normal file
77
src/components/PhotoSwipe/InfoDialog/index.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
import React from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { formatDateTime } from 'utils/file';
|
||||
import { RenderFileName } from './RenderFileName';
|
||||
import { ExifData } from './ExifData';
|
||||
import { RenderCreationTime } from './RenderCreationTime';
|
||||
import { RenderInfoItem } from './RenderInfoItem';
|
||||
import DialogBoxBase from 'components/DialogBox/base';
|
||||
import DialogTitleWithCloseButton from 'components/DialogBox/titleWithCloseButton';
|
||||
import { DialogContent, Typography } from '@mui/material';
|
||||
|
||||
export function InfoModal({
|
||||
shouldDisableEdits,
|
||||
showInfo,
|
||||
handleCloseInfo,
|
||||
items,
|
||||
photoSwipe,
|
||||
metadata,
|
||||
exif,
|
||||
scheduleUpdate,
|
||||
}) {
|
||||
return (
|
||||
<DialogBoxBase
|
||||
sx={{ zIndex: '1501' }}
|
||||
open={showInfo}
|
||||
onClose={handleCloseInfo}>
|
||||
<DialogTitleWithCloseButton onClose={handleCloseInfo}>
|
||||
{constants.INFO}
|
||||
</DialogTitleWithCloseButton>
|
||||
<DialogContent>
|
||||
<Typography variant="subtitle" mb={1}>
|
||||
{constants.METADATA}
|
||||
</Typography>
|
||||
|
||||
{RenderInfoItem(
|
||||
constants.FILE_ID,
|
||||
items[photoSwipe?.getCurrentIndex()]?.id
|
||||
)}
|
||||
{metadata?.title && (
|
||||
<RenderFileName
|
||||
shouldDisableEdits={shouldDisableEdits}
|
||||
file={items[photoSwipe?.getCurrentIndex()]}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
/>
|
||||
)}
|
||||
{metadata?.creationTime && (
|
||||
<RenderCreationTime
|
||||
shouldDisableEdits={shouldDisableEdits}
|
||||
file={items[photoSwipe?.getCurrentIndex()]}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
/>
|
||||
)}
|
||||
{metadata?.modificationTime &&
|
||||
RenderInfoItem(
|
||||
constants.UPDATED_ON,
|
||||
formatDateTime(metadata.modificationTime / 1000)
|
||||
)}
|
||||
{metadata?.longitude > 0 &&
|
||||
metadata?.longitude > 0 &&
|
||||
RenderInfoItem(
|
||||
constants.LOCATION,
|
||||
<a
|
||||
href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{constants.SHOW_MAP}
|
||||
</a>
|
||||
)}
|
||||
{exif && (
|
||||
<>
|
||||
<ExifData exif={exif} />
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</DialogBoxBase>
|
||||
);
|
||||
}
|
|
@ -1,908 +0,0 @@
|
|||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import Photoswipe from 'photoswipe';
|
||||
import PhotoswipeUIDefault from 'photoswipe/dist/photoswipe-ui-default';
|
||||
import classnames from 'classnames';
|
||||
import FavButton from 'components/FavButton';
|
||||
import {
|
||||
addToFavorites,
|
||||
removeFromFavorites,
|
||||
} from 'services/collectionService';
|
||||
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import constants from 'utils/strings/constants';
|
||||
import exifr from 'exifr';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import styled from 'styled-components';
|
||||
import events from './events';
|
||||
import {
|
||||
changeFileCreationTime,
|
||||
changeFileName,
|
||||
downloadFile,
|
||||
formatDateTime,
|
||||
splitFilenameAndExtension,
|
||||
updateExistingFilePubMetadata,
|
||||
} from 'utils/file';
|
||||
import { Col, Form, FormCheck, FormControl } from 'react-bootstrap';
|
||||
import { prettyPrintExif } from 'utils/exif';
|
||||
import EditIcon from 'components/icons/EditIcon';
|
||||
import {
|
||||
FlexWrapper,
|
||||
FreeFlowText,
|
||||
IconButton,
|
||||
Label,
|
||||
Row,
|
||||
Value,
|
||||
} from 'components/Container';
|
||||
import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
|
||||
import { logError } from 'utils/sentry';
|
||||
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import TickIcon from '@mui/icons-material/Done';
|
||||
import { Formik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import EnteDateTimePicker from 'components/EnteDateTimePicker';
|
||||
import { MAX_EDITED_FILE_NAME_LENGTH, FILE_TYPE } from 'constants/file';
|
||||
import { sleep } from 'utils/common';
|
||||
import { playVideo, pauseVideo } from 'utils/photoFrame';
|
||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { AppContext } from 'pages/_app';
|
||||
|
||||
const SmallLoadingSpinner = () => (
|
||||
<EnteSpinner
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
interface Iprops {
|
||||
isOpen: boolean;
|
||||
items: any[];
|
||||
currentIndex?: number;
|
||||
onClose?: (needUpdate: boolean) => void;
|
||||
gettingData: (instance: any, index: number, item: EnteFile) => void;
|
||||
id?: string;
|
||||
className?: string;
|
||||
favItemIds: Set<number>;
|
||||
isSharedCollection: boolean;
|
||||
isTrashCollection: boolean;
|
||||
enableDownload: boolean;
|
||||
isSourceLoaded: boolean;
|
||||
}
|
||||
|
||||
const LegendContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const Legend = styled.span`
|
||||
font-size: 20px;
|
||||
color: #ddd;
|
||||
display: inline;
|
||||
`;
|
||||
|
||||
const Pre = styled.pre`
|
||||
color: #aaa;
|
||||
padding: 7px 15px;
|
||||
`;
|
||||
|
||||
const LivePhotoBtn = styled.button`
|
||||
position: absolute;
|
||||
bottom: 6vh;
|
||||
right: 6vh;
|
||||
height: 40px;
|
||||
width: 80px;
|
||||
background: #d7d7d7;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 10%;
|
||||
z-index: 10;
|
||||
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
|
||||
}
|
||||
`;
|
||||
|
||||
const livePhotoDefaultOptions = {
|
||||
click: () => {},
|
||||
hide: () => {},
|
||||
show: () => {},
|
||||
loading: false,
|
||||
visible: false,
|
||||
};
|
||||
|
||||
const renderInfoItem = (label: string, value: string | JSX.Element) => (
|
||||
<Row>
|
||||
<Label width="30%">{label}</Label>
|
||||
<Value width="70%">{value}</Value>
|
||||
</Row>
|
||||
);
|
||||
|
||||
function RenderCreationTime({
|
||||
shouldDisableEdits,
|
||||
file,
|
||||
scheduleUpdate,
|
||||
}: {
|
||||
shouldDisableEdits: boolean;
|
||||
file: EnteFile;
|
||||
scheduleUpdate: () => void;
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const originalCreationTime = new Date(file?.metadata.creationTime / 1000);
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
|
||||
const [pickedTime, setPickedTime] = useState(originalCreationTime);
|
||||
|
||||
const openEditMode = () => setIsInEditMode(true);
|
||||
const closeEditMode = () => setIsInEditMode(false);
|
||||
|
||||
const saveEdits = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (isInEditMode && file) {
|
||||
const unixTimeInMicroSec = pickedTime.getTime() * 1000;
|
||||
if (unixTimeInMicroSec === file?.metadata.creationTime) {
|
||||
closeEditMode();
|
||||
return;
|
||||
}
|
||||
let updatedFile = await changeFileCreationTime(
|
||||
file,
|
||||
unixTimeInMicroSec
|
||||
);
|
||||
updatedFile = (
|
||||
await updateFilePublicMagicMetadata([updatedFile])
|
||||
)[0];
|
||||
updateExistingFilePubMetadata(file, updatedFile);
|
||||
scheduleUpdate();
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'failed to update creationTime');
|
||||
} finally {
|
||||
closeEditMode();
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const discardEdits = () => {
|
||||
setPickedTime(originalCreationTime);
|
||||
closeEditMode();
|
||||
};
|
||||
const handleChange = (newDate: Date) => {
|
||||
if (newDate instanceof Date) {
|
||||
setPickedTime(newDate);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Label width="30%">{constants.CREATION_TIME}</Label>
|
||||
<Value
|
||||
width={
|
||||
!shouldDisableEdits
|
||||
? isInEditMode
|
||||
? '50%'
|
||||
: '60%'
|
||||
: '70%'
|
||||
}>
|
||||
{isInEditMode ? (
|
||||
<EnteDateTimePicker
|
||||
loading={loading}
|
||||
isInEditMode={isInEditMode}
|
||||
pickedTime={pickedTime}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
) : (
|
||||
formatDateTime(pickedTime)
|
||||
)}
|
||||
</Value>
|
||||
{!shouldDisableEdits && (
|
||||
<Value
|
||||
width={isInEditMode ? '20%' : '10%'}
|
||||
style={{ cursor: 'pointer', marginLeft: '10px' }}>
|
||||
{!isInEditMode ? (
|
||||
<IconButton onClick={openEditMode}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<>
|
||||
<IconButton onClick={saveEdits}>
|
||||
{loading ? (
|
||||
<SmallLoadingSpinner />
|
||||
) : (
|
||||
<TickIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton onClick={discardEdits}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
</Value>
|
||||
)}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
const getFileTitle = (filename, extension) => {
|
||||
if (extension) {
|
||||
return filename + '.' + extension;
|
||||
} else {
|
||||
return filename;
|
||||
}
|
||||
};
|
||||
interface formValues {
|
||||
filename: string;
|
||||
}
|
||||
|
||||
const FileNameEditForm = ({ filename, saveEdits, discardEdits, extension }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onSubmit = async (values: formValues) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await saveEdits(values.filename);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Formik<formValues>
|
||||
initialValues={{ filename }}
|
||||
validationSchema={Yup.object().shape({
|
||||
filename: Yup.string()
|
||||
.required(constants.REQUIRED)
|
||||
.max(
|
||||
MAX_EDITED_FILE_NAME_LENGTH,
|
||||
constants.FILE_NAME_CHARACTER_LIMIT
|
||||
),
|
||||
})}
|
||||
validateOnBlur={false}
|
||||
onSubmit={onSubmit}>
|
||||
{({ values, errors, handleChange, handleSubmit }) => (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Row>
|
||||
<Form.Group
|
||||
bsPrefix="ente-form-group"
|
||||
as={Col}
|
||||
xs={extension ? 7 : 8}>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder={constants.FILE_NAME}
|
||||
value={values.filename}
|
||||
onChange={handleChange('filename')}
|
||||
isInvalid={Boolean(errors.filename)}
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
/>
|
||||
<FormControl.Feedback
|
||||
type="invalid"
|
||||
style={{ textAlign: 'center' }}>
|
||||
{errors.filename}
|
||||
</FormControl.Feedback>
|
||||
</Form.Group>
|
||||
{extension && (
|
||||
<Form.Group
|
||||
bsPrefix="ente-form-group"
|
||||
as={Col}
|
||||
xs={1}
|
||||
controlId="formHorizontalFileName">
|
||||
<FlexWrapper style={{ padding: '5px' }}>
|
||||
{`.${extension}`}
|
||||
</FlexWrapper>
|
||||
</Form.Group>
|
||||
)}
|
||||
<Form.Group bsPrefix="ente-form-group" as={Col} xs={2}>
|
||||
<Value width={'16.67%'}>
|
||||
<IconButton type="submit" disabled={loading}>
|
||||
{loading ? (
|
||||
<SmallLoadingSpinner />
|
||||
) : (
|
||||
<TickIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={discardEdits}
|
||||
disabled={loading}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Value>
|
||||
</Form.Group>
|
||||
</Form.Row>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
||||
|
||||
function RenderFileName({
|
||||
shouldDisableEdits,
|
||||
file,
|
||||
scheduleUpdate,
|
||||
}: {
|
||||
shouldDisableEdits: boolean;
|
||||
file: EnteFile;
|
||||
scheduleUpdate: () => void;
|
||||
}) {
|
||||
const originalTitle = file?.metadata.title;
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
const [originalFileName, extension] =
|
||||
splitFilenameAndExtension(originalTitle);
|
||||
const [filename, setFilename] = useState(originalFileName);
|
||||
const openEditMode = () => setIsInEditMode(true);
|
||||
const closeEditMode = () => setIsInEditMode(false);
|
||||
|
||||
const saveEdits = async (newFilename: string) => {
|
||||
try {
|
||||
if (file) {
|
||||
if (filename === newFilename) {
|
||||
closeEditMode();
|
||||
return;
|
||||
}
|
||||
setFilename(newFilename);
|
||||
const newTitle = getFileTitle(newFilename, extension);
|
||||
let updatedFile = await changeFileName(file, newTitle);
|
||||
updatedFile = (
|
||||
await updateFilePublicMagicMetadata([updatedFile])
|
||||
)[0];
|
||||
updateExistingFilePubMetadata(file, updatedFile);
|
||||
scheduleUpdate();
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'failed to update file name');
|
||||
} finally {
|
||||
closeEditMode();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Label width="30%">{constants.FILE_NAME}</Label>
|
||||
{!isInEditMode ? (
|
||||
<>
|
||||
<Value width={!shouldDisableEdits ? '60%' : '70%'}>
|
||||
<FreeFlowText>
|
||||
{getFileTitle(filename, extension)}
|
||||
</FreeFlowText>
|
||||
</Value>
|
||||
{!shouldDisableEdits && (
|
||||
<Value
|
||||
width="10%"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
marginLeft: '10px',
|
||||
}}>
|
||||
<IconButton onClick={openEditMode}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Value>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<FileNameEditForm
|
||||
extension={extension}
|
||||
filename={filename}
|
||||
saveEdits={saveEdits}
|
||||
discardEdits={closeEditMode}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
function ExifData(props: { exif: any }) {
|
||||
const { exif } = props;
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
const changeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setShowAll(e.target.checked);
|
||||
};
|
||||
|
||||
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>
|
||||
<Legend>{constants.EXIF}</Legend>
|
||||
<FormCheck>
|
||||
<FormCheck.Label>
|
||||
<FormCheck.Input onChange={changeHandler} />
|
||||
{constants.SHOW_ALL}
|
||||
</FormCheck.Label>
|
||||
</FormCheck>
|
||||
</LegendContainer>
|
||||
{showAll ? renderAllValues() : renderSelectedValues()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoModal({
|
||||
shouldDisableEdits,
|
||||
showInfo,
|
||||
handleCloseInfo,
|
||||
items,
|
||||
photoSwipe,
|
||||
metadata,
|
||||
exif,
|
||||
scheduleUpdate,
|
||||
}) {
|
||||
return (
|
||||
<Modal show={showInfo} onHide={handleCloseInfo}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{constants.INFO}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div>
|
||||
<Legend>{constants.METADATA}</Legend>
|
||||
</div>
|
||||
{renderInfoItem(
|
||||
constants.FILE_ID,
|
||||
items[photoSwipe?.getCurrentIndex()]?.id
|
||||
)}
|
||||
{metadata?.title && (
|
||||
<RenderFileName
|
||||
shouldDisableEdits={shouldDisableEdits}
|
||||
file={items[photoSwipe?.getCurrentIndex()]}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
/>
|
||||
)}
|
||||
{metadata?.creationTime && (
|
||||
<RenderCreationTime
|
||||
shouldDisableEdits={shouldDisableEdits}
|
||||
file={items[photoSwipe?.getCurrentIndex()]}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
/>
|
||||
)}
|
||||
{metadata?.modificationTime &&
|
||||
renderInfoItem(
|
||||
constants.UPDATED_ON,
|
||||
formatDateTime(metadata.modificationTime / 1000)
|
||||
)}
|
||||
{metadata?.longitude > 0 &&
|
||||
metadata?.longitude > 0 &&
|
||||
renderInfoItem(
|
||||
constants.LOCATION,
|
||||
<a
|
||||
href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{constants.SHOW_MAP}
|
||||
</a>
|
||||
)}
|
||||
{exif && (
|
||||
<>
|
||||
<ExifData exif={exif} />
|
||||
</>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="outline-secondary" onClick={handleCloseInfo}>
|
||||
{constants.CLOSE}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function PhotoSwipe(props: Iprops) {
|
||||
const pswpElement = useRef<HTMLDivElement>();
|
||||
const [photoSwipe, setPhotoSwipe] =
|
||||
useState<Photoswipe<Photoswipe.Options>>();
|
||||
|
||||
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(
|
||||
livePhotoDefaultOptions
|
||||
);
|
||||
const needUpdate = useRef(false);
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
PublicCollectionGalleryContext
|
||||
);
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pswpElement) return;
|
||||
if (isOpen) {
|
||||
openPhotoSwipe();
|
||||
}
|
||||
if (!isOpen) {
|
||||
closePhotoSwipe();
|
||||
}
|
||||
return () => {
|
||||
closePhotoSwipe();
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
updateItems(items);
|
||||
}, [items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (photoSwipe) {
|
||||
photoSwipe.options.arrowKeys = !showInfo;
|
||||
photoSwipe.options.escKey = !showInfo;
|
||||
}
|
||||
}, [showInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const item = items[photoSwipe?.getCurrentIndex()];
|
||||
if (item && item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
const getVideoAndImage = () => {
|
||||
const video = document.getElementById(
|
||||
`live-photo-video-${item.id}`
|
||||
);
|
||||
const image = document.getElementById(
|
||||
`live-photo-image-${item.id}`
|
||||
);
|
||||
return { video, image };
|
||||
};
|
||||
|
||||
const { video, image } = getVideoAndImage();
|
||||
|
||||
if (video && image) {
|
||||
setLivePhotoBtnOptions({
|
||||
click: async () => {
|
||||
await playVideo(video, image);
|
||||
},
|
||||
hide: async () => {
|
||||
await pauseVideo(video, image);
|
||||
},
|
||||
show: async () => {
|
||||
await playVideo(video, image);
|
||||
},
|
||||
visible: true,
|
||||
loading: false,
|
||||
});
|
||||
} else {
|
||||
setLivePhotoBtnOptions({
|
||||
...livePhotoDefaultOptions,
|
||||
visible: true,
|
||||
loading: true,
|
||||
});
|
||||
}
|
||||
|
||||
const downloadLivePhotoBtn = document.getElementById(
|
||||
`download-btn-${item.id}`
|
||||
) as HTMLButtonElement;
|
||||
if (downloadLivePhotoBtn) {
|
||||
const downloadLivePhoto = () => {
|
||||
downloadFileHelper(photoSwipe.currItem);
|
||||
};
|
||||
|
||||
downloadLivePhotoBtn.addEventListener(
|
||||
'click',
|
||||
downloadLivePhoto
|
||||
);
|
||||
return () => {
|
||||
downloadLivePhotoBtn.removeEventListener(
|
||||
'click',
|
||||
downloadLivePhoto
|
||||
);
|
||||
setLivePhotoBtnOptions(livePhotoDefaultOptions);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
setLivePhotoBtnOptions(livePhotoDefaultOptions);
|
||||
};
|
||||
}
|
||||
}, [photoSwipe?.currItem, isOpen, isSourceLoaded]);
|
||||
|
||||
function updateFavButton() {
|
||||
setIsFav(isInFav(this?.currItem));
|
||||
}
|
||||
|
||||
const openPhotoSwipe = () => {
|
||||
const { items, currentIndex } = props;
|
||||
const options = {
|
||||
history: false,
|
||||
maxSpreadZoom: 5,
|
||||
index: currentIndex,
|
||||
showHideOpacity: true,
|
||||
getDoubleTapZoom(isMouseClick, item) {
|
||||
if (isMouseClick) {
|
||||
return 2.5;
|
||||
}
|
||||
// zoom to original if initial zoom is less than 0.7x,
|
||||
// otherwise to 1.5x, to make sure that double-tap gesture always zooms image
|
||||
return item.initialZoomLevel < 0.7 ? 1 : 1.5;
|
||||
},
|
||||
getThumbBoundsFn: (index) => {
|
||||
try {
|
||||
const file = items[index];
|
||||
const ele = document.getElementById(`thumb-${file.id}`);
|
||||
if (ele) {
|
||||
const rect = ele.getBoundingClientRect();
|
||||
const pageYScroll =
|
||||
window.pageYOffset ||
|
||||
document.documentElement.scrollTop;
|
||||
return {
|
||||
x: rect.left,
|
||||
y: rect.top + pageYScroll,
|
||||
w: rect.width,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
const photoSwipe = new Photoswipe(
|
||||
pswpElement.current,
|
||||
PhotoswipeUIDefault,
|
||||
items,
|
||||
options
|
||||
);
|
||||
events.forEach((event) => {
|
||||
const callback = props[event];
|
||||
if (callback || event === 'destroy') {
|
||||
photoSwipe.listen(event, function (...args) {
|
||||
if (callback) {
|
||||
args.unshift(this);
|
||||
callback(...args);
|
||||
}
|
||||
if (event === 'destroy') {
|
||||
handleClose();
|
||||
}
|
||||
if (event === 'close') {
|
||||
handleClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
photoSwipe.listen('beforeChange', function () {
|
||||
updateInfo.call(this);
|
||||
updateFavButton.call(this);
|
||||
});
|
||||
photoSwipe.listen('resize', checkExifAvailable);
|
||||
photoSwipe.init();
|
||||
needUpdate.current = false;
|
||||
setPhotoSwipe(photoSwipe);
|
||||
};
|
||||
|
||||
const closePhotoSwipe = () => {
|
||||
if (photoSwipe) photoSwipe.close();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
const { onClose } = props;
|
||||
if (typeof onClose === 'function') {
|
||||
onClose(needUpdate.current);
|
||||
}
|
||||
const videoTags = document.getElementsByTagName('video');
|
||||
for (const videoTag of videoTags) {
|
||||
videoTag.pause();
|
||||
}
|
||||
handleCloseInfo();
|
||||
// BE_AWARE: this will clear any notification set, even if they were not set in/by the photoswipe component
|
||||
galleryContext.setNotificationAttributes(null);
|
||||
};
|
||||
const isInFav = (file) => {
|
||||
const { favItemIds } = props;
|
||||
if (favItemIds && file) {
|
||||
return favItemIds.has(file.id);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const onFavClick = async (file) => {
|
||||
const { favItemIds } = props;
|
||||
if (!isInFav(file)) {
|
||||
favItemIds.add(file.id);
|
||||
addToFavorites(file);
|
||||
setIsFav(true);
|
||||
} else {
|
||||
favItemIds.delete(file.id);
|
||||
removeFromFavorites(file);
|
||||
setIsFav(false);
|
||||
}
|
||||
needUpdate.current = true;
|
||||
};
|
||||
|
||||
const updateItems = (items = []) => {
|
||||
if (photoSwipe) {
|
||||
photoSwipe.items.length = 0;
|
||||
items.forEach((item) => {
|
||||
photoSwipe.items.push(item);
|
||||
});
|
||||
photoSwipe.invalidateCurrItems();
|
||||
// photoSwipe.updateSize(true);
|
||||
}
|
||||
};
|
||||
|
||||
const checkExifAvailable = async () => {
|
||||
setExif(null);
|
||||
await sleep(100);
|
||||
try {
|
||||
const img: HTMLImageElement = document.querySelector(
|
||||
'.pswp__img:not(.pswp__img--placeholder)'
|
||||
);
|
||||
if (img) {
|
||||
const exifData = await exifr.parse(img);
|
||||
if (!exifData) {
|
||||
return;
|
||||
}
|
||||
exifData.raw = prettyPrintExif(exifData);
|
||||
setExif(exifData);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'exifr parsing failed');
|
||||
}
|
||||
};
|
||||
|
||||
function updateInfo() {
|
||||
const file: EnteFile = this?.currItem;
|
||||
if (file?.metadata) {
|
||||
setMetaData(file.metadata);
|
||||
setExif(null);
|
||||
checkExifAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseInfo = () => {
|
||||
setShowInfo(false);
|
||||
};
|
||||
const handleOpenInfo = () => {
|
||||
setShowInfo(true);
|
||||
};
|
||||
|
||||
const downloadFileHelper = async (file) => {
|
||||
appContext.startLoading();
|
||||
await downloadFile(
|
||||
file,
|
||||
publicCollectionGalleryContext.accessedThroughSharedURL,
|
||||
publicCollectionGalleryContext.token,
|
||||
publicCollectionGalleryContext.passwordToken
|
||||
);
|
||||
|
||||
appContext.finishLoading();
|
||||
};
|
||||
const scheduleUpdate = () => (needUpdate.current = true);
|
||||
const { id } = props;
|
||||
let { className } = props;
|
||||
className = classnames(['pswp', className]).trim();
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id={id}
|
||||
className={className}
|
||||
tabIndex={Number('-1')}
|
||||
role="dialog"
|
||||
aria-hidden="true"
|
||||
ref={pswpElement}>
|
||||
<div className="pswp__bg" />
|
||||
<div className="pswp__scroll-wrap">
|
||||
<LivePhotoBtn
|
||||
onClick={livePhotoBtnOptions.click}
|
||||
onMouseEnter={livePhotoBtnOptions.show}
|
||||
onMouseLeave={livePhotoBtnOptions.hide}
|
||||
disabled={livePhotoBtnOptions.loading}
|
||||
style={{
|
||||
display: livePhotoBtnOptions.visible
|
||||
? 'block'
|
||||
: 'none',
|
||||
}}>
|
||||
{livePhotoBtnHTML} {constants.LIVE}
|
||||
</LivePhotoBtn>
|
||||
<div className="pswp__container">
|
||||
<div className="pswp__item" />
|
||||
<div className="pswp__item" />
|
||||
<div className="pswp__item" />
|
||||
</div>
|
||||
<div className="pswp__ui pswp__ui--hidden">
|
||||
<div className="pswp__top-bar">
|
||||
<div className="pswp__counter" />
|
||||
|
||||
<button
|
||||
className="pswp__button pswp__button--close"
|
||||
title={constants.CLOSE}
|
||||
/>
|
||||
|
||||
{props.enableDownload && (
|
||||
<button
|
||||
className="pswp-custom download-btn"
|
||||
title={constants.DOWNLOAD}
|
||||
onClick={() =>
|
||||
downloadFileHelper(photoSwipe.currItem)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
className="pswp__button pswp__button--fs"
|
||||
title={constants.TOGGLE_FULLSCREEN}
|
||||
/>
|
||||
<button
|
||||
className="pswp__button pswp__button--zoom"
|
||||
title={constants.ZOOM_IN_OUT}
|
||||
/>
|
||||
{!props.isSharedCollection &&
|
||||
!props.isTrashCollection && (
|
||||
<FavButton
|
||||
size={44}
|
||||
isClick={isFav}
|
||||
onClick={() => {
|
||||
onFavClick(photoSwipe?.currItem);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!props.isSharedCollection && (
|
||||
<button
|
||||
className="pswp-custom info-btn"
|
||||
title={constants.INFO}
|
||||
onClick={handleOpenInfo}
|
||||
/>
|
||||
)}
|
||||
<div className="pswp__preloader">
|
||||
<div className="pswp__preloader__icn">
|
||||
<div className="pswp__preloader__cut">
|
||||
<div className="pswp__preloader__donut" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
|
||||
<div className="pswp__share-tooltip" />
|
||||
</div>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--left"
|
||||
title={constants.PREVIOUS}
|
||||
/>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--right"
|
||||
title={constants.NEXT}
|
||||
/>
|
||||
<div className="pswp__caption">
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!props.isSharedCollection && (
|
||||
<InfoModal
|
||||
shouldDisableEdits={props.isSharedCollection}
|
||||
showInfo={showInfo}
|
||||
handleCloseInfo={handleCloseInfo}
|
||||
items={items}
|
||||
photoSwipe={photoSwipe}
|
||||
metadata={metadata}
|
||||
exif={exif}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PhotoSwipe;
|
437
src/components/PhotoSwipe/index.tsx
Normal file
437
src/components/PhotoSwipe/index.tsx
Normal file
|
@ -0,0 +1,437 @@
|
|||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import Photoswipe from 'photoswipe';
|
||||
import PhotoswipeUIDefault from 'photoswipe/dist/photoswipe-ui-default';
|
||||
import classnames from 'classnames';
|
||||
import FavButton from 'components/FavButton';
|
||||
import {
|
||||
addToFavorites,
|
||||
removeFromFavorites,
|
||||
} from 'services/collectionService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import constants from 'utils/strings/constants';
|
||||
import exifr from 'exifr';
|
||||
import events from './events';
|
||||
import { downloadFile } from 'utils/file';
|
||||
import { prettyPrintExif } from 'utils/exif';
|
||||
import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
|
||||
import { logError } from 'utils/sentry';
|
||||
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
import { sleep } from 'utils/common';
|
||||
import { playVideo, pauseVideo } from 'utils/photoFrame';
|
||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { InfoModal } from './InfoDialog';
|
||||
import { defaultLivePhotoDefaultOptions } from 'constants/photoswipe';
|
||||
import { LivePhotoBtn } from './styledComponents/LivePhotoBtn';
|
||||
|
||||
interface Iprops {
|
||||
isOpen: boolean;
|
||||
items: any[];
|
||||
currentIndex?: number;
|
||||
onClose?: (needUpdate: boolean) => void;
|
||||
gettingData: (instance: any, index: number, item: EnteFile) => void;
|
||||
id?: string;
|
||||
className?: string;
|
||||
favItemIds: Set<number>;
|
||||
isSharedCollection: boolean;
|
||||
isTrashCollection: boolean;
|
||||
enableDownload: boolean;
|
||||
isSourceLoaded: boolean;
|
||||
}
|
||||
|
||||
function PhotoSwipe(props: Iprops) {
|
||||
const pswpElement = useRef<HTMLDivElement>();
|
||||
const [photoSwipe, setPhotoSwipe] =
|
||||
useState<Photoswipe<Photoswipe.Options>>();
|
||||
|
||||
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
|
||||
);
|
||||
const needUpdate = useRef(false);
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
PublicCollectionGalleryContext
|
||||
);
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pswpElement) return;
|
||||
if (isOpen) {
|
||||
openPhotoSwipe();
|
||||
}
|
||||
if (!isOpen) {
|
||||
closePhotoSwipe();
|
||||
}
|
||||
return () => {
|
||||
closePhotoSwipe();
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
updateItems(items);
|
||||
}, [items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (photoSwipe) {
|
||||
photoSwipe.options.arrowKeys = !showInfo;
|
||||
photoSwipe.options.escKey = !showInfo;
|
||||
}
|
||||
}, [showInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const item = items[photoSwipe?.getCurrentIndex()];
|
||||
if (item && item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
const getVideoAndImage = () => {
|
||||
const video = document.getElementById(
|
||||
`live-photo-video-${item.id}`
|
||||
);
|
||||
const image = document.getElementById(
|
||||
`live-photo-image-${item.id}`
|
||||
);
|
||||
return { video, image };
|
||||
};
|
||||
|
||||
const { video, image } = getVideoAndImage();
|
||||
|
||||
if (video && image) {
|
||||
setLivePhotoBtnOptions({
|
||||
click: async () => {
|
||||
await playVideo(video, image);
|
||||
},
|
||||
hide: async () => {
|
||||
await pauseVideo(video, image);
|
||||
},
|
||||
show: async () => {
|
||||
await playVideo(video, image);
|
||||
},
|
||||
visible: true,
|
||||
loading: false,
|
||||
});
|
||||
} else {
|
||||
setLivePhotoBtnOptions({
|
||||
...defaultLivePhotoDefaultOptions,
|
||||
visible: true,
|
||||
loading: true,
|
||||
});
|
||||
}
|
||||
|
||||
const downloadLivePhotoBtn = document.getElementById(
|
||||
`download-btn-${item.id}`
|
||||
) as HTMLButtonElement;
|
||||
if (downloadLivePhotoBtn) {
|
||||
const downloadLivePhoto = () => {
|
||||
downloadFileHelper(photoSwipe.currItem);
|
||||
};
|
||||
|
||||
downloadLivePhotoBtn.addEventListener(
|
||||
'click',
|
||||
downloadLivePhoto
|
||||
);
|
||||
return () => {
|
||||
downloadLivePhotoBtn.removeEventListener(
|
||||
'click',
|
||||
downloadLivePhoto
|
||||
);
|
||||
setLivePhotoBtnOptions(defaultLivePhotoDefaultOptions);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
setLivePhotoBtnOptions(defaultLivePhotoDefaultOptions);
|
||||
};
|
||||
}
|
||||
}, [photoSwipe?.currItem, isOpen, isSourceLoaded]);
|
||||
|
||||
function updateFavButton() {
|
||||
setIsFav(isInFav(this?.currItem));
|
||||
}
|
||||
|
||||
const openPhotoSwipe = () => {
|
||||
const { items, currentIndex } = props;
|
||||
const options = {
|
||||
history: false,
|
||||
maxSpreadZoom: 5,
|
||||
index: currentIndex,
|
||||
showHideOpacity: true,
|
||||
getDoubleTapZoom(isMouseClick, item) {
|
||||
if (isMouseClick) {
|
||||
return 2.5;
|
||||
}
|
||||
// zoom to original if initial zoom is less than 0.7x,
|
||||
// otherwise to 1.5x, to make sure that double-tap gesture always zooms image
|
||||
return item.initialZoomLevel < 0.7 ? 1 : 1.5;
|
||||
},
|
||||
getThumbBoundsFn: (index) => {
|
||||
try {
|
||||
const file = items[index];
|
||||
const ele = document.getElementById(`thumb-${file.id}`);
|
||||
if (ele) {
|
||||
const rect = ele.getBoundingClientRect();
|
||||
const pageYScroll =
|
||||
window.pageYOffset ||
|
||||
document.documentElement.scrollTop;
|
||||
return {
|
||||
x: rect.left,
|
||||
y: rect.top + pageYScroll,
|
||||
w: rect.width,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
const photoSwipe = new Photoswipe(
|
||||
pswpElement.current,
|
||||
PhotoswipeUIDefault,
|
||||
items,
|
||||
options
|
||||
);
|
||||
events.forEach((event) => {
|
||||
const callback = props[event];
|
||||
if (callback || event === 'destroy') {
|
||||
photoSwipe.listen(event, function (...args) {
|
||||
if (callback) {
|
||||
args.unshift(this);
|
||||
callback(...args);
|
||||
}
|
||||
if (event === 'destroy') {
|
||||
handleClose();
|
||||
}
|
||||
if (event === 'close') {
|
||||
handleClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
photoSwipe.listen('beforeChange', function () {
|
||||
updateInfo.call(this);
|
||||
updateFavButton.call(this);
|
||||
});
|
||||
photoSwipe.listen('resize', checkExifAvailable);
|
||||
photoSwipe.init();
|
||||
needUpdate.current = false;
|
||||
setPhotoSwipe(photoSwipe);
|
||||
};
|
||||
|
||||
const closePhotoSwipe = () => {
|
||||
if (photoSwipe) photoSwipe.close();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
const { onClose } = props;
|
||||
if (typeof onClose === 'function') {
|
||||
onClose(needUpdate.current);
|
||||
}
|
||||
const videoTags = document.getElementsByTagName('video');
|
||||
for (const videoTag of videoTags) {
|
||||
videoTag.pause();
|
||||
}
|
||||
handleCloseInfo();
|
||||
// BE_AWARE: this will clear any notification set, even if they were not set in/by the photoswipe component
|
||||
galleryContext.setNotificationAttributes(null);
|
||||
};
|
||||
const isInFav = (file) => {
|
||||
const { favItemIds } = props;
|
||||
if (favItemIds && file) {
|
||||
return favItemIds.has(file.id);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const onFavClick = async (file) => {
|
||||
const { favItemIds } = props;
|
||||
if (!isInFav(file)) {
|
||||
favItemIds.add(file.id);
|
||||
addToFavorites(file);
|
||||
setIsFav(true);
|
||||
} else {
|
||||
favItemIds.delete(file.id);
|
||||
removeFromFavorites(file);
|
||||
setIsFav(false);
|
||||
}
|
||||
needUpdate.current = true;
|
||||
};
|
||||
|
||||
const updateItems = (items = []) => {
|
||||
if (photoSwipe) {
|
||||
photoSwipe.items.length = 0;
|
||||
items.forEach((item) => {
|
||||
photoSwipe.items.push(item);
|
||||
});
|
||||
photoSwipe.invalidateCurrItems();
|
||||
// photoSwipe.updateSize(true);
|
||||
}
|
||||
};
|
||||
|
||||
const checkExifAvailable = async () => {
|
||||
setExif(null);
|
||||
await sleep(100);
|
||||
try {
|
||||
const img: HTMLImageElement = document.querySelector(
|
||||
'.pswp__img:not(.pswp__img--placeholder)'
|
||||
);
|
||||
if (img) {
|
||||
const exifData = await exifr.parse(img);
|
||||
if (!exifData) {
|
||||
return;
|
||||
}
|
||||
exifData.raw = prettyPrintExif(exifData);
|
||||
setExif(exifData);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'exifr parsing failed');
|
||||
}
|
||||
};
|
||||
|
||||
function updateInfo() {
|
||||
const file: EnteFile = this?.currItem;
|
||||
if (file?.metadata) {
|
||||
setMetaData(file.metadata);
|
||||
setExif(null);
|
||||
checkExifAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseInfo = () => {
|
||||
setShowInfo(false);
|
||||
};
|
||||
const handleOpenInfo = () => {
|
||||
setShowInfo(true);
|
||||
};
|
||||
|
||||
const downloadFileHelper = async (file) => {
|
||||
appContext.startLoading();
|
||||
await downloadFile(
|
||||
file,
|
||||
publicCollectionGalleryContext.accessedThroughSharedURL,
|
||||
publicCollectionGalleryContext.token,
|
||||
publicCollectionGalleryContext.passwordToken
|
||||
);
|
||||
|
||||
appContext.finishLoading();
|
||||
};
|
||||
const scheduleUpdate = () => (needUpdate.current = true);
|
||||
const { id } = props;
|
||||
let { className } = props;
|
||||
className = classnames(['pswp', className]).trim();
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id={id}
|
||||
className={className}
|
||||
tabIndex={Number('-1')}
|
||||
role="dialog"
|
||||
aria-hidden="true"
|
||||
ref={pswpElement}>
|
||||
<div className="pswp__bg" />
|
||||
<div className="pswp__scroll-wrap">
|
||||
<LivePhotoBtn
|
||||
onClick={livePhotoBtnOptions.click}
|
||||
onMouseEnter={livePhotoBtnOptions.show}
|
||||
onMouseLeave={livePhotoBtnOptions.hide}
|
||||
disabled={livePhotoBtnOptions.loading}
|
||||
style={{
|
||||
display: livePhotoBtnOptions.visible
|
||||
? 'block'
|
||||
: 'none',
|
||||
}}>
|
||||
{livePhotoBtnHTML} {constants.LIVE}
|
||||
</LivePhotoBtn>
|
||||
<div className="pswp__container">
|
||||
<div className="pswp__item" />
|
||||
<div className="pswp__item" />
|
||||
<div className="pswp__item" />
|
||||
</div>
|
||||
<div className="pswp__ui pswp__ui--hidden">
|
||||
<div className="pswp__top-bar">
|
||||
<div className="pswp__counter" />
|
||||
|
||||
<button
|
||||
className="pswp__button pswp__button--close"
|
||||
title={constants.CLOSE}
|
||||
/>
|
||||
|
||||
{props.enableDownload && (
|
||||
<button
|
||||
className="pswp-custom download-btn"
|
||||
title={constants.DOWNLOAD}
|
||||
onClick={() =>
|
||||
downloadFileHelper(photoSwipe.currItem)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
className="pswp__button pswp__button--fs"
|
||||
title={constants.TOGGLE_FULLSCREEN}
|
||||
/>
|
||||
<button
|
||||
className="pswp__button pswp__button--zoom"
|
||||
title={constants.ZOOM_IN_OUT}
|
||||
/>
|
||||
{!props.isSharedCollection &&
|
||||
!props.isTrashCollection && (
|
||||
<FavButton
|
||||
size={44}
|
||||
isClick={isFav}
|
||||
onClick={() => {
|
||||
onFavClick(photoSwipe?.currItem);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!props.isSharedCollection && (
|
||||
<button
|
||||
className="pswp-custom info-btn"
|
||||
title={constants.INFO}
|
||||
onClick={handleOpenInfo}
|
||||
/>
|
||||
)}
|
||||
<div className="pswp__preloader">
|
||||
<div className="pswp__preloader__icn">
|
||||
<div className="pswp__preloader__cut">
|
||||
<div className="pswp__preloader__donut" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
|
||||
<div className="pswp__share-tooltip" />
|
||||
</div>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--left"
|
||||
title={constants.PREVIOUS}
|
||||
/>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--right"
|
||||
title={constants.NEXT}
|
||||
/>
|
||||
<div className="pswp__caption">
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InfoModal
|
||||
shouldDisableEdits={props.isSharedCollection}
|
||||
showInfo={showInfo}
|
||||
handleCloseInfo={handleCloseInfo}
|
||||
items={items}
|
||||
photoSwipe={photoSwipe}
|
||||
metadata={metadata}
|
||||
exif={exif}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PhotoSwipe;
|
7
src/components/PhotoSwipe/styledComponents/Legend.tsx
Normal file
7
src/components/PhotoSwipe/styledComponents/Legend.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const Legend = styled.span`
|
||||
font-size: 20px;
|
||||
color: #ddd;
|
||||
display: inline;
|
||||
`;
|
|
@ -0,0 +1,6 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const LegendContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
16
src/components/PhotoSwipe/styledComponents/LivePhotoBtn.tsx
Normal file
16
src/components/PhotoSwipe/styledComponents/LivePhotoBtn.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const LivePhotoBtn = styled.button`
|
||||
position: absolute;
|
||||
bottom: 6vh;
|
||||
right: 6vh;
|
||||
height: 40px;
|
||||
width: 80px;
|
||||
background: #d7d7d7;
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 10%;
|
||||
z-index: 10;
|
||||
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
|
||||
}
|
||||
`;
|
6
src/components/PhotoSwipe/styledComponents/Pre.tsx
Normal file
6
src/components/PhotoSwipe/styledComponents/Pre.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const Pre = styled.pre`
|
||||
color: #aaa;
|
||||
padding: 7px 15px;
|
||||
`;
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
|
||||
export const SmallLoadingSpinner = () => (
|
||||
<EnteSpinner
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
}}
|
||||
/>
|
||||
);
|
7
src/constants/photoswipe/index.ts
Normal file
7
src/constants/photoswipe/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const defaultLivePhotoDefaultOptions = {
|
||||
click: () => {},
|
||||
hide: () => {},
|
||||
show: () => {},
|
||||
loading: false,
|
||||
visible: false,
|
||||
};
|
|
@ -463,21 +463,21 @@ const englishConstants = {
|
|||
VIDEO_PLAYBACK_FAILED: 'video format not supported',
|
||||
VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD:
|
||||
'this video cannot be played on your browser',
|
||||
METADATA: 'metadata',
|
||||
INFO: 'information',
|
||||
FILE_ID: 'file id',
|
||||
FILE_NAME: 'file name',
|
||||
CREATION_TIME: 'creation time',
|
||||
UPDATED_ON: 'updated on',
|
||||
LOCATION: 'location',
|
||||
METADATA: 'Metadata',
|
||||
INFO: 'Info',
|
||||
FILE_ID: 'File id',
|
||||
FILE_NAME: 'File name',
|
||||
CREATION_TIME: 'Creation time',
|
||||
UPDATED_ON: 'Updated on',
|
||||
LOCATION: 'Location',
|
||||
SHOW_MAP: 'show on map',
|
||||
EXIF: 'exif',
|
||||
DEVICE: 'device',
|
||||
IMAGE_SIZE: 'image size',
|
||||
FLASH: 'flash',
|
||||
FOCAL_LENGTH: 'focal length',
|
||||
APERTURE: 'aperture',
|
||||
ISO: 'iso',
|
||||
EXIF: 'Exif',
|
||||
DEVICE: 'Device',
|
||||
IMAGE_SIZE: 'Image size',
|
||||
FLASH: 'Flash',
|
||||
FOCAL_LENGTH: 'Focal length',
|
||||
APERTURE: 'Aperture',
|
||||
ISO: 'ISO',
|
||||
SHOW_ALL: 'show all',
|
||||
LOGIN_TO_UPLOAD_FILES: (count: number) =>
|
||||
count === 1
|
||||
|
|
Loading…
Reference in a new issue