[web] File viewer tweaks (#6319)

This commit is contained in:
Manav Rathi
2025-06-20 16:52:13 +05:30
committed by GitHub
10 changed files with 333 additions and 156 deletions

View File

@@ -68,6 +68,7 @@ export type FileListWithViewerProps = {
| "collectionNameByID"
| "pendingFavoriteUpdates"
| "pendingVisibilityUpdates"
| "onFileAndCollectionSyncWithRemote"
| "onVisualFeedback"
| "onToggleFavorite"
| "onFileVisibilityUpdate"
@@ -102,6 +103,7 @@ export const FileListWithViewer: React.FC<FileListWithViewerProps> = ({
setFilesDownloadProgressAttributesCreator,
onSetOpenFileViewer,
onSyncWithRemote,
onFileAndCollectionSyncWithRemote,
onVisualFeedback,
onToggleFavorite,
onFileVisibilityUpdate,
@@ -201,6 +203,7 @@ export const FileListWithViewer: React.FC<FileListWithViewerProps> = ({
collectionNameByID,
pendingFavoriteUpdates,
pendingVisibilityUpdates,
onFileAndCollectionSyncWithRemote,
onVisualFeedback,
onToggleFavorite,
onFileVisibilityUpdate,

View File

@@ -21,11 +21,11 @@ import {
decryptPublicMagicMetadata,
fileCreationPhotoDate,
fileFileName,
updateRemotePublicMagicMetadata,
type ParsedMetadataDate,
} from "ente-media/file-metadata";
import { FileType } from "ente-media/file-type";
import { FileDateTimePicker } from "ente-new/photos/components/FileDateTimePicker";
import { updateFilePublicMagicMetadata } from "ente-new/photos/services/file";
import { useFormik } from "formik";
import { t } from "i18next";
import { GalleryContext } from "pages/gallery";
@@ -279,7 +279,7 @@ const updateFiles = async (
let hadErrors = false;
for (const [i, file] of files.entries()) {
try {
await updateEnteFileDate(file, fixOption, customDate);
await updateFileDate(file, fixOption, customDate);
} catch (e) {
log.error(`Failed to update date of ${fileLogID(file)}`, e);
hadErrors = true;
@@ -291,7 +291,7 @@ const updateFiles = async (
};
/**
* Update the date associated with a given {@link enteFile}.
* 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.
@@ -305,11 +305,11 @@ const updateFiles = async (
* 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.
* {@link file} does not have a corresponding Exif (or related) value, then that
* file is skipped.
*/
const updateEnteFileDate = async (
enteFile: EnteFile,
const updateFileDate = async (
file: EnteFile,
fixOption: FixOption,
customDate: ParsedMetadataDate | undefined,
) => {
@@ -323,11 +323,10 @@ const updateEnteFileDate = async (
offset: undefined,
timestamp: customDate!.timestamp,
};
} else if (enteFile.metadata.fileType == FileType.image) {
const blob = await downloadManager.fileBlob(enteFile);
const file = new File([blob], fileFileName(enteFile));
} else if (file.metadata.fileType == FileType.image) {
const blob = await downloadManager.fileBlob(file);
const { DateTimeOriginal, DateTimeDigitized, MetadataDate, DateTime } =
await extractExifDates(file);
await extractExifDates(new File([blob], fileFileName(file)));
switch (fixOption) {
case "date-time-original":
@@ -345,12 +344,12 @@ const updateEnteFileDate = async (
if (!newDate) return;
const existingDate = fileCreationPhotoDate(
enteFile,
await decryptPublicMagicMetadata(enteFile),
file,
await decryptPublicMagicMetadata(file),
);
if (newDate.timestamp == existingDate.getTime()) return;
await updateRemotePublicMagicMetadata(enteFile, {
await updateFilePublicMagicMetadata(file, {
dateTime: newDate.dateTime,
offsetTime: newDate.offset,
editedTime: newDate.timestamp,

View File

@@ -520,6 +520,23 @@ const Page: React.FC = () => {
setTimeout(hideLoadingBar, 0);
}, [showLoadingBar, hideLoadingBar]);
/**
* Sync the local files and collection with remote.
*
* [Note: Full sync vs file and collection sync]
*
* This is a subset of the sync which happens in {@link syncWithRemote}, but
* in some cases where we know that the changes will not have transitive
* effects outside of the locally stored files and collections this is a
* better option for interactive operations because:
*
* 1. This involves a lesser number of API requests, so it reduces the time
* the user has to wait for their interactive request to complete.
*
* 2. The current implementation {@link syncWithRemote} tries to run only
* only one instance of it is in progress at a time, while each
* invocation of {@link fileAndCollectionSyncWithRemote} is independent.
*/
const fileAndCollectionSyncWithRemote = useCallback(async () => {
const didUpdateFiles = await syncCollectionAndFiles({
onSetCollections: (
@@ -1132,6 +1149,9 @@ const Page: React.FC = () => {
onMarkTempDeleted={handleMarkTempDeleted}
onSetOpenFileViewer={setIsFileViewerOpen}
onSyncWithRemote={syncWithRemote}
onFileAndCollectionSyncWithRemote={
fileAndCollectionSyncWithRemote
}
onVisualFeedback={handleVisualFeedback}
onSelectCollection={handleSelectCollection}
onSelectPerson={handleSelectPerson}

View File

@@ -55,11 +55,6 @@ import log from "ente-base/log";
import type { Location } from "ente-base/types";
import { CopyButton } from "ente-gallery/components/FileInfoComponents";
import { tagNumericValue, type RawExifTags } from "ente-gallery/services/exif";
import {
changeCaption,
changeFileName,
updateExistingFilePubMetadata,
} from "ente-gallery/services/file";
import { formattedByteSize } from "ente-gallery/utils/units";
import { type EnteFile } from "ente-media/file";
import {
@@ -68,7 +63,6 @@ import {
fileFileName,
fileLocation,
filePublicMagicMetadata,
updateRemotePublicMagicMetadata,
type ParsedMetadata,
type ParsedMetadataDate,
} from "ente-media/file-metadata";
@@ -80,6 +74,11 @@ import {
confirmEnableMapsDialogAttributes,
} from "ente-new/photos/components/utils/dialog";
import { useSettingsSnapshot } from "ente-new/photos/components/utils/use-snapshot";
import {
updateFileCaption,
updateFileFileName,
updateFilePublicMagicMetadata,
} from "ente-new/photos/services/file";
import {
getAnnotatedFacesForFile,
isMLEnabled,
@@ -152,26 +151,28 @@ export type FileInfoProps = ModalVisibilityProps & {
*/
collectionNameByID?: Map<number, string>;
/**
* Called when the action on the file info drawer has changed some the
* metadata for some file, and we need to sync with remote to get our
* locally persisted file objects up to date.
* Called when the action on the file info drawer has changed some metadata
* for a file.
*
* The sync is not performed immediately by the file info drawer to give
* faster feedback to the user, and to allow changes to multiple files to be
* batched together into a single sync when the file viewer is closed.
* It should return a promise that settles when the changes have been
* reflected locally. Until the promise settles the UI element that
* triggered the change will show an activity indicator to the user.
*/
onNeedsRemoteSync: () => void;
onFileMetadataUpdate?: () => Promise<void>;
/**
* Called when an action on the file info drawer change the caption of the
* given {@link EnteFile}.
*
* This hook allows the file viewer to update the caption it is displaying
* for the given file.
* for the given file. It is called in addition to, and after the settlement
* of, {@link onFileMetadataUpdate} since the caption update requires a
* special case refresh of the PhotoSwipe dialog.
*
* @param updatedFile The updated file object, containing the updated
* caption.
* @param fileID The ID of the file whose caption was updated.
*
* @param newCaption The updated value of the file's caption.
*/
onUpdateCaption: (updatedFile: EnteFile) => void;
onUpdateCaption: (fileID: number, newCaption: string) => void;
/**
* Called when the user selects a collection from among the collections that
* the file belongs to.
@@ -193,7 +194,7 @@ export const FileInfo: React.FC<FileInfoProps> = ({
showCollections,
fileCollectionIDs,
collectionNameByID,
onNeedsRemoteSync,
onFileMetadataUpdate,
onUpdateCaption,
onSelectCollection,
onSelectPerson,
@@ -273,13 +274,19 @@ export const FileInfo: React.FC<FileInfoProps> = ({
{...{
file,
allowEdits,
onNeedsRemoteSync,
onFileMetadataUpdate,
onUpdateCaption,
onClose,
}}
/>
<CreationTime {...{ file, allowEdits, onNeedsRemoteSync }} />
<CreationTime {...{ file, allowEdits, onFileMetadataUpdate }} />
<FileName
{...{ file, annotatedExif, allowEdits, onNeedsRemoteSync }}
{...{
file,
annotatedExif,
allowEdits,
onFileMetadataUpdate,
}}
/>
{annotatedExif?.takenOnDevice && (
@@ -559,14 +566,19 @@ const EditButton: React.FC<EditButtonProps> = ({ onClick, loading }) => (
type CaptionProps = Pick<
FileInfoProps,
"file" | "allowEdits" | "onNeedsRemoteSync" | "onUpdateCaption"
| "file"
| "allowEdits"
| "onFileMetadataUpdate"
| "onUpdateCaption"
| "onClose"
>;
const Caption: React.FC<CaptionProps> = ({
file,
allowEdits,
onNeedsRemoteSync,
onFileMetadataUpdate,
onUpdateCaption,
onClose,
}) => {
const [isSaving, setIsSaving] = useState(false);
@@ -582,15 +594,16 @@ const Caption: React.FC<CaptionProps> = ({
if (newCaption == caption) return;
setIsSaving(true);
try {
const updatedFile = await changeCaption(file, newCaption);
updateExistingFilePubMetadata(file, updatedFile);
onUpdateCaption(file);
await updateFileCaption(file, newCaption);
await onFileMetadataUpdate?.();
onUpdateCaption(file.id, newCaption);
setIsSaving(false);
onClose();
} catch (e) {
log.error("Failed to update caption", e);
setIsSaving(false);
setFieldError("caption", t("generic_error"));
}
onNeedsRemoteSync();
setIsSaving(false);
},
});
@@ -649,13 +662,13 @@ const CaptionForm = styled("form")(({ theme }) => ({
type CreationTimeProps = Pick<
FileInfoProps,
"allowEdits" | "onNeedsRemoteSync"
"allowEdits" | "onFileMetadataUpdate"
> & { file: EnteFile };
const CreationTime: React.FC<CreationTimeProps> = ({
file,
allowEdits,
onNeedsRemoteSync,
onFileMetadataUpdate,
}) => {
const { onGenericError } = useBaseContext();
@@ -689,14 +702,11 @@ const CreationTime: React.FC<CreationTimeProps> = ({
// we can provide functionality for the user to edit the associated
// offset, but right now it is not even surfaced, so don't also
// potentially overwrite it.
await updateRemotePublicMagicMetadata(file, {
dateTime,
editedTime,
});
await updateFilePublicMagicMetadata(file, { dateTime, editedTime });
await onFileMetadataUpdate?.();
} catch (e) {
onGenericError(e);
}
onNeedsRemoteSync();
setIsSaving(false);
};
@@ -726,16 +736,16 @@ const CreationTime: React.FC<CreationTimeProps> = ({
);
};
type FileNameProps = Pick<FileInfoProps, "allowEdits" | "onNeedsRemoteSync"> & {
file: EnteFile;
annotatedExif: AnnotatedExif | undefined;
};
type FileNameProps = Pick<
FileInfoProps,
"allowEdits" | "onFileMetadataUpdate"
> & { file: EnteFile; annotatedExif: AnnotatedExif | undefined };
const FileName: React.FC<FileNameProps> = ({
file,
annotatedExif,
allowEdits,
onNeedsRemoteSync,
onFileMetadataUpdate,
}) => {
const { show: showRename, props: renameVisibilityProps } =
useModalVisibility();
@@ -743,9 +753,8 @@ const FileName: React.FC<FileNameProps> = ({
const fileName = fileFileName(file);
const handleRename = async (newFileName: string) => {
const updatedFile = await changeFileName(file, newFileName);
updateExistingFilePubMetadata(file, updatedFile);
onNeedsRemoteSync();
await updateFileFileName(file, newFileName);
await onFileMetadataUpdate?.();
};
const icon =

View File

@@ -216,6 +216,17 @@ export type FileViewerProps = ModalVisibilityProps & {
* not defined, then this prop is not used.
*/
onTriggerSyncWithRemote?: () => void;
/**
* Called when an action in the file viewer requires us to sync the local
* files and collections with remote.
*
* Unlike {@link onTriggerSyncWithRemote}, which is a trigger, this function
* returns a promise that will settle once the sync has completed, and thus
* can be used in interactive operations that indicate activity to the user.
*
* See: [Note: Full sync vs file and collection sync]
*/
onFileAndCollectionSyncWithRemote?: () => Promise<void>;
/**
* Called when the user performs an action which does not otherwise have any
* immediate visual impact, to acknowledge it.
@@ -290,6 +301,7 @@ export const FileViewer: React.FC<FileViewerProps> = ({
fileNormalCollectionIDs,
collectionNameByID,
onTriggerSyncWithRemote,
onFileAndCollectionSyncWithRemote,
onVisualFeedback,
onToggleFavorite,
onFileVisibilityUpdate,
@@ -620,9 +632,6 @@ export const FileViewer: React.FC<FileViewerProps> = ({
const handleShortcutsClose = useCallback(() => setOpenShortcuts(false), []);
// TODO: Unused translation t("convert") - can be removed post the upcoming
// streaming changes as they'll provides the equiv.
const shouldIgnoreKeyboardEvent = useCallback(() => {
// Don't handle keydowns if any of the modals are open.
return (
@@ -769,56 +778,16 @@ export const FileViewer: React.FC<FileViewerProps> = ({
performKeyAction,
]);
// Update the active annotated file, if needed, on updates to files or
// favoriteFileIDs.
// Handle updates to files.
//
// If the active annotated file is no longer being shown, move to the next
// slide if possible, or close the viewer otherwise.
// See: [Note: Updates to the files prop for FileViewer]
useEffect(() => {
setActiveAnnotatedFile((af) => {
// Do nothing if we're not showing a file (we might not be open).
if (!af) return af;
if (files.length) {
const updatedFile = files.find(({ id }) => id == af?.file.id);
if (updatedFile) {
// Modify the active annotated file if we found a file with
// the same ID in the (possibly) updated files array.
//
// This is not correct in its full generality, but it works
// fine in the specific cases we would need to handle (and
// we want to avoid refreshing the entire UI unnecessarily
// lest the user lose their zoom/pan etc):
//
// - In case of delete or toggling archived that caused the
// file is no longer part of the list that is shown, we'll
// not get to this code branch.
//
// - In case of toggling archive otherwise, just updating
// the file attribute is enough, the UI state is derived
// from it; none of the other attributes of the annotated
// file currently depend on the archive status change.
af = { ...af, file: updatedFile };
} else {
// The file we were displaying is no longer part of the list
// of files that should be displayed. Refresh the slides,
// adjusting the indexes as necessary.
//
// A special case is when we might've been the last slide,
// in which case we need to go back one slide first. To
// determine this, also pass the expected count of files to
// our PhotoSwipe wrapper.
psRef.current?.refreshCurrentSlideContentAfterRemove(
files.length,
);
}
} else {
// If there are no more files left, close the viewer.
handleClose();
}
return af;
});
if (!files.length) {
// If there are no more files left, close the viewer.
handleClose();
} else {
psRef.current?.refreshSlideOnFilesUpdateIfNeeded();
}
}, [handleClose, files]);
useEffect(() => {
@@ -881,10 +850,24 @@ export const FileViewer: React.FC<FileViewerProps> = ({
handleMore,
]);
const handleUpdateCaption = useCallback((updatedFile: EnteFile) => {
updateItemDataAlt(updatedFile);
psRef.current!.refreshCurrentSlideContent();
}, []);
const handleFileMetadataUpdate = useMemo(() => {
return onFileAndCollectionSyncWithRemote
? async () => {
// Wait for the file and collection sync to complete.
await onFileAndCollectionSyncWithRemote();
// Set the flag to trigger the full sync to later.
handleNeedsRemoteSync();
}
: undefined;
}, [onFileAndCollectionSyncWithRemote, handleNeedsRemoteSync]);
const handleUpdateCaption = useCallback(
(fileID: number, newCaption: string) => {
updateItemDataAlt(fileID, newCaption);
psRef.current!.refreshCurrentSlideContent();
},
[],
);
useEffect(updateFullscreenStatus, [updateFullscreenStatus]);
@@ -903,11 +886,11 @@ export const FileViewer: React.FC<FileViewerProps> = ({
allowMap={haveUser}
showCollections={haveUser && !isInHiddenSection}
fileCollectionIDs={fileNormalCollectionIDs}
collectionNameByID={collectionNameByID}
onNeedsRemoteSync={handleNeedsRemoteSync}
onFileMetadataUpdate={handleFileMetadataUpdate}
onUpdateCaption={handleUpdateCaption}
onSelectCollection={handleSelectCollection}
onSelectPerson={handleSelectPerson}
{...{ collectionNameByID, onFileAndCollectionSyncWithRemote }}
/>
<MoreMenu
open={!!moreMenuAnchorEl}

View File

@@ -404,12 +404,15 @@ export const forgetItemDataForFileIDIfNeeded = (fileID: number) => {
* Update the alt attribute of the {@link ItemData}, if any, associated with the
* given {@link EnteFile}.
*
* @param updatedFile The file whose caption was updated.
* @param fileID The ID of the file whose {@link alt} attribute we want to
* update.
*
* @param newAlt The new value of the {@link alt} attribute.
*/
export const updateItemDataAlt = (updatedFile: EnteFile) => {
const itemData = _state.itemDataByFileID.get(updatedFile.id);
export const updateItemDataAlt = (fileID: number, newAlt: string) => {
const itemData = _state.itemDataByFileID.get(fileID);
if (itemData) {
itemData.alt = fileCaption(updatedFile);
itemData.alt = newAlt;
}
};

View File

@@ -286,7 +286,11 @@ export class FileViewerPhotoSwipe {
const currentAnnotatedFile = () => {
const file = currentFile();
let annotatedFile = _currentAnnotatedFile;
if (!annotatedFile || annotatedFile.file.id != file.id) {
if (
!annotatedFile ||
annotatedFile.file.id != file.id ||
annotatedFile.file.updationTime != file.updationTime
) {
annotatedFile = onAnnotate(file, currSlideData());
_currentAnnotatedFile = annotatedFile;
}
@@ -295,6 +299,92 @@ export class FileViewerPhotoSwipe {
const currentFileAnnotation = () => currentAnnotatedFile().annotation;
/**
* [Note: Updates to the files prop for FileViewer]
*
* This function is called when the list of underlying files changes.
*
* These updates can be possibly spurious, since the list changes on
* identity and not deep equality. Or they might be real but affect a
* file that is not currently being shown.
*
* Apart from those cases, there are real updates that effect us:
*
* - If the file that was previously being shown is no longer being
* shown (e.g. it was deleted, or if the marked it archived in a
* context like "All" where archived files are not being shown), move
* to the next slide if possible, or close the viewer otherwise (e.g.
* user deleted the last one).
*
* - If the file that was previously being shown has moved to a new
* index (e.g. user changed the date in the file info panel), go to
* that new index.
*
* - If the file is still the same but the underlying file's updation
* time has changed (e.g. the user changed the name of the file in the
* file info panel), refresh the annotated file data cached by the
* viewer.
*
* When handling these cases, the intent is to minimize the need to call
* `refreshSlideContent` as much as possible since that would cause the
* zoom and pan state to be reset too.
*/
this.refreshSlideOnFilesUpdateIfNeeded = () => {
const prevFileID = _currentAnnotatedFile?.file.id;
if (!prevFileID) return;
const files = delegate.getFiles();
const newFileCount = files.length;
const newFile = files[pswp.currIndex];
if (!newFile || newFile.id != prevFileID) {
// File is either no longer there, or has moved.
//
// In either case, after repositioning ourselves to the new
// index we also need to refresh the next (or both) neighbours.
//
// E.g. assume item at index 3 was removed. After refreshing,
// the contents of the item previously at index 4, and now at
// index 3, would be displayed. But the preloaded slide next to
// us (showing item at index 4) would already be displaying the
// same item, so that also needs to be refreshed to displaying
// the item previously at index 5 (now at index 4).
const newIndex = files.findIndex(({ id }) => id == prevFileID);
if (newIndex == -1) {
// File is no longer present.
const i = pswp.currIndex;
if (i >= newFileCount) {
// If the last slide was removed, take one step back
// first (the code that calls us ensures that we don't
// get called if there are no more slides left).
this.pswp.prev();
}
// Refresh the slide at current index (the erstwhile
// neighbour), and refresh its subsequent neighbour (to get
// the new neighbour).
pswp.refreshSlideContent(i);
pswp.refreshSlideContent(i + 1 == newFileCount ? 0 : i + 1);
} else {
// File has moved. Go to new index, and also refresh both
// its neighbours.
const i = newIndex;
pswp.goTo(i);
pswp.refreshSlideContent(i == 0 ? newFileCount - 1 : i - 1);
pswp.refreshSlideContent(i + 1 == newFileCount ? 0 : i + 1);
}
} else {
// Calling `currentAnnotatedFile` has the side effect of
// refreshing the cached annotated file if the `updationTime`
// has changed.
currentAnnotatedFile();
}
};
/**
* File (ID)s for which we should render the original, non-streamable,
* video even if a HLS playlist is available.
@@ -1538,38 +1628,10 @@ export class FileViewerPhotoSwipe {
}
/**
* Reload the PhotoSwipe dialog (without recreating it) if the current slide
* that was being viewed is no longer part of the list of files that should
* be shown. This can happen when the user deleted the file, or if they
* marked it archived in a context (like "All") where archived files are not
* being shown.
*
* @param expectedFileCount The count of files that we expect to show after
* the refresh.
* Ask the delegate for the files information afresh, and perform the
* sequence described in [Note: Updates to the files prop for FileViewer].
*/
refreshCurrentSlideContentAfterRemove(newFileCount: number) {
// Refresh the slide, and its subsequent neighbour.
//
// To see why, consider item at index 3 was removed. After refreshing,
// the contents of the item previously at index 4, and now at index 3,
// would be displayed. But the preloaded slide next to us (showing item
// at index 4) would already be displaying the same item, so that also
// needs to be refreshed to displaying the item previously at index 5
// (now at index 4).
const refreshSlideAndNextNeighbour = (i: number) => {
this.pswp.refreshSlideContent(i);
this.pswp.refreshSlideContent(i + 1 == newFileCount ? 0 : i + 1);
};
if (this.pswp.currIndex >= newFileCount) {
// If the last slide was removed, take one step back first (the code
// that calls us ensures that we don't get called if there are no
// more slides left).
this.pswp.prev();
}
refreshSlideAndNextNeighbour(this.pswp.currIndex);
}
refreshSlideOnFilesUpdateIfNeeded: () => void;
/**
* Refresh the favorite button (if indeed it is visible at all) on the

View File

@@ -320,7 +320,7 @@ export interface PublicMagicMetadata {
*/
editedTime?: number;
/**
* Modified name of the {@link EnteFile}.
* Modified file name of the {@link EnteFile}.
*
* This field stores edits to the {@link title} {@link FileMetadata} field.
*/

View File

@@ -4,6 +4,7 @@ import {
} from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs";
import { useIsSmallWidth } from "ente-base/components/utils/hooks";
import type { ParsedMetadataDate } from "ente-media/file-metadata";
import React, { useState } from "react";
@@ -50,6 +51,8 @@ export const FileDateTimePicker: React.FC<FileDateTimePickerProps> = ({
const [open, setOpen] = useState(true);
const [value, setValue] = useState<Dayjs | null>(dayjs(initialValue));
const isSmallWidth = useIsSmallWidth();
const handleAccept = (d: Dayjs | null) => {
if (!dayjs.isDayjs(d))
throw new Error(`Unexpected non-dayjs result ${typeof d}`);
@@ -72,11 +75,8 @@ export const FileDateTimePicker: React.FC<FileDateTimePickerProps> = ({
disableFuture={true}
/* The dialog grows too big on the default portrait mode with
our theme customizations. So we instead use the landscape
layout. This works great on desktop since we have sufficient
width. MUI omits the sidebar on mobile devices (using the
pointer:fine media query), so it remains functional on mobile
devices too. */
orientation="landscape"
layout if the screen is large enough. */
orientation={isSmallWidth ? "portrait" : "landscape"}
onAccept={handleAccept}
slots={{ field: EmptyField }}
slotProps={{

View File

@@ -4,6 +4,7 @@ import type { EnteFile, EnteFile2 } from "ente-media/file";
import type {
FilePrivateMagicMetadataData,
ItemVisibility,
PublicMagicMetadata,
} from "ente-media/file-metadata";
import {
createMagicMetadata,
@@ -51,6 +52,10 @@ export const performInBatches = <T, U>(
*
* Remote only, does not modify local state.
*
* The visibility of an {@link EnteFile} is stored in its private magic
* metadata, so this function in effect updates the private magic metadata of
* the given files on remote.
*
* @param files The list of files whose visibility we want to change. All the
* files will get their visibility updated to the new, provided, value.
*
@@ -146,3 +151,96 @@ const putFilesMagicMetadata = async (
body: JSON.stringify(updateRequest),
}),
);
/**
* Update the file name of the provided file on remote.
*
* Remote only, does not modify local state.
*
* The file name of an {@link EnteFile} is stored in its public magic metadata,
* so this function in effect updates the public magic metadata of the given
* file on remote.
*
* @param file The file whose file name we want to change.
*
* @param newFileName The new file name of the file.
*/
export const updateFileFileName = (file: EnteFile2, newFileName: string) =>
updateFilePublicMagicMetadata(file, { editedName: newFileName });
/**
* Update the caption associated with the provided file on remote.
*
* Remote only, does not modify local state.
*
* The caption of an {@link EnteFile} is stored in its public magic metadata, so
* this function in effect updates the public magic metadata of the given file
* on remote.
*
* @param file The file whose file name we want to change.
*
* @param caption The caption associated with the file.
*
* Fields in magic metadata cannot be removed after being added, so to reset the
* caption to the default (no value) state pass a blank string.
*/
export const updateFileCaption = (file: EnteFile2, caption: string) =>
updateFilePublicMagicMetadata(file, { caption });
/**
* Update the public magic metadata of a file on remote.
*
* Remote only, does not modify local state.
*
* @param file The list of files whose magic metadata we want to update.
*
* @param updates A non-empty but otherwise arbitrary subset of
* {@link FilePrivateMagicMetadataData} entries.
*
* See: [Note: Magic metadata data cannot have nullish values]
*/
export const updateFilePublicMagicMetadata = async (
file: EnteFile2,
updates: PublicMagicMetadata,
) => updateFilesPublicMagicMetadata([file], updates);
/**
* Update the public magic metadata of a list of files on remote.
*
* Remote only, does not modify local state.
*
* This is a variant of {@link updateFilePrivateMagicMetadata} that works with
* the {@link pubMagicMetadata} of the given files.
*/
const updateFilesPublicMagicMetadata = async (
files: EnteFile2[],
updates: PublicMagicMetadata,
) =>
putFilesPublicMagicMetadata({
metadataList: await Promise.all(
files.map(async ({ id, key, pubMagicMetadata }) => ({
id,
magicMetadata: await encryptMagicMetadata(
createMagicMetadata(
{ ...pubMagicMetadata?.data, ...updates },
pubMagicMetadata?.version,
),
key,
),
})),
),
});
/**
* Update the public magic metadata of a list of files on remote.
*/
const putFilesPublicMagicMetadata = async (
updateRequest: UpdateMultipleMagicMetadataRequest,
) =>
ensureOk(
await fetch(await apiURL("/files/public-magic-metadata"), {
method: "PUT",
headers: await authenticatedRequestHeaders(),
body: JSON.stringify(updateRequest),
}),
);