commit
1c23c40185
|
@ -118,6 +118,7 @@ const PhotoFrame = ({
|
|||
dataIndex: index,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
title: item.pubMagicMetadata?.data.caption,
|
||||
}))
|
||||
.filter((item) => {
|
||||
if (
|
||||
|
|
79
src/components/PhotoViewer/InfoDialog/CaptionEditForm.tsx
Normal file
79
src/components/PhotoViewer/InfoDialog/CaptionEditForm.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import React, { useState } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { Col, Form, FormControl } from 'react-bootstrap';
|
||||
import { 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_CAPTION_SIZE } from 'constants/file';
|
||||
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
|
||||
import { IconButton } from '@mui/material';
|
||||
|
||||
export interface formValues {
|
||||
caption: string;
|
||||
}
|
||||
|
||||
export const CaptionEditForm = ({ caption, saveEdits, discardEdits }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onSubmit = async (values: formValues) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await saveEdits(values.caption);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Formik<formValues>
|
||||
initialValues={{ caption }}
|
||||
validationSchema={Yup.object().shape({
|
||||
caption: Yup.string().max(
|
||||
MAX_CAPTION_SIZE,
|
||||
constants.CAPTION_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={9}>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder={constants.CAPTION}
|
||||
value={values.caption}
|
||||
onChange={handleChange('caption')}
|
||||
isInvalid={Boolean(errors.caption)}
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
/>
|
||||
<FormControl.Feedback
|
||||
type="invalid"
|
||||
style={{ textAlign: 'center' }}>
|
||||
{errors.caption}
|
||||
</FormControl.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group bsPrefix="ente-form-group" as={Col} xs={3}>
|
||||
<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>
|
||||
);
|
||||
};
|
100
src/components/PhotoViewer/InfoDialog/RenderCaption.tsx
Normal file
100
src/components/PhotoViewer/InfoDialog/RenderCaption.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
import React, { useState } from 'react';
|
||||
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { changeCaption, updateExistingFilePubMetadata } from 'utils/file';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import { FreeFlowText, Label, Row, Value } from 'components/Container';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { IconButton, Typography } from '@mui/material';
|
||||
import { CaptionEditForm } from './CaptionEditForm';
|
||||
|
||||
export const getFileTitle = (filename, extension) => {
|
||||
if (extension) {
|
||||
return filename + '.' + extension;
|
||||
} else {
|
||||
return filename;
|
||||
}
|
||||
};
|
||||
|
||||
export function RenderCaption({
|
||||
shouldDisableEdits,
|
||||
file,
|
||||
scheduleUpdate,
|
||||
refreshPhotoswipe,
|
||||
}: {
|
||||
shouldDisableEdits: boolean;
|
||||
file: EnteFile;
|
||||
scheduleUpdate: () => void;
|
||||
refreshPhotoswipe: () => void;
|
||||
}) {
|
||||
const [caption, setCaption] = useState(
|
||||
file?.pubMagicMetadata?.data.caption
|
||||
);
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
const openEditMode = () => setIsInEditMode(true);
|
||||
const closeEditMode = () => setIsInEditMode(false);
|
||||
|
||||
const saveEdits = async (newCaption: string) => {
|
||||
try {
|
||||
if (file) {
|
||||
if (caption === newCaption) {
|
||||
closeEditMode();
|
||||
return;
|
||||
}
|
||||
setCaption(newCaption);
|
||||
|
||||
let updatedFile = await changeCaption(file, newCaption);
|
||||
updatedFile = (
|
||||
await updateFilePublicMagicMetadata([updatedFile])
|
||||
)[0];
|
||||
updateExistingFilePubMetadata(file, updatedFile);
|
||||
file.title = file.pubMagicMetadata.data.caption;
|
||||
refreshPhotoswipe();
|
||||
scheduleUpdate();
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'failed to update caption');
|
||||
} finally {
|
||||
closeEditMode();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Label width="30%">{constants.CAPTION}</Label>
|
||||
{!isInEditMode ? (
|
||||
<>
|
||||
<Value width={!shouldDisableEdits ? '60%' : '70%'}>
|
||||
{caption ? (
|
||||
<FreeFlowText>{caption}</FreeFlowText>
|
||||
) : (
|
||||
<Typography color="text.secondary">
|
||||
Add a caption
|
||||
</Typography>
|
||||
)}
|
||||
</Value>
|
||||
{!shouldDisableEdits && (
|
||||
<Value
|
||||
width="10%"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
marginLeft: '10px',
|
||||
}}>
|
||||
<IconButton onClick={openEditMode}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Value>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<CaptionEditForm
|
||||
caption={caption}
|
||||
saveEdits={saveEdits}
|
||||
discardEdits={closeEditMode}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -11,6 +11,7 @@ import { AppContext } from 'pages/_app';
|
|||
import { Location, Metadata } from 'types/upload';
|
||||
import Photoswipe from 'photoswipe';
|
||||
import { getEXIFLocation } from 'services/upload/exifService';
|
||||
import { RenderCaption } from './RenderCaption';
|
||||
|
||||
const FileInfoDialog = styled(Dialog)(({ theme }) => ({
|
||||
zIndex: 1501,
|
||||
|
@ -31,6 +32,7 @@ interface Iprops {
|
|||
metadata: Metadata;
|
||||
exif: any;
|
||||
scheduleUpdate: () => void;
|
||||
refreshPhotoswipe: () => void;
|
||||
}
|
||||
|
||||
export function FileInfo({
|
||||
|
@ -42,6 +44,7 @@ export function FileInfo({
|
|||
metadata,
|
||||
exif,
|
||||
scheduleUpdate,
|
||||
refreshPhotoswipe,
|
||||
}: Iprops) {
|
||||
const appContext = useContext(AppContext);
|
||||
const [location, setLocation] = useState<Location>(null);
|
||||
|
@ -78,11 +81,12 @@ export function FileInfo({
|
|||
<Typography variant="subtitle" mb={1}>
|
||||
{constants.METADATA}
|
||||
</Typography>
|
||||
|
||||
{RenderInfoItem(
|
||||
constants.FILE_ID,
|
||||
items[photoSwipe?.getCurrentIndex()]?.id
|
||||
)}
|
||||
<RenderCaption
|
||||
shouldDisableEdits={shouldDisableEdits}
|
||||
file={items[photoSwipe?.getCurrentIndex()]}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
refreshPhotoswipe={refreshPhotoswipe}
|
||||
/>
|
||||
{metadata?.title && (
|
||||
<RenderFileName
|
||||
shouldDisableEdits={shouldDisableEdits}
|
||||
|
|
|
@ -33,6 +33,7 @@ import ChevronRight from '@mui/icons-material/ChevronRight';
|
|||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { trashFiles } from 'services/fileService';
|
||||
import { getTrashFileMessage } from 'utils/ui';
|
||||
import { ChevronLeft } from '@mui/icons-material';
|
||||
|
||||
interface Iprops {
|
||||
isOpen: boolean;
|
||||
|
@ -299,6 +300,13 @@ function PhotoViewer(props: Iprops) {
|
|||
}
|
||||
};
|
||||
|
||||
const refreshPhotoswipe = () => {
|
||||
photoSwipe.invalidateCurrItems();
|
||||
if (isOpen) {
|
||||
photoSwipe.updateSize(true);
|
||||
}
|
||||
};
|
||||
|
||||
const checkExifAvailable = async () => {
|
||||
setExif(null);
|
||||
await sleep(100);
|
||||
|
@ -456,20 +464,16 @@ function PhotoViewer(props: Iprops) {
|
|||
</div>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--left"
|
||||
title={constants.PREVIOUS}
|
||||
onClick={photoSwipe?.prev}>
|
||||
<ChevronRight
|
||||
sx={{ transform: 'rotate(180deg)' }}
|
||||
/>
|
||||
title={constants.PREVIOUS}>
|
||||
<ChevronLeft sx={{ pointerEvents: 'none' }} />
|
||||
</button>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--right"
|
||||
title={constants.NEXT}
|
||||
onClick={photoSwipe?.next}>
|
||||
<ChevronRight />
|
||||
title={constants.NEXT}>
|
||||
<ChevronRight sx={{ pointerEvents: 'none' }} />
|
||||
</button>
|
||||
<div className="pswp__caption">
|
||||
<div />
|
||||
<div className="pswp__caption pswp-custom-caption-container">
|
||||
<div className="pswp-custom-caption"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -483,6 +487,7 @@ function PhotoViewer(props: Iprops) {
|
|||
metadata={metadata}
|
||||
exif={exif}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
refreshPhotoswipe={refreshPhotoswipe}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
|
|||
export const MAX_EDITED_CREATION_TIME = new Date();
|
||||
|
||||
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
|
||||
export const MAX_CAPTION_SIZE = 280;
|
||||
export const MAX_TRASH_BATCH_SIZE = 1000;
|
||||
|
||||
export const TYPE_HEIC = 'heic';
|
||||
|
|
|
@ -120,6 +120,21 @@ html, body {
|
|||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.pswp-custom-caption {
|
||||
text-align: right;
|
||||
width: 375px;
|
||||
font-size:14px;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.pswp-custom-caption-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
bottom: 56px;
|
||||
background-color: transparent !important;
|
||||
padding:16px;
|
||||
}
|
||||
|
||||
.bg-upload-progress-bar {
|
||||
background-color: #51cd7c;
|
||||
|
|
|
@ -19,6 +19,7 @@ export interface FileMagicMetadata extends Omit<MagicMetadataCore, 'data'> {
|
|||
export interface FilePublicMagicMetadataProps {
|
||||
editedTime?: number;
|
||||
editedName?: string;
|
||||
caption?: string;
|
||||
}
|
||||
|
||||
export interface FilePublicMagicMetadata
|
||||
|
@ -43,6 +44,7 @@ export interface EnteFile {
|
|||
html: string;
|
||||
w: number;
|
||||
h: number;
|
||||
title: string;
|
||||
isDeleted: boolean;
|
||||
isTrashed?: boolean;
|
||||
deleteBy?: number;
|
||||
|
|
|
@ -408,6 +408,19 @@ export async function changeFileName(file: EnteFile, editedName: string) {
|
|||
return file;
|
||||
}
|
||||
|
||||
export async function changeCaption(file: EnteFile, caption: string) {
|
||||
const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = {
|
||||
caption,
|
||||
};
|
||||
|
||||
file.pubMagicMetadata = await updateMagicMetadataProps(
|
||||
file.pubMagicMetadata ?? NEW_FILE_MAGIC_METADATA,
|
||||
file.key,
|
||||
updatedPublicMagicMetadataProps
|
||||
);
|
||||
return file;
|
||||
}
|
||||
|
||||
export function isSharedFile(user: User, file: EnteFile) {
|
||||
if (!user?.id || !file?.ownerID) {
|
||||
return false;
|
||||
|
|
|
@ -435,6 +435,7 @@ const englishConstants = {
|
|||
INFO: 'Info',
|
||||
FILE_ID: 'File ID',
|
||||
FILE_NAME: 'File name',
|
||||
CAPTION: 'Caption',
|
||||
CREATION_TIME: 'Creation time',
|
||||
UPDATED_ON: 'Updated on',
|
||||
LOCATION: 'Location',
|
||||
|
@ -622,6 +623,7 @@ const englishConstants = {
|
|||
<>File time updation failed for some files, please retry</>
|
||||
),
|
||||
FILE_NAME_CHARACTER_LIMIT: '100 characters max',
|
||||
CAPTION_CHARACTER_LIMIT: '280 characters max',
|
||||
|
||||
DATE_TIME_ORIGINAL: 'EXIF:DateTimeOriginal',
|
||||
DATE_TIME_DIGITIZED: 'EXIF:DateTimeDigitized',
|
||||
|
|
Loading…
Reference in a new issue