[web] Code reorg (#5109)

Intermezzo between the photoswipe changes.
This commit is contained in:
Manav Rathi 2025-02-19 10:08:46 +05:30 committed by GitHub
commit bc0980eb8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 230 additions and 268 deletions

View File

@ -1,2 +1 @@
thirdparty/
public/

View File

@ -1,26 +1,20 @@
[
{
"relation": [
"delegate_permission/common.get_login_creds"
],
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "photos-web",
"site": "https://web.ente.io"
}
},
{
"relation": [
"delegate_permission/common.get_login_creds"
],
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "auth-web",
"site": "https://auth.ente.io"
}
},
{
"relation": [
"delegate_permission/common.get_login_creds"
],
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "io.ente.photos.independent",
@ -30,9 +24,7 @@
}
},
{
"relation": [
"delegate_permission/common.get_login_creds"
],
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "io.ente.photos",
@ -42,9 +34,7 @@
}
},
{
"relation": [
"delegate_permission/common.get_login_creds"
],
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "io.ente.auth",

View File

@ -9,14 +9,8 @@
"@/media": "*",
"@/new": "*",
"@ente/shared": "*",
"@stripe/stripe-js": "^1.13.2",
"bip39": "^3.0.4",
"chrono-node": "^2.7.8",
"debounce": "^2.2.0",
"exifreader": "^4.26.1",
"fast-srp-hap": "^2.0.4",
"leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility": "^0.1.2",
"localforage": "^1.9.0",
"memoize-one": "^6.0.0",
"ml-matrix": "^6.12.0",
@ -24,7 +18,6 @@
"photoswipe": "file:./thirdparty/photoswipe",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-dropzone": "14.2.10",
"react-select": "^5.10.0",
"react-top-loading-bar": "^3.0.2",
"react-virtualized-auto-sizer": "^1.0.25",
@ -35,7 +28,6 @@
},
"devDependencies": {
"@/build-config": "*",
"@types/leaflet": "^1.9.16",
"@types/node": "^20",
"@types/photoswipe": "^4.1.1",
"@types/react": "^19.0.8",

View File

@ -1,26 +1,20 @@
[
{
"relation": [
"delegate_permission/common.get_login_creds"
],
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "photos-web",
"site": "https://web.ente.io"
}
},
{
"relation": [
"delegate_permission/common.get_login_creds"
],
{
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "auth-web",
"site": "https://auth.ente.io"
}
},
{
"relation": [
"delegate_permission/common.get_login_creds"
],
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "io.ente.photos.independent",
@ -30,9 +24,7 @@
}
},
{
"relation": [
"delegate_permission/common.get_login_creds"
],
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "io.ente.photos",
@ -44,9 +36,9 @@
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "io.ente.photos",
"sha256_cert_fingerprints": [
"namespace": "android_app",
"package_name": "io.ente.photos",
"sha256_cert_fingerprints": [
"37:D4:0B:10:3B:BF:86:43:EB:AE:23:B3:BB:73:F8:65:B4:E9:3A:BF:65:45:EF:37:12:8A:4C:EA:5B:C2:7E:2E"
]
}
@ -54,17 +46,15 @@
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "io.ente.photos.independent",
"sha256_cert_fingerprints": [
"namespace": "android_app",
"package_name": "io.ente.photos.independent",
"sha256_cert_fingerprints": [
"37:D4:0B:10:3B:BF:86:43:EB:AE:23:B3:BB:73:F8:65:B4:E9:3A:BF:65:45:EF:37:12:8A:4C:EA:5B:C2:7E:2E"
]
}
},
{
"relation": [
"delegate_permission/common.get_login_creds"
],
"relation": ["delegate_permission/common.get_login_creds"],
"target": {
"namespace": "android_app",
"package_name": "io.ente.auth",

View File

@ -42,13 +42,13 @@ import ExportInProgress from "./ExportInProgress";
import ExportInit from "./ExportInit";
type ExportProps = ModalVisibilityProps & {
collectionNameMap: Map<number, string>;
allCollectionsNameByID: Map<number, string>;
};
export const Export: React.FC<ExportProps> = ({
open,
onClose,
collectionNameMap,
allCollectionsNameByID,
}) => {
const { showMiniDialog } = useBaseContext();
const [exportStage, setExportStage] = useState(ExportStage.INIT);
@ -202,7 +202,7 @@ export const Export: React.FC<ExportProps> = ({
lastExportTime={lastExportTime}
exportProgress={exportProgress}
pendingExports={pendingExports}
collectionNameMap={collectionNameMap}
allCollectionsNameByID={allCollectionsNameByID}
/>
</Dialog>
);
@ -291,7 +291,7 @@ const ExportDynamicContent = ({
lastExportTime,
exportProgress,
pendingExports,
collectionNameMap,
allCollectionsNameByID,
}: {
exportStage: ExportStage;
startExport: (opts?: ExportOpts) => void;
@ -300,7 +300,7 @@ const ExportDynamicContent = ({
lastExportTime: number;
exportProgress: ExportProgress;
pendingExports: EnteFile[];
collectionNameMap: Map<number, string>;
allCollectionsNameByID: Map<number, string>;
}) => {
switch (exportStage) {
case ExportStage.INIT:
@ -326,7 +326,7 @@ const ExportDynamicContent = ({
onHide={onHide}
lastExportTime={lastExportTime}
pendingExports={pendingExports}
collectionNameMap={collectionNameMap}
allCollectionsNameByID={allCollectionsNameByID}
onResync={() => startExport({ resync: true })}
/>
);

View File

@ -11,7 +11,7 @@ import ExportPendingList from "./ExportPendingList";
interface Props {
pendingExports: EnteFile[];
collectionNameMap: Map<number, string>;
allCollectionsNameByID: Map<number, string>;
onHide: () => void;
lastExportTime: number;
/** Called when the user presses the "Resync" button. */
@ -73,7 +73,7 @@ export default function ExportFinished(props: Props) {
</DialogActions>
<ExportPendingList
pendingExports={props.pendingExports}
collectionNameMap={props.collectionNameMap}
allCollectionsNameByID={props.allCollectionsNameByID}
isOpen={pendingFileListView}
onClose={closePendingFileList}
/>

View File

@ -10,7 +10,7 @@ import { t } from "i18next";
interface Iprops {
isOpen: boolean;
onClose: () => void;
collectionNameMap: Map<number, string>;
allCollectionsNameByID: Map<number, string>;
pendingExports: EnteFile[];
}
@ -36,7 +36,7 @@ const ExportPendingList = (props: Iprops) => {
/>
</Box>
<ItemContainer>
{`${props.collectionNameMap.get(file.collectionID)} / ${
{`${props.allCollectionsNameByID.get(file.collectionID)} / ${
file.metadata.title
}`}
</ItemContainer>
@ -45,7 +45,7 @@ const ExportPendingList = (props: Iprops) => {
};
const getItemTitle = (file: EnteFile) => {
return `${props.collectionNameMap.get(file.collectionID)} / ${
return `${props.allCollectionsNameByID.get(file.collectionID)} / ${
file.metadata.title
}`;
};

View File

@ -2,6 +2,7 @@ import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton";
import type { ModalVisibilityProps } from "@/base/components/utils/modal";
import log from "@/base/log";
import { downloadManager } from "@/gallery/services/download";
import { extractExifDates } from "@/gallery/services/exif";
import { fileLogID, type EnteFile } from "@/media/file";
import {
decryptPublicMagicMetadata,
@ -11,7 +12,6 @@ import {
} from "@/media/file-metadata";
import { FileType } from "@/media/file-type";
import { PhotoDateTimePicker } from "@/new/photos/components/PhotoDateTimePicker";
import { extractExifDates } from "@/new/photos/services/exif";
import {
Dialog,
DialogContent,

View File

@ -112,8 +112,8 @@ export interface PhotoFrameProps {
/** This will be set if mode is "people". */
activePersonID?: string | undefined;
enableDownload?: boolean;
fileToCollectionsMap?: Map<number, number[]>;
collectionNameMap?: Map<number, string>;
fileCollectionIDs?: Map<number, number[]>;
allCollectionsNameByID?: Map<number, string>;
showAppDownloadBanner?: boolean;
setIsPhotoSwipeOpen?: (value: boolean) => void;
isInHiddenSection?: boolean;
@ -138,8 +138,8 @@ const PhotoFrame = ({
activeCollectionID,
activePersonID,
enableDownload,
fileToCollectionsMap,
collectionNameMap,
fileCollectionIDs,
allCollectionsNameByID,
showAppDownloadBanner,
setIsPhotoSwipeOpen,
isInHiddenSection,
@ -555,8 +555,8 @@ const PhotoFrame = ({
favoriteFileIDs,
markUnsyncedFavoriteUpdate,
markTempDeleted,
collectionNameMap,
fileToCollectionsMap,
allCollectionsNameByID,
fileCollectionIDs,
setFilesDownloadProgressAttributesCreator,
onSelectPerson,
}}

View File

@ -12,6 +12,7 @@ import log from "@/base/log";
import { FileInfo, type FileInfoProps } from "@/gallery/components/FileInfo";
import { type FileInfoExif } from "@/gallery/components/viewer/data-source";
import { downloadManager } from "@/gallery/services/download";
import { extractRawExif, parseExif } from "@/gallery/services/exif";
import type { Collection } from "@/media/collection";
import { fileLogID, type EnteFile } from "@/media/file";
import { FileType } from "@/media/file-type";
@ -19,7 +20,6 @@ import { isHEICExtension, needsJPEGConversion } from "@/media/formats";
import { ConfirmDeleteFileDialog } from "@/new/photos/components/FileViewerComponents";
import { ImageEditorOverlay } from "@/new/photos/components/ImageEditorOverlay";
import { moveToTrash } from "@/new/photos/services/collection";
import { extractRawExif, parseExif } from "@/new/photos/services/exif";
import { usePhotosAppContext } from "@/new/photos/types/context";
import AlbumOutlinedIcon from "@mui/icons-material/AlbumOutlined";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
@ -102,8 +102,8 @@ export type PhotoViewerProps = Pick<
isInHiddenSection: boolean;
enableDownload: boolean;
setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator;
fileToCollectionsMap?: Map<number, number[]>;
collectionNameMap?: Map<number, string>;
fileCollectionIDs?: Map<number, number[]>;
allCollectionsNameByID?: Map<number, string>;
onSelectPerson?: FileInfoProps["onSelectPerson"];
};
@ -136,8 +136,8 @@ export const PhotoViewer: React.FC<PhotoViewerProps> = ({
isInHiddenSection,
enableDownload,
setFilesDownloadProgressAttributesCreator,
fileToCollectionsMap,
collectionNameMap,
fileCollectionIDs,
allCollectionsNameByID,
onSelectPerson,
}) => {
const { showLoadingBar, hideLoadingBar } = usePhotosAppContext();
@ -993,15 +993,15 @@ export const PhotoViewer: React.FC<PhotoViewerProps> = ({
onClose={handleCloseInfo}
file={photoSwipe?.currItem as EnteFile}
exif={exif?.value}
shouldDisableEdits={!isOwnFile}
allowEdits={isOwnFile}
allowMap={!publicCollectionGalleryContext.credentials}
showCollectionChips={
showCollections={
!isTrashCollection && isOwnFile && !isInHiddenSection
}
fileCollectionIDs={fileCollectionIDs}
allCollectionsNameByID={allCollectionsNameByID}
scheduleUpdate={scheduleUpdate}
refreshPhotoswipe={refreshPhotoswipe}
fileToCollectionsMap={fileToCollectionsMap}
collectionNameMap={collectionNameMap}
onSelectCollection={handleSelectCollection}
onSelectPerson={handleSelectPerson}
/>

View File

@ -1102,8 +1102,8 @@ const Page: React.FC = () => {
activeCollectionID={activeCollectionID}
activePersonID={activePerson?.id}
enableDownload={true}
fileToCollectionsMap={state.fileCollectionIDs}
collectionNameMap={state.allCollectionNameByID}
fileCollectionIDs={state.fileCollectionIDs}
allCollectionsNameByID={state.allCollectionsNameByID}
showAppDownloadBanner={
files.length < 30 && !isInSearchMode
}
@ -1119,7 +1119,7 @@ const Page: React.FC = () => {
)}
<Export
{...exportVisibilityProps}
collectionNameMap={state.allCollectionNameByID}
allCollectionsNameByID={state.allCollectionsNameByID}
/>
<AuthenticateUserModal
open={authenticateUserModalView}

View File

@ -548,8 +548,8 @@ export default function PublicCollectionGallery() {
selected={selected}
activeCollectionID={ALL_SECTION}
enableDownload={downloadEnabled}
fileToCollectionsMap={undefined}
collectionNameMap={undefined}
fileCollectionIDs={undefined}
allCollectionsNameByID={undefined}
setFilesDownloadProgressAttributesCreator={
setFilesDownloadProgressAttributesCreator
}

View File

@ -5,6 +5,7 @@ import { ensureElectron } from "@/base/electron";
import { basename, nameAndExtension } from "@/base/file-name";
import type { PublicAlbumsCredentials } from "@/base/http";
import log from "@/base/log";
import { extractExif } from "@/gallery/services/exif";
import { extractVideoMetadata } from "@/gallery/services/ffmpeg";
import {
getNonEmptyMagicMetadataProps,
@ -38,7 +39,6 @@ import {
import { FileType, type FileTypeInfo } from "@/media/file-type";
import { encodeLivePhoto } from "@/media/live-photo";
import { addToCollection } from "@/new/photos/services/collection";
import { extractExif } from "@/new/photos/services/exif";
import { mergeUint8Arrays } from "@/utils/array";
import { ensureInteger, ensureNumber } from "@/utils/ensure";
import { CustomError, handleUploadError } from "@ente/shared/error";

View File

@ -348,14 +348,6 @@ export function isValidReplacementAlbum(
);
}
export function getCollectionNameMap(
collections: Collection[],
): Map<number, string> {
return new Map<number, string>(
collections.map((collection) => [collection.id, collection.name]),
);
}
export const getOrCreateAlbum = async (
albumName: string,
existingCollections: Collection[],

View File

@ -34,14 +34,6 @@ that can be then deployed to any web server.
There is one `yarn build:foo` for each app, e.g. `yarn build:auth`. The output
will be placed in `apps/<foo>/out`, e.g. `apps/auth/out`.
### yarn preview:\*
Build a production export and start a local web server to serve it. This uses
Python's built in web server, and is okay for quick testing but should not be
used in production.
The ports are the same as that for `yarn dev:*`
### lint, lint-fix
Use `yarn lint` to check that your code formatting is as expected, and that

View File

@ -21,13 +21,7 @@
"dev:payments": "yarn workspace payments dev",
"dev:photos": "yarn workspace photos next dev -p 3000",
"lint": "concurrently --names 'prettier,eslint,tsc' \"yarn prettier --check --log-level warn .\" \"yarn workspaces run eslint\" \"yarn workspaces run tsc\"",
"lint-fix": "concurrently --names 'prettier,eslint,tsc' \"yarn prettier --write --log-level warn .\" \"yarn workspaces run eslint --fix\" \"yarn workspaces run tsc\"",
"preview": "yarn preview:photos",
"preview:accounts": "yarn build:accounts && python3 -m http.server -d apps/accounts/out 3001",
"preview:auth": "yarn build:auth && python3 -m http.server -d apps/auth/out 3000",
"preview:cast": "yarn build:cast && python3 -m http.server -d apps/accounts/out 3001",
"preview:payments": "yarn workspace payments preview",
"preview:photos": "yarn build:photos && python3 -m http.server -d apps/photos/out 3000"
"lint-fix": "concurrently --names 'prettier,eslint,tsc' \"yarn prettier --write --log-level warn .\" \"yarn workspaces run eslint --fix\" \"yarn workspaces run tsc\""
},
"devDependencies": {
"concurrently": "^9.1.1",

View File

@ -6,6 +6,8 @@
"@/base": "*",
"@ente/shared": "*",
"@types/zxcvbn": "^4.4.5",
"bip39": "^3.0.4",
"fast-srp-hap": "^2.0.4",
"react-otp-input": "^3.1.1",
"uuid": "^11.0.5",
"zxcvbn": "^4.4.2"

View File

@ -1,7 +1,4 @@
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
/* eslint-disable @typescript-eslint/no-unused-expressions */
/* TODO: Audit this file
Plan of action:
- Move common components into FileInfoComponents.tsx
@ -9,7 +6,6 @@ Plan of action:
- Move the rest out to files in the apps themeselves: albums/SharedFileInfo
and photos/FileInfo to deal with the @/new/photos imports here.
*/
/* @ts-nocheck */
import { LinkButtonUndecorated } from "@/base/components/LinkButton";
import { TitledMiniDialog } from "@/base/components/MiniDialog";
@ -28,6 +24,7 @@ import { nameAndExtension } from "@/base/file-name";
import log from "@/base/log";
import type { Location } from "@/base/types";
import { CopyButton } from "@/gallery/components/FileInfoComponents";
import { tagNumericValue, type RawExifTags } from "@/gallery/services/exif";
import {
changeCaption,
changeFileName,
@ -53,7 +50,6 @@ import {
aboveFileViewerContentZ,
fileInfoDrawerZ,
} from "@/new/photos/components/utils/z-index";
import { tagNumericValue, type RawExifTags } from "@/new/photos/services/exif";
import {
getAnnotatedFacesForFile,
isMLEnabled,
@ -98,7 +94,7 @@ import type { FileInfoExif } from "./viewer/data-source";
// Re-uses images from ~leaflet package.
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css";
import "leaflet/dist/leaflet.css";
// eslint-disable-next-line @typescript-eslint/no-require-imports
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unused-expressions
haveWindow() && require("leaflet-defaulticon-compatibility");
const leaflet = haveWindow()
? // eslint-disable-next-line @typescript-eslint/no-require-imports
@ -110,21 +106,42 @@ export type FileInfoProps = ModalVisibilityProps & {
* The file whose information we are showing.
*/
file: EnteFile | undefined;
/**
* Exif information for {@link file}.
*/
exif: FileInfoExif | undefined;
/**
* TODO: Rename and flip to allowEdits.
* If set, then controls to edit the file's metadata (name, date, caption)
* will be shown.
*/
shouldDisableEdits: boolean;
allowEdits?: boolean;
/**
* If `true`, an inline map will be shown (if the user has enabled it) using
* the file's location.
* If set, then an inline map will be shown (if the user has enabled it)
* using the file's location.
*/
allowMap: boolean;
allowMap?: boolean;
/**
* If set, then a clickable chip will be shown for each collection that this
* file is a part of.
*
* Uses {@link fileCollectionIDs} and {@link allCollectionsNameByID}, so
* both of those props should also be set for this to have an effect.
*/
showCollections?: boolean;
/**
* A map from file IDs to the IDs of the collections that they're a part of.
*
* Used when {@link showCollections} is set.
*/
fileCollectionIDs?: Map<number, number[]>;
/**
* A map from collection IDs to their name.
*
* Used when {@link showCollections} is set.
*/
allCollectionsNameByID?: Map<number, string>;
scheduleUpdate: () => void;
refreshPhotoswipe: () => void;
fileToCollectionsMap?: Map<number, number[]>;
collectionNameMap?: Map<number, string>;
showCollectionChips: boolean;
/**
* Called when the user selects a collection from among the collections that
* the file belongs to.
@ -140,14 +157,14 @@ export const FileInfo: React.FC<FileInfoProps> = ({
open,
onClose,
file,
shouldDisableEdits,
allowMap,
exif,
allowEdits,
allowMap,
showCollections,
fileCollectionIDs,
allCollectionsNameByID,
scheduleUpdate,
refreshPhotoswipe,
fileToCollectionsMap,
collectionNameMap,
showCollectionChips,
onSelectCollection,
onSelectPerson,
}) => {
@ -155,42 +172,33 @@ export const FileInfo: React.FC<FileInfoProps> = ({
const { mapEnabled } = useSettingsSnapshot();
const [exifInfo, setExifInfo] = useState<ExifInfo | undefined>();
const { show: showRawExif, props: rawExifVisibilityProps } =
useModalVisibility();
const [annotatedFaces, setAnnotatedFaces] = useState<AnnotatedFaceID[]>([]);
const location = useMemo(() => {
if (file) {
const location = fileLocation(file);
if (location) return location;
}
return exif?.parsed?.location;
}, [file, exif]);
const { show: showRawExif, props: rawExifVisibilityProps } =
useModalVisibility();
const location = useMemo(
// Prefer the location in the EnteFile, then fall back to Exif.
() => (file ? fileLocation(file) : undefined) ?? exif?.parsed?.location,
[file, exif],
);
const annotatedExif = useMemo(() => annotateExif(exif), [exif]);
useEffect(() => {
if (!file) return;
let didCancel = false;
void (async () => {
const result = await getAnnotatedFacesForFile(file);
!didCancel && setAnnotatedFaces(result);
})();
void getAnnotatedFacesForFile(file).then(
(faces) => !didCancel && setAnnotatedFaces(faces),
);
return () => {
didCancel = true;
};
}, [file]);
useEffect(() => {
setExifInfo(parseExifInfo(exif));
}, [exif]);
if (!file) {
return <></>;
}
const openEnableMapConfirmationDialog = () =>
showMiniDialog(
confirmEnableMapsDialogAttributes(() => updateMapEnabled(true)),
@ -204,39 +212,37 @@ export const FileInfo: React.FC<FileInfoProps> = ({
const handleSelectFace = ({ personID }: AnnotatedFaceID) =>
onSelectPerson?.(personID);
if (!file) {
return <></>;
}
return (
<FileInfoSidebar open={open} onClose={onClose}>
<Titlebar onClose={onClose} title={t("info")} backIsClose />
<Stack sx={{ pt: 1, pb: 3, gap: "20px" }}>
<RenderCaption
<Caption
{...{
file,
shouldDisableEdits,
allowEdits,
scheduleUpdate,
refreshPhotoswipe,
}}
/>
<CreationTime
{...{ file, shouldDisableEdits, scheduleUpdate }}
/>
<RenderFileName
<CreationTime {...{ file, allowEdits, scheduleUpdate }} />
<FileName
{...{
file,
exifInfo: exifInfo,
shouldDisableEdits,
exifInfo: annotatedExif,
allowEdits,
scheduleUpdate,
}}
/>
{exifInfo?.takenOnDevice && (
{annotatedExif?.takenOnDevice && (
<InfoItem
icon={<CameraOutlinedIcon />}
title={exifInfo?.takenOnDevice}
caption={
<BasicDeviceCamera {...{ parsedExif: exifInfo }} />
}
title={annotatedExif.takenOnDevice}
caption={<BasicDeviceCamera {...{ annotatedExif }} />}
/>
)}
@ -307,35 +313,41 @@ export const FileInfo: React.FC<FileInfoProps> = ({
/>
</InfoItem>
)}
{showCollectionChips && collectionNameMap && (
<InfoItem icon={<FolderOutlinedIcon />}>
<Stack
direction="row"
sx={{
gap: 1,
flexWrap: "wrap",
justifyContent: "flex-start",
alignItems: "flex-start",
}}
>
{fileToCollectionsMap
?.get(file.id)
?.filter((collectionID) =>
collectionNameMap.has(collectionID),
)
?.map((collectionID) => (
<ChipButton
key={collectionID}
onClick={() =>
onSelectCollection(collectionID)
}
>
{collectionNameMap.get(collectionID)}
</ChipButton>
))}
</Stack>
</InfoItem>
)}
{showCollections &&
fileCollectionIDs &&
allCollectionsNameByID && (
<InfoItem icon={<FolderOutlinedIcon />}>
<Stack
direction="row"
sx={{
gap: 1,
flexWrap: "wrap",
justifyContent: "flex-start",
alignItems: "flex-start",
}}
>
{fileCollectionIDs
.get(file.id)
?.filter((collectionID) =>
allCollectionsNameByID.has(
collectionID,
),
)
.map((collectionID) => (
<ChipButton
key={collectionID}
onClick={() =>
onSelectCollection(collectionID)
}
>
{allCollectionsNameByID.get(
collectionID,
)}
</ChipButton>
))}
</Stack>
</InfoItem>
)}
</Stack>
<RawExif
{...rawExifVisibilityProps}
@ -351,7 +363,7 @@ export const FileInfo: React.FC<FileInfoProps> = ({
* Some immediate fields of interest, in the form that we want to display on the
* info panel for a file.
*/
type ExifInfo = Required<FileInfoExif> & {
type AnnotatedExif = Required<FileInfoExif> & {
resolution?: string;
megaPixels?: string;
takenOnDevice?: string;
@ -360,13 +372,13 @@ type ExifInfo = Required<FileInfoExif> & {
iso?: string;
};
const parseExifInfo = (
const annotateExif = (
fileInfoExif: FileInfoExif | undefined,
): ExifInfo | undefined => {
): AnnotatedExif | undefined => {
if (!fileInfoExif || !fileInfoExif.tags || !fileInfoExif.parsed)
return undefined;
const info: ExifInfo = { ...fileInfoExif };
const info: AnnotatedExif = { ...fileInfoExif };
const { width, height } = fileInfoExif.parsed;
if (width && height) {
@ -391,6 +403,7 @@ const parseExifInfo = (
if (exif.ISOSpeedRatings)
info.iso = `ISO${tagNumericValue(exif.ISOSpeedRatings)}`;
}
return info;
};
@ -466,9 +479,7 @@ const InfoItem: React.FC<React.PropsWithChildren<InfoItemProps>> = ({
>
<InfoItemIconContainer>{icon}</InfoItemIconContainer>
<Box sx={{ flex: 1, mt: "4px" }}>
{children ? (
children
) : (
{children ?? (
<>
<Typography sx={{ wordBreak: "break-all" }}>
{title}
@ -517,51 +528,49 @@ const EditButton: React.FC<EditButtonProps> = ({ onClick, loading }) => (
</IconButton>
);
interface RenderCaptionFormValues {
interface CaptionFormValues {
caption: string;
}
function RenderCaption({
file,
scheduleUpdate,
refreshPhotoswipe,
shouldDisableEdits,
}: {
shouldDisableEdits: boolean;
/* TODO: This is DisplayFile, but that's meant to be deprecated */
type CaptionProps = Pick<
FileInfoProps,
"allowEdits" | "scheduleUpdate" | "refreshPhotoswipe"
> & {
/* TODO(PS): This is DisplayFile, but that's meant to be removed */
file: EnteFile & {
title?: string;
};
scheduleUpdate: () => void;
refreshPhotoswipe: () => void;
}) {
const [caption, setCaption] = useState(
file?.pubMagicMetadata?.data.caption,
);
};
const Caption: React.FC<CaptionProps> = ({
file,
allowEdits,
scheduleUpdate,
refreshPhotoswipe,
}) => {
const [caption, setCaption] = useState(file.pubMagicMetadata?.data.caption);
const [loading, setLoading] = useState(false);
const saveEdits = async (newCaption: string) => {
try {
if (file) {
if (caption === newCaption) {
return;
}
setCaption(newCaption);
const updatedFile = await changeCaption(file, newCaption);
updateExistingFilePubMetadata(file, updatedFile);
// @ts-ignore
file.title = file.pubMagicMetadata.data.caption;
refreshPhotoswipe();
scheduleUpdate();
if (caption === newCaption) {
return;
}
setCaption(newCaption);
const updatedFile = await changeCaption(file, newCaption);
updateExistingFilePubMetadata(file, updatedFile);
// @ts-ignore
file.title = file.pubMagicMetadata.data.caption;
refreshPhotoswipe();
scheduleUpdate();
} catch (e) {
log.error("failed to update caption", e);
}
};
const onSubmit = async (values: RenderCaptionFormValues) => {
const onSubmit = async (values: CaptionFormValues) => {
try {
setLoading(true);
await saveEdits(values.caption);
@ -569,12 +578,14 @@ function RenderCaption({
setLoading(false);
}
};
if (!caption?.length && shouldDisableEdits) {
if (!caption?.length && !allowEdits) {
return <></>;
}
return (
<Box sx={{ p: 1 }}>
<Formik<RenderCaptionFormValues>
<Formik<CaptionFormValues>
// @ts-ignore
initialValues={{ caption }}
validationSchema={Yup.object().shape({
@ -606,7 +617,7 @@ function RenderCaption({
onChange={handleChange("caption")}
error={Boolean(errors.caption)}
helperText={errors.caption}
disabled={loading || shouldDisableEdits}
disabled={!allowEdits || loading}
/>
{values.caption !== caption && (
<FlexWrapper justifyContent={"flex-end"}>
@ -638,17 +649,18 @@ function RenderCaption({
</Formik>
</Box>
);
}
};
interface CreationTimeProps {
type CreationTimeProps = Pick<
FileInfoProps,
"allowEdits" | "scheduleUpdate"
> & {
file: EnteFile;
shouldDisableEdits: boolean;
scheduleUpdate: () => void;
}
};
const CreationTime: React.FC<CreationTimeProps> = ({
file,
shouldDisableEdits,
allowEdits,
scheduleUpdate,
}) => {
const [loading, setLoading] = useState(false);
@ -663,7 +675,7 @@ const CreationTime: React.FC<CreationTimeProps> = ({
const saveEdits = async (pickedTime: ParsedMetadataDate) => {
try {
setLoading(true);
if (isInEditMode && file) {
if (isInEditMode) {
// [Note: Don't modify offsetTime when editing date via picker]
//
// Use the updated date time (both in its canonical dateTime
@ -706,7 +718,7 @@ const CreationTime: React.FC<CreationTimeProps> = ({
title={formatDate(originalDate)}
caption={formatTime(originalDate)}
trailingButton={
shouldDisableEdits || (
allowEdits && (
<EditButton
onClick={openEditMode}
loading={loading}
@ -727,17 +739,15 @@ const CreationTime: React.FC<CreationTimeProps> = ({
);
};
interface RenderFileNameProps {
type FileNameProps = Pick<FileInfoProps, "allowEdits" | "scheduleUpdate"> & {
file: EnteFile;
shouldDisableEdits: boolean;
exifInfo: ExifInfo | undefined;
scheduleUpdate: () => void;
}
exifInfo: AnnotatedExif | undefined;
};
const RenderFileName: React.FC<RenderFileNameProps> = ({
const FileName: React.FC<FileNameProps> = ({
file,
shouldDisableEdits,
exifInfo,
allowEdits,
scheduleUpdate,
}) => {
const [isInEditMode, setIsInEditMode] = useState(false);
@ -753,7 +763,6 @@ const RenderFileName: React.FC<RenderFileNameProps> = ({
}, [file]);
const saveEdits = async (newFilename: string) => {
if (!file) return;
if (fileName === newFilename) {
closeEditMode();
return;
@ -778,7 +787,7 @@ const RenderFileName: React.FC<RenderFileNameProps> = ({
title={[fileName, extension].join(".")}
caption={getCaption(file, exifInfo)}
trailingButton={
shouldDisableEdits || <EditButton onClick={openEditMode} />
allowEdits && <EditButton onClick={openEditMode} />
}
/>
<FileNameEditDialog
@ -792,7 +801,7 @@ const RenderFileName: React.FC<RenderFileNameProps> = ({
);
};
const getCaption = (file: EnteFile, exifInfo: ExifInfo | undefined) => {
const getCaption = (file: EnteFile, exifInfo: AnnotatedExif | undefined) => {
const megaPixels = exifInfo?.megaPixels;
const resolution = exifInfo?.resolution;
const fileSize = file.info?.fileSize;
@ -864,17 +873,15 @@ const FileNameEditDialog: React.FC<FileNameEditDialogProps> = ({
);
};
const BasicDeviceCamera: React.FC<{ parsedExif: ExifInfo }> = ({
parsedExif,
}) => {
return (
<FlexWrapper gap={1}>
<Box>{parsedExif.fNumber}</Box>
<Box>{parsedExif.exposureTime}</Box>
<Box>{parsedExif.iso}</Box>
</FlexWrapper>
);
};
const BasicDeviceCamera: React.FC<{ annotatedExif: AnnotatedExif }> = ({
annotatedExif,
}) => (
<Stack direction="row" sx={{ gap: 1 }}>
<Box>{annotatedExif.fNumber}</Box>
<Box>{annotatedExif.exposureTime}</Box>
<Box>{annotatedExif.iso}</Box>
</Stack>
);
const openStreetMapLink = ({ latitude, longitude }: Location) =>
`https://www.openstreetmap.org/?mlat=${latitude}&mlon=${longitude}#map=15/${latitude}/${longitude}`;

View File

@ -12,7 +12,7 @@ import {
extractRawExif,
parseExif,
type RawExifTags,
} from "@/new/photos/services/exif";
} from "@/gallery/services/exif";
/**
* This is a subset of the fields expected by PhotoSwipe itself (see the

View File

@ -400,7 +400,7 @@ export class FileViewerPhotoSwipe {
private autoHideIfInactive() {
if (this.lastActivityDate == "already-hidden") return;
if (this.lastActivityDate == "auto-hidden") return;
if (Date.now() - this.lastActivityDate.getTime() > 3000) {
if (Date.now() - this.lastActivityDate.getTime() > 5000 /* 5s */) {
if (this.areUIControlsVisible()) {
this.hideUIControlsIfNotFocused();
this.lastActivityDate = "auto-hidden";

View File

@ -6,9 +6,14 @@
"@/base": "*",
"@/utils": "*",
"@ffmpeg/ffmpeg": "^0.12.10",
"bs58": "^6.0.0"
"bs58": "^6.0.0",
"exifreader": "^4.26.1",
"leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility": "^0.1.2",
"react-dropzone": "14.2.10"
},
"devDependencies": {
"@/build-config": "*"
"@/build-config": "*",
"@types/leaflet": "^1.9.16"
}
}

View File

@ -195,14 +195,13 @@ export interface GalleryState {
*/
favoriteFileIDs: Set<number>;
/**
* User visible collection names indexed by collection IDs for fast lookup.
* A map from collection IDs to their user visible name.
*
* This map will contain entries for all (both normal and hidden)
* collections.
* It will contain entries for all collections (both normal and hidden).
*/
allCollectionNameByID: Map<number, string>;
allCollectionsNameByID: Map<number, string>;
/**
* A list of collection IDs to which a file belongs, indexed by file ID.
* A map from file IDs to the IDs of the collections that they're a part of.
*/
fileCollectionIDs: Map<number, number[]>;
@ -420,7 +419,7 @@ const initialGalleryState: GalleryState = {
hiddenFileIDs: new Set(),
archivedFileIDs: new Set(),
favoriteFileIDs: new Set(),
allCollectionNameByID: new Map(),
allCollectionsNameByID: new Map(),
fileCollectionIDs: new Map(),
collectionSummaries: new Map(),
hiddenCollectionSummaries: new Map(),
@ -481,7 +480,7 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
action.files,
state.unsyncedFavoriteUpdates,
),
allCollectionNameByID: createCollectionNameByID(
allCollectionsNameByID: createCollectionNameByID(
action.allCollections,
),
fileCollectionIDs: createFileCollectionIDs(action.files),
@ -539,7 +538,7 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
state.files,
state.unsyncedFavoriteUpdates,
),
allCollectionNameByID: createCollectionNameByID(
allCollectionsNameByID: createCollectionNameByID(
collections.concat(state.hiddenCollections),
),
collectionSummaries,
@ -608,7 +607,7 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
state.files,
state.unsyncedFavoriteUpdates,
),
allCollectionNameByID: createCollectionNameByID(
allCollectionsNameByID: createCollectionNameByID(
collections.concat(hiddenCollections),
),
collectionSummaries,

View File

@ -980,7 +980,7 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz#c82f04a09ba481e13857d6f2516e072aaa51b7f4"
integrity sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==
"@stripe/stripe-js@^1.13.2", "@stripe/stripe-js@^1.17.0":
"@stripe/stripe-js@^1.17.0":
version "1.54.2"
resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.54.2.tgz#0665848e22cbda936cfd05256facdfbba121438d"
integrity sha512-R1PwtDvUfs99cAjfuQ/WpwJ3c92+DAMy9xGApjqlWQMj0FKQabUAys2swfTRNzuYAYJh7NqK2dzcYVNkKLEKUg==