Parse wip
This commit is contained in:
@@ -1,94 +0,0 @@
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import CopyButton from "@ente/shared/components/CodeBlock/CopyButton";
|
||||
import { formatDateTimeFull } from "@ente/shared/time/format";
|
||||
import { Box, Stack, styled, Typography } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import React from "react";
|
||||
import { FileInfoSidebar } from ".";
|
||||
|
||||
const ExifItem = styled(Box)`
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
function parseExifValue(value: any) {
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
case "number":
|
||||
return value;
|
||||
default:
|
||||
if (value instanceof Date) {
|
||||
return formatDateTimeFull(value);
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(Array.from(value));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
export function ExifData(props: {
|
||||
exif: any;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
filename: string;
|
||||
onInfoClose: () => void;
|
||||
}) {
|
||||
const { exif, open, onClose, filename, onInfoClose } = props;
|
||||
|
||||
if (!exif) {
|
||||
return <></>;
|
||||
}
|
||||
const handleRootClose = () => {
|
||||
onClose();
|
||||
onInfoClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<FileInfoSidebar open={open} onClose={onClose}>
|
||||
<Titlebar
|
||||
onClose={onClose}
|
||||
title={t("exif")}
|
||||
caption={filename}
|
||||
onRootClose={handleRootClose}
|
||||
actionButton={
|
||||
<CopyButton
|
||||
code={JSON.stringify(exif)}
|
||||
color={"secondary"}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Stack py={3} px={1} spacing={2}>
|
||||
{[...Object.entries(exif)]
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([key, value]) =>
|
||||
value ? (
|
||||
<ExifItem key={key}>
|
||||
<Typography
|
||||
variant="small"
|
||||
color={"text.muted"}
|
||||
>
|
||||
{key}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
width: "100%",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{parseExifValue(value)}
|
||||
</Typography>
|
||||
</ExifItem>
|
||||
) : (
|
||||
<React.Fragment key={key}></React.Fragment>
|
||||
),
|
||||
)}
|
||||
</Stack>
|
||||
</FileInfoSidebar>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<FlexWrapper gap={1}>
|
||||
{captionParts.map((caption) => (
|
||||
<Box key={caption}> {caption}</Box>
|
||||
))}
|
||||
</FlexWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export function RenderFileName({
|
||||
parsedExifData,
|
||||
shouldDisableEdits,
|
||||
file,
|
||||
scheduleUpdate,
|
||||
}: {
|
||||
parsedExifData: Record<string, any>;
|
||||
shouldDisableEdits: boolean;
|
||||
file: EnteFile;
|
||||
scheduleUpdate: () => void;
|
||||
}) {
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
const openEditMode = () => setIsInEditMode(true);
|
||||
const closeEditMode = () => setIsInEditMode(false);
|
||||
const [filename, setFilename] = useState<string>();
|
||||
const [extension, setExtension] = useState<string>();
|
||||
|
||||
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 (
|
||||
<>
|
||||
<InfoItem
|
||||
icon={
|
||||
file.metadata.fileType === FILE_TYPE.VIDEO ? (
|
||||
<VideocamOutlined />
|
||||
) : (
|
||||
<PhotoOutlined />
|
||||
)
|
||||
}
|
||||
title={getFileTitle(filename, extension)}
|
||||
caption={getCaption(file, parsedExifData)}
|
||||
openEditor={openEditMode}
|
||||
hideEditOption={shouldDisableEdits || isInEditMode}
|
||||
/>
|
||||
<FileNameEditDialog
|
||||
isInEditMode={isInEditMode}
|
||||
closeEditMode={closeEditMode}
|
||||
filename={filename}
|
||||
extension={extension}
|
||||
saveEdits={saveEdits}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import { nameAndExtension } from "@/base/file";
|
||||
import log from "@/base/log";
|
||||
import { FILE_TYPE } from "@/media/file-type";
|
||||
import { UnidentifiedFaces } from "@/new/photos/components/PeopleList";
|
||||
import type { RawExifTags } from "@/new/photos/services/exif";
|
||||
import type { ParsedExif, 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";
|
||||
@@ -12,7 +16,9 @@ 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 VideocamOutlined from "@mui/icons-material/VideocamOutlined";
|
||||
import { Box, DialogProps, Link, Stack, styled } from "@mui/material";
|
||||
import { Chip } from "components/Chip";
|
||||
import LinkButton from "components/pages/gallery/LinkButton";
|
||||
@@ -20,33 +26,29 @@ import { t } from "i18next";
|
||||
import { AppContext } from "pages/_app";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
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) => (
|
||||
<EnteDrawer {...props} anchor="right" />
|
||||
))({
|
||||
zIndex: 1501,
|
||||
"& .MuiPaper-root": {
|
||||
padding: 8,
|
||||
},
|
||||
});
|
||||
|
||||
export interface FileInfoExif {
|
||||
tags: RawExifTags;
|
||||
parsed: ParsedExif;
|
||||
}
|
||||
interface FileInfoProps {
|
||||
shouldDisableEdits?: boolean;
|
||||
showInfo: boolean;
|
||||
handleCloseInfo: () => void;
|
||||
file: EnteFile;
|
||||
rawExif: RawExifTags | undefined;
|
||||
exif: FileInfoExif | undefined;
|
||||
scheduleUpdate: () => void;
|
||||
refreshPhotoswipe: () => void;
|
||||
fileToCollectionsMap?: Map<number, number[]>;
|
||||
@@ -55,33 +57,12 @@ interface FileInfoProps {
|
||||
closePhotoViewer: () => void;
|
||||
}
|
||||
|
||||
function BasicDeviceCamera({
|
||||
parsedExifData,
|
||||
}: {
|
||||
parsedExifData: Record<string, any>;
|
||||
}) {
|
||||
return (
|
||||
<FlexWrapper gap={1}>
|
||||
<Box>{parsedExifData["fNumber"]}</Box>
|
||||
<Box>{parsedExifData["exposureTime"]}</Box>
|
||||
<Box>{parsedExifData["ISO"]}</Box>
|
||||
</FlexWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
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 const FileInfo: React.FC<FileInfoProps> = ({
|
||||
shouldDisableEdits,
|
||||
showInfo,
|
||||
handleCloseInfo,
|
||||
file,
|
||||
rawExif,
|
||||
exif,
|
||||
scheduleUpdate,
|
||||
refreshPhotoswipe,
|
||||
fileToCollectionsMap,
|
||||
@@ -95,11 +76,10 @@ export const FileInfo: React.FC<FileInfoProps> = ({
|
||||
PublicCollectionGalleryContext,
|
||||
);
|
||||
|
||||
const [parsedExifData, setParsedExifData] = useState<Record<string, any>>();
|
||||
const [showExif, setShowExif] = useState(false);
|
||||
|
||||
const openExif = () => setShowExif(true);
|
||||
const closeExif = () => setShowExif(false);
|
||||
const [parsedExif, setParsedExif] = useState<
|
||||
ParsedFileInfoExif | undefined
|
||||
>();
|
||||
const [openRawExif, setOpenRawExif] = useState(false);
|
||||
|
||||
const location = useMemo(() => {
|
||||
if (file && file.metadata) {
|
||||
@@ -113,48 +93,12 @@ export const FileInfo: React.FC<FileInfoProps> = ({
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return exif.parsed.location;
|
||||
}, [file]);
|
||||
|
||||
useEffect(() => {
|
||||
setParsedExifData(parseInfoExif(rawExif));
|
||||
|
||||
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"]}`;
|
||||
}
|
||||
}, [rawExif]);
|
||||
setParsedExif(exif ? parseFileInfoExif(exif) : undefined);
|
||||
}, [exif]);
|
||||
|
||||
if (!file) {
|
||||
return <></>;
|
||||
@@ -272,7 +216,7 @@ export const FileInfo: React.FC<FileInfoProps> = ({
|
||||
<EnteSpinner size={11.33} />
|
||||
) : exif !== null ? (
|
||||
<LinkButton
|
||||
onClick={openExif}
|
||||
onClick={() => setOpenRawExif(true)}
|
||||
sx={{
|
||||
textDecoration: "none",
|
||||
color: "text.muted",
|
||||
@@ -329,9 +273,9 @@ export const FileInfo: React.FC<FileInfoProps> = ({
|
||||
)}
|
||||
</Stack>
|
||||
<ExifData
|
||||
exif={exif}
|
||||
open={showExif}
|
||||
onClose={closeExif}
|
||||
exif={exif.tags}
|
||||
open={openRawExif}
|
||||
onClose={() => setOpenRawExif(false)}
|
||||
onInfoClose={handleCloseInfo}
|
||||
filename={file.metadata.title}
|
||||
/>
|
||||
@@ -339,54 +283,276 @@ export const FileInfo: React.FC<FileInfoProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface ParsedInfoExif {
|
||||
/**
|
||||
* Some immediate fields of interest, in the form that we want to display on the
|
||||
* info panel for a file.
|
||||
*/
|
||||
type ParsedFileInfoExif = FileInfoExif & {
|
||||
resolution?: string;
|
||||
megaPixels?: string;
|
||||
takenOnDevice?: string;
|
||||
fNumber?: string;
|
||||
exposureTime?: string;
|
||||
iso?: string;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract some immediate fields of interest and in the form that we want to
|
||||
* display on the info panel for a file.
|
||||
*/
|
||||
const parseInfoExif = (rawExif: RawExifTags | undefined): ParsedInfoExif => {
|
||||
const parsed = {};
|
||||
if (!rawExif) return {};
|
||||
const parseFileInfoExif = (fileInfoExif: FileInfoExif): ParsedFileInfoExif => {
|
||||
const parsed: ParsedFileInfoExif = { ...fileInfoExif };
|
||||
|
||||
|
||||
if (rawExif.exif)
|
||||
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 { width, height } = fileInfoExif.parsed;
|
||||
if (width && height) {
|
||||
parsed.resolution = `${width} x ${height}`;
|
||||
const mp = Math.round((width * height) / 1000000);
|
||||
if (mp) parsed.megaPixels = `${mp}MP`;
|
||||
}
|
||||
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`;
|
||||
|
||||
const { tags } = fileInfoExif;
|
||||
const { exif } = tags;
|
||||
|
||||
if (exif) {
|
||||
if (exif.Make && exif.Model) {
|
||||
parsed["takenOnDevice"] =
|
||||
`${exif.Make.description} ${exif.Model.description}`;
|
||||
}
|
||||
|
||||
if (exif.FNumber) {
|
||||
parsed.fNumber = `f/${Math.ceil(exif.FNumber.value)}`;
|
||||
} else if (exif.FocalLength && exif.ApertureValue) {
|
||||
parsed.fNumber = `f/${Math.ceil(
|
||||
exif.FocalLength.value / exif.ApertureValue.value,
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (exif.ExposureTime) {
|
||||
parsed["exposureTime"] = `1/${1 / exif.ExposureTime.value}`;
|
||||
}
|
||||
|
||||
if (exif.ISOSpeedRatings) {
|
||||
const iso = exif.ISOSpeedRatings;
|
||||
const n = Array.isArray(iso) ? (iso[0] ?? 0) / (iso[1] ?? 1) : iso;
|
||||
parsed.iso = `ISO${n}`;
|
||||
}
|
||||
}
|
||||
if (exif["Make"] && exif["Model"]) {
|
||||
parsedExifData["takenOnDevice"] =
|
||||
`${exif["Make"]} ${exif["Model"]}`;
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const FileInfoSidebar = styled((props: DialogProps) => (
|
||||
<EnteDrawer {...props} anchor="right" />
|
||||
))({
|
||||
zIndex: 1501,
|
||||
"& .MuiPaper-root": {
|
||||
padding: 8,
|
||||
},
|
||||
});
|
||||
|
||||
function RenderFileName({
|
||||
parsedExifData,
|
||||
shouldDisableEdits,
|
||||
file,
|
||||
scheduleUpdate,
|
||||
}: {
|
||||
parsedExifData: Record<string, any>;
|
||||
shouldDisableEdits: boolean;
|
||||
file: EnteFile;
|
||||
scheduleUpdate: () => void;
|
||||
}) {
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
const openEditMode = () => setIsInEditMode(true);
|
||||
const closeEditMode = () => setIsInEditMode(false);
|
||||
const [filename, setFilename] = useState<string>();
|
||||
const [extension, setExtension] = useState<string>();
|
||||
|
||||
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 (
|
||||
<>
|
||||
<InfoItem
|
||||
icon={
|
||||
file.metadata.fileType === FILE_TYPE.VIDEO ? (
|
||||
<VideocamOutlined />
|
||||
) : (
|
||||
<PhotoOutlined />
|
||||
)
|
||||
}
|
||||
title={getFileTitle(filename, extension)}
|
||||
caption={getCaption(file, parsedExifData)}
|
||||
openEditor={openEditMode}
|
||||
hideEditOption={shouldDisableEdits || isInEditMode}
|
||||
/>
|
||||
<FileNameEditDialog
|
||||
isInEditMode={isInEditMode}
|
||||
closeEditMode={closeEditMode}
|
||||
filename={filename}
|
||||
extension={extension}
|
||||
saveEdits={saveEdits}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const getFileTitle = (filename, extension) => {
|
||||
if (extension) {
|
||||
return filename + "." + extension;
|
||||
} else {
|
||||
return filename;
|
||||
}
|
||||
if (exif["ExposureTime"]) {
|
||||
parsedExifData["exposureTime"] = `1/${
|
||||
1 / parseFloat(exif["ExposureTime"])
|
||||
}`;
|
||||
};
|
||||
|
||||
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 (exif["ISO"]) {
|
||||
parsedExifData["ISO"] = `ISO${exif["ISO"]}`;
|
||||
if (resolution) {
|
||||
captionParts.push(resolution);
|
||||
}
|
||||
setParsedExifData(parsedExifData);
|
||||
}, [exif]);
|
||||
if (fileSize) {
|
||||
captionParts.push(formattedByteSize(fileSize));
|
||||
}
|
||||
return (
|
||||
<FlexWrapper gap={1}>
|
||||
{captionParts.map((caption) => (
|
||||
<Box key={caption}> {caption}</Box>
|
||||
))}
|
||||
</FlexWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
function BasicDeviceCamera({
|
||||
parsedExifData,
|
||||
}: {
|
||||
parsedExifData: Record<string, any>;
|
||||
}) {
|
||||
return (
|
||||
<FlexWrapper gap={1}>
|
||||
<Box>{parsedExifData["fNumber"]}</Box>
|
||||
<Box>{parsedExifData["exposureTime"]}</Box>
|
||||
<Box>{parsedExifData["ISO"]}</Box>
|
||||
</FlexWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function getOpenStreetMapLink(location: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}) {
|
||||
return `https://www.openstreetmap.org/?mlat=${location.latitude}&mlon=${location.longitude}#map=15/${location.latitude}/${location.longitude}`;
|
||||
}
|
||||
|
||||
import { formatDateTimeFull } from "@ente/shared/time/format";
|
||||
import { Typography } from "@mui/material";
|
||||
import { FileInfoSidebar } from ".";
|
||||
|
||||
const ExifItem = styled(Box)`
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
function parseExifValue(value: any) {
|
||||
switch (typeof value) {
|
||||
case "string":
|
||||
case "number":
|
||||
return value;
|
||||
default:
|
||||
if (value instanceof Date) {
|
||||
return formatDateTimeFull(value);
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(Array.from(value));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
export function ExifData(props: {
|
||||
exif: any;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
filename: string;
|
||||
onInfoClose: () => void;
|
||||
}) {
|
||||
const { exif, open, onClose, filename, onInfoClose } = props;
|
||||
|
||||
if (!exif) {
|
||||
return <></>;
|
||||
}
|
||||
const handleRootClose = () => {
|
||||
onClose();
|
||||
onInfoClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<FileInfoSidebar open={open} onClose={onClose}>
|
||||
<Titlebar
|
||||
onClose={onClose}
|
||||
title={t("exif")}
|
||||
caption={filename}
|
||||
onRootClose={handleRootClose}
|
||||
actionButton={
|
||||
<CopyButton
|
||||
code={JSON.stringify(exif)}
|
||||
color={"secondary"}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Stack py={3} px={1} spacing={2}>
|
||||
{[...Object.entries(exif)]
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([key, value]) =>
|
||||
value ? (
|
||||
<ExifItem key={key}>
|
||||
<Typography
|
||||
variant="small"
|
||||
color={"text.muted"}
|
||||
>
|
||||
{key}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
width: "100%",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{parseExifValue(value)}
|
||||
</Typography>
|
||||
</ExifItem>
|
||||
) : (
|
||||
<React.Fragment key={key}></React.Fragment>
|
||||
),
|
||||
)}
|
||||
</Stack>
|
||||
</FileInfoSidebar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { lowercaseExtension } from "@/base/file";
|
||||
import { FILE_TYPE } from "@/media/file-type";
|
||||
import { isHEICExtension, needsJPEGConversion } from "@/media/formats";
|
||||
import downloadManager from "@/new/photos/services/download";
|
||||
import { extractRawExif, type RawExifTags } from "@/new/photos/services/exif";
|
||||
import { extractRawExif, parseExif } from "@/new/photos/services/exif";
|
||||
import type { LoadedLivePhotoSourceURL } from "@/new/photos/types/file";
|
||||
import { FlexWrapper } from "@ente/shared/components/Container";
|
||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||
@@ -52,7 +52,7 @@ 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";
|
||||
@@ -107,11 +107,11 @@ function PhotoViewer(props: Iprops) {
|
||||
useState<Photoswipe<Photoswipe.Options>>();
|
||||
const [isFav, setIsFav] = useState(false);
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
const [rawExif, setRawExif] = useState<{
|
||||
const [exif, setExif] = useState<{
|
||||
key: string;
|
||||
value: RawExifTags;
|
||||
value: FileInfoExif | undefined;
|
||||
}>();
|
||||
const rawExifCopy = useRef(null);
|
||||
const exifCopy = useRef(null);
|
||||
const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState(
|
||||
defaultLivePhotoDefaultOptions,
|
||||
);
|
||||
@@ -290,8 +290,8 @@ function PhotoViewer(props: Iprops) {
|
||||
}, [photoSwipe?.currItem, isOpen, isSourceLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
rawExifCopy.current = rawExif;
|
||||
}, [rawExif]);
|
||||
exifCopy.current = exif;
|
||||
}, [exif]);
|
||||
|
||||
function updateFavButton(file: EnteFile) {
|
||||
setIsFav(isInFav(file));
|
||||
@@ -306,14 +306,14 @@ function PhotoViewer(props: Iprops) {
|
||||
|
||||
function updateExif(file: EnteFile) {
|
||||
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
|
||||
setRawExif({ key: file.src, value: null });
|
||||
setExif({ key: file.src, value: undefined });
|
||||
return;
|
||||
}
|
||||
if (!file.isSourceLoaded || file.conversionFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file || !rawExifCopy?.current?.value === null) {
|
||||
if (!file || !exifCopy?.current?.value) {
|
||||
return;
|
||||
}
|
||||
const key =
|
||||
@@ -321,10 +321,10 @@ function PhotoViewer(props: Iprops) {
|
||||
? file.src
|
||||
: (file.srcURLs.url as LoadedLivePhotoSourceURL).image;
|
||||
|
||||
if (rawExifCopy?.current?.key === key) {
|
||||
if (exifCopy?.current?.key === key) {
|
||||
return;
|
||||
}
|
||||
setRawExif({ key, value: undefined });
|
||||
setExif({ key, value: undefined });
|
||||
checkExifAvailable(file);
|
||||
}
|
||||
|
||||
@@ -584,40 +584,32 @@ function PhotoViewer(props: Iprops) {
|
||||
}
|
||||
};
|
||||
|
||||
const checkExifAvailable = async (file: EnteFile) => {
|
||||
const checkExifAvailable = async (enteFile: EnteFile) => {
|
||||
try {
|
||||
if (exifExtractionInProgress.current === file.src) {
|
||||
if (exifExtractionInProgress.current === enteFile.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 rawExif = await extractRawExif(fileObject);
|
||||
// TODO: Exif
|
||||
// if (await wipNewLib()) {
|
||||
// const newLib = await extractExif(fileObject);
|
||||
// cmpNewLib(file.metadata, newLib);
|
||||
// }
|
||||
if (exifExtractionInProgress.current === file.src) {
|
||||
setRawExif({ key: file.src, value: rawExif });
|
||||
exifExtractionInProgress.current = enteFile.src;
|
||||
const file = await getFileFromURL(
|
||||
enteFile.metadata.fileType === FILE_TYPE.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 } });
|
||||
}
|
||||
} finally {
|
||||
exifExtractionInProgress.current = null;
|
||||
}
|
||||
} catch (e) {
|
||||
setRawExif({ key: file.src, value: null });
|
||||
setExif({ key: enteFile.src, value: undefined });
|
||||
log.error(
|
||||
`checkExifAvailable failed for file ${file.metadata.title}`,
|
||||
`checkExifAvailable failed for file ${enteFile.metadata.title}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
@@ -946,7 +938,7 @@ function PhotoViewer(props: Iprops) {
|
||||
showInfo={showInfo}
|
||||
handleCloseInfo={handleCloseInfo}
|
||||
file={photoSwipe?.currItem as EnteFile}
|
||||
rawExif={rawExif?.value}
|
||||
exif={exif?.value}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
refreshPhotoswipe={refreshPhotoswipe}
|
||||
fileToCollectionsMap={props.fileToCollectionsMap}
|
||||
|
||||
@@ -34,7 +34,7 @@ export const cmpNewLib = (
|
||||
* be attached to an {@link EnteFile} allows us to perform operations using
|
||||
* these attributes without needing to re-download the original image.
|
||||
*/
|
||||
interface ParsedExif {
|
||||
export interface ParsedExif {
|
||||
/** The width of the image, in pixels. */
|
||||
width?: number;
|
||||
/** The height of the image, in pixels. */
|
||||
|
||||
Reference in New Issue
Block a user