Merge pull request #762 from ente-io/caption-support

Caption support
This commit is contained in:
Abhinav Kumar 2022-11-03 10:07:03 +05:30 committed by GitHub
commit 1c23c40185
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 237 additions and 15 deletions

View file

@ -118,6 +118,7 @@ const PhotoFrame = ({
dataIndex: index, dataIndex: index,
w: window.innerWidth, w: window.innerWidth,
h: window.innerHeight, h: window.innerHeight,
title: item.pubMagicMetadata?.data.caption,
})) }))
.filter((item) => { .filter((item) => {
if ( if (

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

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

View file

@ -11,6 +11,7 @@ import { AppContext } from 'pages/_app';
import { Location, Metadata } from 'types/upload'; import { Location, Metadata } from 'types/upload';
import Photoswipe from 'photoswipe'; import Photoswipe from 'photoswipe';
import { getEXIFLocation } from 'services/upload/exifService'; import { getEXIFLocation } from 'services/upload/exifService';
import { RenderCaption } from './RenderCaption';
const FileInfoDialog = styled(Dialog)(({ theme }) => ({ const FileInfoDialog = styled(Dialog)(({ theme }) => ({
zIndex: 1501, zIndex: 1501,
@ -31,6 +32,7 @@ interface Iprops {
metadata: Metadata; metadata: Metadata;
exif: any; exif: any;
scheduleUpdate: () => void; scheduleUpdate: () => void;
refreshPhotoswipe: () => void;
} }
export function FileInfo({ export function FileInfo({
@ -42,6 +44,7 @@ export function FileInfo({
metadata, metadata,
exif, exif,
scheduleUpdate, scheduleUpdate,
refreshPhotoswipe,
}: Iprops) { }: Iprops) {
const appContext = useContext(AppContext); const appContext = useContext(AppContext);
const [location, setLocation] = useState<Location>(null); const [location, setLocation] = useState<Location>(null);
@ -78,11 +81,12 @@ export function FileInfo({
<Typography variant="subtitle" mb={1}> <Typography variant="subtitle" mb={1}>
{constants.METADATA} {constants.METADATA}
</Typography> </Typography>
<RenderCaption
{RenderInfoItem( shouldDisableEdits={shouldDisableEdits}
constants.FILE_ID, file={items[photoSwipe?.getCurrentIndex()]}
items[photoSwipe?.getCurrentIndex()]?.id scheduleUpdate={scheduleUpdate}
)} refreshPhotoswipe={refreshPhotoswipe}
/>
{metadata?.title && ( {metadata?.title && (
<RenderFileName <RenderFileName
shouldDisableEdits={shouldDisableEdits} shouldDisableEdits={shouldDisableEdits}

View file

@ -33,6 +33,7 @@ import ChevronRight from '@mui/icons-material/ChevronRight';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import { trashFiles } from 'services/fileService'; import { trashFiles } from 'services/fileService';
import { getTrashFileMessage } from 'utils/ui'; import { getTrashFileMessage } from 'utils/ui';
import { ChevronLeft } from '@mui/icons-material';
interface Iprops { interface Iprops {
isOpen: boolean; isOpen: boolean;
@ -299,6 +300,13 @@ function PhotoViewer(props: Iprops) {
} }
}; };
const refreshPhotoswipe = () => {
photoSwipe.invalidateCurrItems();
if (isOpen) {
photoSwipe.updateSize(true);
}
};
const checkExifAvailable = async () => { const checkExifAvailable = async () => {
setExif(null); setExif(null);
await sleep(100); await sleep(100);
@ -456,20 +464,16 @@ function PhotoViewer(props: Iprops) {
</div> </div>
<button <button
className="pswp__button pswp__button--arrow--left" className="pswp__button pswp__button--arrow--left"
title={constants.PREVIOUS} title={constants.PREVIOUS}>
onClick={photoSwipe?.prev}> <ChevronLeft sx={{ pointerEvents: 'none' }} />
<ChevronRight
sx={{ transform: 'rotate(180deg)' }}
/>
</button> </button>
<button <button
className="pswp__button pswp__button--arrow--right" className="pswp__button pswp__button--arrow--right"
title={constants.NEXT} title={constants.NEXT}>
onClick={photoSwipe?.next}> <ChevronRight sx={{ pointerEvents: 'none' }} />
<ChevronRight />
</button> </button>
<div className="pswp__caption"> <div className="pswp__caption pswp-custom-caption-container">
<div /> <div className="pswp-custom-caption"></div>
</div> </div>
</div> </div>
</div> </div>
@ -483,6 +487,7 @@ function PhotoViewer(props: Iprops) {
metadata={metadata} metadata={metadata}
exif={exif} exif={exif}
scheduleUpdate={scheduleUpdate} scheduleUpdate={scheduleUpdate}
refreshPhotoswipe={refreshPhotoswipe}
/> />
</> </>
); );

View file

@ -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_CREATION_TIME = new Date();
export const MAX_EDITED_FILE_NAME_LENGTH = 100; export const MAX_EDITED_FILE_NAME_LENGTH = 100;
export const MAX_CAPTION_SIZE = 280;
export const MAX_TRASH_BATCH_SIZE = 1000; export const MAX_TRASH_BATCH_SIZE = 1000;
export const TYPE_HEIC = 'heic'; export const TYPE_HEIC = 'heic';

View file

@ -120,6 +120,21 @@ html, body {
margin-right: 20px; 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 { .bg-upload-progress-bar {
background-color: #51cd7c; background-color: #51cd7c;

View file

@ -19,6 +19,7 @@ export interface FileMagicMetadata extends Omit<MagicMetadataCore, 'data'> {
export interface FilePublicMagicMetadataProps { export interface FilePublicMagicMetadataProps {
editedTime?: number; editedTime?: number;
editedName?: string; editedName?: string;
caption?: string;
} }
export interface FilePublicMagicMetadata export interface FilePublicMagicMetadata
@ -43,6 +44,7 @@ export interface EnteFile {
html: string; html: string;
w: number; w: number;
h: number; h: number;
title: string;
isDeleted: boolean; isDeleted: boolean;
isTrashed?: boolean; isTrashed?: boolean;
deleteBy?: number; deleteBy?: number;

View file

@ -408,6 +408,19 @@ export async function changeFileName(file: EnteFile, editedName: string) {
return file; 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) { export function isSharedFile(user: User, file: EnteFile) {
if (!user?.id || !file?.ownerID) { if (!user?.id || !file?.ownerID) {
return false; return false;

View file

@ -435,6 +435,7 @@ const englishConstants = {
INFO: 'Info', INFO: 'Info',
FILE_ID: 'File ID', FILE_ID: 'File ID',
FILE_NAME: 'File name', FILE_NAME: 'File name',
CAPTION: 'Caption',
CREATION_TIME: 'Creation time', CREATION_TIME: 'Creation time',
UPDATED_ON: 'Updated on', UPDATED_ON: 'Updated on',
LOCATION: 'Location', LOCATION: 'Location',
@ -622,6 +623,7 @@ const englishConstants = {
<>File time updation failed for some files, please retry</> <>File time updation failed for some files, please retry</>
), ),
FILE_NAME_CHARACTER_LIMIT: '100 characters max', FILE_NAME_CHARACTER_LIMIT: '100 characters max',
CAPTION_CHARACTER_LIMIT: '280 characters max',
DATE_TIME_ORIGINAL: 'EXIF:DateTimeOriginal', DATE_TIME_ORIGINAL: 'EXIF:DateTimeOriginal',
DATE_TIME_DIGITIZED: 'EXIF:DateTimeDigitized', DATE_TIME_DIGITIZED: 'EXIF:DateTimeDigitized',