{
justifyContent: "space-around",
}}
>
- {(step === undefined || step === "completed-with-errors") && (
+ {(!step || step == "completed-with-errors") && (
)}
- {step === "completed" && (
+ {step == "completed" && (
)}
- {(step === undefined || step === "completed-with-errors") && (
+ {(!step || step == "completed-with-errors") && (
<>
@@ -234,31 +267,88 @@ const Footer = ({ step, startFix, ...props }) => {
);
};
-const FixCreationTimeRunning = ({ progressTracker }) => {
- const progress = Math.round(
- (progressTracker.current * 100) / progressTracker.total,
- );
- return (
- <>
-
-
- {" "}
- {progressTracker.current} / {progressTracker.total}{" "}
- {" "}
-
- {" "}
- {t("CREATION_TIME_UPDATED")}
-
-
-
-
-
- >
- );
+type SetProgressTracker = React.Dispatch<
+ React.SetStateAction<{
+ current: number;
+ total: number;
+ }>
+>;
+
+const updateFiles = async (
+ enteFiles: EnteFile[],
+ fixOption: FixOption,
+ customDate: ParsedMetadataDate,
+ setProgressTracker: SetProgressTracker,
+) => {
+ setProgressTracker({ current: 0, total: enteFiles.length });
+ let hadErrors = false;
+ for (const [i, enteFile] of enteFiles.entries()) {
+ try {
+ await updateEnteFileDate(enteFile, fixOption, customDate);
+ } catch (e) {
+ log.error(`Failed to update date of ${fileLogID(enteFile)}`, e);
+ hadErrors = true;
+ } finally {
+ setProgressTracker({ current: i + 1, total: enteFiles.length });
+ }
+ }
+ return hadErrors;
+};
+
+/**
+ * Update the date associated with a given {@link enteFile}.
+ *
+ * This is generally treated as the creation date of the underlying asset
+ * (photo, video, live photo) that this file stores.
+ *
+ * - For images, this function allows us to update this date from the Exif and
+ * other metadata embedded in the file.
+ *
+ * - For all types of files (including images), this function allows us to
+ * update this date to an explicitly provided value.
+ *
+ * If an Exif-involving {@link fixOption} is passed for an non-image file, then
+ * that file is just skipped over. Similarly, if an Exif-involving
+ * {@link fixOption} is provided, but the given underlying image for the given
+ * {@link enteFile} does not have a corresponding Exif (or related) value, then
+ * that file is skipped.
+ *
+ * Note that metadata associated with an {@link EnteFile} is immutable, and we
+ * instead modify the mutable metadata section associated with the file. See
+ * [Note: Metadatum] for more details.
+ */
+export const updateEnteFileDate = async (
+ enteFile: EnteFile,
+ fixOption: FixOption,
+ customDate: ParsedMetadataDate,
+) => {
+ let newDate: ParsedMetadataDate | undefined;
+ if (fixOption === "custom") {
+ newDate = customDate;
+ } else if (enteFile.metadata.fileType == FileType.image) {
+ const stream = await downloadManager.getFile(enteFile);
+ const blob = await new Response(stream).blob();
+ const file = new File([blob], enteFile.metadata.title);
+ const { DateTimeOriginal, DateTimeDigitized, MetadataDate, DateTime } =
+ await extractExifDates(file);
+ switch (fixOption) {
+ case "date-time-original":
+ newDate = DateTimeOriginal ?? DateTime;
+ break;
+ case "date-time-digitized":
+ newDate = DateTimeDigitized;
+ break;
+ case "metadata-date":
+ newDate = MetadataDate;
+ break;
+ }
+ }
+
+ if (newDate && newDate.timestamp !== enteFile.metadata.creationTime) {
+ const updatedFile = await changeFileCreationTime(
+ enteFile,
+ newDate.timestamp,
+ );
+ updateExistingFilePubMetadata(enteFile, updatedFile);
+ }
};
diff --git a/web/apps/photos/src/components/MemberSubscriptionManage.tsx b/web/apps/photos/src/components/MemberSubscriptionManage.tsx
index f06fd0be23..7ffbee1648 100644
--- a/web/apps/photos/src/components/MemberSubscriptionManage.tsx
+++ b/web/apps/photos/src/components/MemberSubscriptionManage.tsx
@@ -7,19 +7,14 @@ import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleW
import { Box, Button, Dialog, DialogContent, Typography } from "@mui/material";
import { t } from "i18next";
import { AppContext } from "pages/_app";
-import { useContext, useEffect } from "react";
+import { useContext } from "react";
import billingService from "services/billingService";
-import { preloadImage } from "utils/common";
import { getFamilyPlanAdmin } from "utils/user/family";
export function MemberSubscriptionManage({ open, userDetails, onClose }) {
const { setDialogMessage } = useContext(AppContext);
const fullScreen = useIsMobileWidth();
- useEffect(() => {
- preloadImage("/images/family-plan");
- }, []);
-
async function onLeaveFamilyClick() {
try {
await billingService.leaveFamily();
diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx
index b8f5e333b7..c88d9dde1b 100644
--- a/web/apps/photos/src/components/PhotoFrame.tsx
+++ b/web/apps/photos/src/components/PhotoFrame.tsx
@@ -1,5 +1,5 @@
import log from "@/base/log";
-import { FILE_TYPE } from "@/media/file-type";
+import { FileType } from "@/media/file-type";
import DownloadManager from "@/new/photos/services/download";
import type { LivePhotoSourceURL, SourceURLs } from "@/new/photos/types/file";
import { EnteFile } from "@/new/photos/types/file";
@@ -360,7 +360,7 @@ const PhotoFrame = ({
log.info(`[${item.id}] new file src request`);
fetching[item.id] = true;
const srcURLs = await DownloadManager.getFileForPreview(item);
- if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
+ if (item.metadata.fileType === FileType.livePhoto) {
const srcImgURL = srcURLs.url as LivePhotoSourceURL;
const imageURL = await srcImgURL.image();
@@ -453,8 +453,8 @@ const PhotoFrame = ({
item: EnteFile,
) => {
if (
- item.metadata.fileType !== FILE_TYPE.VIDEO &&
- item.metadata.fileType !== FILE_TYPE.LIVE_PHOTO
+ item.metadata.fileType !== FileType.video &&
+ item.metadata.fileType !== FileType.livePhoto
) {
log.error("getConvertedVideo called for non video file");
return;
diff --git a/web/apps/photos/src/components/PhotoList/index.tsx b/web/apps/photos/src/components/PhotoList/index.tsx
index 1f4b0bfb06..fec632b416 100644
--- a/web/apps/photos/src/components/PhotoList/index.tsx
+++ b/web/apps/photos/src/components/PhotoList/index.tsx
@@ -550,14 +550,14 @@ export function PhotoList({
),
b: (
),
}}
@@ -579,13 +579,22 @@ export function PhotoList({
span={columns}
hasReferral={!!publicCollectionGalleryContext.referralCode}
>
+ {/* Make the entire area tappable, otherwise it is hard to
+ get at on mobile devices. */}
-
- {t("SHARED_USING")}{" "}
-
- ente.io
-
-
+
+
+ {t("SHARED_USING")}{" "}
+
+ ente.io
+
+
+
{publicCollectionGalleryContext.referralCode ? (
void;
- filename: string;
- onInfoClose: () => void;
-}) {
- const { exif, open, onClose, filename, onInfoClose } = props;
-
- if (!exif) {
- return <>>;
- }
- const handleRootClose = () => {
- onClose();
- onInfoClose();
- };
-
- return (
-
-
- }
- />
-
- {[...Object.entries(exif)]
- .sort((a, b) => a[0].localeCompare(b[0]))
- .map(([key, value]) =>
- value ? (
-
-
- {key}
-
-
- {parseExifValue(value)}
-
-
- ) : (
-
- ),
- )}
-
-
- );
-}
diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderCreationTime.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderCreationTime.tsx
index 83d7554ac3..636ecc8b8b 100644
--- a/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderCreationTime.tsx
+++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderCreationTime.tsx
@@ -1,9 +1,10 @@
import log from "@/base/log";
+import type { ParsedMetadataDate } from "@/media/file-metadata";
+import { PhotoDateTimePicker } from "@/new/photos/components/PhotoDateTimePicker";
import { EnteFile } from "@/new/photos/types/file";
import { FlexWrapper } from "@ente/shared/components/Container";
import { formatDate, formatTime } from "@ente/shared/time/format";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
-import EnteDateTimePicker from "components/EnteDateTimePicker";
import { useState } from "react";
import {
changeFileCreationTime,
@@ -27,11 +28,11 @@ export function RenderCreationTime({
const openEditMode = () => setIsInEditMode(true);
const closeEditMode = () => setIsInEditMode(false);
- const saveEdits = async (pickedTime: Date) => {
+ const saveEdits = async (pickedTime: ParsedMetadataDate) => {
try {
setLoading(true);
if (isInEditMode && file) {
- const unixTimeInMicroSec = pickedTime.getTime() * 1000;
+ const unixTimeInMicroSec = pickedTime.timestamp;
if (unixTimeInMicroSec === file?.metadata.creationTime) {
closeEditMode();
return;
@@ -63,10 +64,10 @@ export function RenderCreationTime({
hideEditOption={shouldDisableEdits || isInEditMode}
/>
{isInEditMode && (
-
)}
diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderFileName.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderFileName.tsx
deleted file mode 100644
index e9443c84c1..0000000000
--- a/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderFileName.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import { nameAndExtension } from "@/base/file";
-import log from "@/base/log";
-import { FILE_TYPE } from "@/media/file-type";
-import { EnteFile } from "@/new/photos/types/file";
-import { formattedByteSize } from "@/new/photos/utils/units";
-import { FlexWrapper } from "@ente/shared/components/Container";
-import PhotoOutlined from "@mui/icons-material/PhotoOutlined";
-import VideocamOutlined from "@mui/icons-material/VideocamOutlined";
-import Box from "@mui/material/Box";
-import { useEffect, useState } from "react";
-import { changeFileName, updateExistingFilePubMetadata } from "utils/file";
-import { FileNameEditDialog } from "./FileNameEditDialog";
-import InfoItem from "./InfoItem";
-
-const getFileTitle = (filename, extension) => {
- if (extension) {
- return filename + "." + extension;
- } else {
- return filename;
- }
-};
-
-const getCaption = (file: EnteFile, parsedExifData) => {
- const megaPixels = parsedExifData?.["megaPixels"];
- const resolution = parsedExifData?.["resolution"];
- const fileSize = file.info?.fileSize;
-
- const captionParts = [];
- if (megaPixels) {
- captionParts.push(megaPixels);
- }
- if (resolution) {
- captionParts.push(resolution);
- }
- if (fileSize) {
- captionParts.push(formattedByteSize(fileSize));
- }
- return (
-
- {captionParts.map((caption) => (
- {caption}
- ))}
-
- );
-};
-
-export function RenderFileName({
- parsedExifData,
- shouldDisableEdits,
- file,
- scheduleUpdate,
-}: {
- parsedExifData: Record;
- shouldDisableEdits: boolean;
- file: EnteFile;
- scheduleUpdate: () => void;
-}) {
- const [isInEditMode, setIsInEditMode] = useState(false);
- const openEditMode = () => setIsInEditMode(true);
- const closeEditMode = () => setIsInEditMode(false);
- const [filename, setFilename] = useState();
- const [extension, setExtension] = useState();
-
- useEffect(() => {
- const [filename, extension] = nameAndExtension(file.metadata.title);
- setFilename(filename);
- setExtension(extension);
- }, [file]);
-
- const saveEdits = async (newFilename: string) => {
- try {
- if (file) {
- if (filename === newFilename) {
- closeEditMode();
- return;
- }
- setFilename(newFilename);
- const newTitle = getFileTitle(newFilename, extension);
- const updatedFile = await changeFileName(file, newTitle);
- updateExistingFilePubMetadata(file, updatedFile);
- scheduleUpdate();
- }
- } catch (e) {
- log.error("failed to update file name", e);
- throw e;
- }
- };
-
- return (
- <>
-
- ) : (
-
- )
- }
- title={getFileTitle(filename, extension)}
- caption={getCaption(file, parsedExifData)}
- openEditor={openEditMode}
- hideEditOption={shouldDisableEdits || isInEditMode}
- />
-
- >
- );
-}
diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
index f029d19952..8c1bb6321f 100644
--- a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
+++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
@@ -1,8 +1,14 @@
import { EnteDrawer } from "@/base/components/EnteDrawer";
import { Titlebar } from "@/base/components/Titlebar";
+import { nameAndExtension } from "@/base/file";
+import type { ParsedMetadata } from "@/media/file-metadata";
+import { FileType } from "@/media/file-type";
import { UnidentifiedFaces } from "@/new/photos/components/PeopleList";
+import { photoSwipeZIndex } from "@/new/photos/components/PhotoViewer";
+import { tagNumericValue, type RawExifTags } from "@/new/photos/services/exif";
import { isMLEnabled } from "@/new/photos/services/ml";
import { EnteFile } from "@/new/photos/types/file";
+import { formattedByteSize } from "@/new/photos/utils/units";
import CopyButton from "@ente/shared/components/CodeBlock/CopyButton";
import { FlexWrapper } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
@@ -11,72 +17,55 @@ import BackupOutlined from "@mui/icons-material/BackupOutlined";
import CameraOutlined from "@mui/icons-material/CameraOutlined";
import FolderOutlined from "@mui/icons-material/FolderOutlined";
import LocationOnOutlined from "@mui/icons-material/LocationOnOutlined";
+import PhotoOutlined from "@mui/icons-material/PhotoOutlined";
import TextSnippetOutlined from "@mui/icons-material/TextSnippetOutlined";
-import { Box, DialogProps, Link, Stack, styled } from "@mui/material";
+import VideocamOutlined from "@mui/icons-material/VideocamOutlined";
+import {
+ Box,
+ DialogProps,
+ Link,
+ Stack,
+ styled,
+ Typography,
+} from "@mui/material";
import { Chip } from "components/Chip";
import LinkButton from "components/pages/gallery/LinkButton";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { GalleryContext } from "pages/gallery";
-import { useContext, useEffect, useMemo, useState } from "react";
-import { getEXIFLocation } from "services/exif";
+import React, { useContext, useEffect, useMemo, useState } from "react";
+import { changeFileName, updateExistingFilePubMetadata } from "utils/file";
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
import {
getMapDisableConfirmationDialog,
getMapEnableConfirmationDialog,
} from "utils/ui";
-import { ExifData } from "./ExifData";
+import { FileNameEditDialog } from "./FileNameEditDialog";
import InfoItem from "./InfoItem";
import MapBox from "./MapBox";
import { RenderCaption } from "./RenderCaption";
import { RenderCreationTime } from "./RenderCreationTime";
-import { RenderFileName } from "./RenderFileName";
-export const FileInfoSidebar = styled((props: DialogProps) => (
-
-))({
- zIndex: 1501,
- "& .MuiPaper-root": {
- padding: 8,
- },
-});
+export interface FileInfoExif {
+ tags: RawExifTags | undefined;
+ parsed: ParsedMetadata | undefined;
+}
-interface Iprops {
- shouldDisableEdits?: boolean;
+interface FileInfoProps {
showInfo: boolean;
handleCloseInfo: () => void;
- file: EnteFile;
- exif: any;
+ closePhotoViewer: () => void;
+ file: EnteFile | undefined;
+ exif: FileInfoExif | undefined;
+ shouldDisableEdits?: boolean;
scheduleUpdate: () => void;
refreshPhotoswipe: () => void;
fileToCollectionsMap?: Map;
collectionNameMap?: Map;
showCollectionChips: boolean;
- closePhotoViewer: () => void;
}
-function BasicDeviceCamera({
- parsedExifData,
-}: {
- parsedExifData: Record;
-}) {
- return (
-
- {parsedExifData["fNumber"]}
- {parsedExifData["exposureTime"]}
- {parsedExifData["ISO"]}
-
- );
-}
-
-function getOpenStreetMapLink(location: {
- latitude: number;
- longitude: number;
-}) {
- return `https://www.openstreetmap.org/?mlat=${location.latitude}&mlon=${location.longitude}#map=15/${location.latitude}/${location.longitude}`;
-}
-
-export function FileInfo({
+export const FileInfo: React.FC = ({
shouldDisableEdits,
showInfo,
handleCloseInfo,
@@ -88,18 +77,16 @@ export function FileInfo({
collectionNameMap,
showCollectionChips,
closePhotoViewer,
-}: Iprops) {
- const appContext = useContext(AppContext);
+}) => {
+ const { mapEnabled, updateMapEnabled, setDialogBoxAttributesV2 } =
+ useContext(AppContext);
const galleryContext = useContext(GalleryContext);
const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext,
);
- const [parsedExifData, setParsedExifData] = useState>();
- const [showExif, setShowExif] = useState(false);
-
- const openExif = () => setShowExif(true);
- const closeExif = () => setShowExif(false);
+ const [exifInfo, setExifInfo] = useState();
+ const [openRawExif, setOpenRawExif] = useState(false);
const location = useMemo(() => {
if (file && file.metadata) {
@@ -113,60 +100,17 @@ export function FileInfo({
};
}
}
- if (exif) {
- const exifLocation = getEXIFLocation(exif);
- if (
- (exifLocation.latitude || exifLocation.latitude === 0) &&
- !(exifLocation.longitude === 0 && exifLocation.latitude === 0)
- ) {
- return exifLocation;
- }
- }
- return null;
+ return exif?.parsed?.location;
}, [file, exif]);
useEffect(() => {
- if (!exif) {
- setParsedExifData({});
- return;
- }
- const parsedExifData = {};
- if (exif["fNumber"]) {
- parsedExifData["fNumber"] = `f/${Math.ceil(exif["FNumber"])}`;
- } else if (exif["ApertureValue"] && exif["FocalLength"]) {
- parsedExifData["fNumber"] = `f/${Math.ceil(
- exif["FocalLength"] / exif["ApertureValue"],
- )}`;
- }
- const imageWidth = exif["ImageWidth"] ?? exif["ExifImageWidth"];
- const imageHeight = exif["ImageHeight"] ?? exif["ExifImageHeight"];
- if (imageWidth && imageHeight) {
- parsedExifData["resolution"] = `${imageWidth} x ${imageHeight}`;
- const megaPixels = Math.round((imageWidth * imageHeight) / 1000000);
- if (megaPixels) {
- parsedExifData["megaPixels"] = `${Math.round(
- (imageWidth * imageHeight) / 1000000,
- )}MP`;
- }
- }
- if (exif["Make"] && exif["Model"]) {
- parsedExifData["takenOnDevice"] =
- `${exif["Make"]} ${exif["Model"]}`;
- }
- if (exif["ExposureTime"]) {
- parsedExifData["exposureTime"] = `1/${
- 1 / parseFloat(exif["ExposureTime"])
- }`;
- }
- if (exif["ISO"]) {
- parsedExifData["ISO"] = `ISO${exif["ISO"]}`;
- }
- setParsedExifData(parsedExifData);
+ setExifInfo(parseExifInfo(exif));
}, [exif]);
if (!file) {
return <>>;
}
+
const onCollectionChipClick = (collectionID) => {
galleryContext.setActiveCollectionID(collectionID);
galleryContext.setIsInSearchMode(false);
@@ -174,17 +118,13 @@ export function FileInfo({
};
const openEnableMapConfirmationDialog = () =>
- appContext.setDialogBoxAttributesV2(
- getMapEnableConfirmationDialog(() =>
- appContext.updateMapEnabled(true),
- ),
+ setDialogBoxAttributesV2(
+ getMapEnableConfirmationDialog(() => updateMapEnabled(true)),
);
const openDisableMapConfirmationDialog = () =>
- appContext.setDialogBoxAttributesV2(
- getMapDisableConfirmationDialog(() =>
- appContext.updateMapEnabled(false),
- ),
+ setDialogBoxAttributesV2(
+ getMapDisableConfirmationDialog(() => updateMapEnabled(false)),
);
return (
@@ -192,32 +132,33 @@ export function FileInfo({
- {parsedExifData && parsedExifData["takenOnDevice"] && (
+
+ {exifInfo?.takenOnDevice && (
}
- title={parsedExifData["takenOnDevice"]}
+ title={exifInfo?.takenOnDevice}
caption={
-
+
}
hideEditOption
/>
@@ -229,11 +170,12 @@ export function FileInfo({
icon={}
title={t("LOCATION")}
caption={
- !appContext.mapEnabled ||
+ !mapEnabled ||
publicCollectionGalleryContext.accessedThroughSharedURL ? (
{t("SHOW_ON_MAP")}
@@ -264,7 +206,7 @@ export function FileInfo({
{!publicCollectionGalleryContext.accessedThroughSharedURL && (
}
title={t("DETAILS")}
caption={
- typeof exif === "undefined" ? (
-
- ) : exif !== null ? (
+ !exif ? (
+
+ ) : !exif.tags ? (
+ t("no_exif")
+ ) : (
setOpenRawExif(true)}
sx={{
textDecoration: "none",
color: "text.muted",
@@ -289,8 +233,6 @@ export function FileInfo({
>
{t("view_exif")}
- ) : (
- t("no_exif")
)
}
hideEditOption
@@ -336,13 +278,275 @@ export function FileInfo({
>
)}
- setOpenRawExif(false)}
onInfoClose={handleCloseInfo}
- filename={file.metadata.title}
+ tags={exif?.tags}
+ fileName={file.metadata.title}
/>
);
+};
+
+/**
+ * Some immediate fields of interest, in the form that we want to display on the
+ * info panel for a file.
+ */
+type ExifInfo = Required & {
+ resolution?: string;
+ megaPixels?: string;
+ takenOnDevice?: string;
+ fNumber?: string;
+ exposureTime?: string;
+ iso?: string;
+};
+
+const parseExifInfo = (
+ fileInfoExif: FileInfoExif | undefined,
+): ExifInfo | undefined => {
+ if (!fileInfoExif || !fileInfoExif.tags || !fileInfoExif.parsed)
+ return undefined;
+
+ const info: ExifInfo = { ...fileInfoExif };
+
+ const { width, height } = fileInfoExif.parsed;
+ if (width && height) {
+ info.resolution = `${width} x ${height}`;
+ const mp = Math.round((width * height) / 1000000);
+ if (mp) info.megaPixels = `${mp}MP`;
+ }
+
+ const { tags } = fileInfoExif;
+ const { exif } = tags;
+
+ if (exif) {
+ if (exif.Make && exif.Model)
+ info["takenOnDevice"] =
+ `${exif.Make.description} ${exif.Model.description}`;
+
+ if (exif.FNumber)
+ info.fNumber = exif.FNumber.description; /* e.g. "f/16" */
+
+ if (exif.ExposureTime)
+ info["exposureTime"] = exif.ExposureTime.description; /* "1/10" */
+
+ if (exif.ISOSpeedRatings)
+ info.iso = `ISO${tagNumericValue(exif.ISOSpeedRatings)}`;
+ }
+ return info;
+};
+
+const FileInfoSidebar = styled((props: DialogProps) => (
+
+))({
+ zIndex: photoSwipeZIndex + 1,
+ "& .MuiPaper-root": {
+ padding: 8,
+ },
+});
+
+interface RenderFileNameProps {
+ file: EnteFile;
+ shouldDisableEdits: boolean;
+ exifInfo: ExifInfo | undefined;
+ scheduleUpdate: () => void;
+}
+
+const RenderFileName: React.FC = ({
+ file,
+ shouldDisableEdits,
+ exifInfo,
+ scheduleUpdate,
+}) => {
+ const [isInEditMode, setIsInEditMode] = useState(false);
+ const openEditMode = () => setIsInEditMode(true);
+ const closeEditMode = () => setIsInEditMode(false);
+ const [fileName, setFileName] = useState();
+ const [extension, setExtension] = useState();
+
+ useEffect(() => {
+ const [filename, extension] = nameAndExtension(file.metadata.title);
+ setFileName(filename);
+ setExtension(extension);
+ }, [file]);
+
+ const saveEdits = async (newFilename: string) => {
+ if (!file) return;
+ if (fileName === newFilename) {
+ closeEditMode();
+ return;
+ }
+ setFileName(newFilename);
+ const newTitle = [newFilename, extension].join(".");
+ const updatedFile = await changeFileName(file, newTitle);
+ updateExistingFilePubMetadata(file, updatedFile);
+ scheduleUpdate();
+ };
+
+ return (
+ <>
+
+ ) : (
+
+ )
+ }
+ title={[fileName, extension].join(".")}
+ caption={getCaption(file, exifInfo)}
+ openEditor={openEditMode}
+ hideEditOption={shouldDisableEdits || isInEditMode}
+ />
+
+ >
+ );
+};
+
+const getCaption = (file: EnteFile, exifInfo: ExifInfo | undefined) => {
+ const megaPixels = exifInfo?.megaPixels;
+ const resolution = exifInfo?.resolution;
+ const fileSize = file.info?.fileSize;
+
+ const captionParts = [];
+ if (megaPixels) {
+ captionParts.push(megaPixels);
+ }
+ if (resolution) {
+ captionParts.push(resolution);
+ }
+ if (fileSize) {
+ captionParts.push(formattedByteSize(fileSize));
+ }
+ return (
+
+ {captionParts.map((caption) => (
+ {caption}
+ ))}
+
+ );
+};
+
+const BasicDeviceCamera: React.FC<{ parsedExif: ExifInfo }> = ({
+ parsedExif,
+}) => {
+ return (
+
+ {parsedExif.fNumber}
+ {parsedExif.exposureTime}
+ {parsedExif.iso}
+
+ );
+};
+
+const getOpenStreetMapLink = (location: {
+ latitude: number;
+ longitude: number;
+}) =>
+ `https://www.openstreetmap.org/?mlat=${location.latitude}&mlon=${location.longitude}#map=15/${location.latitude}/${location.longitude}`;
+
+interface RawExifProps {
+ open: boolean;
+ onClose: () => void;
+ onInfoClose: () => void;
+ tags: RawExifTags | undefined;
+ fileName: string;
}
+
+const RawExif: React.FC = ({
+ open,
+ onClose,
+ onInfoClose,
+ tags,
+ fileName,
+}) => {
+ if (!tags) {
+ return <>>;
+ }
+
+ const handleRootClose = () => {
+ onClose();
+ onInfoClose();
+ };
+
+ const items: (readonly [string, string, string, string])[] = Object.entries(
+ tags,
+ )
+ .map(([namespace, namespaceTags]) => {
+ return Object.entries(namespaceTags).map(([tagName, tag]) => {
+ const key = `${namespace}:${tagName}`;
+ let description = "<...>";
+ if (typeof tag == "string") {
+ description = tag;
+ } else if (typeof tag == "number") {
+ description = `${tag}`;
+ } else if (
+ tag &&
+ typeof tag == "object" &&
+ "description" in tag
+ ) {
+ description = tag.description;
+ }
+ return [key, namespace, tagName, description] as const;
+ });
+ })
+ .flat()
+ .filter(([, , , description]) => description);
+
+ return (
+
+
+ }
+ />
+
+ {items.map(([key, namespace, tagName, description]) => (
+
+
+
+ {tagName}
+
+
+ {namespace}
+
+
+
+ {description}
+
+
+ ))}
+
+
+ );
+};
+
+const ExifItem = styled(Box)`
+ padding-left: 8px;
+ padding-right: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+`;
diff --git a/web/apps/photos/src/components/PhotoViewer/index.tsx b/web/apps/photos/src/components/PhotoViewer/index.tsx
index 1c16a7a773..8e8a02d3bc 100644
--- a/web/apps/photos/src/components/PhotoViewer/index.tsx
+++ b/web/apps/photos/src/components/PhotoViewer/index.tsx
@@ -15,11 +15,12 @@ import {
import { isDesktop } from "@/base/app";
import { lowercaseExtension } from "@/base/file";
-import { FILE_TYPE } from "@/media/file-type";
+import { FileType } from "@/media/file-type";
import { isHEICExtension, needsJPEGConversion } from "@/media/formats";
import downloadManager from "@/new/photos/services/download";
+import { extractRawExif, parseExif } from "@/new/photos/services/exif";
import type { LoadedLivePhotoSourceURL } from "@/new/photos/types/file";
-import { detectFileTypeInfo } from "@/new/photos/utils/detect-type";
+import { fileLogID } from "@/new/photos/utils/file";
import { FlexWrapper } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import AlbumOutlined from "@mui/icons-material/AlbumOutlined";
@@ -46,14 +47,12 @@ import { t } from "i18next";
import isElectron from "is-electron";
import { AppContext } from "pages/_app";
import { GalleryContext } from "pages/gallery";
-import { getParsedExifData } from "services/exif";
import { trashFiles } from "services/fileService";
import { SetFilesDownloadProgressAttributesCreator } from "types/gallery";
-import { isClipboardItemPresent } from "utils/common";
import { pauseVideo, playVideo } from "utils/photoFrame";
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
import { getTrashFileMessage } from "utils/ui";
-import { FileInfo } from "./FileInfo";
+import { FileInfo, type FileInfoExif } from "./FileInfo";
import ImageEditorOverlay from "./ImageEditorOverlay";
import CircularProgressWithLabel from "./styledComponents/CircularProgressWithLabel";
import { ConversionFailedNotification } from "./styledComponents/ConversionFailedNotification";
@@ -108,10 +107,13 @@ function PhotoViewer(props: Iprops) {
useState>();
const [isFav, setIsFav] = useState(false);
const [showInfo, setShowInfo] = useState(false);
- const [exif, setExif] = useState<{
- key: string;
- value: Record;
- }>();
+ const [exif, setExif] = useState<
+ | {
+ key: string;
+ value: FileInfoExif | undefined;
+ }
+ | undefined
+ >();
const exifCopy = useRef(null);
const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState(
defaultLivePhotoDefaultOptions,
@@ -123,7 +125,10 @@ function PhotoViewer(props: Iprops) {
const needUpdate = useRef(false);
const exifExtractionInProgress = useRef(null);
- const shouldShowCopyOption = useMemo(() => isClipboardItemPresent(), []);
+ const shouldShowCopyOption = useMemo(
+ () => typeof ClipboardItem != "undefined",
+ [],
+ );
const [showImageEditorOverlay, setShowImageEditorOverlay] = useState(false);
@@ -235,7 +240,7 @@ function PhotoViewer(props: Iprops) {
if (!isOpen) return;
const item = items[photoSwipe?.getCurrentIndex()];
if (!item) return;
- if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
+ if (item.metadata.fileType === FileType.livePhoto) {
const getVideoAndImage = () => {
const video = document.getElementById(
`live-photo-video-${item.id}`,
@@ -306,25 +311,24 @@ function PhotoViewer(props: Iprops) {
}
function updateExif(file: EnteFile) {
- if (file.metadata.fileType === FILE_TYPE.VIDEO) {
- setExif({ key: file.src, value: null });
+ if (file.metadata.fileType === FileType.video) {
+ setExif({
+ key: file.src,
+ value: { tags: undefined, parsed: undefined },
+ });
return;
}
- if (!file.isSourceLoaded || file.conversionFailed) {
+ if (!file || !file.isSourceLoaded || file.conversionFailed) {
return;
}
- if (!file || !exifCopy?.current?.value === null) {
- return;
- }
const key =
- file.metadata.fileType === FILE_TYPE.IMAGE
+ file.metadata.fileType === FileType.image
? file.src
: (file.srcURLs.url as LoadedLivePhotoSourceURL).image;
- if (exifCopy?.current?.key === key) {
- return;
- }
+ if (exifCopy?.current?.key === key) return;
+
setExif({ key, value: undefined });
checkExifAvailable(file);
}
@@ -332,8 +336,8 @@ function PhotoViewer(props: Iprops) {
function updateShowConvertBtn(file: EnteFile) {
const shouldShowConvertBtn =
isElectron() &&
- (file.metadata.fileType === FILE_TYPE.VIDEO ||
- file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) &&
+ (file.metadata.fileType === FileType.video ||
+ file.metadata.fileType === FileType.livePhoto) &&
!file.isConverted &&
file.isSourceLoaded &&
!file.conversionFailed;
@@ -361,12 +365,12 @@ function PhotoViewer(props: Iprops) {
}
}
setShowEditButton(
- file.metadata.fileType === FILE_TYPE.IMAGE && isSupported,
+ file.metadata.fileType === FileType.image && isSupported,
);
}
function updateShowZoomButton(file: EnteFile) {
- setShowZoomButton(file.metadata.fileType === FILE_TYPE.IMAGE);
+ setShowZoomButton(file.metadata.fileType === FileType.image);
}
const openPhotoSwipe = () => {
@@ -585,45 +589,30 @@ function PhotoViewer(props: Iprops) {
}
};
- const checkExifAvailable = async (file: EnteFile) => {
+ const checkExifAvailable = async (enteFile: EnteFile) => {
+ if (exifExtractionInProgress.current === enteFile.src) return;
+
try {
- if (exifExtractionInProgress.current === file.src) {
- return;
- }
- try {
- exifExtractionInProgress.current = file.src;
- let fileObject: File;
- if (file.metadata.fileType === FILE_TYPE.IMAGE) {
- fileObject = await getFileFromURL(
- file.src as string,
- file.metadata.title,
- );
- } else {
- const url = (file.srcURLs.url as LoadedLivePhotoSourceURL)
- .image;
- fileObject = await getFileFromURL(url, file.metadata.title);
- }
- const fileTypeInfo = await detectFileTypeInfo(fileObject);
- const exifData = await getParsedExifData(
- fileObject,
- fileTypeInfo,
- );
- if (exifExtractionInProgress.current === file.src) {
- if (exifData) {
- setExif({ key: file.src, value: exifData });
- } else {
- setExif({ key: file.src, value: null });
- }
- }
- } finally {
- exifExtractionInProgress.current = null;
+ exifExtractionInProgress.current = enteFile.src;
+ const file = await getFileFromURL(
+ enteFile.metadata.fileType === FileType.image
+ ? (enteFile.src as string)
+ : (enteFile.srcURLs.url as LoadedLivePhotoSourceURL).image,
+ enteFile.metadata.title,
+ );
+ const tags = await extractRawExif(file);
+ const parsed = parseExif(tags);
+ if (exifExtractionInProgress.current === enteFile.src) {
+ setExif({ key: enteFile.src, value: { tags, parsed } });
}
} catch (e) {
- setExif({ key: file.src, value: null });
- log.error(
- `checkExifAvailable failed for file ${file.metadata.title}`,
- e,
- );
+ log.error(`Failed to extract Exif from ${fileLogID(enteFile)}`, e);
+ setExif({
+ key: enteFile.src,
+ value: { tags: undefined, parsed: undefined },
+ });
+ } finally {
+ exifExtractionInProgress.current = null;
}
};
@@ -941,21 +930,21 @@ function PhotoViewer(props: Iprops) {