[web] PhotoSwipe update - WIP (#5200)
This commit is contained in:
@@ -217,12 +217,14 @@ const CodeDisplay: React.FC<CodeDisplayProps> = ({ code }) => {
|
||||
<Snackbar
|
||||
open={openCopied}
|
||||
message={t("copied")}
|
||||
ContentProps={{
|
||||
sx: (theme) => ({
|
||||
backgroundColor: theme.vars.palette.fill.faint,
|
||||
color: theme.vars.palette.primary.main,
|
||||
backdropFilter: "blur(10px)",
|
||||
}),
|
||||
slotProps={{
|
||||
content: {
|
||||
sx: {
|
||||
backgroundColor: "fill.faint",
|
||||
color: "primary.main",
|
||||
backdropFilter: "blur(10px)",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</ButtonBase>
|
||||
|
||||
@@ -717,9 +717,11 @@ const CollectionSortOrderMenu: React.FC<CollectionSortOrderMenuProps> = ({
|
||||
anchorEl={overFlowMenuIconRef.current}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
MenuListProps={{
|
||||
disablePadding: true,
|
||||
"aria-labelledby": "collection-files-sort",
|
||||
slotProps={{
|
||||
list: {
|
||||
disablePadding: true,
|
||||
"aria-labelledby": "collection-files-sort",
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
|
||||
@@ -21,8 +21,12 @@ import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
import PhotoSwipe from "photoswipe";
|
||||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import AutoSizer from "react-virtualized-auto-sizer";
|
||||
import {
|
||||
addToFavorites,
|
||||
removeFromFavorites,
|
||||
} from "services/collectionService";
|
||||
import uploadManager from "services/upload/uploadManager";
|
||||
import {
|
||||
SelectedState,
|
||||
@@ -101,7 +105,6 @@ export type PhotoFrameProps = Pick<
|
||||
*/
|
||||
modePlus?: GalleryBarMode | "search";
|
||||
files: EnteFile[];
|
||||
syncWithRemote: () => Promise<void>;
|
||||
setSelected: (
|
||||
selected: SelectedState | ((selected: SelectedState) => SelectedState),
|
||||
) => void;
|
||||
@@ -121,7 +124,10 @@ export type PhotoFrameProps = Pick<
|
||||
*
|
||||
* Not set in the context of the shared albums app.
|
||||
*/
|
||||
markUnsyncedFavoriteUpdate?: (fileID: number, isFavorite: boolean) => void;
|
||||
onMarkUnsyncedFavoriteUpdate?: (
|
||||
fileID: number,
|
||||
isFavorite: boolean,
|
||||
) => void;
|
||||
/**
|
||||
* Called when the component wants to mark the given files as deleted in the
|
||||
* the in-memory, unsynced, state maintained by the top level gallery.
|
||||
@@ -131,7 +137,7 @@ export type PhotoFrameProps = Pick<
|
||||
*
|
||||
* Not set in the context of the shared albums app.
|
||||
*/
|
||||
markTempDeleted?: (files: EnteFile[]) => void;
|
||||
onMarkTempDeleted?: (files: EnteFile[]) => void;
|
||||
/** This will be set if mode is not "people". */
|
||||
activeCollectionID: number;
|
||||
/** This will be set if mode is "people". */
|
||||
@@ -142,6 +148,7 @@ export type PhotoFrameProps = Pick<
|
||||
isInHiddenSection?: boolean;
|
||||
setFilesDownloadProgressAttributesCreator?: SetFilesDownloadProgressAttributesCreator;
|
||||
selectable?: boolean;
|
||||
onSyncWithRemote: () => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -151,12 +158,11 @@ const PhotoFrame = ({
|
||||
mode,
|
||||
modePlus,
|
||||
files,
|
||||
syncWithRemote,
|
||||
setSelected,
|
||||
selected,
|
||||
favoriteFileIDs,
|
||||
markUnsyncedFavoriteUpdate,
|
||||
markTempDeleted,
|
||||
onMarkUnsyncedFavoriteUpdate,
|
||||
onMarkTempDeleted,
|
||||
activeCollectionID,
|
||||
activePersonID,
|
||||
enableDownload,
|
||||
@@ -167,6 +173,7 @@ const PhotoFrame = ({
|
||||
isInHiddenSection,
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
selectable,
|
||||
onSyncWithRemote,
|
||||
onSelectCollection,
|
||||
onSelectPerson,
|
||||
}: PhotoFrameProps) => {
|
||||
@@ -259,10 +266,23 @@ const PhotoFrame = ({
|
||||
}, [selected]);
|
||||
|
||||
const handleTriggerSyncWithRemote = useCallback(
|
||||
() => void syncWithRemote(),
|
||||
[syncWithRemote],
|
||||
() => void onSyncWithRemote(),
|
||||
[onSyncWithRemote],
|
||||
);
|
||||
|
||||
const handleToggleFavorite = useMemo(() => {
|
||||
return favoriteFileIDs
|
||||
? async (file: EnteFile) => {
|
||||
const isFavorite = favoriteFileIDs!.has(file.id);
|
||||
await (isFavorite ? removeFromFavorites : addToFavorites)(
|
||||
file,
|
||||
true,
|
||||
);
|
||||
onMarkUnsyncedFavoriteUpdate(file.id, !isFavorite);
|
||||
}
|
||||
: undefined;
|
||||
}, [favoriteFileIDs, onMarkUnsyncedFavoriteUpdate]);
|
||||
|
||||
const handleSaveEditedImageCopy = useCallback(
|
||||
(editedFile: File, collection: Collection, enteFile: EnteFile) => {
|
||||
uploadManager.prepareForNewUpload();
|
||||
@@ -300,7 +320,7 @@ const PhotoFrame = ({
|
||||
throw new Error("Not implemented");
|
||||
} else {
|
||||
setOpen(false);
|
||||
needUpdate && syncWithRemote();
|
||||
needUpdate && onSyncWithRemote();
|
||||
setIsPhotoSwipeOpen?.(false);
|
||||
}
|
||||
};
|
||||
@@ -550,10 +570,11 @@ const PhotoFrame = ({
|
||||
user={galleryContext.user ?? undefined}
|
||||
files={files}
|
||||
initialIndex={currentIndex}
|
||||
isInTrashSection={activeCollectionID === TRASH_SECTION}
|
||||
isInHiddenSection={isInHiddenSection}
|
||||
disableDownload={!enableDownload}
|
||||
isInHiddenSection={isInHiddenSection}
|
||||
isInTrashSection={activeCollectionID === TRASH_SECTION}
|
||||
onTriggerSyncWithRemote={handleTriggerSyncWithRemote}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
onSaveEditedImageCopy={handleSaveEditedImageCopy}
|
||||
{...{
|
||||
favoriteFileIDs,
|
||||
@@ -591,8 +612,8 @@ const PhotoFrame = ({
|
||||
enableDownload={enableDownload}
|
||||
{...{
|
||||
favoriteFileIDs,
|
||||
markUnsyncedFavoriteUpdate,
|
||||
markTempDeleted,
|
||||
onMarkUnsyncedFavoriteUpdate,
|
||||
onMarkTempDeleted,
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
fileCollectionIDs,
|
||||
allCollectionsNameByID,
|
||||
|
||||
@@ -76,8 +76,8 @@ import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
|
||||
export type PhotoViewerProps = Pick<
|
||||
PhotoFrameProps,
|
||||
| "favoriteFileIDs"
|
||||
| "markUnsyncedFavoriteUpdate"
|
||||
| "markTempDeleted"
|
||||
| "onMarkUnsyncedFavoriteUpdate"
|
||||
| "onMarkTempDeleted"
|
||||
| "fileCollectionIDs"
|
||||
| "allCollectionsNameByID"
|
||||
| "onSelectCollection"
|
||||
@@ -132,8 +132,8 @@ export const PhotoViewer: React.FC<PhotoViewerProps> = ({
|
||||
gettingData,
|
||||
forceConvertItem,
|
||||
favoriteFileIDs,
|
||||
markUnsyncedFavoriteUpdate,
|
||||
markTempDeleted,
|
||||
onMarkUnsyncedFavoriteUpdate,
|
||||
onMarkTempDeleted,
|
||||
isTrashCollection,
|
||||
isInHiddenSection,
|
||||
enableDownload,
|
||||
@@ -511,16 +511,16 @@ export const PhotoViewer: React.FC<PhotoViewerProps> = ({
|
||||
const isFavorite = favoriteFileIDs!.has(file.id);
|
||||
|
||||
if (!isFavorite) {
|
||||
markUnsyncedFavoriteUpdate(file.id, true);
|
||||
onMarkUnsyncedFavoriteUpdate(file.id, true);
|
||||
void addToFavorites(file).catch((e: unknown) => {
|
||||
log.error("Failed to add favorite", e);
|
||||
markUnsyncedFavoriteUpdate(file.id, undefined);
|
||||
onMarkUnsyncedFavoriteUpdate(file.id, undefined);
|
||||
});
|
||||
} else {
|
||||
markUnsyncedFavoriteUpdate(file.id, false);
|
||||
onMarkUnsyncedFavoriteUpdate(file.id, false);
|
||||
void removeFromFavorites(file).catch((e: unknown) => {
|
||||
log.error("Failed to remove favorite", e);
|
||||
markUnsyncedFavoriteUpdate(file.id, undefined);
|
||||
onMarkUnsyncedFavoriteUpdate(file.id, undefined);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -538,7 +538,7 @@ export const PhotoViewer: React.FC<PhotoViewerProps> = ({
|
||||
const handleDeleteFile = async () => {
|
||||
const file = fileToDelete!;
|
||||
await moveToTrash([file]);
|
||||
markTempDeleted?.([file]);
|
||||
onMarkTempDeleted?.([file]);
|
||||
updateItems(items.filter((item) => item.id !== file.id));
|
||||
setFileToDelete(undefined);
|
||||
needUpdate.current = true;
|
||||
|
||||
@@ -515,88 +515,96 @@ const Page: React.FC = () => {
|
||||
};
|
||||
}, [selectAll, clearSelection]);
|
||||
|
||||
const showSessionExpiredDialog = () =>
|
||||
showMiniDialog(sessionExpiredDialogAttributes(logout));
|
||||
const showSessionExpiredDialog = useCallback(
|
||||
() => showMiniDialog(sessionExpiredDialogAttributes(logout)),
|
||||
[showMiniDialog, logout],
|
||||
);
|
||||
|
||||
const syncWithRemote = async (force = false, silent = false) => {
|
||||
if (!navigator.onLine) return;
|
||||
if (syncInProgress.current && !force) {
|
||||
resync.current = { force, silent };
|
||||
return;
|
||||
}
|
||||
const isForced = syncInProgress.current && force;
|
||||
syncInProgress.current = true;
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
const handleSyncWithRemote = useCallback(
|
||||
async (force = false, silent = false) => {
|
||||
if (!navigator.onLine) return;
|
||||
if (syncInProgress.current && !force) {
|
||||
resync.current = { force, silent };
|
||||
return;
|
||||
}
|
||||
const tokenValid = await isTokenValid(token);
|
||||
if (!tokenValid) {
|
||||
throw new Error(CustomError.SESSION_EXPIRED);
|
||||
const isForced = syncInProgress.current && force;
|
||||
syncInProgress.current = true;
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const tokenValid = await isTokenValid(token);
|
||||
if (!tokenValid) {
|
||||
throw new Error(CustomError.SESSION_EXPIRED);
|
||||
}
|
||||
!silent && showLoadingBar();
|
||||
await preCollectionsAndFilesSync();
|
||||
const allCollections = await getAllLatestCollections();
|
||||
const [hiddenCollections, collections] = splitByPredicate(
|
||||
allCollections,
|
||||
isHiddenCollection,
|
||||
);
|
||||
dispatch({
|
||||
type: "setAllCollections",
|
||||
collections,
|
||||
hiddenCollections,
|
||||
});
|
||||
const didUpdateNormalFiles = await syncFiles(
|
||||
"normal",
|
||||
collections,
|
||||
(files) => dispatch({ type: "setFiles", files }),
|
||||
(files) => dispatch({ type: "fetchFiles", files }),
|
||||
);
|
||||
const didUpdateHiddenFiles = await syncFiles(
|
||||
"hidden",
|
||||
hiddenCollections,
|
||||
(hiddenFiles) =>
|
||||
dispatch({ type: "setHiddenFiles", hiddenFiles }),
|
||||
(hiddenFiles) =>
|
||||
dispatch({ type: "fetchHiddenFiles", hiddenFiles }),
|
||||
);
|
||||
if (didUpdateNormalFiles || didUpdateHiddenFiles)
|
||||
exportService.onLocalFilesUpdated();
|
||||
await syncTrash(allCollections, (trashedFiles: EnteFile[]) =>
|
||||
dispatch({ type: "setTrashedFiles", trashedFiles }),
|
||||
);
|
||||
// syncWithRemote is called with the force flag set to true before
|
||||
// doing an upload. So it is possible, say when resuming a pending
|
||||
// upload, that we get two syncWithRemotes happening in parallel.
|
||||
//
|
||||
// Do the non-file-related sync only for one of these parallel ones.
|
||||
if (!isForced) {
|
||||
await sync();
|
||||
}
|
||||
} catch (e) {
|
||||
switch (e.message) {
|
||||
case CustomError.SESSION_EXPIRED:
|
||||
showSessionExpiredDialog();
|
||||
break;
|
||||
case CustomError.KEY_MISSING:
|
||||
clearKeys();
|
||||
router.push(PAGES.CREDENTIALS);
|
||||
break;
|
||||
default:
|
||||
log.error("syncWithRemote failed", e);
|
||||
}
|
||||
} finally {
|
||||
dispatch({ type: "clearUnsyncedState" });
|
||||
!silent && hideLoadingBar();
|
||||
}
|
||||
!silent && showLoadingBar();
|
||||
await preCollectionsAndFilesSync();
|
||||
const allCollections = await getAllLatestCollections();
|
||||
const [hiddenCollections, collections] = splitByPredicate(
|
||||
allCollections,
|
||||
isHiddenCollection,
|
||||
);
|
||||
dispatch({
|
||||
type: "setAllCollections",
|
||||
collections,
|
||||
hiddenCollections,
|
||||
});
|
||||
const didUpdateNormalFiles = await syncFiles(
|
||||
"normal",
|
||||
collections,
|
||||
(files) => dispatch({ type: "setFiles", files }),
|
||||
(files) => dispatch({ type: "fetchFiles", files }),
|
||||
);
|
||||
const didUpdateHiddenFiles = await syncFiles(
|
||||
"hidden",
|
||||
hiddenCollections,
|
||||
(hiddenFiles) =>
|
||||
dispatch({ type: "setHiddenFiles", hiddenFiles }),
|
||||
(hiddenFiles) =>
|
||||
dispatch({ type: "fetchHiddenFiles", hiddenFiles }),
|
||||
);
|
||||
if (didUpdateNormalFiles || didUpdateHiddenFiles)
|
||||
exportService.onLocalFilesUpdated();
|
||||
await syncTrash(allCollections, (trashedFiles: EnteFile[]) =>
|
||||
dispatch({ type: "setTrashedFiles", trashedFiles }),
|
||||
);
|
||||
// syncWithRemote is called with the force flag set to true before
|
||||
// doing an upload. So it is possible, say when resuming a pending
|
||||
// upload, that we get two syncWithRemotes happening in parallel.
|
||||
//
|
||||
// Do the non-file-related sync only for one of these parallel ones.
|
||||
if (!isForced) {
|
||||
await sync();
|
||||
syncInProgress.current = false;
|
||||
if (resync.current) {
|
||||
const { force, silent } = resync.current;
|
||||
setTimeout(() => handleSyncWithRemote(force, silent), 0);
|
||||
resync.current = undefined;
|
||||
}
|
||||
} catch (e) {
|
||||
switch (e.message) {
|
||||
case CustomError.SESSION_EXPIRED:
|
||||
showSessionExpiredDialog();
|
||||
break;
|
||||
case CustomError.KEY_MISSING:
|
||||
clearKeys();
|
||||
router.push(PAGES.CREDENTIALS);
|
||||
break;
|
||||
default:
|
||||
log.error("syncWithRemote failed", e);
|
||||
}
|
||||
} finally {
|
||||
dispatch({ type: "clearUnsyncedState" });
|
||||
!silent && hideLoadingBar();
|
||||
}
|
||||
syncInProgress.current = false;
|
||||
if (resync.current) {
|
||||
const { force, silent } = resync.current;
|
||||
setTimeout(() => syncWithRemote(force, silent), 0);
|
||||
resync.current = undefined;
|
||||
}
|
||||
};
|
||||
},
|
||||
[showLoadingBar, hideLoadingBar, router, showSessionExpiredDialog],
|
||||
);
|
||||
|
||||
// Alias for existing code.
|
||||
const syncWithRemote = handleSyncWithRemote;
|
||||
|
||||
const setupSelectAllKeyBoardShortcutHandler = () => {
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
@@ -695,7 +703,7 @@ const Page: React.FC = () => {
|
||||
await handleFileOps(
|
||||
ops,
|
||||
toProcessFiles,
|
||||
(files) => dispatch({ type: "markTempDeleted", files }),
|
||||
handleMarkTempDeleted,
|
||||
() => dispatch({ type: "clearTempDeleted" }),
|
||||
(files) => dispatch({ type: "markTempHidden", files }),
|
||||
() => dispatch({ type: "clearTempHidden" }),
|
||||
@@ -793,6 +801,35 @@ const Page: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleMarkUnsyncedFavoriteUpdate = useCallback(
|
||||
(fileID: number, isFavorite: boolean) =>
|
||||
dispatch({
|
||||
type: "markUnsyncedFavoriteUpdate",
|
||||
fileID,
|
||||
isFavorite,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleMarkTempDeleted = useCallback(
|
||||
(files: EnteFile[]) => dispatch({ type: "markTempDeleted", files }),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSelectCollection = useCallback(
|
||||
(collectionID: number) =>
|
||||
dispatch({
|
||||
type: "showNormalOrHiddenCollectionSummary",
|
||||
collectionSummaryID: collectionID,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSelectPerson = useCallback(
|
||||
(personID: string) => dispatch({ type: "showPerson", personID }),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleOpenCollectionSelector = useCallback(
|
||||
(attributes: CollectionSelectorAttributes) => {
|
||||
setCollectionSelectorAttributes(attributes);
|
||||
@@ -934,9 +971,7 @@ const Page: React.FC = () => {
|
||||
onSelectPeople={() =>
|
||||
dispatch({ type: "showPeople" })
|
||||
}
|
||||
onSelectPerson={(personID) =>
|
||||
dispatch({ type: "showPerson", personID })
|
||||
}
|
||||
onSelectPerson={handleSelectPerson}
|
||||
/>
|
||||
)}
|
||||
</NavbarBase>
|
||||
@@ -959,8 +994,7 @@ const Page: React.FC = () => {
|
||||
? state.view.visiblePeople
|
||||
: undefined) ?? [],
|
||||
activePerson,
|
||||
onSelectPerson: (personID) =>
|
||||
dispatch({ type: "showPerson", personID }),
|
||||
onSelectPerson: handleSelectPerson,
|
||||
setCollectionNamerAttributes,
|
||||
setPhotoListHeader,
|
||||
setFilesDownloadProgressAttributesCreator,
|
||||
@@ -1021,20 +1055,9 @@ const Page: React.FC = () => {
|
||||
mode={barMode}
|
||||
modePlus={isInSearchMode ? "search" : barMode}
|
||||
files={filteredFiles}
|
||||
syncWithRemote={syncWithRemote}
|
||||
setSelected={setSelected}
|
||||
selected={selected}
|
||||
favoriteFileIDs={state.favoriteFileIDs}
|
||||
markUnsyncedFavoriteUpdate={(fileID, isFavorite) =>
|
||||
dispatch({
|
||||
type: "markUnsyncedFavoriteUpdate",
|
||||
fileID,
|
||||
isFavorite,
|
||||
})
|
||||
}
|
||||
markTempDeleted={(files) =>
|
||||
dispatch({ type: "markTempDeleted", files })
|
||||
}
|
||||
setIsPhotoSwipeOpen={setIsPhotoSwipeOpen}
|
||||
activeCollectionID={activeCollectionID}
|
||||
activePersonID={activePerson?.id}
|
||||
@@ -1049,15 +1072,13 @@ const Page: React.FC = () => {
|
||||
setFilesDownloadProgressAttributesCreator
|
||||
}
|
||||
selectable={true}
|
||||
onSelectCollection={(collectionID) =>
|
||||
dispatch({
|
||||
type: "showNormalOrHiddenCollectionSummary",
|
||||
collectionSummaryID: collectionID,
|
||||
})
|
||||
onMarkUnsyncedFavoriteUpdate={
|
||||
handleMarkUnsyncedFavoriteUpdate
|
||||
}
|
||||
onSelectPerson={(personID) => {
|
||||
dispatch({ type: "showPerson", personID });
|
||||
}}
|
||||
onMarkTempDeleted={handleMarkTempDeleted}
|
||||
onSyncWithRemote={handleSyncWithRemote}
|
||||
onSelectCollection={handleSelectCollection}
|
||||
onSelectPerson={handleSelectPerson}
|
||||
/>
|
||||
)}
|
||||
<Export
|
||||
|
||||
@@ -60,7 +60,7 @@ import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList";
|
||||
import { Upload } from "components/Upload";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { type FileWithPath } from "react-dropzone";
|
||||
import {
|
||||
getLocalPublicCollection,
|
||||
@@ -289,7 +289,7 @@ export default function PublicCollectionGallery() {
|
||||
);
|
||||
}, [onAddPhotos]);
|
||||
|
||||
const syncWithRemote = async () => {
|
||||
const handleSyncWithRemote = useCallback(async () => {
|
||||
const collectionUID = getPublicCollectionUID(
|
||||
credentials.current.accessToken,
|
||||
);
|
||||
@@ -369,7 +369,10 @@ export default function PublicCollectionGallery() {
|
||||
hideLoadingBar();
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [showLoadingBar, hideLoadingBar]);
|
||||
|
||||
// TODO: See gallery
|
||||
const syncWithRemote = handleSyncWithRemote;
|
||||
|
||||
const verifyLinkPassword: SingleInputFormProps["callback"] = async (
|
||||
password,
|
||||
@@ -510,7 +513,7 @@ export default function PublicCollectionGallery() {
|
||||
|
||||
<PhotoFrame
|
||||
files={publicFiles}
|
||||
syncWithRemote={syncWithRemote}
|
||||
onSyncWithRemote={handleSyncWithRemote}
|
||||
setSelected={setSelected}
|
||||
selected={selected}
|
||||
activeCollectionID={ALL_SECTION}
|
||||
|
||||
@@ -139,11 +139,17 @@ export const createFavoritesCollection = () => {
|
||||
return createCollection(FAVORITE_COLLECTION_NAME, CollectionType.favorites);
|
||||
};
|
||||
|
||||
export const addToFavorites = async (file: EnteFile) => {
|
||||
await addMultipleToFavorites([file]);
|
||||
export const addToFavorites = async (
|
||||
file: EnteFile,
|
||||
disableOldWorkaround?: boolean,
|
||||
) => {
|
||||
await addMultipleToFavorites([file], disableOldWorkaround);
|
||||
};
|
||||
|
||||
export const addMultipleToFavorites = async (files: EnteFile[]) => {
|
||||
export const addMultipleToFavorites = async (
|
||||
files: EnteFile[],
|
||||
disableOldWorkaround?: boolean,
|
||||
) => {
|
||||
try {
|
||||
let favCollection = await getFavCollection();
|
||||
if (!favCollection) {
|
||||
@@ -152,10 +158,19 @@ export const addMultipleToFavorites = async (files: EnteFile[]) => {
|
||||
await addToCollection(favCollection, files);
|
||||
} catch (e) {
|
||||
log.error("failed to add to favorite", e);
|
||||
// Old code swallowed the error here. This isn't good, but to
|
||||
// avoid changing existing behaviour only new code will set the
|
||||
// disableOldWorkaround flag to instead rethrow it.
|
||||
//
|
||||
// TODO: Migrate old code, remove this flag, always throw.
|
||||
if (disableOldWorkaround) throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeFromFavorites = async (file: EnteFile) => {
|
||||
export const removeFromFavorites = async (
|
||||
file: EnteFile,
|
||||
disableOldWorkaround?: boolean,
|
||||
) => {
|
||||
try {
|
||||
const favCollection = await getFavCollection();
|
||||
if (!favCollection) {
|
||||
@@ -164,6 +179,8 @@ export const removeFromFavorites = async (file: EnteFile) => {
|
||||
await removeFromCollection(favCollection.id, [file]);
|
||||
} catch (e) {
|
||||
log.error("remove from favorite failed", e);
|
||||
// TODO: See disableOldWorkaround in addMultipleToFavorites.
|
||||
if (disableOldWorkaround) throw e;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -148,10 +148,24 @@ body {
|
||||
height: 60px;
|
||||
/* Unlike the loading indicator, "display" is used to toggle visibility, and
|
||||
the opacity is fixed to be similar to that of the counter. */
|
||||
opacity: 0.85;
|
||||
display: none;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.pswp-ente .pswp__error--active {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
/* Scale the built in controls to better fit our requirements */
|
||||
.pswp-ente .pswp__button--zoom .pswp__icn {
|
||||
transform: scale(0.85);
|
||||
}
|
||||
|
||||
.pswp-ente .pswp__button--arrow--prev .pswp__icn {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.pswp-ente .pswp__button--arrow--next .pswp__icn {
|
||||
/* default is a horizontal flip, transform: scale(-1, 1); */
|
||||
transform: scale(-0.8, 0.8);
|
||||
}
|
||||
|
||||
@@ -74,12 +74,14 @@ export const OverflowMenu: React.FC<
|
||||
{...(anchorEl ? { anchorEl } : {})}
|
||||
open={!!anchorEl}
|
||||
onClose={() => setAnchorEl(undefined)}
|
||||
MenuListProps={{
|
||||
// Disable padding at the top and bottom of the menu list.
|
||||
disablePadding: true,
|
||||
"aria-labelledby": ariaID,
|
||||
slotProps={{
|
||||
paper: { sx: menuPaperSxProps },
|
||||
list: {
|
||||
// Disable padding at the top and bottom of the menu list.
|
||||
disablePadding: true,
|
||||
"aria-labelledby": ariaID,
|
||||
},
|
||||
}}
|
||||
slotProps={{ paper: { sx: menuPaperSxProps } }}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
>
|
||||
|
||||
@@ -23,16 +23,23 @@ import React from "react";
|
||||
* It also does some trickery with a sticky opaque bar to ensure that the
|
||||
* content scrolls below our inline title bar on desktop.
|
||||
*/
|
||||
export const SidebarDrawer: React.FC<DrawerProps> = ({ children, ...rest }) => (
|
||||
export const SidebarDrawer: React.FC<DrawerProps> = ({
|
||||
slotProps,
|
||||
children,
|
||||
...rest
|
||||
}) => (
|
||||
<Drawer
|
||||
{...rest}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
maxWidth: "375px",
|
||||
width: "100%",
|
||||
scrollbarWidth: "thin",
|
||||
// Need to increase specificity to override inherited padding.
|
||||
"&&": { padding: 0 },
|
||||
slotProps={{
|
||||
...(slotProps ?? {}),
|
||||
paper: {
|
||||
sx: {
|
||||
maxWidth: "375px",
|
||||
width: "100%",
|
||||
scrollbarWidth: "thin",
|
||||
// Need to increase specificity to override inherited padding.
|
||||
"&&": { padding: 0 },
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -41,7 +41,7 @@ const getTheme = (appName: AppName): Theme => {
|
||||
*
|
||||
* 2. These can be groups of color values that have roughly the same hue, but
|
||||
* different levels of saturation. Such hue groups are arranged together into
|
||||
* a "Colors" exported by "@/mui/material":
|
||||
* a "Colors" exported by "@mui/material":
|
||||
*
|
||||
* export interface Color {
|
||||
* 50: string;
|
||||
|
||||
@@ -104,6 +104,10 @@ if (process.env.NEXT_PUBLIC_ENTE_FAMILY_URL) {
|
||||
const nextConfig = {
|
||||
// Generate a static export when we run `next build`.
|
||||
output: "export",
|
||||
// Instead of the nice and useful HMR indicator that used to exist prior to
|
||||
// 15.2, the Next.js folks have made this a persistent "branding" indicator
|
||||
// that gets in the way and needs to be disabled.
|
||||
devIndicators: false,
|
||||
compiler: {
|
||||
emotion: true,
|
||||
},
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@fontsource-variable/inter": "^5.1.1",
|
||||
"@mui/icons-material": "^6.4.3",
|
||||
"@mui/material": "^6.4.3",
|
||||
"@mui/icons-material": "^6.4.6",
|
||||
"@mui/material": "^6.4.6",
|
||||
"comlink": "^4.4.2",
|
||||
"formik": "^2.4.6",
|
||||
"get-user-locale": "^2.3.2",
|
||||
@@ -16,18 +16,18 @@
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"idb": "^8.0.2",
|
||||
"libsodium-wrappers-sumo": "^0.7.15",
|
||||
"nanoid": "^5.0.9",
|
||||
"next": "^15.1.6",
|
||||
"nanoid": "^5.1.2",
|
||||
"next": "^15.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-i18next": "^15.4.1",
|
||||
"yup": "^1.6.1",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@/build-config": "*",
|
||||
"@types/libsodium-wrappers-sumo": "^0.7.8",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3"
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ export const FileInfo: React.FC<FileInfoProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!file) return;
|
||||
if (!isMLEnabled()) return;
|
||||
|
||||
let didCancel = false;
|
||||
|
||||
@@ -317,7 +318,7 @@ export const FileInfo: React.FC<FileInfoProps> = ({
|
||||
)
|
||||
}
|
||||
/>
|
||||
{isMLEnabled() && annotatedFaces.length > 0 && (
|
||||
{annotatedFaces.length > 0 && (
|
||||
<InfoItem icon={<FaceRetouchingNaturalIcon />}>
|
||||
<FilePeopleList
|
||||
file={file}
|
||||
|
||||
@@ -15,7 +15,9 @@ if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) {
|
||||
|
||||
import { isDesktop } from "@/base/app";
|
||||
import { type ModalVisibilityProps } from "@/base/components/utils/modal";
|
||||
import { useBaseContext } from "@/base/context";
|
||||
import { lowercaseExtension } from "@/base/file-name";
|
||||
import { pt } from "@/base/i18n";
|
||||
import type { LocalUser } from "@/base/local-user";
|
||||
import log from "@/base/log";
|
||||
import {
|
||||
@@ -31,12 +33,17 @@ import {
|
||||
ImageEditorOverlay,
|
||||
type ImageEditorOverlayProps,
|
||||
} from "@/new/photos/components/ImageEditorOverlay";
|
||||
import { Button, styled } from "@mui/material";
|
||||
import { Button, Menu, MenuItem, styled } from "@mui/material";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { fileInfoExifForFile } from "./data-source";
|
||||
import {
|
||||
FileViewerPhotoSwipe,
|
||||
moreButtonID,
|
||||
moreMenuID,
|
||||
resetMoreMenuButtonOnMenuClose,
|
||||
type FileViewerAnnotatedFile,
|
||||
type FileViewerFileAnnotation,
|
||||
type FileViewerPhotoSwipeDelegate,
|
||||
} from "./photoswipe";
|
||||
|
||||
export type FileViewerProps = ModalVisibilityProps & {
|
||||
@@ -106,6 +113,16 @@ export type FileViewerProps = ModalVisibilityProps & {
|
||||
* not defined, then this prop is not used.
|
||||
*/
|
||||
onTriggerSyncWithRemote?: () => void;
|
||||
/**
|
||||
* Called when the favorite status of given {@link file} should be toggled
|
||||
* from its current value.
|
||||
*
|
||||
* If this is not provided then the favorite toggle button will not be shown
|
||||
* in the file actions.
|
||||
*
|
||||
* See also {@link favoriteFileIDs}.
|
||||
*/
|
||||
onToggleFavorite?: (file: EnteFile) => Promise<void>;
|
||||
/**
|
||||
* Called when the user edits an image in the image editor and asks us to
|
||||
* save their edits as a copy.
|
||||
@@ -139,16 +156,39 @@ const FileViewer: React.FC<FileViewerProps> = ({
|
||||
favoriteFileIDs,
|
||||
fileCollectionIDs,
|
||||
allCollectionsNameByID,
|
||||
onTriggerSyncWithRemote,
|
||||
onToggleFavorite,
|
||||
onSelectCollection,
|
||||
onSelectPerson,
|
||||
onTriggerSyncWithRemote,
|
||||
onSaveEditedImageCopy,
|
||||
}) => {
|
||||
const pswpRef = useRef<FileViewerPhotoSwipe | undefined>();
|
||||
const { onGenericError } = useBaseContext();
|
||||
|
||||
// There are 3 things involved in this dance:
|
||||
//
|
||||
// 1. Us, "FileViewer". We're a React component.
|
||||
// 2. The custom PhotoSwipe wrapper, "FileViewerPhotoSwipe". It is a class.
|
||||
// 3. The delegate, "FileViewerPhotoSwipeDelegate".
|
||||
//
|
||||
// The delegate acts as a bridge between us and (our custom) photoswipe
|
||||
// class, to avoid recreating the class each time a "dynamic" prop changes.
|
||||
// The delegate has a stable identity, we just keep updating the callback
|
||||
// functions that it holds.
|
||||
//
|
||||
// The word "dynamic" here means a prop on whose change we should not
|
||||
// recreate the photoswipe dialog.
|
||||
const delegateRef = useRef<FileViewerPhotoSwipeDelegate | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// We also need to maintain a ref to the currently displayed dialog since we
|
||||
// might need to ask it to refresh its contents.
|
||||
const psRef = useRef<FileViewerPhotoSwipe | undefined>(undefined);
|
||||
|
||||
// Whenever we get a callback from our custom PhotoSwipe instance, we also
|
||||
// get the active file on which that action was performed as an argument.
|
||||
// Save it as a prop so that the rest of our React tree can use it.
|
||||
// get the active file on which that action was performed as an argument. We
|
||||
// save it as the `activeAnnotatedFile` state so that the rest of our React
|
||||
// tree can use it.
|
||||
//
|
||||
// This is not guaranteed, or even intended, to be in sync with the active
|
||||
// file shown within the file viewer. All that this guarantees is this will
|
||||
@@ -156,13 +196,16 @@ const FileViewer: React.FC<FileViewerProps> = ({
|
||||
const [activeAnnotatedFile, setActiveAnnotatedFile] = useState<
|
||||
FileViewerAnnotatedFile | undefined
|
||||
>(undefined);
|
||||
// With semantics similar to activeFile, this is the exif data associated
|
||||
// with the activeAnnotatedFile, if any.
|
||||
|
||||
// With semantics similar to `activeAnnotatedFile`, this is the exif data
|
||||
// associated with the `activeAnnotatedFile`, if any.
|
||||
const [activeFileExif, setActiveFileExif] = useState<
|
||||
FileInfoExif | undefined
|
||||
>(undefined);
|
||||
|
||||
const [openFileInfo, setOpenFileInfo] = useState(false);
|
||||
const [moreMenuAnchorEl, setMoreMenuAnchorEl] =
|
||||
useState<HTMLElement | null>(null);
|
||||
const [openImageEditor, setOpenImageEditor] = useState(false);
|
||||
|
||||
// If `true`, then we need to trigger a sync with remote when we close.
|
||||
@@ -170,48 +213,18 @@ const FileViewer: React.FC<FileViewerProps> = ({
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setNeedsSync((needSync) => {
|
||||
console.log("needs sync", needSync);
|
||||
if (needSync) onTriggerSyncWithRemote?.();
|
||||
return false;
|
||||
});
|
||||
setOpenFileInfo(false);
|
||||
setOpenImageEditor(false);
|
||||
// No need to `resetMoreMenuButtonOnMenuClose` since we're closing
|
||||
// anyway and it'll be removed from the DOM.
|
||||
setMoreMenuAnchorEl(null);
|
||||
onClose();
|
||||
}, [onTriggerSyncWithRemote, onClose]);
|
||||
|
||||
const handleAnnotate = useCallback(
|
||||
(file: EnteFile) => {
|
||||
log.debug(() => ["viewer", { action: "annotate", file }]);
|
||||
const fileID = file.id;
|
||||
const isOwnFile = file.ownerID == user?.id;
|
||||
const canFavoriteOrEdit =
|
||||
isOwnFile && !isInTrashSection && !isInHiddenSection;
|
||||
const isFavorite = canFavoriteOrEdit
|
||||
? favoriteFileIDs?.has(file.id)
|
||||
: undefined;
|
||||
const isEditableImage =
|
||||
onSaveEditedImageCopy && canFavoriteOrEdit
|
||||
? fileIsEditableImage(file)
|
||||
: undefined;
|
||||
return { fileID, isOwnFile, isFavorite, isEditableImage };
|
||||
},
|
||||
[
|
||||
user,
|
||||
isInTrashSection,
|
||||
isInHiddenSection,
|
||||
favoriteFileIDs,
|
||||
onSaveEditedImageCopy,
|
||||
],
|
||||
);
|
||||
|
||||
const handleToggleFavorite = useMemo(() => {
|
||||
return favoriteFileIDs
|
||||
? (annotatedFile: FileViewerAnnotatedFile) => {
|
||||
setActiveAnnotatedFile(annotatedFile);
|
||||
console.log("handleToggleFavorite", annotatedFile);
|
||||
}
|
||||
: undefined;
|
||||
}, [favoriteFileIDs]);
|
||||
|
||||
const handleViewInfo = useCallback(
|
||||
(annotatedFile: FileViewerAnnotatedFile) => {
|
||||
setActiveAnnotatedFile(annotatedFile);
|
||||
@@ -225,7 +238,64 @@ const FileViewer: React.FC<FileViewerProps> = ({
|
||||
[],
|
||||
);
|
||||
|
||||
const handleInfoClose = useCallback(() => setOpenFileInfo(false), []);
|
||||
const handleFileInfoClose = useCallback(() => setOpenFileInfo(false), []);
|
||||
|
||||
const handleMore = useCallback(
|
||||
(
|
||||
annotatedFile: FileViewerAnnotatedFile,
|
||||
buttonElement: HTMLElement,
|
||||
) => {
|
||||
setActiveAnnotatedFile(annotatedFile);
|
||||
setMoreMenuAnchorEl(buttonElement);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleMoreMenuClose = useCallback(() => {
|
||||
setMoreMenuAnchorEl((el) => {
|
||||
resetMoreMenuButtonOnMenuClose(el);
|
||||
return null;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleEditImage = useMemo(() => {
|
||||
return onSaveEditedImageCopy
|
||||
? () => {
|
||||
handleMoreMenuClose();
|
||||
setOpenImageEditor(true);
|
||||
}
|
||||
: undefined;
|
||||
}, [onSaveEditedImageCopy, handleMoreMenuClose]);
|
||||
|
||||
const handleImageEditorClose = useCallback(
|
||||
() => setOpenImageEditor(false),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleSaveEditedCopy = useCallback(
|
||||
(editedFile: File, collection: Collection, enteFile: EnteFile) => {
|
||||
onSaveEditedImageCopy(editedFile, collection, enteFile);
|
||||
handleClose();
|
||||
},
|
||||
[onSaveEditedImageCopy, handleClose],
|
||||
);
|
||||
|
||||
const handleAnnotate = useCallback(
|
||||
(file: EnteFile): FileViewerFileAnnotation => {
|
||||
log.debug(() => ["viewer", { action: "annotate", file }]);
|
||||
const fileID = file.id;
|
||||
const isOwnFile = file.ownerID == user?.id;
|
||||
const canModify =
|
||||
isOwnFile && !isInTrashSection && !isInHiddenSection;
|
||||
const showFavorite = canModify;
|
||||
const isEditableImage =
|
||||
handleEditImage && canModify
|
||||
? fileIsEditableImage(file)
|
||||
: undefined;
|
||||
return { fileID, isOwnFile, showFavorite, isEditableImage };
|
||||
},
|
||||
[user, isInTrashSection, isInHiddenSection, handleEditImage],
|
||||
);
|
||||
|
||||
const handleScheduleUpdate = useCallback(() => setNeedsSync(true), []);
|
||||
|
||||
@@ -246,97 +316,116 @@ const FileViewer: React.FC<FileViewerProps> = ({
|
||||
: undefined;
|
||||
}, [onSelectPerson, handleClose]);
|
||||
|
||||
const handleEditImage = useMemo(() => {
|
||||
return onSaveEditedImageCopy
|
||||
? (annotatedFile: FileViewerAnnotatedFile) => {
|
||||
setActiveAnnotatedFile(annotatedFile);
|
||||
setOpenImageEditor(true);
|
||||
}
|
||||
: undefined;
|
||||
}, [onSaveEditedImageCopy]);
|
||||
const haveUser = !!user;
|
||||
|
||||
const handleImageEditorClose = useCallback(
|
||||
() => setOpenImageEditor(false),
|
||||
[],
|
||||
);
|
||||
const getFiles = useCallback(() => files, [files]);
|
||||
|
||||
const handleSaveEditedCopy = useCallback(
|
||||
(editedFile: File, collection: Collection, enteFile: EnteFile) => {
|
||||
onSaveEditedImageCopy(editedFile, collection, enteFile);
|
||||
handleImageEditorClose();
|
||||
handleClose();
|
||||
const isFavorite = useCallback(
|
||||
({ file }: FileViewerAnnotatedFile) => {
|
||||
if (!haveUser || !favoriteFileIDs || !onToggleFavorite) {
|
||||
return undefined;
|
||||
}
|
||||
return favoriteFileIDs.has(file.id);
|
||||
},
|
||||
[onSaveEditedImageCopy, handleImageEditorClose, handleClose],
|
||||
[haveUser, favoriteFileIDs, onToggleFavorite],
|
||||
);
|
||||
|
||||
const toggleFavorite = useCallback(
|
||||
({ file }: FileViewerAnnotatedFile) =>
|
||||
onToggleFavorite!(file)
|
||||
.then(handleScheduleUpdate)
|
||||
.catch(onGenericError),
|
||||
[onToggleFavorite, handleScheduleUpdate, onGenericError],
|
||||
);
|
||||
|
||||
// Initial value of delegate.
|
||||
if (!delegateRef.current) {
|
||||
delegateRef.current = { getFiles, isFavorite, toggleFavorite };
|
||||
}
|
||||
|
||||
// Updates to delegate callbacks.
|
||||
useEffect(() => {
|
||||
const delegate = delegateRef.current!;
|
||||
delegate.getFiles = getFiles;
|
||||
delegate.isFavorite = isFavorite;
|
||||
delegate.toggleFavorite = toggleFavorite;
|
||||
}, [getFiles, isFavorite, toggleFavorite]);
|
||||
|
||||
useEffect(() => {
|
||||
log.debug(() => ["viewer", { action: "useEffect", open }]);
|
||||
if (open) {
|
||||
// We're open. Create psRef. This will show the file viewer dialog.
|
||||
log.debug(() => ["viewer", { action: "open" }]);
|
||||
|
||||
if (!open) {
|
||||
// The close state will be handled by the cleanup function.
|
||||
return;
|
||||
const pswp = new FileViewerPhotoSwipe({
|
||||
initialIndex,
|
||||
disableDownload,
|
||||
haveUser,
|
||||
delegate: delegateRef.current!,
|
||||
onClose: handleClose,
|
||||
onAnnotate: handleAnnotate,
|
||||
onViewInfo: handleViewInfo,
|
||||
onMore: handleMore,
|
||||
});
|
||||
|
||||
psRef.current = pswp;
|
||||
|
||||
return () => {
|
||||
// Close dialog in the effect callback.
|
||||
log.debug(() => ["viewer", { action: "close" }]);
|
||||
pswp.closeIfNeeded();
|
||||
};
|
||||
}
|
||||
|
||||
const pswp = new FileViewerPhotoSwipe({
|
||||
files,
|
||||
initialIndex,
|
||||
disableDownload,
|
||||
onClose: handleClose,
|
||||
onAnnotate: handleAnnotate,
|
||||
onToggleFavorite: handleToggleFavorite,
|
||||
onViewInfo: handleViewInfo,
|
||||
onEditImage: handleEditImage,
|
||||
});
|
||||
pswpRef.current = pswp;
|
||||
|
||||
return () => {
|
||||
log.debug(() => [
|
||||
"viewer",
|
||||
{ action: "useEffect/cleanup", pswpRef: pswpRef.current },
|
||||
]);
|
||||
pswpRef.current?.closeIfNeeded();
|
||||
pswpRef.current = undefined;
|
||||
};
|
||||
// The hook is missing dependencies; this is intentional - we don't want
|
||||
// to recreate the PhotoSwipe dialog when these dependencies change.
|
||||
//
|
||||
// - Updates to initialIndex can be safely ignored: they don't matter,
|
||||
// only their initial value at the time of open mattered.
|
||||
//
|
||||
// - Updates to other properties are not expected after open. We could've
|
||||
// also added it to the dependencies array, not adding it was a more
|
||||
// conservative choice to be on the safer side and trigger too few
|
||||
// instead of too many updates.
|
||||
//
|
||||
// - Updates to files matter, but these are conveyed separately.
|
||||
// TODO(PS):
|
||||
//
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, onClose, handleViewInfo]);
|
||||
}, [
|
||||
open,
|
||||
onClose,
|
||||
user,
|
||||
initialIndex,
|
||||
disableDownload,
|
||||
haveUser,
|
||||
handleClose,
|
||||
handleAnnotate,
|
||||
handleViewInfo,
|
||||
handleMore,
|
||||
]);
|
||||
|
||||
const handleRefreshPhotoswipe = useCallback(() => {
|
||||
pswpRef.current.refreshCurrentSlideContent();
|
||||
psRef.current!.refreshCurrentSlideContent();
|
||||
}, []);
|
||||
|
||||
log.debug(() => ["viewer", { action: "render", pswpRef: pswpRef.current }]);
|
||||
log.debug(() => ["viewer", { action: "render", psRef: psRef.current }]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Button>Test</Button>
|
||||
<FileInfo
|
||||
open={openFileInfo}
|
||||
onClose={handleInfoClose}
|
||||
onClose={handleFileInfoClose}
|
||||
file={activeAnnotatedFile?.file}
|
||||
exif={activeFileExif}
|
||||
allowEdits={!!activeAnnotatedFile?.annotation.isOwnFile}
|
||||
allowMap={!!user}
|
||||
showCollections={!!user}
|
||||
allowMap={haveUser}
|
||||
showCollections={haveUser}
|
||||
scheduleUpdate={handleScheduleUpdate}
|
||||
refreshPhotoswipe={handleRefreshPhotoswipe}
|
||||
onSelectCollection={handleSelectCollection}
|
||||
onSelectPerson={handleSelectPerson}
|
||||
{...{ fileCollectionIDs, allCollectionsNameByID }}
|
||||
/>
|
||||
<Menu
|
||||
open={!!moreMenuAnchorEl}
|
||||
onClose={handleMoreMenuClose}
|
||||
anchorEl={moreMenuAnchorEl}
|
||||
id={moreMenuID}
|
||||
slotProps={{
|
||||
list: { "aria-labelledby": moreButtonID },
|
||||
}}
|
||||
>
|
||||
{activeAnnotatedFile?.annotation.isEditableImage && (
|
||||
<MenuItem onClick={handleEditImage}>
|
||||
{/*TODO */ pt("Edit image")}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
<ImageEditorOverlay
|
||||
open={openImageEditor}
|
||||
onClose={handleImageEditorClose}
|
||||
|
||||
@@ -224,8 +224,11 @@ export const itemDataForFile = (file: EnteFile, needsRefresh: () => void) => {
|
||||
* This is called when the user moves away from a slide so that we attempt a
|
||||
* full retry when they come back the next time.
|
||||
*/
|
||||
export const forgetFailedItemDataForFile = (file: EnteFile) =>
|
||||
forgetFailedItemDataForFileID(file.id);
|
||||
export const forgetFailedItemDataForFileID = (fileID: number) => {
|
||||
if (_state.itemDataByFileID.get(fileID)?.fetchFailed) {
|
||||
_state.itemDataByFileID.delete(fileID);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Forget item data for the all files whose fetch had failed.
|
||||
@@ -236,12 +239,6 @@ export const forgetFailedItemDataForFile = (file: EnteFile) =>
|
||||
export const forgetFailedItems = () =>
|
||||
[..._state.itemDataByFileID.keys()].forEach(forgetFailedItemDataForFileID);
|
||||
|
||||
const forgetFailedItemDataForFileID = (fileID: number) => {
|
||||
if (_state.itemDataByFileID.get(fileID)?.fetchFailed) {
|
||||
_state.itemDataByFileID.delete(fileID);
|
||||
}
|
||||
};
|
||||
|
||||
const enqueueUpdates = async (file: EnteFile) => {
|
||||
const fileID = file.id;
|
||||
const fileType = file.metadata.fileType;
|
||||
|
||||
@@ -21,14 +21,14 @@ const paths = {
|
||||
info: '<path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8" transform="translate(3.5, 3.5)"',
|
||||
// "@mui/icons-material/ErrorOutline"
|
||||
error: '<path d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2M12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8" transform="translate(7, 5.7) scale(0.85)"',
|
||||
// "@mui/icons-material/Edit"
|
||||
edit: '<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75z" transform="translate(3, 3) scale(0.97)"',
|
||||
// "@mui/icons-material/FavoriteBorderRounded"
|
||||
favorite:
|
||||
'<path d="M19.66 3.99c-2.64-1.8-5.9-.96-7.66 1.1-1.76-2.06-5.02-2.91-7.66-1.1-1.4.96-2.28 2.58-2.34 4.29-.14 3.88 3.3 6.99 8.55 11.76l.1.09c.76.69 1.93.69 2.69-.01l.11-.1c5.25-4.76 8.68-7.87 8.55-11.75-.06-1.7-.94-3.32-2.34-4.28M12.1 18.55l-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05" transform="translate(3, 3)"',
|
||||
// "@mui/icons-material/FavoriteRounded"
|
||||
unfavorite:
|
||||
'<path d="M13.35 20.13c-.76.69-1.93.69-2.69-.01l-.11-.1C5.3 15.27 1.87 12.16 2 8.28c.06-1.7.93-3.33 2.34-4.29 2.64-1.8 5.9-.96 7.66 1.1 1.76-2.06 5.02-2.91 7.66-1.1 1.41.96 2.28 2.59 2.34 4.29.14 3.88-3.3 6.99-8.55 11.76z" transform="translate(3, 3)"',
|
||||
// "@mui/icons-material/MoreHoriz"
|
||||
more: '<path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2m12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2m-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2" transform="translate(3, 4)"',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,12 +9,11 @@ import { t } from "i18next";
|
||||
import {
|
||||
forgetExif,
|
||||
forgetExifForItemData,
|
||||
forgetFailedItemDataForFile,
|
||||
forgetFailedItemDataForFileID,
|
||||
forgetFailedItems,
|
||||
itemDataForFile,
|
||||
updateFileInfoExifIfNeeded,
|
||||
} from "./data-source";
|
||||
import type { FileViewerProps } from "./FileViewer";
|
||||
import { createPSRegisterElementIconHTML } from "./icons";
|
||||
|
||||
// TODO(PS): WIP gallery using upstream photoswipe
|
||||
@@ -51,55 +50,101 @@ export interface FileViewerFileAnnotation {
|
||||
*/
|
||||
isOwnFile: boolean;
|
||||
/**
|
||||
* `true` if this file has been marked as a favorite by the user.
|
||||
*
|
||||
* The toggle favorite button will not be shown if this is not defined.
|
||||
* Otherwise it determines the toggle state of the toggle favorite button.
|
||||
* `true` if the toggle favorite action should be shown for this file.
|
||||
*/
|
||||
isFavorite?: boolean | undefined;
|
||||
showFavorite: boolean;
|
||||
/**
|
||||
* `true` if this is an image which can be edited.
|
||||
*
|
||||
* The edit button is shown when this is true. See also the
|
||||
* {@link onEditImage} option for {@link FileViewerPhotoSwipe} constructor.
|
||||
* `true` if this is an image which can be edited, and editing is possible,
|
||||
* and the edit action should therefore be shown for this file.
|
||||
*/
|
||||
isEditableImage?: boolean | undefined;
|
||||
isEditableImage: boolean;
|
||||
}
|
||||
|
||||
type FileViewerPhotoSwipeOptions = {
|
||||
export interface FileViewerPhotoSwipeDelegate {
|
||||
/**
|
||||
* Called to obtain the latest list of files.
|
||||
*
|
||||
* [Note: Changes to underlying files when file viewer is open]
|
||||
*
|
||||
* The list of files shown by the viewer might change while the viewer is
|
||||
* open. We do not actively refresh the viewer when this happens since that
|
||||
* would result in the user's zoom / pan state being lost.
|
||||
*
|
||||
* However, we always read the latest list via the delegate, so any
|
||||
* subsequent user initiated slide navigation (e.g. moving to the next
|
||||
* slide) will use the new list.
|
||||
*/
|
||||
getFiles: () => EnteFile[];
|
||||
/**
|
||||
* Return `true` if the provided file has been marked as a favorite by the
|
||||
* user.
|
||||
*
|
||||
* The toggle favorite button will not be shown for the file if
|
||||
* this callback returns `undefined`. Otherwise the return value determines
|
||||
* the toggle state of the toggle favorite button for the file.
|
||||
*/
|
||||
isFavorite: (annotatedFile: FileViewerAnnotatedFile) => boolean | undefined;
|
||||
/**
|
||||
* Called when the user activates the toggle favorite action on a file.
|
||||
*
|
||||
* The toggle favorite button will be disabled for the file until the
|
||||
* promise returned by this function returns fulfills.
|
||||
*
|
||||
* > Note: The caller is expected to handle any errors that occur, and
|
||||
* > should not reject for foreseeable failures, otherwise the button will
|
||||
* > remain in the disabled state (until the file viewer is closed).
|
||||
*/
|
||||
toggleFavorite: (annotatedFile: FileViewerAnnotatedFile) => Promise<void>;
|
||||
}
|
||||
|
||||
type FileViewerPhotoSwipeOptions = Pick<
|
||||
FileViewerProps,
|
||||
"initialIndex" | "disableDownload"
|
||||
> & {
|
||||
/**
|
||||
* `true` if we're running in the context of a logged in user, and so
|
||||
* various actions that modify the file should be shown.
|
||||
*
|
||||
* This is the static variant of various per file annotations that control
|
||||
* various modifications. If this is not `true`, then various actions like
|
||||
* favorite, delete etc are never shown. If this is `true`, then their
|
||||
* visibility depends on the corresponding annotation.
|
||||
*
|
||||
* For example, the favorite action is shown only if both this and the
|
||||
* {@link showFavorite} file annotation are true.
|
||||
*/
|
||||
haveUser: boolean;
|
||||
/**
|
||||
* Dynamic callbacks.
|
||||
*
|
||||
* The extra level of indirection allows these to be updated without
|
||||
* recreating us.
|
||||
*/
|
||||
delegate: FileViewerPhotoSwipeDelegate;
|
||||
/**
|
||||
* Called when the file viewer is closed.
|
||||
*/
|
||||
onClose: () => void;
|
||||
/**
|
||||
* Called whenever the slide changes to obtain the derived data for the file
|
||||
* that is about to be displayed.
|
||||
* Called whenever the slide is initially displayed or changes, to obtain
|
||||
* various derived data for the file that is about to be displayed.
|
||||
*/
|
||||
onAnnotate: (file: EnteFile) => FileViewerFileAnnotation;
|
||||
/**
|
||||
* Called when the user activates the toggle favorite action on a file.
|
||||
*
|
||||
* If this callback is not provided, then the toggle favorite button is not
|
||||
* shown. If this callback is provided, then the favorite button is shown if
|
||||
* the {@link isFavorite} property of {@link FileViewerFileAnnotation} for
|
||||
* the file is provided. In that case, the value of the {@link isFavorite}
|
||||
* property will determine the current toggle state of the favorite button.
|
||||
*/
|
||||
onToggleFavorite?: (annotatedFile: FileViewerAnnotatedFile) => void;
|
||||
/**
|
||||
* Called when the user activates the info action on a file.
|
||||
*/
|
||||
onViewInfo: (annotatedFile: FileViewerAnnotatedFile) => void;
|
||||
/**
|
||||
* Called when the user activates the edit action on an image.
|
||||
* Called when the user activates the more action on a file.
|
||||
*
|
||||
* If this callback is not provided, then the edit button is never shown. If
|
||||
* this callback is provided, then the visibility of the edit button is
|
||||
* determined by the {@link isEditableImage} property of
|
||||
* {@link FileViewerFileAnnotation} for the file.
|
||||
* In addition to the file, callback is also passed a reference to the HTML
|
||||
* DOM more button element.
|
||||
*/
|
||||
onEditImage?: (annotatedFile: FileViewerAnnotatedFile) => void;
|
||||
} & Pick<FileViewerProps, "files" | "initialIndex" | "disableDownload">;
|
||||
onMore: (
|
||||
annotatedFile: FileViewerAnnotatedFile,
|
||||
buttonElement: HTMLElement,
|
||||
) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A file and its annotation, in a nice cosy box.
|
||||
@@ -109,6 +154,21 @@ export interface FileViewerAnnotatedFile {
|
||||
annotation: FileViewerFileAnnotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ID that is used by the "more" action button (if one is being displayed).
|
||||
*
|
||||
* @see also {@link moreMenuID}.
|
||||
*/
|
||||
export const moreButtonID = "ente-pswp-more-button";
|
||||
|
||||
/**
|
||||
* The ID this is expected to be used by the more menu that is shown in response
|
||||
* to the more action button being activated.
|
||||
*
|
||||
* @see also {@link moreButtonID}.
|
||||
*/
|
||||
export const moreMenuID = "ente-pswp-more-menu";
|
||||
|
||||
/**
|
||||
* A wrapper over {@link PhotoSwipe} to tailor its interface for use by our file
|
||||
* viewer.
|
||||
@@ -169,18 +229,21 @@ export class FileViewerPhotoSwipe {
|
||||
* scope.
|
||||
*/
|
||||
private activeFileAnnotation: FileViewerFileAnnotation | undefined;
|
||||
/**
|
||||
* IDs of files for which a there is a favorite update in progress.
|
||||
*/
|
||||
private pendingFavoriteUpdates = new Set<number>();
|
||||
|
||||
constructor({
|
||||
files,
|
||||
initialIndex,
|
||||
disableDownload,
|
||||
haveUser,
|
||||
delegate,
|
||||
onClose,
|
||||
onAnnotate,
|
||||
onToggleFavorite,
|
||||
onViewInfo,
|
||||
onEditImage,
|
||||
onMore,
|
||||
}: FileViewerPhotoSwipeOptions) {
|
||||
this.files = files;
|
||||
this.opts = { disableDownload };
|
||||
this.lastActivityDate = new Date();
|
||||
|
||||
@@ -241,9 +304,9 @@ export class FileViewerPhotoSwipe {
|
||||
|
||||
this.pswp = pswp;
|
||||
|
||||
// Helper routines to obtain the file at `currIndex`.
|
||||
// Various helper routines to obtain the file at `currIndex`.
|
||||
|
||||
const currentFile = () => this.files[pswp.currIndex]!;
|
||||
const currentFile = () => delegate.getFiles()[pswp.currIndex]!;
|
||||
|
||||
const currentAnnotatedFile = () => {
|
||||
const file = currentFile();
|
||||
@@ -265,23 +328,18 @@ export class FileViewerPhotoSwipe {
|
||||
|
||||
const currentFileAnnotation = () => currentAnnotatedFile().annotation;
|
||||
|
||||
const withCurrentAnnotatedFile =
|
||||
(cb: (af: AnnotatedFile) => void) => () =>
|
||||
cb(currentAnnotatedFile());
|
||||
|
||||
// Provide data about slides to PhotoSwipe via callbacks
|
||||
// https://photoswipe.com/data-sources/#dynamically-generated-data
|
||||
|
||||
pswp.addFilter("numItems", () => {
|
||||
return this.files.length;
|
||||
});
|
||||
pswp.addFilter("numItems", () => delegate.getFiles().length);
|
||||
|
||||
pswp.addFilter("itemData", (_, index) => {
|
||||
const files = delegate.getFiles();
|
||||
const file = files[index]!;
|
||||
|
||||
let itemData = itemDataForFile(file, () => {
|
||||
this.pswp.refreshSlideContent(index);
|
||||
});
|
||||
let itemData = itemDataForFile(file, () =>
|
||||
pswp.refreshSlideContent(index),
|
||||
);
|
||||
|
||||
const { fileType, videoURL, ...rest } = itemData;
|
||||
if (fileType === FileType.video && videoURL) {
|
||||
@@ -367,7 +425,8 @@ export class FileViewerPhotoSwipe {
|
||||
// more than 2 slides and then back, or if they reopen the viewer.
|
||||
//
|
||||
// See: [Note: File viewer error handling]
|
||||
forgetFailedItemDataForFile(currentFile());
|
||||
const fileID = e.content?.data?.fileID;
|
||||
if (fileID) forgetFailedItemDataForFileID(fileID);
|
||||
|
||||
// Pause the video element, if any, when we move away from the
|
||||
// slide.
|
||||
@@ -391,7 +450,7 @@ export class FileViewerPhotoSwipe {
|
||||
);
|
||||
|
||||
pswp.on("change", (e) => {
|
||||
const itemData = pswp.currSlide.content.data;
|
||||
const itemData = this.pswp.currSlide.content.data;
|
||||
updateFileInfoExifIfNeeded(itemData);
|
||||
});
|
||||
|
||||
@@ -423,6 +482,17 @@ export class FileViewerPhotoSwipe {
|
||||
// - zoom: 10
|
||||
// - close: 20
|
||||
pswp.on("uiRegister", () => {
|
||||
// Move the zoom button to the left so that it is in the same place
|
||||
// as the other items like preloader or the error indicator that
|
||||
// come and go as files get loaded.
|
||||
//
|
||||
// We cannot use the PhotoSwipe "uiElement" filter to modify the
|
||||
// order since that only allows us to edit the DOM element, not the
|
||||
// underlying UI element data.
|
||||
pswp.ui.uiElementsData.find((e) => e.name == "zoom").order = 6;
|
||||
|
||||
// Register our custom elements...
|
||||
|
||||
pswp.ui.registerElement({
|
||||
name: "error",
|
||||
order: 6,
|
||||
@@ -440,22 +510,61 @@ export class FileViewerPhotoSwipe {
|
||||
},
|
||||
});
|
||||
|
||||
if (onToggleFavorite) {
|
||||
// Only one of these two will end up being shown, so they can
|
||||
// safely share the same order.
|
||||
if (haveUser) {
|
||||
const toggleFavorite = async (
|
||||
buttonElement: HTMLButtonElement,
|
||||
) => {
|
||||
const af = currentAnnotatedFile();
|
||||
this.pendingFavoriteUpdates.add(af.file.id);
|
||||
buttonElement.disabled = true;
|
||||
await delegate.toggleFavorite(af);
|
||||
// TODO: This can be improved in two ways:
|
||||
//
|
||||
// 1. We currently have a setTimeout to ensure that the
|
||||
// updated `favoriteFileIDs` have made their way to our
|
||||
// delegate before we query for the status again.
|
||||
// Obviously, this is hacky. Note that a timeout of 0
|
||||
// (i.e., just deferring till the next tick) isn't enough
|
||||
// here, for reasons I need to investigate more (hence
|
||||
// this TODO).
|
||||
//
|
||||
// 2. We reload the entire slide instead of just updating
|
||||
// the button state. This is because there are two
|
||||
// buttons, instead of a single button toggling between
|
||||
// two states (e.g. like the zoom button). A single
|
||||
// button can be achieved by moving the fill as a layer.
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
this.pendingFavoriteUpdates.delete(af.file.id);
|
||||
this.refreshCurrentSlideContent();
|
||||
};
|
||||
|
||||
const showFavoriteIf = (
|
||||
buttonElement: HTMLButtonElement,
|
||||
value: boolean,
|
||||
) => {
|
||||
const af = currentAnnotatedFile();
|
||||
const isFavorite = delegate.isFavorite(af);
|
||||
showIf(
|
||||
buttonElement,
|
||||
af.annotation.showFavorite && isFavorite === value,
|
||||
);
|
||||
buttonElement.disabled = this.pendingFavoriteUpdates.has(
|
||||
af.file.id,
|
||||
);
|
||||
};
|
||||
|
||||
// Only one of these two ("favorite" or "unfavorite") will end
|
||||
// up being shown, so they can safely share the same order.
|
||||
pswp.ui.registerElement({
|
||||
name: "favorite",
|
||||
title: t("favorite_key"),
|
||||
order: 8,
|
||||
isButton: true,
|
||||
html: createPSRegisterElementIconHTML("favorite"),
|
||||
onClick: withCurrentAnnotatedFile(onToggleFavorite),
|
||||
onClick: (e) => toggleFavorite(e.target),
|
||||
onInit: (buttonElement) =>
|
||||
pswp.on("change", () =>
|
||||
showIf(
|
||||
buttonElement,
|
||||
currentFileAnnotation().isFavorite === false,
|
||||
),
|
||||
showFavoriteIf(buttonElement, false),
|
||||
),
|
||||
});
|
||||
pswp.ui.registerElement({
|
||||
@@ -464,13 +573,10 @@ export class FileViewerPhotoSwipe {
|
||||
order: 8,
|
||||
isButton: true,
|
||||
html: createPSRegisterElementIconHTML("unfavorite"),
|
||||
onClick: withCurrentAnnotatedFile(onToggleFavorite),
|
||||
onClick: (e) => toggleFavorite(e.target),
|
||||
onInit: (buttonElement) =>
|
||||
pswp.on("change", () =>
|
||||
showIf(
|
||||
buttonElement,
|
||||
currentFileAnnotation().isFavorite === true,
|
||||
),
|
||||
showFavoriteIf(buttonElement, true),
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -481,26 +587,34 @@ export class FileViewerPhotoSwipe {
|
||||
order: 9,
|
||||
isButton: true,
|
||||
html: createPSRegisterElementIconHTML("info"),
|
||||
onClick: withCurrentAnnotatedFile(onViewInfo),
|
||||
onClick: () => onViewInfo(currentAnnotatedFile()),
|
||||
});
|
||||
|
||||
if (onEditImage) {
|
||||
if (haveUser) {
|
||||
pswp.ui.registerElement({
|
||||
name: "edit",
|
||||
name: "more",
|
||||
// TODO(PS):
|
||||
// title: t("edit_image"),
|
||||
title: pt("Edit image"),
|
||||
title: pt("More"),
|
||||
order: 16,
|
||||
isButton: true,
|
||||
html: createPSRegisterElementIconHTML("edit"),
|
||||
onClick: withCurrentAnnotatedFile(onEditImage),
|
||||
onInit: (buttonElement) =>
|
||||
html: createPSRegisterElementIconHTML("more"),
|
||||
onInit: (buttonElement) => {
|
||||
buttonElement.setAttribute("id", moreButtonID);
|
||||
buttonElement.setAttribute("aria-haspopup", "true");
|
||||
pswp.on("change", () =>
|
||||
showIf(
|
||||
buttonElement,
|
||||
!!currentFileAnnotation().isEditableImage,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onClick: (e) => {
|
||||
const buttonElement = e.target;
|
||||
// See also: `resetMoreMenuButtonOnMenuClose`.
|
||||
buttonElement.setAttribute("aria-controls", moreMenuID);
|
||||
buttonElement.setAttribute("aria-expanded", true);
|
||||
onMore(currentAnnotatedFile(), buttonElement);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -548,10 +662,6 @@ export class FileViewerPhotoSwipe {
|
||||
this.pswp.refreshSlideContent(this.pswp.currIndex);
|
||||
}
|
||||
|
||||
updateFiles(files: EnteFile[]) {
|
||||
// TODO(PS)
|
||||
}
|
||||
|
||||
private clearAutoHideIntervalIfNeeded() {
|
||||
if (this.autoHideCheckIntervalId) {
|
||||
clearInterval(this.autoHideCheckIntervalId);
|
||||
@@ -626,3 +736,12 @@ const createElementFromHTMLString = (htmlString: string) => {
|
||||
template.innerHTML = htmlString.trim();
|
||||
return template.content.firstChild;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the ARIA attributes for the button that controls the more menu when
|
||||
* the menu is closed.
|
||||
*/
|
||||
export const resetMoreMenuButtonOnMenuClose = (buttonElement: HTMLElement) => {
|
||||
buttonElement.removeAttribute("aria-controls");
|
||||
buttonElement.removeAttribute("aria-expanded");
|
||||
};
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
"@/gallery": "*",
|
||||
"@/utils": "*",
|
||||
"@ente/shared": "*",
|
||||
"@mui/material": "^6.4.3",
|
||||
"@mui/system": "^6.4.3",
|
||||
"@mui/x-date-pickers": "^7.25.0",
|
||||
"@mui/material": "^6.4.6",
|
||||
"@mui/system": "^6.4.6",
|
||||
"@mui/x-date-pickers": "^7.27.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
|
||||
@@ -73,17 +73,17 @@ export const DropdownInput = <T extends string>({
|
||||
// maxWidth to 0 forces element widths to equal minWidth.
|
||||
sx: { maxWidth: 0 },
|
||||
},
|
||||
},
|
||||
MenuListProps: {
|
||||
sx: {
|
||||
backgroundColor: "background.paper2",
|
||||
".MuiMenuItem-root": {
|
||||
color: "text.faint",
|
||||
whiteSpace: "normal",
|
||||
},
|
||||
// Make the selected item pop out by using color.
|
||||
"&&& > .Mui-selected": {
|
||||
color: "text.base",
|
||||
list: {
|
||||
sx: {
|
||||
backgroundColor: "background.paper2",
|
||||
".MuiMenuItem-root": {
|
||||
color: "text.faint",
|
||||
whiteSpace: "normal",
|
||||
},
|
||||
// Make the selected item pop out by using color.
|
||||
"&&& > .Mui-selected": {
|
||||
color: "text.base",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -654,6 +654,8 @@ export interface AnnotatedFaceID {
|
||||
export const getAnnotatedFacesForFile = async (
|
||||
file: EnteFile,
|
||||
): Promise<AnnotatedFaceID[]> => {
|
||||
if (!isMLEnabled()) return [];
|
||||
|
||||
const index = await savedFaceIndex(file.id);
|
||||
if (!index) return [];
|
||||
|
||||
|
||||
222
web/yarn.lock
222
web/yarn.lock
@@ -684,28 +684,28 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@mui/core-downloads-tracker@^6.4.3":
|
||||
version "6.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.3.tgz#aa9506e4d43e02d61fb82e23de9741fd24b22b61"
|
||||
integrity sha512-hlyOzo2ObarllAOeT1ZSAusADE5NZNencUeIvXrdQ1Na+FL1lcznhbxfV5He1KqGiuR8Az3xtCUcYKwMVGFdzg==
|
||||
"@mui/core-downloads-tracker@^6.4.6":
|
||||
version "6.4.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.6.tgz#42820be160159df81976f467ce496f5310e08eb1"
|
||||
integrity sha512-rho5Q4IscbrVmK9rCrLTJmjLjfH6m/NcqKr/mchvck0EIXlyYUB9+Z0oVmkt/+Mben43LMRYBH8q/Uzxj/c4Vw==
|
||||
|
||||
"@mui/icons-material@^6.4.3":
|
||||
version "6.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-6.4.3.tgz#11f275781f442f864ca5522a6196a0a17cc063ea"
|
||||
integrity sha512-3IY9LpjkwIJVgL/SkZQKKCUcumdHdQEsJaIavvsQze2QEztBt0HJ17naToN0DBBdhKdtwX5xXrfD6ZFUeWWk8g==
|
||||
"@mui/icons-material@^6.4.6":
|
||||
version "6.4.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-6.4.6.tgz#a26eaeae2f7f1359b48dac3fe8a8eec61640c325"
|
||||
integrity sha512-rGJBvIQQbQAlyKYljHQ8wAQS/K2/uYwvemcpygnAmCizmCI4zSF9HQPuiG8Ql4YLZ6V/uKjA3WHIYmF/8sV+pQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.26.0"
|
||||
|
||||
"@mui/material@^6.4.3":
|
||||
version "6.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-6.4.3.tgz#18cb003b6526bf4b3ad6f2bb46ab53154a51b19f"
|
||||
integrity sha512-ubtQjplbWneIEU8Y+4b2VA0CDBlyH5I3AmVFGmsLyDe/bf0ubxav5t11c8Afem6rkSFWPlZA2DilxmGka1xiKQ==
|
||||
"@mui/material@^6.4.6":
|
||||
version "6.4.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-6.4.6.tgz#6114a02977735d70170243efc487aed0ca974197"
|
||||
integrity sha512-6UyAju+DBOdMogfYmLiT3Nu7RgliorimNBny1pN/acOjc+THNFVE7hlxLyn3RDONoZJNDi/8vO4AQQr6dLAXqA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.26.0"
|
||||
"@mui/core-downloads-tracker" "^6.4.3"
|
||||
"@mui/system" "^6.4.3"
|
||||
"@mui/core-downloads-tracker" "^6.4.6"
|
||||
"@mui/system" "^6.4.6"
|
||||
"@mui/types" "^7.2.21"
|
||||
"@mui/utils" "^6.4.3"
|
||||
"@mui/utils" "^6.4.6"
|
||||
"@popperjs/core" "^2.11.8"
|
||||
"@types/react-transition-group" "^4.4.12"
|
||||
clsx "^2.1.1"
|
||||
@@ -714,19 +714,19 @@
|
||||
react-is "^19.0.0"
|
||||
react-transition-group "^4.4.5"
|
||||
|
||||
"@mui/private-theming@^6.4.3":
|
||||
version "6.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-6.4.3.tgz#40d7d95316e9e52d465f0c96da23f9fb8f6a989f"
|
||||
integrity sha512-7x9HaNwDCeoERc4BoEWLieuzKzXu5ZrhRnEM6AUcRXUScQLvF1NFkTlP59+IJfTbEMgcGg1wWHApyoqcksrBpQ==
|
||||
"@mui/private-theming@^6.4.6":
|
||||
version "6.4.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-6.4.6.tgz#77c0b150be94c061b34b34ce00eb60cdfb92837f"
|
||||
integrity sha512-T5FxdPzCELuOrhpA2g4Pi6241HAxRwZudzAuL9vBvniuB5YU82HCmrARw32AuCiyTfWzbrYGGpZ4zyeqqp9RvQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.26.0"
|
||||
"@mui/utils" "^6.4.3"
|
||||
"@mui/utils" "^6.4.6"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/styled-engine@^6.4.3":
|
||||
version "6.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-6.4.3.tgz#fbd7a6b925dfaeaa84ffbf8ed9be78a0ff0b3d6e"
|
||||
integrity sha512-OC402VfK+ra2+f12Gef8maY7Y9n7B6CZcoQ9u7mIkh/7PKwW/xH81xwX+yW+Ak1zBT3HYcVjh2X82k5cKMFGoQ==
|
||||
"@mui/styled-engine@^6.4.6":
|
||||
version "6.4.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-6.4.6.tgz#cd0783adbb066a349e1995f0e1a7b8c3c2d59738"
|
||||
integrity sha512-vSWYc9ZLX46be5gP+FCzWVn5rvDr4cXC5JBZwSIkYk9xbC7GeV+0kCvB8Q6XLFQJy+a62bbqtmdwS4Ghi9NBlQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.26.0"
|
||||
"@emotion/cache" "^11.13.5"
|
||||
@@ -735,16 +735,16 @@
|
||||
csstype "^3.1.3"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/system@^6.4.3":
|
||||
version "6.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@mui/system/-/system-6.4.3.tgz#f1e093850c8cc23c6605297c8a4134bea6fe290b"
|
||||
integrity sha512-Q0iDwnH3+xoxQ0pqVbt8hFdzhq1g2XzzR4Y5pVcICTNtoCLJmpJS3vI4y/OIM1FHFmpfmiEC2IRIq7YcZ8nsmg==
|
||||
"@mui/system@^6.4.6":
|
||||
version "6.4.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/system/-/system-6.4.6.tgz#f7078e403bd11377c539a05829cd71c441c8b6e6"
|
||||
integrity sha512-FQjWwPec7pMTtB/jw5f9eyLynKFZ6/Ej9vhm5kGdtmts1z5b7Vyn3Rz6kasfYm1j2TfrfGnSXRvvtwVWxjpz6g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.26.0"
|
||||
"@mui/private-theming" "^6.4.3"
|
||||
"@mui/styled-engine" "^6.4.3"
|
||||
"@mui/private-theming" "^6.4.6"
|
||||
"@mui/styled-engine" "^6.4.6"
|
||||
"@mui/types" "^7.2.21"
|
||||
"@mui/utils" "^6.4.3"
|
||||
"@mui/utils" "^6.4.6"
|
||||
clsx "^2.1.1"
|
||||
csstype "^3.1.3"
|
||||
prop-types "^15.8.1"
|
||||
@@ -771,10 +771,10 @@
|
||||
prop-types "^15.8.1"
|
||||
react-is "^19.0.0"
|
||||
|
||||
"@mui/utils@^6.4.3":
|
||||
version "6.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.4.3.tgz#e08bc3a5ae1552a48dd13ddc7c65e3eebdb4cd58"
|
||||
integrity sha512-jxHRHh3BqVXE9ABxDm+Tc3wlBooYz/4XPa0+4AI+iF38rV1/+btJmSUgG4shDtSWVs/I97aDn5jBCt6SF2Uq2A==
|
||||
"@mui/utils@^6.4.6":
|
||||
version "6.4.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.4.6.tgz#307828bee501d30ed5cd1e339ca28c9efcc4e3f9"
|
||||
integrity sha512-43nZeE1pJF2anGafNydUcYFPtHwAqiBiauRtaMvurdrZI3YrUjHkAu43RBsxef7OFtJMXGiHFvq43kb7lig0sA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.26.0"
|
||||
"@mui/types" "^7.2.21"
|
||||
@@ -783,71 +783,71 @@
|
||||
prop-types "^15.8.1"
|
||||
react-is "^19.0.0"
|
||||
|
||||
"@mui/x-date-pickers@^7.25.0":
|
||||
version "7.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.25.0.tgz#3cbd6733e84061bf1643571248de217f24075027"
|
||||
integrity sha512-t62OSFAKwj7KYQ8KcwTuKj6OgDuLQPSe4QUJcKDzD9rEhRIJVRUw2x27gBSdcls4l0PTrba19TghvDxCZprriw==
|
||||
"@mui/x-date-pickers@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.27.1.tgz#5c9769744598b1e10c1919ce0589b8d235131a17"
|
||||
integrity sha512-2YPhTM9TM39dmIkEQdSB6P6NASePB9LuhXXKQqq0PX4FXGymYEPz/acQXkk617zwfxJJaDhJZ6g8SAv5pklTJQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.25.7"
|
||||
"@mui/utils" "^5.16.6 || ^6.0.0"
|
||||
"@mui/x-internals" "7.25.0"
|
||||
"@mui/x-internals" "7.26.0"
|
||||
"@types/react-transition-group" "^4.4.11"
|
||||
clsx "^2.1.1"
|
||||
prop-types "^15.8.1"
|
||||
react-transition-group "^4.4.5"
|
||||
|
||||
"@mui/x-internals@7.25.0":
|
||||
version "7.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.25.0.tgz#981a5365391190ab3aa0e9c7a346315b3d1f621c"
|
||||
integrity sha512-tBUN54YznAkmtCIRAOl35Kgl0MjFDIjUbzIrbWRgVSIR3QJ8bXnVSkiRBi+P91SZEl9+ZW0rDj+osq7xFJV0kg==
|
||||
"@mui/x-internals@7.26.0":
|
||||
version "7.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/x-internals/-/x-internals-7.26.0.tgz#e8c3060582c102127ab55b0a93e881930dac107b"
|
||||
integrity sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.25.7"
|
||||
"@mui/utils" "^5.16.6 || ^6.0.0"
|
||||
|
||||
"@next/env@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.1.6.tgz#2fa863d8c568a56b1c8328a86e621b8bdd4f2a20"
|
||||
integrity sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==
|
||||
"@next/env@15.2.0":
|
||||
version "15.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-15.2.0.tgz#4c3508ca2c0bb2bc324066818bb8d0415f767641"
|
||||
integrity sha512-eMgJu1RBXxxqqnuRJQh5RozhskoNUDHBFybvi+Z+yK9qzKeG7dadhv/Vp1YooSZmCnegf7JxWuapV77necLZNA==
|
||||
|
||||
"@next/swc-darwin-arm64@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz#92f99badab6cb41f4c5c11a3feffa574bd6a9276"
|
||||
integrity sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==
|
||||
"@next/swc-darwin-arm64@15.2.0":
|
||||
version "15.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.0.tgz#51ebba2162330ee3e8b3412bf31defd94a7b85e7"
|
||||
integrity sha512-rlp22GZwNJjFCyL7h5wz9vtpBVuCt3ZYjFWpEPBGzG712/uL1bbSkS675rVAUCRZ4hjoTJ26Q7IKhr5DfJrHDA==
|
||||
|
||||
"@next/swc-darwin-x64@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz#f56f4f8d5f6cb5d3915912ac95590d387f897da5"
|
||||
integrity sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==
|
||||
"@next/swc-darwin-x64@15.2.0":
|
||||
version "15.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.0.tgz#90fd6c6cee494d4348342434cfb9ca9506eae895"
|
||||
integrity sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz#0aaffae519c93d1006419d7b98c34ebfd80ecacd"
|
||||
integrity sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==
|
||||
"@next/swc-linux-arm64-gnu@15.2.0":
|
||||
version "15.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.0.tgz#f10a26cdbacf2e3de2a02a926c72857b3cb613e1"
|
||||
integrity sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ==
|
||||
|
||||
"@next/swc-linux-arm64-musl@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz#e7398d3d31ca60033f708a718cd6c31edcee2e9a"
|
||||
integrity sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==
|
||||
"@next/swc-linux-arm64-musl@15.2.0":
|
||||
version "15.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.0.tgz#1821c9a1dd17c441d8182f5cefd586f7902fcdb5"
|
||||
integrity sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw==
|
||||
|
||||
"@next/swc-linux-x64-gnu@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz#d76c72508f4d79d6016cab0c52640b93e590cffb"
|
||||
integrity sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==
|
||||
"@next/swc-linux-x64-gnu@15.2.0":
|
||||
version "15.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.0.tgz#522f55c7672346bab43bce0bcb35c4cb668ad20f"
|
||||
integrity sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q==
|
||||
|
||||
"@next/swc-linux-x64-musl@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz#0b8ba80a53e65bf8970ed11ea923001e2512c7cb"
|
||||
integrity sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==
|
||||
"@next/swc-linux-x64-musl@15.2.0":
|
||||
version "15.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.0.tgz#e23be1d046c9a630a0315588f9d692d9705ac355"
|
||||
integrity sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz#81b5dbbfdada2c05deef688e799af4a24097b65f"
|
||||
integrity sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==
|
||||
"@next/swc-win32-arm64-msvc@15.2.0":
|
||||
version "15.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.0.tgz#9e2cb008b82c676dad7d632a43549f969cb2194f"
|
||||
integrity sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw==
|
||||
|
||||
"@next/swc-win32-x64-msvc@15.1.6":
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz#131993c45ffd124fb4b15258e2f3f9669c143e3c"
|
||||
integrity sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==
|
||||
"@next/swc-win32-x64-msvc@15.2.0":
|
||||
version "15.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.0.tgz#d280f450a5b6dbb7437c3265f81ea62febf4bf3c"
|
||||
integrity sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw==
|
||||
|
||||
"@noble/hashes@^1.2.0":
|
||||
version "1.7.0"
|
||||
@@ -1114,6 +1114,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.3.tgz#0804dfd279a165d5a0ad8b53a5b9e65f338050a4"
|
||||
integrity sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==
|
||||
|
||||
"@types/react-dom@^19.0.4":
|
||||
version "19.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.4.tgz#bedba97f9346bd4c0fe5d39e689713804ec9ac89"
|
||||
integrity sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==
|
||||
|
||||
"@types/react-transition-group@^4.4.0", "@types/react-transition-group@^4.4.11", "@types/react-transition-group@^4.4.12":
|
||||
version "4.4.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044"
|
||||
@@ -1140,6 +1145,13 @@
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@^19.0.10":
|
||||
version "19.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.10.tgz#d0c66dafd862474190fe95ce11a68de69ed2b0eb"
|
||||
integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@^19.0.8":
|
||||
version "19.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.8.tgz#7098e6159f2a61e4f4cef2c1223c044a9bec590e"
|
||||
@@ -2972,22 +2984,22 @@ nanoid@^3.3.6, nanoid@^3.3.7:
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf"
|
||||
integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==
|
||||
|
||||
nanoid@^5.0.9:
|
||||
version "5.0.9"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.9.tgz#977dcbaac055430ce7b1e19cf0130cea91a20e50"
|
||||
integrity sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==
|
||||
nanoid@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.1.2.tgz#b87c6cb6941d127a23b24dffc4659bba48b219d7"
|
||||
integrity sha512-b+CiXQCNMUGe0Ri64S9SXFcP9hogjAJ2Rd6GdVxhPLRm7mhGaM7VgOvCAJ1ZshfHbqVDI3uqTI5C8/GaKuLI7g==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
|
||||
|
||||
next@^15.1.6:
|
||||
version "15.1.6"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.1.6.tgz#ce22fd0a8f36da1fc4aba86e3ec7e98eb248c555"
|
||||
integrity sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==
|
||||
next@^15.2.0:
|
||||
version "15.2.0"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-15.2.0.tgz#00f4619ae4322102b08c1a8bf315f7b757525508"
|
||||
integrity sha512-VaiM7sZYX8KIAHBrRGSFytKknkrexNfGb8GlG6e93JqueCspuGte8i4ybn8z4ww1x3f2uzY4YpTaBEW4/hvsoQ==
|
||||
dependencies:
|
||||
"@next/env" "15.1.6"
|
||||
"@next/env" "15.2.0"
|
||||
"@swc/counter" "0.1.3"
|
||||
"@swc/helpers" "0.5.15"
|
||||
busboy "1.6.0"
|
||||
@@ -2995,14 +3007,14 @@ next@^15.1.6:
|
||||
postcss "8.4.31"
|
||||
styled-jsx "5.1.6"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "15.1.6"
|
||||
"@next/swc-darwin-x64" "15.1.6"
|
||||
"@next/swc-linux-arm64-gnu" "15.1.6"
|
||||
"@next/swc-linux-arm64-musl" "15.1.6"
|
||||
"@next/swc-linux-x64-gnu" "15.1.6"
|
||||
"@next/swc-linux-x64-musl" "15.1.6"
|
||||
"@next/swc-win32-arm64-msvc" "15.1.6"
|
||||
"@next/swc-win32-x64-msvc" "15.1.6"
|
||||
"@next/swc-darwin-arm64" "15.2.0"
|
||||
"@next/swc-darwin-x64" "15.2.0"
|
||||
"@next/swc-linux-arm64-gnu" "15.2.0"
|
||||
"@next/swc-linux-arm64-musl" "15.2.0"
|
||||
"@next/swc-linux-x64-gnu" "15.2.0"
|
||||
"@next/swc-linux-x64-musl" "15.2.0"
|
||||
"@next/swc-win32-arm64-msvc" "15.2.0"
|
||||
"@next/swc-win32-x64-msvc" "15.2.0"
|
||||
sharp "^0.33.5"
|
||||
|
||||
node-releases@^2.0.19:
|
||||
@@ -3289,10 +3301,10 @@ react-fast-compare@^2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||
|
||||
react-i18next@^15.4.0:
|
||||
version "15.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.4.0.tgz#87c755fb6d7a567eec134e4759b022a0baacb19e"
|
||||
integrity sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==
|
||||
react-i18next@^15.4.1:
|
||||
version "15.4.1"
|
||||
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.4.1.tgz#33f3e89c2f6c68e2bfcbf9aa59986ad42fe78758"
|
||||
integrity sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.25.0"
|
||||
html-parse-stringify "^3.0.1"
|
||||
@@ -4199,10 +4211,10 @@ yup@^1.6.1:
|
||||
toposort "^2.0.2"
|
||||
type-fest "^2.19.0"
|
||||
|
||||
zod@^3.24.1:
|
||||
version "3.24.1"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee"
|
||||
integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==
|
||||
zod@^3.24.2:
|
||||
version "3.24.2"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
|
||||
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==
|
||||
|
||||
zxcvbn@^4.4.2:
|
||||
version "4.4.2"
|
||||
|
||||
Reference in New Issue
Block a user