commit
1c23c40185
|
@ -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 (
|
||||||
|
|
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 { 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}
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in a new issue