From 57a425e14c37ef3822322a3e82f631ed2e3185b7 Mon Sep 17 00:00:00 2001
From: Neeraj Gupta <254676+ua741@users.noreply.github.com>
Date: Thu, 26 Sep 2024 17:13:29 +0530
Subject: [PATCH 01/61] [mob] Run discovery forcefully when ml is toggled
---
mobile/lib/services/machine_learning/ml_service.dart | 1 +
1 file changed, 1 insertion(+)
diff --git a/mobile/lib/services/machine_learning/ml_service.dart b/mobile/lib/services/machine_learning/ml_service.dart
index b384c7286e..c643bd20ff 100644
--- a/mobile/lib/services/machine_learning/ml_service.dart
+++ b/mobile/lib/services/machine_learning/ml_service.dart
@@ -117,6 +117,7 @@ class MLService {
try {
if (force) {
_mlControllerStatus = true;
+ MagicCacheService.instance.queueUpdate('forced run');
}
if (_cannotRunMLFunction() && !force) return;
_isRunningML = true;
From 2c0f2d43e7db63dbaf09685b3573b72d3aebf3ba Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 08:15:56 +0530
Subject: [PATCH 02/61] Allow flex
---
.../new/photos/services/ml/cluster.ts | 28 +++++++++++--------
1 file changed, 16 insertions(+), 12 deletions(-)
diff --git a/web/packages/new/photos/services/ml/cluster.ts b/web/packages/new/photos/services/ml/cluster.ts
index 4cd7e6850f..5e9229358f 100644
--- a/web/packages/new/photos/services/ml/cluster.ts
+++ b/web/packages/new/photos/services/ml/cluster.ts
@@ -1,3 +1,4 @@
+import { assertionFailed } from "@/base/assert";
import { newNonSecureID } from "@/base/id-worker";
import log from "@/base/log";
import { ensure } from "@/utils/ensure";
@@ -196,26 +197,29 @@ function* enumerateFaces(faceIndices: FaceIndex[]) {
*
* Sorting faces temporally is meant as a heuristic for better clusters.
*/
-const sortFacesNewestOnesFirst = (
- faces: ClusterFace[],
- localFiles: EnteFile[],
-) => {
+const sortFacesNewestOnesFirst = (faces: ClusterFace[], localFiles: EnteFile[]) => {
const localFileByID = new Map(localFiles.map((f) => [f.id, f]));
const fileForFaceID = new Map(
faces.map(({ faceID }) => [
faceID,
- ensure(localFileByID.get(ensure(fileIDFromFaceID(faceID)))),
+ localFileByID.get(ensure(fileIDFromFaceID(faceID))),
]),
);
- const fileForFace = ({ faceID }: { faceID: string }) =>
- ensure(fileForFaceID.get(faceID));
+ // In unexpected scenarios, we might run clustering without having the
+ // corresponding EnteFile available locally. This shouldn't happen, so log
+ // an warning, but meanwhile let the clustering proceed by assigning such
+ // files an arbitrary creationTime.
+ const sortTimeForFace = ({ faceID }: { faceID: string }) => {
+ const file = fileForFaceID.get(faceID);
+ if (!file) {
+ assertionFailed(`Did not find a local file for faceID ${faceID}`);
+ return 0;
+ }
+ return file.metadata.creationTime;
+ };
- return faces.sort(
- (a, b) =>
- fileForFace(b).metadata.creationTime -
- fileForFace(a).metadata.creationTime,
- );
+ return faces.sort((a, b) => sortTimeForFace(b) - sortTimeForFace(a));
};
/**
From 57ea097a5df424496856b229a32bf098e1a1f17d Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 08:21:58 +0530
Subject: [PATCH 03/61] Use new nomenclature
---
.../new/photos/services/ml/cluster.ts | 33 +++++++++----------
1 file changed, 16 insertions(+), 17 deletions(-)
diff --git a/web/packages/new/photos/services/ml/cluster.ts b/web/packages/new/photos/services/ml/cluster.ts
index 5e9229358f..c0d81173b9 100644
--- a/web/packages/new/photos/services/ml/cluster.ts
+++ b/web/packages/new/photos/services/ml/cluster.ts
@@ -197,7 +197,10 @@ function* enumerateFaces(faceIndices: FaceIndex[]) {
*
* Sorting faces temporally is meant as a heuristic for better clusters.
*/
-const sortFacesNewestOnesFirst = (faces: ClusterFace[], localFiles: EnteFile[]) => {
+const sortFacesNewestOnesFirst = (
+ faces: ClusterFace[],
+ localFiles: EnteFile[],
+) => {
const localFileByID = new Map(localFiles.map((f) => [f.id, f]));
const fileForFaceID = new Map(
faces.map(({ faceID }) => [
@@ -344,22 +347,22 @@ export const reconcileClusters = async (
const clusterByID = new Map(clusters.map((c) => [c.id, c]));
// Get the existing remote cluster groups.
- const cgroupEntities = await savedCGroups();
+ const cgroups = await savedCGroups();
// Find the cgroups that have changed since we started.
- const changedCGroupEntities = cgroupEntities
- .map((cgroupEntity) => {
- for (const oldCluster of cgroupEntity.data.assigned) {
+ const changedCGroups = cgroups
+ .map((cgroup) => {
+ for (const oldCluster of cgroup.data.assigned) {
// The clustering algorithm does not remove any existing faces, it
// can only add new ones to the cluster. So we can use the count as
// an indication if something changed.
const newCluster = ensure(clusterByID.get(oldCluster.id));
if (oldCluster.faces.length != newCluster.faces.length) {
return {
- ...cgroupEntity,
+ ...cgroup,
data: {
- ...cgroupEntity.data,
- assigned: cgroupEntity.data.assigned.map(({ id }) =>
+ ...cgroup.data,
+ assigned: cgroup.data.assigned.map(({ id }) =>
ensure(clusterByID.get(id)),
),
},
@@ -371,19 +374,15 @@ export const reconcileClusters = async (
.filter((g) => !!g);
// Update remote if needed.
- if (changedCGroupEntities.length) {
- await updateOrCreateUserEntities(
- "cgroup",
- changedCGroupEntities,
- masterKey,
- );
- log.info(`Updated ${changedCGroupEntities.length} remote cgroups`);
+ if (changedCGroups.length) {
+ await updateOrCreateUserEntities("cgroup", changedCGroups, masterKey);
+ log.info(`Updated ${changedCGroups.length} remote cgroups`);
}
// Find which clusters are part of remote cgroups.
const isRemoteClusterID = new Set();
- for (const cgroupEntity of cgroupEntities) {
- for (const cluster of cgroupEntity.data.assigned)
+ for (const cgroup of cgroups) {
+ for (const cluster of cgroup.data.assigned)
isRemoteClusterID.add(cluster.id);
}
From 27a0d7707e891b383cd7db0c3997c96c3e0457c2 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 08:39:29 +0530
Subject: [PATCH 04/61] Return the count of items indexed
---
web/packages/new/photos/services/ml/worker.ts | 58 ++++++++++++-------
1 file changed, 36 insertions(+), 22 deletions(-)
diff --git a/web/packages/new/photos/services/ml/worker.ts b/web/packages/new/photos/services/ml/worker.ts
index b9156e4106..ce460d3610 100644
--- a/web/packages/new/photos/services/ml/worker.ts
+++ b/web/packages/new/photos/services/ml/worker.ts
@@ -109,7 +109,13 @@ export class MLWorker {
private liveQ: IndexableItem[] = [];
private idleTimeout: ReturnType | undefined;
private idleDuration = idleDurationStart; /* unit: seconds */
- private onNextIdles: (() => void)[] = [];
+ /** Resolvers for pending promises returned from calls to {@link index}. */
+ private onNextIdles: ((count: number) => void)[] = [];
+ /**
+ * Number of items processed since the last time {@link onNextIdles} was
+ * drained.
+ */
+ private countSinceLastIdle = 0;
/**
* Initialize a new {@link MLWorker}.
@@ -140,9 +146,12 @@ export class MLWorker {
* During a backfill, we first attempt to fetch ML data for files which
* don't have that data locally. If on fetching we find what we need, we
* save it locally. Otherwise we index them.
+ *
+ * @return The count of items processed since the last last time we were
+ * idle.
*/
index() {
- const nextIdle = new Promise((resolve) =>
+ const nextIdle = new Promise((resolve) =>
this.onNextIdles.push(resolve),
);
this.wakeUp();
@@ -225,17 +234,23 @@ export class MLWorker {
// Use the liveQ if present, otherwise get the next batch to backfill.
const items = liveQ.length ? liveQ : await this.backfillQ();
- const allSuccess = await indexNextBatch(
- items,
- ensure(this.electron),
- this.delegate,
- );
- if (allSuccess) {
- // Everything is running smoothly. Reset the idle duration.
- this.idleDuration = idleDurationStart;
- // And tick again.
- scheduleTick();
- return;
+ this.countSinceLastIdle += items.length;
+
+ // If there is items remaining,
+ if (items.length > 0) {
+ // Index them.
+ const allSuccess = await indexNextBatch(
+ items,
+ ensure(this.electron),
+ this.delegate,
+ );
+ if (allSuccess) {
+ // Everything is running smoothly. Reset the idle duration.
+ this.idleDuration = idleDurationStart;
+ // And tick again.
+ scheduleTick();
+ return;
+ }
}
// We come here in three scenarios - either there is nothing left to do,
@@ -255,8 +270,10 @@ export class MLWorker {
// Resolve any awaiting promises returned from `index`.
const onNextIdles = this.onNextIdles;
+ const countSinceLastIdle = this.countSinceLastIdle;
this.onNextIdles = [];
- onNextIdles.forEach((f) => f());
+ this.countSinceLastIdle = 0;
+ onNextIdles.forEach((f) => f(countSinceLastIdle));
}
/** Return the next batch of items to backfill (if any). */
@@ -321,13 +338,13 @@ export class MLWorker {
expose(MLWorker);
/**
- * Find out files which need to be indexed. Then index the next batch of them.
+ * Index the given batch of items.
*
- * Returns `false` to indicate that either an error occurred, or there are no
- * more files to process, or that we cannot currently process files.
+ * Returns `false` to indicate that either an error occurred, or that we cannot
+ * currently process files since we don't have network connectivity.
*
- * Which means that when it returns true, all is well and there are more
- * things pending to process, so we should chug along at full speed.
+ * Which means that when it returns true, all is well and if there are more
+ * things pending to process, we should chug along at full speed.
*/
const indexNextBatch = async (
items: IndexableItem[],
@@ -342,9 +359,6 @@ const indexNextBatch = async (
return false;
}
- // Nothing to do.
- if (items.length == 0) return false;
-
// Keep track if any of the items failed.
let allSuccess = true;
From 4d4b3f8bef90f4ab4ce29f5a16bec7920f929112 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 10:06:56 +0530
Subject: [PATCH 05/61] Notify about live uploads
---
web/packages/new/photos/services/ml/index.ts | 21 +++++++++++--------
.../new/photos/services/ml/worker-types.ts | 13 ++++++++++++
web/packages/new/photos/services/ml/worker.ts | 6 ++++++
3 files changed, 31 insertions(+), 9 deletions(-)
diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts
index ddb88dd69c..f7cc7a9a17 100644
--- a/web/packages/new/photos/services/ml/index.ts
+++ b/web/packages/new/photos/services/ml/index.ts
@@ -107,7 +107,7 @@ const worker = () =>
const createComlinkWorker = async () => {
const electron = ensureElectron();
- const delegate = { workerDidUpdateStatus };
+ const delegate = { workerDidUpdateStatus, workerDidUnawaitedIndex };
// Obtain a message port from the Electron layer.
const messagePort = await createMLWorker(electron);
@@ -313,14 +313,20 @@ export const mlSync = async () => {
// Dependency order for the sync
//
- // files -> faces -> cgroups -> clusters
+ // files -> faces -> cgroups -> clusters -> people
//
- const w = await worker();
-
// Fetch indexes, or index locally if needed.
- await w.index();
+ await (await worker()).index();
+ await updateClustersAndPeople();
+
+ _state.isSyncing = false;
+};
+
+const workerDidUnawaitedIndex = () => void updateClustersAndPeople();
+
+const updateClustersAndPeople = async () => {
// TODO-Cluster
if (await wipClusterEnable()) {
const masterKey = await masterKeyFromSession();
@@ -329,12 +335,9 @@ export const mlSync = async () => {
await pullUserEntities("cgroup", masterKey);
// Generate or update local clusters.
- await w.clusterFaces(masterKey);
+ await (await worker()).clusterFaces(masterKey);
}
-
await updatePeople();
-
- _state.isSyncing = false;
};
/**
diff --git a/web/packages/new/photos/services/ml/worker-types.ts b/web/packages/new/photos/services/ml/worker-types.ts
index b12598d52b..16153b3b2b 100644
--- a/web/packages/new/photos/services/ml/worker-types.ts
+++ b/web/packages/new/photos/services/ml/worker-types.ts
@@ -13,6 +13,19 @@ export interface MLWorkerDelegate {
* indicating the indexing or clustering status to be updated.
*/
workerDidUpdateStatus: () => void;
+ /**
+ * Called when the worker indexes some files, but then notices that the main
+ * thread was not awaiting the indexing (e.g. it was not initiated by the
+ * main thread during a sync, but happened because of a live upload).
+ *
+ * In such cases, it uses this method to inform the main thread that some
+ * files were indexed, so that it can update any dependent state (e.g.
+ * clusters).
+ *
+ * It doesn't always call this because otherwise the main thread would need
+ * some extra code to avoid updating the dependent state twice.
+ */
+ workerDidUnawaitedIndex: () => void;
}
/**
diff --git a/web/packages/new/photos/services/ml/worker.ts b/web/packages/new/photos/services/ml/worker.ts
index ce460d3610..42ee2a439f 100644
--- a/web/packages/new/photos/services/ml/worker.ts
+++ b/web/packages/new/photos/services/ml/worker.ts
@@ -274,6 +274,12 @@ export class MLWorker {
this.onNextIdles = [];
this.countSinceLastIdle = 0;
onNextIdles.forEach((f) => f(countSinceLastIdle));
+
+ // If no one was waiting, then let the main thread know via a different
+ // channel so that it can update the clusters and people.
+ if (onNextIdles.length == 0 && countSinceLastIdle > 0) {
+ this.delegate?.workerDidUnawaitedIndex();
+ }
}
/** Return the next batch of items to backfill (if any). */
From 924f5ce19bfc5dfc23b3638e6f716957fe827a93 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 10:14:53 +0530
Subject: [PATCH 06/61] Keep people first
---
web/packages/new/photos/services/ml/people.ts | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/web/packages/new/photos/services/ml/people.ts b/web/packages/new/photos/services/ml/people.ts
index 6ad95d3fad..827d78acca 100644
--- a/web/packages/new/photos/services/ml/people.ts
+++ b/web/packages/new/photos/services/ml/people.ts
@@ -247,10 +247,12 @@ export const reconstructPeople = async (): Promise => {
};
});
- return cgroupPeople
- .concat(clusterPeople)
- .filter((c) => !!c)
- .sort((a, b) => b.fileIDs.length - a.fileIDs.length);
+ const sorted = (ps: Interim) =>
+ ps
+ .filter((c) => !!c)
+ .sort((a, b) => b.fileIDs.length - a.fileIDs.length);
+
+ return sorted(cgroupPeople).concat(sorted(clusterPeople));
};
/**
From 71369bf5c9f8c4f1279e8825736838c100ed681f Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 10:29:33 +0530
Subject: [PATCH 07/61] State
---
.../components/Gallery/PeopleHeader.tsx | 28 +++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/web/packages/new/photos/components/Gallery/PeopleHeader.tsx b/web/packages/new/photos/components/Gallery/PeopleHeader.tsx
index 616ae53acd..347f4e3748 100644
--- a/web/packages/new/photos/components/Gallery/PeopleHeader.tsx
+++ b/web/packages/new/photos/components/Gallery/PeopleHeader.tsx
@@ -31,6 +31,34 @@ import { NameInputDialog } from "../NameInputDialog";
import type { GalleryBarImplProps } from "./BarImpl";
import { GalleryItemsHeaderAdapter, GalleryItemsSummary } from "./ListHeader";
+/**
+ * UI state backing the gallery when it is in "people" mode.
+ *
+ * This may be different from the actual underlying state since there might be
+ * unsynced data (hidden or deleted that have not yet been synced with remote)
+ * that should be taken into account for the UI state.
+ */
+export interface GalleryPeopleState {
+ /**
+ * The ID of the currently selected person.
+ *
+ * We do not have an empty state currently, so this is guaranteed to be
+ * present whenever the gallery is in the "people" mode.
+ */
+ activePersonID: string;
+ /**
+ * The currently selected person.
+ *
+ * This is a convenience property that contains a direct reference to the
+ * active {@link Person} from amongst {@link people}.
+ */
+ activePerson: Person;
+ /**
+ * The list of people to show.
+ */
+ people: Person[];
+}
+
type PeopleHeaderProps = Pick & {
person: Person;
appContext: NewAppContextPhotos;
From 9235e41855e41fb177dfb530b890e30a1de36e6e Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 10:46:34 +0530
Subject: [PATCH 08/61] Prepare to allow filtering people at the gallery layer
---
web/apps/photos/src/pages/gallery.tsx | 49 ++++++++++++++++---
.../components/Gallery/PeopleHeader.tsx | 2 +-
2 files changed, 42 insertions(+), 9 deletions(-)
diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx
index 32adaf4d61..e36537c131 100644
--- a/web/apps/photos/src/pages/gallery.tsx
+++ b/web/apps/photos/src/pages/gallery.tsx
@@ -5,6 +5,7 @@ import log from "@/base/log";
import type { Collection } from "@/media/collection";
import { SearchResultsHeader } from "@/new/photos/components/Gallery";
import type { GalleryBarMode } from "@/new/photos/components/Gallery/BarImpl";
+import { GalleryPeopleState } from "@/new/photos/components/Gallery/PeopleHeader";
import {
SearchBar,
type SearchBarProps,
@@ -526,9 +527,14 @@ export default function Gallery() {
);
}, [collections, activeCollectionID]);
- const filteredData = useMemoSingleThreaded(async (): Promise<
- EnteFile[]
- > => {
+ // The derived UI state when we are in "people" mode.
+ // TODO: Move this to a reducer/store.
+ type DerivedState1 = {
+ filteredData: EnteFile[];
+ galleryPeopleState: GalleryPeopleState | undefined;
+ };
+
+ const derived1: DerivedState1 = useMemoSingleThreaded(async () => {
if (
!files ||
!user ||
@@ -536,17 +542,19 @@ export default function Gallery() {
!hiddenFiles ||
!archivedCollections
) {
- return;
+ return { filteredData: [], galleryPeopleState: undefined };
}
if (activeCollectionID === TRASH_SECTION && !selectedSearchOption) {
- return getUniqueFiles([
+ const filteredData = getUniqueFiles([
...trashedFiles,
...files.filter((file) => tempDeletedFileIds?.has(file.id)),
]);
+ return { filteredData, galleryPeopleState: undefined };
}
let filteredFiles: EnteFile[] = [];
+ let galleryPeopleState: GalleryPeopleState;
if (selectedSearchOption) {
filteredFiles = await filterSearchableFiles(
selectedSearchOption.suggestion,
@@ -565,6 +573,11 @@ export default function Gallery() {
return true;
}),
);
+ galleryPeopleState = {
+ activePerson,
+ activePersonID,
+ people,
+ };
} else {
const baseFiles = barMode == "hidden-albums" ? hiddenFiles : files;
filteredFiles = getUniqueFiles(
@@ -630,10 +643,10 @@ export default function Gallery() {
}
const sortAsc = activeCollection?.pubMagicMetadata?.data?.asc ?? false;
if (sortAsc) {
- return sortFiles(filteredFiles, true);
- } else {
- return filteredFiles;
+ filteredFiles = sortFiles(filteredFiles, true);
}
+
+ return { filteredData: filteredFiles, galleryPeopleState };
}, [
barMode,
files,
@@ -649,6 +662,26 @@ export default function Gallery() {
activePersonID,
]);
+ const { filteredData, galleryPeopleState } = derived1;
+
+ // Calling setState during rendering is frowned upon for good reasons, but
+ // it is not verboten, and it has documented semantics:
+ //
+ // > React will discard the currently rendering component's output and
+ // > immediately attempt to render it again with the new state.
+ // >
+ // > https://react.dev/reference/react/useState
+ //
+ // That said, we should try to refactor this code to use a reducer or some
+ // other store so that this is not needed.
+ if (barMode == "people" && !galleryPeopleState) {
+ log.info(
+ "Resetting gallery to all section since people mode is no longer valid",
+ );
+ setBarMode("albums");
+ setActiveCollectionID(ALL_SECTION);
+ }
+
const selectAll = (e: KeyboardEvent) => {
// ignore ctrl/cmd + a if the user is typing in a text field
if (
diff --git a/web/packages/new/photos/components/Gallery/PeopleHeader.tsx b/web/packages/new/photos/components/Gallery/PeopleHeader.tsx
index 347f4e3748..ad6e71925f 100644
--- a/web/packages/new/photos/components/Gallery/PeopleHeader.tsx
+++ b/web/packages/new/photos/components/Gallery/PeopleHeader.tsx
@@ -32,7 +32,7 @@ import type { GalleryBarImplProps } from "./BarImpl";
import { GalleryItemsHeaderAdapter, GalleryItemsSummary } from "./ListHeader";
/**
- * UI state backing the gallery when it is in "people" mode.
+ * Derived UI state backing the gallery when it is in "people" mode.
*
* This may be different from the actual underlying state since there might be
* unsynced data (hidden or deleted that have not yet been synced with remote)
From a37ff3cf57a59b30d4fdff1f300f3d0026f19780 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 10:53:28 +0530
Subject: [PATCH 09/61] Workarounds
---
web/apps/photos/src/pages/gallery.tsx | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx
index e36537c131..71c69d40d9 100644
--- a/web/apps/photos/src/pages/gallery.tsx
+++ b/web/apps/photos/src/pages/gallery.tsx
@@ -662,7 +662,10 @@ export default function Gallery() {
activePersonID,
]);
- const { filteredData, galleryPeopleState } = derived1;
+ const { filteredData, galleryPeopleState } = derived1 ?? {
+ filteredData: [],
+ galleryPeopleState: undefined,
+ };
// Calling setState during rendering is frowned upon for good reasons, but
// it is not verboten, and it has documented semantics:
@@ -674,7 +677,7 @@ export default function Gallery() {
//
// That said, we should try to refactor this code to use a reducer or some
// other store so that this is not needed.
- if (barMode == "people" && !galleryPeopleState) {
+ if (barMode == "people" && galleryPeopleState?.people.length === 0) {
log.info(
"Resetting gallery to all section since people mode is no longer valid",
);
From e70f9b5ccdb028eed75c376f209b92a477754353 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 11:03:30 +0530
Subject: [PATCH 10/61] Ignore temp deleted etc
---
web/apps/photos/src/pages/gallery.tsx | 23 ++++++++++++++++++-----
1 file changed, 18 insertions(+), 5 deletions(-)
diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx
index 71c69d40d9..02e1fe97d7 100644
--- a/web/apps/photos/src/pages/gallery.tsx
+++ b/web/apps/photos/src/pages/gallery.tsx
@@ -560,23 +560,36 @@ export default function Gallery() {
selectedSearchOption.suggestion,
);
} else if (barMode == "people") {
+ let filteredPeople = people;
+ if (tempDeletedFileIds?.size ?? tempHiddenFileIds?.size) {
+ // Prune the in-memory temp updates from the actual state to
+ // obtain the UI state.
+ filteredPeople = people
+ .map((p) => ({
+ ...p,
+ fileIDs: p.fileIDs.filter(
+ (id) =>
+ !tempDeletedFileIds?.has(id) &&
+ !tempHiddenFileIds?.has(id),
+ ),
+ }))
+ .filter((p) => p.fileIDs.length > 0);
+ }
const activePerson = ensure(
- people.find((p) => p.id == activePersonID) ?? people[0],
+ filteredPeople.find((p) => p.id == activePersonID) ??
+ filteredPeople[0],
);
const pfSet = new Set(activePerson.fileIDs);
filteredFiles = getUniqueFiles(
files.filter(({ id }) => {
if (!pfSet.has(id)) return false;
- // TODO-Cluster
- // if (tempDeletedFileIds?.has(id)) return false;
- // if (tempHiddenFileIds?.has(id)) return false;
return true;
}),
);
galleryPeopleState = {
activePerson,
activePersonID,
- people,
+ people: filteredPeople,
};
} else {
const baseFiles = barMode == "hidden-albums" ? hiddenFiles : files;
From 7644900bd8fb63e9475722db93ade73967a4c3e0 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 11:16:05 +0530
Subject: [PATCH 11/61] Use
---
.../Collections/GalleryBarAndListHeader.tsx | 12 ++++------
web/apps/photos/src/pages/gallery.tsx | 23 +++++++++++--------
.../new/photos/components/Gallery/BarImpl.tsx | 19 +++++++--------
3 files changed, 25 insertions(+), 29 deletions(-)
diff --git a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx
index 0e9c83dde4..882879e7ba 100644
--- a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx
+++ b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx
@@ -9,7 +9,6 @@ import {
type CollectionsSortBy,
type CollectionSummaries,
} from "@/new/photos/types/collection";
-import { ensure } from "@/utils/ensure";
import { includes } from "@/utils/type-guards";
import {
getData,
@@ -95,7 +94,7 @@ export const GalleryBarAndListHeader: React.FC = ({
setActiveCollectionID,
hiddenCollectionSummaries,
people,
- activePersonID,
+ activePerson,
onSelectPerson,
setCollectionNamerAttributes,
setPhotoListHeader,
@@ -173,10 +172,7 @@ export const GalleryBarAndListHeader: React.FC = ({
/>
) : (
p.id == activePersonID) ??
- people[0],
- )}
+ person={activePerson}
{...{ onSelectPerson, appContext }}
/>
),
@@ -190,7 +186,7 @@ export const GalleryBarAndListHeader: React.FC = ({
activeCollectionID,
isActiveCollectionDownloadInProgress,
people,
- activePersonID,
+ activePerson,
]);
if (shouldBeHidden) {
@@ -205,7 +201,7 @@ export const GalleryBarAndListHeader: React.FC = ({
onChangeMode,
activeCollectionID,
people,
- activePersonID,
+ activePerson,
onSelectPerson,
collectionsSortBy,
}}
diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx
index 02e1fe97d7..fb516fe35b 100644
--- a/web/apps/photos/src/pages/gallery.tsx
+++ b/web/apps/photos/src/pages/gallery.tsx
@@ -575,11 +575,10 @@ export default function Gallery() {
}))
.filter((p) => p.fileIDs.length > 0);
}
- const activePerson = ensure(
+ const activePerson =
filteredPeople.find((p) => p.id == activePersonID) ??
- filteredPeople[0],
- );
- const pfSet = new Set(activePerson.fileIDs);
+ filteredPeople[0];
+ const pfSet = new Set(activePerson?.fileIDs ?? []);
filteredFiles = getUniqueFiles(
files.filter(({ id }) => {
if (!pfSet.has(id)) return false;
@@ -728,8 +727,11 @@ export default function Gallery() {
count: 0,
collectionID: activeCollectionID,
context:
- barMode == "people" && activePersonID
- ? { mode: "people" as const, personID: activePersonID }
+ barMode == "people" && galleryPeopleState?.activePersonID
+ ? {
+ mode: "people" as const,
+ personID: galleryPeopleState.activePersonID,
+ }
: {
mode: "albums" as const,
collectionID: ensure(activeCollectionID),
@@ -1107,7 +1109,7 @@ export default function Gallery() {
// when the user clicks the "People" header in the search empty state (it
// is guaranteed that this header will only be shown if there is at
// least one person).
- setActivePersonID(person?.id ?? ensure(people[0]).id);
+ setActivePersonID(person?.id ?? galleryPeopleState?.people[0]?.id);
setBarMode("people");
};
@@ -1219,8 +1221,9 @@ export default function Gallery() {
activeCollectionID,
setActiveCollectionID,
hiddenCollectionSummaries,
- people,
- activePersonID,
+ people: galleryPeopleState?.people,
+ activePersonID: galleryPeopleState?.activePersonID,
+ activePerson: galleryPeopleState?.activePerson,
onSelectPerson: handleSelectPerson,
setCollectionNamerAttributes,
setPhotoListHeader,
@@ -1296,7 +1299,7 @@ export default function Gallery() {
setTempDeletedFileIds={setTempDeletedFileIds}
setIsPhotoSwipeOpen={setIsPhotoSwipeOpen}
activeCollectionID={activeCollectionID}
- activePersonID={activePersonID}
+ activePersonID={galleryPeopleState?.activePersonID}
enableDownload={true}
fileToCollectionsMap={fileToCollectionsMap}
collectionNameMap={collectionNameMap}
diff --git a/web/packages/new/photos/components/Gallery/BarImpl.tsx b/web/packages/new/photos/components/Gallery/BarImpl.tsx
index 80a65e8dde..c71ee1fc62 100644
--- a/web/packages/new/photos/components/Gallery/BarImpl.tsx
+++ b/web/packages/new/photos/components/Gallery/BarImpl.tsx
@@ -94,11 +94,11 @@ export interface GalleryBarImplProps {
*/
people: Person[];
/**
- * The ID of the currently selected person.
+ * The currently selected person.
*
* Required if mode is "people".
*/
- activePersonID: string | undefined;
+ activePerson: Person | undefined;
/**
* Called when the selection should be moved to a new person in the bar, or
* reset to the default state (when {@link person} is `undefined`).
@@ -116,7 +116,7 @@ export const GalleryBarImpl: React.FC = ({
collectionsSortBy,
onChangeCollectionsSortBy,
people,
- activePersonID,
+ activePerson,
onSelectPerson,
}) => {
const isMobile = useIsMobileWidth();
@@ -194,11 +194,11 @@ export const GalleryBarImpl: React.FC = ({
);
break;
case "people":
- i = people.findIndex(({ id }) => id == activePersonID);
+ i = people.findIndex(({ id }) => id == activePerson?.id);
break;
}
if (i != -1) listRef.current.scrollToItem(i, "smart");
- }, [mode, collectionSummaries, activeCollectionID, people, activePersonID]);
+ }, [mode, collectionSummaries, activeCollectionID, people, activePerson]);
const itemData = useMemo(
() =>
@@ -210,12 +210,9 @@ export const GalleryBarImpl: React.FC = ({
onSelectCollectionID,
}
: {
- type: "people",
+ type: "people" as const,
people,
- activePerson: ensure(
- people.find((p) => p.id == activePersonID) ??
- people[0],
- ),
+ activePerson: ensure(activePerson),
onSelectPerson,
},
[
@@ -224,7 +221,7 @@ export const GalleryBarImpl: React.FC = ({
activeCollectionID,
onSelectCollectionID,
people,
- activePersonID,
+ activePerson,
onSelectPerson,
],
);
From 393878a52edac8f347d2912c2ae7cd247e197d0f Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 11:33:42 +0530
Subject: [PATCH 12/61] More workarounds
---
web/apps/photos/src/pages/gallery.tsx | 25 ++++++++++++++++++-------
1 file changed, 18 insertions(+), 7 deletions(-)
diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx
index fb516fe35b..881c28ac91 100644
--- a/web/apps/photos/src/pages/gallery.tsx
+++ b/web/apps/photos/src/pages/gallery.tsx
@@ -528,7 +528,9 @@ export default function Gallery() {
}, [collections, activeCollectionID]);
// The derived UI state when we are in "people" mode.
- // TODO: Move this to a reducer/store.
+ //
+ // TODO: This spawns even more workarounds below. Move this to a
+ // reducer/store.
type DerivedState1 = {
filteredData: EnteFile[];
galleryPeopleState: GalleryPeopleState | undefined;
@@ -697,6 +699,13 @@ export default function Gallery() {
setActiveCollectionID(ALL_SECTION);
}
+ // Derived1 is async, leading to even more workarounds.
+ const resolvedBarMode = galleryPeopleState
+ ? barMode
+ : barMode == "people"
+ ? "albums"
+ : barMode;
+
const selectAll = (e: KeyboardEvent) => {
// ignore ctrl/cmd + a if the user is typing in a text field
if (
@@ -1193,7 +1202,7 @@ export default function Gallery() {
marginBottom: "12px",
}}
>
- {barMode == "hidden-albums" ? (
+ {resolvedBarMode == "hidden-albums" ? (
@@ -1214,7 +1223,7 @@ export default function Gallery() {
)}
Date: Fri, 27 Sep 2024 11:46:01 +0530
Subject: [PATCH 13/61] Another
---
web/apps/photos/src/pages/gallery.tsx | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx
index 881c28ac91..a3c3a78937 100644
--- a/web/apps/photos/src/pages/gallery.tsx
+++ b/web/apps/photos/src/pages/gallery.tsx
@@ -736,13 +736,14 @@ export default function Gallery() {
count: 0,
collectionID: activeCollectionID,
context:
- barMode == "people" && galleryPeopleState?.activePersonID
+ resolvedBarMode == "people" &&
+ galleryPeopleState?.activePersonID
? {
mode: "people" as const,
personID: galleryPeopleState.activePersonID,
}
: {
- mode: "albums" as const,
+ mode: resolvedBarMode as "albums" | "hidden-albums",
collectionID: ensure(activeCollectionID),
},
};
From e2e374fbf4c4a75ce71cabe58a59b90db495d494 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 12:25:53 +0530
Subject: [PATCH 14/61] wip checkpoint
---
.../new/photos/components/PeopleList.tsx | 70 +++++++++++++++++++
web/packages/new/photos/services/ml/index.ts | 33 +++++++--
2 files changed, 98 insertions(+), 5 deletions(-)
diff --git a/web/packages/new/photos/components/PeopleList.tsx b/web/packages/new/photos/components/PeopleList.tsx
index 1f9862f308..07157b5622 100644
--- a/web/packages/new/photos/components/PeopleList.tsx
+++ b/web/packages/new/photos/components/PeopleList.tsx
@@ -6,6 +6,7 @@ import { Skeleton, Typography, styled } from "@mui/material";
import { t } from "i18next";
import React, { useEffect, useState } from "react";
import { UnstyledButton } from "./mui-custom";
+import type { CGroup } from "../services/user-entity";
export interface SearchPeopleListProps {
people: Person[];
@@ -66,6 +67,75 @@ const SearchPeopleButton = styled(UnstyledButton)(
`,
);
+export interface CGroupPeopleListProps {
+ /**
+ * List of cgroup people to show.
+ *
+ * The current types don't reflect this, but these are all guaranteed to be
+ * {@link Person}s with type "cgroup"
+ */
+ people: Person[];
+ /**
+ * Called when the user selects a person in the list.
+ */
+ onSelectPerson: (person: Person) => void;
+}
+
+/**
+ * Show the list of faces in the given file that are not linked to a a specific
+ * cgroup ("people").
+ */
+export const CGroupPeopleList: React.FC = ({
+ people,
+ onSelectPerson,
+}) => {
+ const isMobileWidth = useIsMobileWidth();
+ return (
+ 3 ? "center" : "start" }}
+ >
+ {people.slice(0, isMobileWidth ? 6 : 7).map((person) => (
+ onSelectPerson(person)}
+ >
+
+
+ ))}
+
+ );
+};
+
+const SearchPeopleContainer = styled("div")`
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 5px;
+ margin-block-start: 12px;
+ margin-block-end: 15px;
+`;
+
+const SearchPeopleButton = styled(UnstyledButton)(
+ ({ theme }) => `
+ width: 87px;
+ height: 87px;
+ border-radius: 50%;
+ overflow: hidden;
+ & > img {
+ width: 100%;
+ height: 100%;
+ }
+ :hover {
+ outline: 1px solid ${theme.colors.stroke.faint};
+ outline-offset: 2px;
+ }
+`,
+);
+
const FaceChipContainer = styled("div")`
display: flex;
flex-wrap: wrap;
diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts
index f7cc7a9a17..9330cf1e4c 100644
--- a/web/packages/new/photos/services/ml/index.ts
+++ b/web/packages/new/photos/services/ml/index.ts
@@ -593,14 +593,37 @@ export const clipMatches = (
worker().then((w) => w.clipMatches(searchPhrase));
/**
- * Return the IDs of all the faces in the given {@link enteFile} that are not
- * associated with a person cluster.
+ * Return the list of faces found in the given {@link enteFile}.
+ *
+ * Each item is returned as a (faceID, personID) tuple, where the faceID is the
+ * ID of the face, and the personID is the id of the corresponding person that
+ * this face is associated to (if any).
*/
-export const unidentifiedFaceIDs = async (
+export const peopleIDsAndOtherFaceIDsInFile = async (
enteFile: EnteFile,
-): Promise => {
+): Promise<[string, string | undefined][]> => {
const index = await getFaceIndex(enteFile.id);
- return index?.faces.map((f) => f.faceID) ?? [];
+ if (!index) return [];
+
+ const people = _state.peopleSnapshot ?? [];
+
+ const faceIDToPersonID = new Map();
+ for (const person of people) {
+ let faceIDs: string[];
+ if (person.type == "cgroup") {
+ faceIDs = person.cgroup.data.assigned.map((c) => c.faces).flat();
+ } else {
+ faceIDs = person.cluster.faces;
+ }
+ for (const faceID of faceIDs) {
+ faceIDToPersonID.set(faceID, person.id);
+ }
+ }
+
+ return index.faces.map(({ faceID }) => [
+ faceID,
+ faceIDToPersonID.get(faceID),
+ ]);
};
/**
From 7a60b1e15ebc4f3629b1829b2334c38f3ce0c881 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 12:34:41 +0530
Subject: [PATCH 15/61] wip checkpoint
---
.../components/PhotoViewer/FileInfo/index.tsx | 20 ++++++++++++++++++-
web/packages/new/photos/services/ml/index.ts | 2 +-
2 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
index 20157caab7..a8c5957ba9 100644
--- a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
+++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
@@ -16,13 +16,14 @@ import { UnidentifiedFaces } from "@/new/photos/components/PeopleList";
import { PhotoDateTimePicker } from "@/new/photos/components/PhotoDateTimePicker";
import { photoSwipeZIndex } from "@/new/photos/components/PhotoViewer";
import { tagNumericValue, type RawExifTags } from "@/new/photos/services/exif";
-import { isMLEnabled } from "@/new/photos/services/ml";
+import { annotatedFaceIDsForFile, isMLEnabled } from "@/new/photos/services/ml";
import { EnteFile } from "@/new/photos/types/file";
import { formattedByteSize } from "@/new/photos/utils/units";
import CopyButton from "@ente/shared/components/CodeBlock/CopyButton";
import { FlexWrapper } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { getPublicMagicMetadataSync } from "@ente/shared/file-metadata";
+import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded";
import { formatDate, formatTime } from "@ente/shared/time/format";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
import CameraOutlined from "@mui/icons-material/CameraOutlined";
@@ -106,6 +107,23 @@ export const FileInfo: React.FC = ({
return exif?.parsed?.location;
}, [file, exif]);
+ const [annotatedPeopleFaceIDs, otherFaceIDs] =
+ useMemoSingleThreaded(async () => {
+ if (!file) return [[], []];
+ const annotatedFaceIDs = await annotatedFaceIDsForFile(file);
+ return annotatedFaceIDs.reduce(
+ ([people, other], item) => {
+ if (item[1]) {
+ people.push(item);
+ } else {
+ other.push(item[0]);
+ }
+ return [people, other];
+ },
+ [[], []],
+ );
+ }, [file]);
+
useEffect(() => {
setExifInfo(parseExifInfo(exif));
}, [exif]);
diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts
index 9330cf1e4c..70d6762349 100644
--- a/web/packages/new/photos/services/ml/index.ts
+++ b/web/packages/new/photos/services/ml/index.ts
@@ -599,7 +599,7 @@ export const clipMatches = (
* ID of the face, and the personID is the id of the corresponding person that
* this face is associated to (if any).
*/
-export const peopleIDsAndOtherFaceIDsInFile = async (
+export const annotatedFaceIDsForFile = async (
enteFile: EnteFile,
): Promise<[string, string | undefined][]> => {
const index = await getFaceIndex(enteFile.id);
From 4e04739d546ea3510f14346037c6d56c310598e3 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 12:54:17 +0530
Subject: [PATCH 16/61] wip checkpoint
---
.../components/PhotoViewer/FileInfo/index.tsx | 32 ++++++------
.../new/photos/components/PeopleList.tsx | 43 ++++------------
web/packages/new/photos/services/ml/index.ts | 49 ++++++++++++++-----
3 files changed, 63 insertions(+), 61 deletions(-)
diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
index a8c5957ba9..bfd86e0db3 100644
--- a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
+++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
@@ -16,7 +16,7 @@ import { UnidentifiedFaces } from "@/new/photos/components/PeopleList";
import { PhotoDateTimePicker } from "@/new/photos/components/PhotoDateTimePicker";
import { photoSwipeZIndex } from "@/new/photos/components/PhotoViewer";
import { tagNumericValue, type RawExifTags } from "@/new/photos/services/exif";
-import { annotatedFaceIDsForFile, isMLEnabled } from "@/new/photos/services/ml";
+import { annotatedFaceIDsForFile, AnnotatedFacesForFile, getAnnotatedFacesForFile, getFacesForFile, isMLEnabled } from "@/new/photos/services/ml";
import { EnteFile } from "@/new/photos/types/file";
import { formattedByteSize } from "@/new/photos/utils/units";
import CopyButton from "@ente/shared/components/CodeBlock/CopyButton";
@@ -98,6 +98,7 @@ export const FileInfo: React.FC = ({
const [exifInfo, setExifInfo] = useState();
const [openRawExif, setOpenRawExif] = useState(false);
+ const [annotatedFaces, setAnnotatedFaces] = useState();
const location = useMemo(() => {
if (file) {
@@ -107,23 +108,19 @@ export const FileInfo: React.FC = ({
return exif?.parsed?.location;
}, [file, exif]);
- const [annotatedPeopleFaceIDs, otherFaceIDs] =
- useMemoSingleThreaded(async () => {
- if (!file) return [[], []];
- const annotatedFaceIDs = await annotatedFaceIDsForFile(file);
- return annotatedFaceIDs.reduce(
- ([people, other], item) => {
- if (item[1]) {
- people.push(item);
- } else {
- other.push(item[0]);
- }
- return [people, other];
- },
- [[], []],
- );
+
+ useEffect(() => {
+ let didCancel = false;
+
+ void (async () => {
+ const result = await getAnnotatedFacesForFile(file);
+ !didCancel && setAnnotatedFaces(result);
+ })();
+
+ return () => { didCancel = true;}
}, [file]);
+
useEffect(() => {
setExifInfo(parseExifInfo(exif));
}, [exif]);
@@ -287,7 +284,8 @@ export const FileInfo: React.FC = ({
{isMLEnabled() && (
<>
- {/* TODO-Cluster */}
+ {annotatedFaces?.annotatedFaceIDs.length &&
+ // {/* TODO-Cluster */}
>
)}
diff --git a/web/packages/new/photos/components/PeopleList.tsx b/web/packages/new/photos/components/PeopleList.tsx
index 07157b5622..3553ed78aa 100644
--- a/web/packages/new/photos/components/PeopleList.tsx
+++ b/web/packages/new/photos/components/PeopleList.tsx
@@ -1,12 +1,11 @@
import { useIsMobileWidth } from "@/base/hooks";
-import { faceCrop, unidentifiedFaceIDs } from "@/new/photos/services/ml";
+import { faceCrop, type AnnotatedFaceID } from "@/new/photos/services/ml";
import type { Person } from "@/new/photos/services/ml/people";
import type { EnteFile } from "@/new/photos/types/file";
import { Skeleton, Typography, styled } from "@mui/material";
import { t } from "i18next";
import React, { useEffect, useState } from "react";
import { UnstyledButton } from "./mui-custom";
-import type { CGroup } from "../services/user-entity";
export interface SearchPeopleListProps {
people: Person[];
@@ -67,28 +66,21 @@ const SearchPeopleButton = styled(UnstyledButton)(
`,
);
-export interface CGroupPeopleListProps {
+export interface AnnotatedFacePeopleListProps {
+ annotatedFaceIDs: AnnotatedFaceID[];
/**
- * List of cgroup people to show.
- *
- * The current types don't reflect this, but these are all guaranteed to be
- * {@link Person}s with type "cgroup"
+ * Called when the user selects a face in the list.
*/
- people: Person[];
- /**
- * Called when the user selects a person in the list.
- */
- onSelectPerson: (person: Person) => void;
+ onSelectFace: (annotatedFaceID: AnnotatedFaceID) => void;
}
/**
- * Show the list of faces in the given file that are not linked to a a specific
- * cgroup ("people").
+ * Show the list of faces in the given file that are associated with a specific
+ * person.
*/
-export const CGroupPeopleList: React.FC = ({
- people,
- onSelectPerson,
-}) => {
+export const AnnotatedFacePeopleList: React.FC<
+ AnnotatedFacePeopleListProps
+> = ({ annotatedFaceIDs, onSelectFace }) => {
const isMobileWidth = useIsMobileWidth();
return (
= ({
}) => {
const [faceIDs, setFaceIDs] = useState([]);
- useEffect(() => {
- let didCancel = false;
-
- const go = async () => {
- const faceIDs = await unidentifiedFaceIDs(enteFile);
- !didCancel && setFaceIDs(faceIDs);
- };
-
- void go();
-
- return () => {
- didCancel = true;
- };
- }, [enteFile]);
-
if (faceIDs.length == 0) return <>>;
return (
diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts
index 70d6762349..bc7db78572 100644
--- a/web/packages/new/photos/services/ml/index.ts
+++ b/web/packages/new/photos/services/ml/index.ts
@@ -592,18 +592,39 @@ export const clipMatches = (
): Promise =>
worker().then((w) => w.clipMatches(searchPhrase));
+/** A face ID annotated with the ID of the person to which it is associated. */
+export interface AnnotatedFaceID {
+ faceID: string;
+ personID: string;
+}
+
+/**
+ * List of faces found in a file
+ *
+ * It is actually a pair of lists, one annotated by the person ids, and one with
+ * just the face ids.
+ */
+export interface AnnotatedFacesForFile {
+ /**
+ * A list of {@link AnnotatedFaceID}s for all faces in the file that are
+ * also associated with a {@link Person}.
+ */
+ annotatedFaceIDs: AnnotatedFaceID[];
+ /* A list of the remaining face (ids). */
+ otherFaceIDs: string[];
+}
+
/**
* Return the list of faces found in the given {@link enteFile}.
- *
- * Each item is returned as a (faceID, personID) tuple, where the faceID is the
- * ID of the face, and the personID is the id of the corresponding person that
- * this face is associated to (if any).
*/
-export const annotatedFaceIDsForFile = async (
+export const getAnnotatedFacesForFile = async (
enteFile: EnteFile,
-): Promise<[string, string | undefined][]> => {
+): Promise => {
+ const annotatedFaceIDs: AnnotatedFaceID[] = [];
+ const otherFaceIDs: string[] = [];
+
const index = await getFaceIndex(enteFile.id);
- if (!index) return [];
+ if (!index) return { annotatedFaceIDs, otherFaceIDs };
const people = _state.peopleSnapshot ?? [];
@@ -620,10 +641,16 @@ export const annotatedFaceIDsForFile = async (
}
}
- return index.faces.map(({ faceID }) => [
- faceID,
- faceIDToPersonID.get(faceID),
- ]);
+ for (const { faceID } of index.faces) {
+ const personID = faceIDToPersonID.get(faceID);
+ if (personID) {
+ annotatedFaceIDs.push({ faceID, personID });
+ } else {
+ otherFaceIDs.push(faceID);
+ }
+ }
+
+ return { annotatedFaceIDs, otherFaceIDs };
};
/**
From 2827a166dccc0a1bd1d689db563ed1641e4b089e Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 13:23:17 +0530
Subject: [PATCH 17/61] people list checkpoint
---
.../components/PhotoViewer/FileInfo/index.tsx | 55 +++++--
.../new/photos/components/PeopleList.tsx | 153 +++++++++---------
2 files changed, 118 insertions(+), 90 deletions(-)
diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
index bfd86e0db3..ead1211fdc 100644
--- a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
+++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
@@ -12,18 +12,25 @@ import {
type ParsedMetadataDate,
} from "@/media/file-metadata";
import { FileType } from "@/media/file-type";
-import { UnidentifiedFaces } from "@/new/photos/components/PeopleList";
+import {
+ AnnotatedFacePeopleList,
+ UnclusteredFaceList,
+} from "@/new/photos/components/PeopleList";
import { PhotoDateTimePicker } from "@/new/photos/components/PhotoDateTimePicker";
import { photoSwipeZIndex } from "@/new/photos/components/PhotoViewer";
import { tagNumericValue, type RawExifTags } from "@/new/photos/services/exif";
-import { annotatedFaceIDsForFile, AnnotatedFacesForFile, getAnnotatedFacesForFile, getFacesForFile, isMLEnabled } from "@/new/photos/services/ml";
+import {
+ AnnotatedFacesForFile,
+ getAnnotatedFacesForFile,
+ isMLEnabled,
+ type AnnotatedFaceID,
+} from "@/new/photos/services/ml";
import { EnteFile } from "@/new/photos/types/file";
import { formattedByteSize } from "@/new/photos/utils/units";
import CopyButton from "@ente/shared/components/CodeBlock/CopyButton";
import { FlexWrapper } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { getPublicMagicMetadataSync } from "@ente/shared/file-metadata";
-import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded";
import { formatDate, formatTime } from "@ente/shared/time/format";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
import CameraOutlined from "@mui/icons-material/CameraOutlined";
@@ -98,7 +105,9 @@ export const FileInfo: React.FC = ({
const [exifInfo, setExifInfo] = useState();
const [openRawExif, setOpenRawExif] = useState(false);
- const [annotatedFaces, setAnnotatedFaces] = useState();
+ const [annotatedFaces, setAnnotatedFaces] = useState<
+ AnnotatedFacesForFile | undefined
+ >();
const location = useMemo(() => {
if (file) {
@@ -108,18 +117,20 @@ export const FileInfo: React.FC = ({
return exif?.parsed?.location;
}, [file, exif]);
+ useEffect(() => {
+ if (!file) return;
- useEffect(() => {
- let didCancel = false;
+ let didCancel = false;
- void (async () => {
- const result = await getAnnotatedFacesForFile(file);
- !didCancel && setAnnotatedFaces(result);
- })();
-
- return () => { didCancel = true;}
- }, [file]);
+ void (async () => {
+ const result = await getAnnotatedFacesForFile(file);
+ !didCancel && setAnnotatedFaces(result);
+ })();
+ return () => {
+ didCancel = true;
+ };
+ }, [file]);
useEffect(() => {
setExifInfo(parseExifInfo(exif));
@@ -144,6 +155,10 @@ export const FileInfo: React.FC = ({
getMapDisableConfirmationDialog(() => updateMapEnabled(false)),
);
+ const handleSelectFace = (annotatedFaceID: AnnotatedFaceID) => {
+ console.log(annotatedFaceID);
+ };
+
return (
@@ -282,11 +297,17 @@ export const FileInfo: React.FC = ({
)}
- {isMLEnabled() && (
+ {isMLEnabled() && annotatedFaces && (
<>
- {annotatedFaces?.annotatedFaceIDs.length &&
- // {/* TODO-Cluster */}
-
+
+
>
)}
diff --git a/web/packages/new/photos/components/PeopleList.tsx b/web/packages/new/photos/components/PeopleList.tsx
index 3553ed78aa..8ced5627e2 100644
--- a/web/packages/new/photos/components/PeopleList.tsx
+++ b/web/packages/new/photos/components/PeopleList.tsx
@@ -1,4 +1,5 @@
import { useIsMobileWidth } from "@/base/hooks";
+import { pt } from "@/base/i18n";
import { faceCrop, type AnnotatedFaceID } from "@/new/photos/services/ml";
import type { Person } from "@/new/photos/services/ml/people";
import type { EnteFile } from "@/new/photos/types/file";
@@ -25,7 +26,7 @@ export const SearchPeopleList: React.FC = ({
sx={{ justifyContent: people.length > 3 ? "center" : "start" }}
>
{people.slice(0, isMobileWidth ? 6 : 7).map((person) => (
- onSelectPerson(person)}
>
@@ -34,7 +35,7 @@ export const SearchPeopleList: React.FC = ({
enteFile={person.displayFaceFile}
placeholderDimension={87}
/>
-
+
))}
);
@@ -49,7 +50,7 @@ const SearchPeopleContainer = styled("div")`
margin-block-end: 15px;
`;
-const SearchPeopleButton = styled(UnstyledButton)(
+const SearchPersonButton = styled(UnstyledButton)(
({ theme }) => `
width: 87px;
height: 87px;
@@ -67,6 +68,13 @@ const SearchPeopleButton = styled(UnstyledButton)(
);
export interface AnnotatedFacePeopleListProps {
+ /**
+ * The {@link EnteFile} whose information we are showing.
+ */
+ enteFile: EnteFile;
+ /**
+ * The list of faces in the file that are associated with a person.
+ */
annotatedFaceIDs: AnnotatedFaceID[];
/**
* Called when the user selects a face in the list.
@@ -80,41 +88,43 @@ export interface AnnotatedFacePeopleListProps {
*/
export const AnnotatedFacePeopleList: React.FC<
AnnotatedFacePeopleListProps
-> = ({ annotatedFaceIDs, onSelectFace }) => {
- const isMobileWidth = useIsMobileWidth();
+> = ({ enteFile, annotatedFaceIDs, onSelectFace }) => {
+ if (annotatedFaceIDs.length == 0) return <>>;
+
return (
- 3 ? "center" : "start" }}
- >
- {people.slice(0, isMobileWidth ? 6 : 7).map((person) => (
- onSelectPerson(person)}
- >
-
-
- ))}
-
+ <>
+
+ {t("people")}
+
+
+ {annotatedFaceIDs.map((annotatedFaceID) => (
+ onSelectFace(annotatedFaceID)}
+ >
+
+
+ ))}
+
+ >
);
};
-const SearchPeopleContainer = styled("div")`
+const AnnotatedFacePeopleContainer = styled("div")`
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
- margin-block-start: 12px;
- margin-block-end: 15px;
`;
-const SearchPeopleButton = styled(UnstyledButton)(
+const AnnotatedFaceButton = styled(UnstyledButton)(
({ theme }) => `
- width: 87px;
- height: 87px;
+ width: 112px;
+ height: 112px;
border-radius: 50%;
overflow: hidden;
& > img {
@@ -128,7 +138,48 @@ const SearchPeopleButton = styled(UnstyledButton)(
`,
);
-const FaceChipContainer = styled("div")`
+export interface UnclusteredFaceListProps {
+ /**
+ * The {@link EnteFile} whose information we are showing.
+ */
+ enteFile: EnteFile;
+ /**
+ * The list of faces in the file that are not associated with a person.
+ */
+ faceIDs: string[];
+}
+
+/**
+ * Show the list of faces in the given file that are not associated with a
+ * specific person.
+ */
+export const UnclusteredFaceList: React.FC = ({
+ enteFile,
+ faceIDs,
+}) => {
+ if (faceIDs.length == 0) return <>>;
+
+ return (
+ <>
+
+ {pt("Other faces")}
+ {/*t("UNIDENTIFIED_FACES") TODO-Cluster */}
+
+
+ {faceIDs.map((faceID) => (
+
+
+
+ ))}
+
+ >
+ );
+};
+
+const UnclusteredFacesContainer = styled("div")`
display: flex;
flex-wrap: wrap;
justify-content: center;
@@ -138,63 +189,19 @@ const FaceChipContainer = styled("div")`
overflow: auto;
`;
-const FaceChip = styled("div")<{ clickable?: boolean }>`
+const UnclusteredFace = styled("div")`
width: 112px;
height: 112px;
margin: 5px;
border-radius: 50%;
overflow: hidden;
position: relative;
- cursor: ${({ clickable }) => (clickable ? "pointer" : "normal")};
& > img {
width: 100%;
height: 100%;
}
`;
-export interface PhotoPeopleListProps {
- file: EnteFile;
- onSelect?: (person: Person, index: number) => void;
-}
-
-export function PhotoPeopleList() {
- return <>>;
-}
-
-interface UnidentifiedFacesProps {
- enteFile: EnteFile;
-}
-
-/**
- * Show the list of faces in the given file that are not linked to a specific
- * person ("face cluster").
- */
-export const UnidentifiedFaces: React.FC = ({
- enteFile,
-}) => {
- const [faceIDs, setFaceIDs] = useState([]);
-
- if (faceIDs.length == 0) return <>>;
-
- return (
- <>
-
- {t("UNIDENTIFIED_FACES")}
-
-
- {faceIDs.map((faceID) => (
-
-
-
- ))}
-
- >
- );
-};
-
interface FaceCropImageViewProps {
/** The ID of the face to display. */
faceID: string;
From 8a953cab88fd44d5a37342555ffad16cbfb8e739 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 13:33:03 +0530
Subject: [PATCH 18/61] Fix alignment etc
---
.../new/photos/components/PeopleList.tsx | 23 ++++++-------------
1 file changed, 7 insertions(+), 16 deletions(-)
diff --git a/web/packages/new/photos/components/PeopleList.tsx b/web/packages/new/photos/components/PeopleList.tsx
index 8ced5627e2..ba7711af59 100644
--- a/web/packages/new/photos/components/PeopleList.tsx
+++ b/web/packages/new/photos/components/PeopleList.tsx
@@ -96,7 +96,7 @@ export const AnnotatedFacePeopleList: React.FC<
{t("people")}
-
+
{annotatedFaceIDs.map((annotatedFaceID) => (
))}
-
+
>
);
};
-const AnnotatedFacePeopleContainer = styled("div")`
+const FileFaceList = styled("div")`
display: flex;
flex-wrap: wrap;
+ justify-content: center;
align-items: center;
gap: 5px;
+ margin: 5px;
`;
const AnnotatedFaceButton = styled(UnstyledButton)(
@@ -165,7 +167,7 @@ export const UnclusteredFaceList: React.FC = ({
{pt("Other faces")}
{/*t("UNIDENTIFIED_FACES") TODO-Cluster */}
-
+
{faceIDs.map((faceID) => (
= ({
/>
))}
-
+
>
);
};
-const UnclusteredFacesContainer = styled("div")`
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- align-items: center;
- margin-top: 5px;
- margin-bottom: 5px;
- overflow: auto;
-`;
-
const UnclusteredFace = styled("div")`
width: 112px;
height: 112px;
margin: 5px;
border-radius: 50%;
overflow: hidden;
- position: relative;
& > img {
width: 100%;
height: 100%;
From 57d245d9e04378fa978555a419d2b27353618f9d Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 13:54:09 +0530
Subject: [PATCH 19/61] Select person
---
web/apps/photos/src/components/PhotoFrame.tsx | 5 ++++-
.../src/components/PhotoViewer/FileInfo/index.tsx | 12 ++++++++++--
web/apps/photos/src/components/PhotoViewer/index.tsx | 9 ++++++---
web/apps/photos/src/pages/gallery.tsx | 6 ++++++
4 files changed, 26 insertions(+), 6 deletions(-)
diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx
index 59bb80b954..ccf0908a2b 100644
--- a/web/apps/photos/src/components/PhotoFrame.tsx
+++ b/web/apps/photos/src/components/PhotoFrame.tsx
@@ -8,7 +8,7 @@ import { PHOTOS_PAGES } from "@ente/shared/constants/pages";
import { CustomError } from "@ente/shared/error";
import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded";
import { styled } from "@mui/material";
-import PhotoViewer from "components/PhotoViewer";
+import PhotoViewer, { type PhotoViewerProps } from "components/PhotoViewer";
import { useRouter } from "next/router";
import { GalleryContext } from "pages/gallery";
import PhotoSwipe from "photoswipe";
@@ -72,6 +72,7 @@ interface Props {
isInHiddenSection?: boolean;
setFilesDownloadProgressAttributesCreator?: SetFilesDownloadProgressAttributesCreator;
selectable?: boolean;
+ onSelectPerson?: PhotoViewerProps["onSelectPerson"];
}
const PhotoFrame = ({
@@ -95,6 +96,7 @@ const PhotoFrame = ({
isInHiddenSection,
setFilesDownloadProgressAttributesCreator,
selectable,
+ onSelectPerson,
}: Props) => {
const [open, setOpen] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
@@ -580,6 +582,7 @@ const PhotoFrame = ({
setFilesDownloadProgressAttributesCreator={
setFilesDownloadProgressAttributesCreator
}
+ onSelectPerson={onSelectPerson}
/>
);
diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
index ead1211fdc..a528aa1c2e 100644
--- a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
+++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx
@@ -69,7 +69,7 @@ export interface FileInfoExif {
parsed: ParsedMetadata | undefined;
}
-interface FileInfoProps {
+export interface FileInfoProps {
showInfo: boolean;
handleCloseInfo: () => void;
closePhotoViewer: () => void;
@@ -81,6 +81,10 @@ interface FileInfoProps {
fileToCollectionsMap?: Map;
collectionNameMap?: Map;
showCollectionChips: boolean;
+ /**
+ * Called when the user selects a person in the file info panel.
+ */
+ onSelectPerson?: ((personID: string) => void) | undefined;
}
export const FileInfo: React.FC = ({
@@ -95,6 +99,7 @@ export const FileInfo: React.FC = ({
collectionNameMap,
showCollectionChips,
closePhotoViewer,
+ onSelectPerson,
}) => {
const { mapEnabled, updateMapEnabled, setDialogBoxAttributesV2 } =
useContext(AppContext);
@@ -156,7 +161,10 @@ export const FileInfo: React.FC = ({
);
const handleSelectFace = (annotatedFaceID: AnnotatedFaceID) => {
- console.log(annotatedFaceID);
+ if (onSelectPerson) {
+ onSelectPerson(annotatedFaceID.personID);
+ closePhotoViewer();
+ }
};
return (
diff --git a/web/apps/photos/src/components/PhotoViewer/index.tsx b/web/apps/photos/src/components/PhotoViewer/index.tsx
index 10d2f310e2..b15f1e3f44 100644
--- a/web/apps/photos/src/components/PhotoViewer/index.tsx
+++ b/web/apps/photos/src/components/PhotoViewer/index.tsx
@@ -48,7 +48,7 @@ import { SetFilesDownloadProgressAttributesCreator } from "types/gallery";
import { pauseVideo, playVideo } from "utils/photoFrame";
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
import { getTrashFileMessage } from "utils/ui";
-import { FileInfo, type FileInfoExif } from "./FileInfo";
+import { FileInfo, type FileInfoExif, type FileInfoProps } from "./FileInfo";
import ImageEditorOverlay from "./ImageEditorOverlay";
import CircularProgressWithLabel from "./styledComponents/CircularProgressWithLabel";
import { ConversionFailedNotification } from "./styledComponents/ConversionFailedNotification";
@@ -98,7 +98,8 @@ const CaptionContainer = styled("div")(({ theme }) => ({
backgroundColor: theme.colors.backdrop.faint,
backdropFilter: `blur(${theme.colors.blur.base})`,
}));
-interface Iprops {
+
+export interface PhotoViewerProps {
isOpen: boolean;
items: any[];
currentIndex?: number;
@@ -115,9 +116,10 @@ interface Iprops {
fileToCollectionsMap: Map;
collectionNameMap: Map;
setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator;
+ onSelectPerson?: FileInfoProps["onSelectPerson"];
}
-function PhotoViewer(props: Iprops) {
+function PhotoViewer(props: PhotoViewerProps) {
const galleryContext = useContext(GalleryContext);
const appContext = useContext(AppContext);
const publicCollectionGalleryContext = useContext(
@@ -969,6 +971,7 @@ function PhotoViewer(props: Iprops) {
refreshPhotoswipe={refreshPhotoswipe}
fileToCollectionsMap={props.fileToCollectionsMap}
collectionNameMap={props.collectionNameMap}
+ onSelectPerson={props.onSelectPerson}
/>
{
+ setActivePersonID(personID);
+ setBarMode("people");
+ };
+
if (!collectionSummaries || !filteredData) {
return ;
}
@@ -1321,6 +1326,7 @@ export default function Gallery() {
setFilesDownloadProgressAttributesCreator
}
selectable={true}
+ onSelectPerson={handleSelectFileInfoPerson}
/>
)}
{selected.count > 0 &&
From 7bdbaec4432287d75e7c2c01f5ed85a640881bfe Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 14:01:44 +0530
Subject: [PATCH 20/61] Unconditionally enable for internal
---
web/packages/new/photos/services/ml/index.ts | 26 +++++++------------
web/packages/new/photos/services/ml/people.ts | 3 ---
2 files changed, 9 insertions(+), 20 deletions(-)
diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts
index bc7db78572..6de92a6205 100644
--- a/web/packages/new/photos/services/ml/index.ts
+++ b/web/packages/new/photos/services/ml/index.ts
@@ -5,7 +5,6 @@
import { isDesktop } from "@/base/app";
import { blobCache } from "@/base/blob-cache";
import { ensureElectron } from "@/base/electron";
-import { isDevBuild } from "@/base/env";
import log from "@/base/log";
import { masterKeyFromSession } from "@/base/session-store";
import type { Electron } from "@/base/types/ipc";
@@ -327,16 +326,17 @@ export const mlSync = async () => {
const workerDidUnawaitedIndex = () => void updateClustersAndPeople();
const updateClustersAndPeople = async () => {
- // TODO-Cluster
- if (await wipClusterEnable()) {
- const masterKey = await masterKeyFromSession();
+ if (!(await isInternalUser())) return;
- // Fetch existing cgroups from remote.
- await pullUserEntities("cgroup", masterKey);
+ const masterKey = await masterKeyFromSession();
- // Generate or update local clusters.
- await (await worker()).clusterFaces(masterKey);
- }
+ // Fetch existing cgroups from remote.
+ await pullUserEntities("cgroup", masterKey);
+
+ // Generate or update local clusters.
+ await (await worker()).clusterFaces(masterKey);
+
+ // Update the people shown in the UI.
await updatePeople();
};
@@ -364,14 +364,6 @@ export const indexNewUpload = (enteFile: EnteFile, uploadItem: UploadItem) => {
void worker().then((w) => w.onUpload(enteFile, uploadItem));
};
-/**
- * WIP! Don't enable, dragon eggs are hatching here.
- * TODO-Cluster
- */
-export const wipClusterEnable = async (): Promise =>
- (!!process.env.NEXT_PUBLIC_ENTE_WIP_CL && isDevBuild) ||
- (await isInternalUser());
-
export type MLStatus =
| { phase: "disabled" /* The ML remote flag is off */ }
| {
diff --git a/web/packages/new/photos/services/ml/people.ts b/web/packages/new/photos/services/ml/people.ts
index 827d78acca..79e9152b86 100644
--- a/web/packages/new/photos/services/ml/people.ts
+++ b/web/packages/new/photos/services/ml/people.ts
@@ -1,4 +1,3 @@
-import { wipClusterEnable } from ".";
import type { EnteFile } from "../../types/file";
import { getLocalFiles } from "../files";
import { savedCGroups, type CGroup } from "../user-entity";
@@ -138,8 +137,6 @@ export type Person = (
* reference.
*/
export const reconstructPeople = async (): Promise => {
- if (!(await wipClusterEnable())) return [];
-
const files = await getLocalFiles("normal");
const fileByID = new Map(files.map((f) => [f.id, f]));
From 01f31c352b90044f36880e78c825b8b6c86a1846 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 16:47:35 +0530
Subject: [PATCH 21/61] Support face crops
---
.../new/photos/components/ItemCards.tsx | 61 ++++++++++++++++---
web/packages/new/photos/services/download.ts | 12 ++++
2 files changed, 66 insertions(+), 7 deletions(-)
diff --git a/web/packages/new/photos/components/ItemCards.tsx b/web/packages/new/photos/components/ItemCards.tsx
index 95aab19609..9a5de57b20 100644
--- a/web/packages/new/photos/components/ItemCards.tsx
+++ b/web/packages/new/photos/components/ItemCards.tsx
@@ -6,6 +6,7 @@ import downloadManager from "@/new/photos/services/download";
import { type EnteFile } from "@/new/photos/types/file";
import { styled } from "@mui/material";
import React, { useEffect, useState } from "react";
+import { faceCrop } from "../services/ml";
interface ItemCardProps {
/**
@@ -16,11 +17,17 @@ interface ItemCardProps {
* Optional file whose thumbnail (if any) should be should be shown.
*/
coverFile?: EnteFile | undefined;
+ /**
+ * Optional ID of a specific face within {@link coverFile} to show.
+ *
+ * Precondition: {@link faceID} must be an ID of a face that belongs to the
+ * given {@link coverFile}.
+ */
+ coverFaceID?: string | undefined;
/**
* Optional boolean indicating if the user is currently scrolling.
*
- * This is used as a hint by the cover file downloader to prioritize
- * downloads.
+ * This is used as a hint by the file downloader to prioritize downloads.
*/
isScrolling?: boolean;
/**
@@ -28,25 +35,65 @@ interface ItemCardProps {
*/
onClick?: () => void;
}
+
/**
* A generic card that can be be used to represent collections, files, people -
* anything that (usually) has an associated "cover photo".
+ *
+ * Usually, we provide it a {@link coverFile} prop to set the file whose
+ * thumbnail should be shown in the card. However, an additional
+ * {@link coverFaceID} prop can be used to show the face crop for that specific
+ * face within the cover file.
+ *
+ * Note that while the common use case is to use this with a cover photo (and an
+ * additional cover faceID), both of these are optional and the item card can
+ * also be used as a static component without an associated cover image by
+ * covering it with an opaque overlay.
*/
export const ItemCard: React.FC> = ({
TileComponent,
coverFile,
+ coverFaceID,
isScrolling,
onClick,
children,
}) => {
- const [coverImageURL, setCoverImageURL] = useState("");
+ const [coverImageURL, setCoverImageURL] = useState();
useEffect(() => {
if (!coverFile) return;
- void downloadManager
- .getThumbnailForPreview(coverFile, isScrolling)
- .then((url) => url && setCoverImageURL(url));
- }, [coverFile, isScrolling]);
+
+ let didCancel = false;
+ let thisObjectURL: string | undefined;
+
+ const go = async () => {
+ if (coverFaceID) {
+ const blob = await faceCrop(coverFaceID, coverFile);
+ if (!didCancel) {
+ thisObjectURL = blob
+ ? URL.createObjectURL(blob)
+ : undefined;
+ setCoverImageURL(thisObjectURL);
+ }
+ } else {
+ const url = await downloadManager.getThumbnailForPreview(
+ coverFile,
+ isScrolling,
+ );
+ if (!didCancel) {
+ thisObjectURL = url;
+ setCoverImageURL(thisObjectURL);
+ }
+ }
+ };
+
+ void go();
+
+ return () => {
+ didCancel = true;
+ if (thisObjectURL) URL.revokeObjectURL(thisObjectURL);
+ };
+ }, [coverFile, coverFaceID, isScrolling]);
return (
diff --git a/web/packages/new/photos/services/download.ts b/web/packages/new/photos/services/download.ts
index 4d76e29ac2..a126fdf724 100644
--- a/web/packages/new/photos/services/download.ts
+++ b/web/packages/new/photos/services/download.ts
@@ -144,6 +144,18 @@ class DownloadManagerImpl {
return thumb;
}
+ /**
+ * Resolves with an object URL that points to the file's thumbnail.
+ *
+ * The thumbnail will be downloaded (unless {@link localOnly} is true) and
+ * cached.
+ *
+ * The optional {@link localOnly} parameter can be set to indicate that this
+ * is being called as part of a scroll, so the downloader should not attempt
+ * to download the file but should instead fulfill the request from the
+ * cache. This avoids an unbounded flurry of requests on scroll, only
+ * downloading when the position has quiescized.
+ */
async getThumbnailForPreview(
file: EnteFile,
localOnly = false,
From 4bb6aa2b39a133d340f41bfad49ccdecfee1835c Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 16:48:18 +0530
Subject: [PATCH 22/61] Use
---
web/packages/new/photos/components/Gallery/BarImpl.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/web/packages/new/photos/components/Gallery/BarImpl.tsx b/web/packages/new/photos/components/Gallery/BarImpl.tsx
index c71ee1fc62..089e8df0e2 100644
--- a/web/packages/new/photos/components/Gallery/BarImpl.tsx
+++ b/web/packages/new/photos/components/Gallery/BarImpl.tsx
@@ -583,6 +583,7 @@ const PersonCard: React.FC = ({
onSelectPerson(person)}
>
{person.name && }
From 370d4af0085e39cb55f02a96a6b23eb6d33daaed Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 17:02:51 +0530
Subject: [PATCH 23/61] Thumbnails shouldn't be revoked
So make the face crops behave the same too
---
.../new/photos/components/ItemCards.tsx | 33 +++++--------------
.../new/photos/components/PeopleList.tsx | 15 ++++-----
web/packages/new/photos/services/download.ts | 5 ++-
web/packages/new/photos/services/ml/index.ts | 28 ++++++++++++++--
4 files changed, 44 insertions(+), 37 deletions(-)
diff --git a/web/packages/new/photos/components/ItemCards.tsx b/web/packages/new/photos/components/ItemCards.tsx
index 9a5de57b20..8c73fb3ad6 100644
--- a/web/packages/new/photos/components/ItemCards.tsx
+++ b/web/packages/new/photos/components/ItemCards.tsx
@@ -64,34 +64,19 @@ export const ItemCard: React.FC> = ({
if (!coverFile) return;
let didCancel = false;
- let thisObjectURL: string | undefined;
- const go = async () => {
- if (coverFaceID) {
- const blob = await faceCrop(coverFaceID, coverFile);
- if (!didCancel) {
- thisObjectURL = blob
- ? URL.createObjectURL(blob)
- : undefined;
- setCoverImageURL(thisObjectURL);
- }
- } else {
- const url = await downloadManager.getThumbnailForPreview(
- coverFile,
- isScrolling,
- );
- if (!didCancel) {
- thisObjectURL = url;
- setCoverImageURL(thisObjectURL);
- }
- }
- };
-
- void go();
+ if (coverFaceID) {
+ void faceCrop(coverFaceID, coverFile).then(
+ (url) => !didCancel && setCoverImageURL(url),
+ );
+ } else {
+ void downloadManager
+ .getThumbnailForPreview(coverFile, isScrolling)
+ .then((url) => !didCancel && setCoverImageURL(url));
+ }
return () => {
didCancel = true;
- if (thisObjectURL) URL.revokeObjectURL(thisObjectURL);
};
}, [coverFile, coverFaceID, isScrolling]);
diff --git a/web/packages/new/photos/components/PeopleList.tsx b/web/packages/new/photos/components/PeopleList.tsx
index ba7711af59..ac41dc7deb 100644
--- a/web/packages/new/photos/components/PeopleList.tsx
+++ b/web/packages/new/photos/components/PeopleList.tsx
@@ -216,25 +216,22 @@ const FaceCropImageView: React.FC = ({
enteFile,
placeholderDimension,
}) => {
- const [objectURL, setObjectURL] = useState();
+ const [url, setURL] = useState();
useEffect(() => {
let didCancel = false;
- let thisObjectURL: string | undefined;
- void faceCrop(faceID, enteFile).then((blob) => {
- if (blob && !didCancel)
- setObjectURL((thisObjectURL = URL.createObjectURL(blob)));
- });
+ void faceCrop(faceID, enteFile).then(
+ (url) => !didCancel && setURL(url),
+ );
return () => {
didCancel = true;
- if (thisObjectURL) URL.revokeObjectURL(thisObjectURL);
};
}, [faceID, enteFile]);
- return objectURL ? (
-
+ return url ? (
+
) : (
>();
+
+ /**
+ * Cached object URLs to face crops that we have previously vended out.
+ *
+ * The cache is only cleared on logout.
+ */
+ faceCropObjectURLCache = new Map();
}
/** State shared by the functions in this module. See {@link MLState}. */
@@ -187,6 +194,9 @@ export const logoutML = async () => {
// execution contexts], it gets called first in the logout sequence, and
// then this function (`logoutML`) gets called at a later point in time.
+ [..._state.faceCropObjectURLCache.values()].forEach((url) =>
+ URL.revokeObjectURL(url),
+ );
_state = new MLState();
await clearMLDB();
};
@@ -646,7 +656,10 @@ export const getAnnotatedFacesForFile = async (
};
/**
- * Return the cached face crop for the given face, regenerating it if needed.
+ * Return a URL to the face crop for the given face, regenerating it if needed.
+ *
+ * The resultant URL is cached (both the object URL itself, and the underlying
+ * file crop blob used to generete it).
*
* @param faceID The id of the face whose face crop we want.
*
@@ -662,8 +675,17 @@ export const faceCrop = async (faceID: string, enteFile: EnteFile) => {
await inFlight;
- const cache = await blobCache("face-crops");
- return cache.get(faceID);
+ let url = _state.faceCropObjectURLCache.get(faceID);
+ if (!url) {
+ const cache = await blobCache("face-crops");
+ const blob = await cache.get(faceID);
+ if (blob) {
+ url = URL.createObjectURL(blob);
+ if (url) _state.faceCropObjectURLCache.set(faceID, url);
+ }
+ }
+
+ return url;
};
/**
From eafc8fc4cbca64b41180ea3b14554e32b9c0e126 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 17:11:47 +0530
Subject: [PATCH 24/61] Fix logout
---
web/apps/photos/src/pages/gallery.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx
index 86a3b88e02..8d617af009 100644
--- a/web/apps/photos/src/pages/gallery.tsx
+++ b/web/apps/photos/src/pages/gallery.tsx
@@ -562,11 +562,11 @@ export default function Gallery() {
selectedSearchOption.suggestion,
);
} else if (barMode == "people") {
- let filteredPeople = people;
+ let filteredPeople = people ?? [];
if (tempDeletedFileIds?.size ?? tempHiddenFileIds?.size) {
// Prune the in-memory temp updates from the actual state to
// obtain the UI state.
- filteredPeople = people
+ filteredPeople = (people ?? [])
.map((p) => ({
...p,
fileIDs: p.fileIDs.filter(
From 5b73eee14c9f49c9f44e387af5b93e85645da1d2 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 18:01:03 +0530
Subject: [PATCH 25/61] Don't show bar controls in people section
---
web/packages/new/photos/components/Gallery/BarImpl.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/web/packages/new/photos/components/Gallery/BarImpl.tsx b/web/packages/new/photos/components/Gallery/BarImpl.tsx
index 089e8df0e2..ce1153d8bd 100644
--- a/web/packages/new/photos/components/Gallery/BarImpl.tsx
+++ b/web/packages/new/photos/components/Gallery/BarImpl.tsx
@@ -226,7 +226,7 @@ export const GalleryBarImpl: React.FC = ({
],
);
- const controls1 = isMobile && (
+ const controls1 = isMobile && mode != "people" && (
= ({
);
- const controls2 = !isMobile && (
+ const controls2 = !isMobile && mode != "people" && (
Date: Fri, 27 Sep 2024 18:05:49 +0530
Subject: [PATCH 26/61] wip empty state
---
web/apps/photos/src/pages/gallery.tsx | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx
index 8d617af009..05a3b03577 100644
--- a/web/apps/photos/src/pages/gallery.tsx
+++ b/web/apps/photos/src/pages/gallery.tsx
@@ -1132,6 +1132,12 @@ export default function Gallery() {
return ;
}
+ const showEmptySectionState =
+ !isInSearchMode &&
+ !isFirstLoad &&
+ !files?.length &&
+ !hiddenFiles?.length;
+
return (
setOpenWhatsNew(false)}
/>
- {!isInSearchMode &&
- !isFirstLoad &&
- !files?.length &&
- !hiddenFiles?.length &&
- activeCollectionID === ALL_SECTION ? (
+ {showEmptySectionState && activeCollectionID === ALL_SECTION ? (
+ ) : showEmptySectionState && resolvedBarMode == "people" ? (
+
Empty
) : (
Date: Fri, 27 Sep 2024 18:23:50 +0530
Subject: [PATCH 27/61] wip checkpoint people empty state
---
.../Collections/GalleryBarAndListHeader.tsx | 4 +-
web/apps/photos/src/pages/gallery.tsx | 70 ++++++-------------
.../new/photos/components/Gallery/BarImpl.tsx | 12 ++--
.../components/Gallery/PeopleHeader.tsx | 15 ++--
4 files changed, 32 insertions(+), 69 deletions(-)
diff --git a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx
index 882879e7ba..e34768d212 100644
--- a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx
+++ b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx
@@ -170,11 +170,13 @@ export const GalleryBarAndListHeader: React.FC = ({
}
onCollectionCast={() => setOpenAlbumCastDialog(true)}
/>
- ) : (
+ ) : activePerson ? (
+ ) : (
+ <>>
),
itemType: ITEM_TYPE.HEADER,
height: 68,
diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx
index 05a3b03577..3cbf5ef0cd 100644
--- a/web/apps/photos/src/pages/gallery.tsx
+++ b/web/apps/photos/src/pages/gallery.tsx
@@ -565,8 +565,9 @@ export default function Gallery() {
let filteredPeople = people ?? [];
if (tempDeletedFileIds?.size ?? tempHiddenFileIds?.size) {
// Prune the in-memory temp updates from the actual state to
- // obtain the UI state.
- filteredPeople = (people ?? [])
+ // obtain the UI state. Kept inside an preflight check to so
+ // that the common path remains fast.
+ filteredPeople = filteredPeople
.map((p) => ({
...p,
fileIDs: p.fileIDs.filter(
@@ -579,6 +580,8 @@ export default function Gallery() {
}
const activePerson =
filteredPeople.find((p) => p.id == activePersonID) ??
+ // We don't have an "All" pseudo-album in people mode currently,
+ // so default to the first person in the list.
filteredPeople[0];
const pfSet = new Set(activePerson?.fileIDs ?? []);
filteredFiles = getUniqueFiles(
@@ -589,7 +592,6 @@ export default function Gallery() {
);
galleryPeopleState = {
activePerson,
- activePersonID,
people: filteredPeople,
};
} else {
@@ -681,31 +683,6 @@ export default function Gallery() {
galleryPeopleState: undefined,
};
- // Calling setState during rendering is frowned upon for good reasons, but
- // it is not verboten, and it has documented semantics:
- //
- // > React will discard the currently rendering component's output and
- // > immediately attempt to render it again with the new state.
- // >
- // > https://react.dev/reference/react/useState
- //
- // That said, we should try to refactor this code to use a reducer or some
- // other store so that this is not needed.
- if (barMode == "people" && galleryPeopleState?.people.length === 0) {
- log.info(
- "Resetting gallery to all section since people mode is no longer valid",
- );
- setBarMode("albums");
- setActiveCollectionID(ALL_SECTION);
- }
-
- // Derived1 is async, leading to even more workarounds.
- const resolvedBarMode = galleryPeopleState
- ? barMode
- : barMode == "people"
- ? "albums"
- : barMode;
-
const selectAll = (e: KeyboardEvent) => {
// ignore ctrl/cmd + a if the user is typing in a text field
if (
@@ -736,14 +713,13 @@ export default function Gallery() {
count: 0,
collectionID: activeCollectionID,
context:
- resolvedBarMode == "people" &&
- galleryPeopleState?.activePersonID
+ barMode == "people" && galleryPeopleState?.activePerson?.id
? {
mode: "people" as const,
- personID: galleryPeopleState.activePersonID,
+ personID: galleryPeopleState.activePerson.id,
}
: {
- mode: resolvedBarMode as "albums" | "hidden-albums",
+ mode: barMode as "albums" | "hidden-albums",
collectionID: ensure(activeCollectionID),
},
};
@@ -1114,12 +1090,7 @@ export default function Gallery() {
};
const handleSelectPerson = (person: Person | undefined) => {
- // The person bar currently does not have an "all" mode, so default to
- // the first person when no specific person is provided. This can happen
- // when the user clicks the "People" header in the search empty state (it
- // is guaranteed that this header will only be shown if there is at
- // least one person).
- setActivePersonID(person?.id ?? galleryPeopleState?.people[0]?.id);
+ setActivePersonID(person?.id);
setBarMode("people");
};
@@ -1214,7 +1185,7 @@ export default function Gallery() {
marginBottom: "12px",
}}
>
- {resolvedBarMode == "hidden-albums" ? (
+ {barMode == "hidden-albums" ? (
@@ -1235,15 +1206,14 @@ export default function Gallery() {
{showEmptySectionState && activeCollectionID === ALL_SECTION ? (
- ) : showEmptySectionState && resolvedBarMode == "people" ? (
+ ) : showEmptySectionState &&
+ barMode == "people" &&
+ !galleryPeopleState?.activePerson ? (
Empty
) : (
)}
= ({
: {
type: "people" as const,
people,
- activePerson: ensure(activePerson),
+ activePerson,
onSelectPerson,
},
[
@@ -404,7 +402,7 @@ type ItemData =
| {
type: "people";
people: Person[];
- activePerson: Person;
+ activePerson: Person | undefined;
onSelectPerson: (person: Person) => void;
};
@@ -570,7 +568,7 @@ const ActiveIndicator = styled("div")`
interface PersonCardProps {
person: Person;
- activePerson: Person;
+ activePerson: Person | undefined;
onSelectPerson: (person: Person) => void;
}
@@ -588,6 +586,6 @@ const PersonCard: React.FC = ({
>
{person.name && }
- {activePerson.id === person.id && }
+ {activePerson?.id === person.id && }
);
diff --git a/web/packages/new/photos/components/Gallery/PeopleHeader.tsx b/web/packages/new/photos/components/Gallery/PeopleHeader.tsx
index ad6e71925f..8f13eb60c5 100644
--- a/web/packages/new/photos/components/Gallery/PeopleHeader.tsx
+++ b/web/packages/new/photos/components/Gallery/PeopleHeader.tsx
@@ -40,19 +40,12 @@ import { GalleryItemsHeaderAdapter, GalleryItemsSummary } from "./ListHeader";
*/
export interface GalleryPeopleState {
/**
- * The ID of the currently selected person.
+ * The currently selected person, if any.
*
- * We do not have an empty state currently, so this is guaranteed to be
- * present whenever the gallery is in the "people" mode.
+ * Whenever this is present, it is guaranteed to be one of the items from
+ * within {@link people}.
*/
- activePersonID: string;
- /**
- * The currently selected person.
- *
- * This is a convenience property that contains a direct reference to the
- * active {@link Person} from amongst {@link people}.
- */
- activePerson: Person;
+ activePerson: Person | undefined;
/**
* The list of people to show.
*/
From 2f27ae7b1933511ec3a00cd40c2f4040cbacc1ab Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 18:37:16 +0530
Subject: [PATCH 28/61] Empty state
---
web/apps/photos/src/pages/gallery.tsx | 19 +++++++++----------
.../new/photos/components/Gallery/index.tsx | 10 ++++++++++
2 files changed, 19 insertions(+), 10 deletions(-)
diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx
index 3cbf5ef0cd..4723ddd02c 100644
--- a/web/apps/photos/src/pages/gallery.tsx
+++ b/web/apps/photos/src/pages/gallery.tsx
@@ -3,7 +3,7 @@ import { NavbarBase } from "@/base/components/Navbar";
import { useIsMobileWidth } from "@/base/hooks";
import log from "@/base/log";
import type { Collection } from "@/media/collection";
-import { SearchResultsHeader } from "@/new/photos/components/Gallery";
+import { PeopleEmptyState, SearchResultsHeader } from "@/new/photos/components/Gallery";
import type { GalleryBarMode } from "@/new/photos/components/Gallery/BarImpl";
import { GalleryPeopleState } from "@/new/photos/components/Gallery/PeopleHeader";
import {
@@ -1103,12 +1103,6 @@ export default function Gallery() {
return ;
}
- const showEmptySectionState =
- !isInSearchMode &&
- !isFirstLoad &&
- !files?.length &&
- !hiddenFiles?.length;
-
return (
setOpenWhatsNew(false)}
/>
- {showEmptySectionState && activeCollectionID === ALL_SECTION ? (
+ {!isInSearchMode &&
+ !isFirstLoad &&
+ !files?.length &&
+ !hiddenFiles?.length &&
+ activeCollectionID === ALL_SECTION ? (
- ) : showEmptySectionState &&
+ ) : !isInSearchMode &&
+ !isFirstLoad &&
barMode == "people" &&
!galleryPeopleState?.activePerson ? (
-
Empty
+
) : (
= ({
/>
);
+
+export const PeopleEmptyState: React.FC = () => (
+
+
+ {pt("People will appear here once indexing completes")}
+
+
+);
From 72c93a170317effcc8c440cb454cc78d6704aab2 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 18:44:53 +0530
Subject: [PATCH 29/61] Tweak styling
---
web/packages/new/photos/components/Gallery/BarImpl.tsx | 5 ++++-
web/packages/new/photos/components/Gallery/index.tsx | 8 +++++++-
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/web/packages/new/photos/components/Gallery/BarImpl.tsx b/web/packages/new/photos/components/Gallery/BarImpl.tsx
index 21ca50f357..a3db5a1e5e 100644
--- a/web/packages/new/photos/components/Gallery/BarImpl.tsx
+++ b/web/packages/new/photos/components/Gallery/BarImpl.tsx
@@ -250,7 +250,10 @@ export const GalleryBarImpl: React.FC = ({
);
return (
-
+ // Hide the bottom border if we're showing the empty state for people.
+
{controls1}
diff --git a/web/packages/new/photos/components/Gallery/index.tsx b/web/packages/new/photos/components/Gallery/index.tsx
index 5ac9cda5b9..ddf14e4dea 100644
--- a/web/packages/new/photos/components/Gallery/index.tsx
+++ b/web/packages/new/photos/components/Gallery/index.tsx
@@ -45,7 +45,13 @@ export const SearchResultsHeader: React.FC = ({
export const PeopleEmptyState: React.FC = () => (
-
+
{pt("People will appear here once indexing completes")}
From cc262aad0ca8ebea5201176ca21eba3de46b8e17 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 19:06:32 +0530
Subject: [PATCH 30/61] New semantics
---
web/packages/new/photos/services/ml/index.ts | 23 ++++++++++++++++----
1 file changed, 19 insertions(+), 4 deletions(-)
diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts
index a970173d1e..aeecff8f47 100644
--- a/web/packages/new/photos/services/ml/index.ts
+++ b/web/packages/new/photos/services/ml/index.ts
@@ -87,6 +87,9 @@ class MLState {
/**
* Snapshot of the {@link Person}s returned by the {@link peopleSnapshot}
* function.
+ *
+ * It will be `undefined` only if ML is disabled. Otherwise, it will be an
+ * empty array even if the snapshot is pending its first sync.
*/
peopleSnapshot: Person[] | undefined;
@@ -186,6 +189,7 @@ export const isMLSupported = isDesktop;
*/
export const initML = () => {
_state.isMLEnabled = isMLEnabledLocal();
+ resetPeopleSnapshot();
};
export const logoutML = async () => {
@@ -222,6 +226,7 @@ export const enableML = async () => {
setIsMLEnabledLocal(true);
_state.isMLEnabled = true;
setInterimScheduledStatus();
+ resetPeopleSnapshot();
// Trigger updates, but don't wait for them to finish.
void updateMLStatusSnapshot().then(mlSync);
};
@@ -239,6 +244,7 @@ export const disableML = async () => {
_state.isSyncing = false;
await terminateMLWorker();
triggerStatusUpdate();
+ resetPeopleSnapshot();
};
/**
@@ -544,16 +550,25 @@ export const peopleSubscribe = (onChange: () => void): (() => void) => {
};
};
+/**
+ * If ML is enabled, set the people snapshot to an empty array to indicate that
+ * ML is enabled, but we're still reading in the set of people.
+ *
+ * Otherwise, if ML is disabled, set the people snapshot to `undefined`.
+ */
+const resetPeopleSnapshot = () =>
+ setPeopleSnapshot(_state.isMLEnabled ? [] : undefined);
+
/**
* Return the last known, cached {@link people}.
*
* This, along with {@link peopleSnapshot}, is meant to be used as arguments to
* React's {@link useSyncExternalStore}.
*
- * A return value of `undefined` indicates that we're either still loading the
- * initial list of people, or that the user has ML disabled and thus doesn't
- * have any people (this is distinct from the case where the user has ML enabled
- * but doesn't have any named "person" clusters so far).
+ * A return value of `undefined` indicates that ML is disabled. In all other
+ * cases, the list will be either empty (if we're either still loading the
+ * initial list of people, or if the user doesn't have any people), or, well,
+ * non-empty.
*/
export const peopleSnapshot = () => _state.peopleSnapshot;
From 2aaa23312b35e5f12228242c0af25eab1a601a36 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 19:15:12 +0530
Subject: [PATCH 31/61] Both buttons
---
.../Collections/GalleryBarAndListHeader.tsx | 2 +
web/apps/photos/src/pages/gallery.tsx | 10 ++++-
.../new/photos/components/Gallery/BarImpl.tsx | 44 +++++++++++++------
3 files changed, 42 insertions(+), 14 deletions(-)
diff --git a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx
index e34768d212..af963430c5 100644
--- a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx
+++ b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx
@@ -86,6 +86,7 @@ type CollectionsProps = Omit<
*/
export const GalleryBarAndListHeader: React.FC = ({
shouldHide,
+ showPeopleSectionButton,
mode,
onChangeMode,
collectionSummaries,
@@ -199,6 +200,7 @@ export const GalleryBarAndListHeader: React.FC = ({
<>
;
}
+ // `people` will be undefined only when ML is disabled, otherwise it'll be
+ // an empty array (even if people are loading).
+ const showPeopleSectionButton = people !== undefined;
+
return (
= ({
+ showPeopleSectionButton,
mode,
onChangeMode,
collectionSummaries,
@@ -255,7 +260,9 @@ export const GalleryBarImpl: React.FC = ({
sx={people.length ? {} : { borderBlockEndColor: "transparent" }}
>
-
+
{controls1}
@@ -312,33 +319,44 @@ export const Row2 = styled(Box)`
`;
const ModeIndicator: React.FC<
- Pick
-> = ({ mode, onChangeMode }) => {
+ Pick<
+ GalleryBarImplProps,
+ "showPeopleSectionButton" | "mode" | "onChangeMode"
+ >
+> = ({ showPeopleSectionButton, mode, onChangeMode }) => {
// Mode switcher is not shown in the hidden albums section.
if (mode == "hidden-albums") {
return {t("hidden_albums")};
}
- // Show the static mode indicator with only the "Albums" title unless we
- // come here with the people mode already set. This is because we don't
- // currently have an empty state for the People mode when ML is not enabled.
- if (mode == "albums") {
+ // Show the static mode indicator with only the "Albums" title if we have
+ // not been asked to show the people button (there are no other sections to
+ // switch to in such a case).
+ if (!showPeopleSectionButton) {
return {t("albums")};
}
return (
- onChangeMode("albums")}>
+ onChangeMode("albums")}
+ >
{t("albums")}
-
- {t("people")}
+
+ onChangeMode("people")}
+ >
+ {t("people")}
+
);
};
-const AlbumModeButton = styled(UnstyledButton)(
- ({ theme }) => `
- p { color: ${theme.colors.text.muted} }
+const ModeButton = styled(UnstyledButton)<{ $active: boolean }>(
+ ({ $active, theme }) => `
+ p { color: ${$active ? theme.colors.text.base : theme.colors.text.muted} }
p:hover { color: ${theme.colors.text.base} }
`,
);
From 99ba5a31d39bd2d7d6cb5cf1abd49a2d0f47963d Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 19:27:48 +0530
Subject: [PATCH 32/61] Fix warning
---
.../new/photos/components/Gallery/BarImpl.tsx | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/web/packages/new/photos/components/Gallery/BarImpl.tsx b/web/packages/new/photos/components/Gallery/BarImpl.tsx
index b076e9eecd..ac8ec9920d 100644
--- a/web/packages/new/photos/components/Gallery/BarImpl.tsx
+++ b/web/packages/new/photos/components/Gallery/BarImpl.tsx
@@ -339,13 +339,13 @@ const ModeIndicator: React.FC<
return (
onChangeMode("albums")}
>
{t("albums")} onChangeMode("people")}
>
{t("people")}
@@ -354,9 +354,11 @@ const ModeIndicator: React.FC<
);
};
-const ModeButton = styled(UnstyledButton)<{ $active: boolean }>(
- ({ $active, theme }) => `
- p { color: ${$active ? theme.colors.text.base : theme.colors.text.muted} }
+const ModeButton = styled(UnstyledButton, {
+ shouldForwardProp: (propName) => propName != "active",
+})<{ active: boolean }>(
+ ({ active, theme }) => `
+ p { color: ${active ? theme.colors.text.base : theme.colors.text.muted} }
p:hover { color: ${theme.colors.text.base} }
`,
);
From d7e2330f20068fa27cdcc41cb466999cbb5b36e0 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Fri, 27 Sep 2024 20:29:14 +0530
Subject: [PATCH 33/61] Fix render loop
---
.../src/components/Collections/GalleryBarAndListHeader.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx
index af963430c5..3fe4db7fed 100644
--- a/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx
+++ b/web/apps/photos/src/components/Collections/GalleryBarAndListHeader.tsx
@@ -188,7 +188,6 @@ export const GalleryBarAndListHeader: React.FC = ({
toShowCollectionSummaries,
activeCollectionID,
isActiveCollectionDownloadInProgress,
- people,
activePerson,
]);
From da8326229c3743bfae147aa80c2f262bc6cfbca1 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Sat, 28 Sep 2024 09:32:25 +0530
Subject: [PATCH 34/61] [web] Redirect to password input on no-email-MFA + new
tab
Fixes the following bug report, for a user who has email verification disabled:
> and about verify in new tab...
> it happens when u r at password page after entering email and opening
ente.auth.io in new tab opens the verify page instead of password
---
web/apps/photos/src/pages/index.tsx | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/web/apps/photos/src/pages/index.tsx b/web/apps/photos/src/pages/index.tsx
index 635685b7e3..201a514a2e 100644
--- a/web/apps/photos/src/pages/index.tsx
+++ b/web/apps/photos/src/pages/index.tsx
@@ -1,5 +1,6 @@
import { Login } from "@/accounts/components/Login";
import { SignUp } from "@/accounts/components/SignUp";
+import type { SRPAttributes } from "@/accounts/types/srp";
import log from "@/base/log";
import { albumsAppOrigin, customAPIHost } from "@/base/origins";
import { DevSettings } from "@/new/photos/components/DevSettings";
@@ -89,7 +90,26 @@ export default function LandingPage() {
if (key && token) {
await router.push(PAGES.GALLERY);
} else if (user?.email) {
- await router.push(PAGES.VERIFY);
+ // The user had previously entered their email on the login screen
+ // but closed the tab before proceeding (or opened a us in a new tab
+ // at this point).
+ //
+ // In such cases, we'll have an email present.
+ //
+ // Where to go next depends on whether they have enabled email
+ // verification or not.
+ //
+ // The login page would have fetched and saved SRP attributes, so we
+ // can see if they are present and indicate the email verification
+ // is not required. Otherwise, move to the verification page.
+ const srpAttributes: SRPAttributes = getData(
+ LS_KEYS.SRP_ATTRIBUTES,
+ );
+ if (srpAttributes && !srpAttributes.isEmailMFAEnabled) {
+ await router.push(PAGES.CREDENTIALS);
+ } else {
+ await router.push(PAGES.VERIFY);
+ }
}
await initLocalForage();
setLoading(false);
From 6d969ab72a6505c3c646ce1bc530edf2e9980dd5 Mon Sep 17 00:00:00 2001
From: omove <61330514+omove@users.noreply.github.com>
Date: Sat, 28 Sep 2024 00:05:59 -0400
Subject: [PATCH 35/61] [auth] fix x64 install on arm64 Windows
Inno Setup's 'x64' option only allows install on x64 Windows, changing to 'x64compatible' allows x64 installation on arm64 and x64 Windows.
---
auth/windows/packaging/exe/inno_setup.iss | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/auth/windows/packaging/exe/inno_setup.iss b/auth/windows/packaging/exe/inno_setup.iss
index 5906ecbd0e..653f293264 100644
--- a/auth/windows/packaging/exe/inno_setup.iss
+++ b/auth/windows/packaging/exe/inno_setup.iss
@@ -16,8 +16,8 @@ SetupIconFile={{SETUP_ICON_FILE}}
WizardStyle=modern
;PrivilegesRequired={{PRIVILEGES_REQUIRED}}
PrivilegesRequiredOverridesAllowed=dialog
-ArchitecturesAllowed=x64
-ArchitecturesInstallIn64BitMode=x64
+ArchitecturesAllowed=x64compatible
+ArchitecturesInstallIn64BitMode=x64compatible
UninstallDisplayIcon={app}\auth.exe
[Languages]
From 08f84c9cf8f27501dfeb218ec374f4e05a32488a Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Sat, 28 Sep 2024 09:54:51 +0530
Subject: [PATCH 36/61] Also handle for auth
---
web/apps/photos/src/pages/index.tsx | 22 +---------------------
web/packages/accounts/pages/verify.tsx | 23 +++++++++++++++++++++--
2 files changed, 22 insertions(+), 23 deletions(-)
diff --git a/web/apps/photos/src/pages/index.tsx b/web/apps/photos/src/pages/index.tsx
index 201a514a2e..635685b7e3 100644
--- a/web/apps/photos/src/pages/index.tsx
+++ b/web/apps/photos/src/pages/index.tsx
@@ -1,6 +1,5 @@
import { Login } from "@/accounts/components/Login";
import { SignUp } from "@/accounts/components/SignUp";
-import type { SRPAttributes } from "@/accounts/types/srp";
import log from "@/base/log";
import { albumsAppOrigin, customAPIHost } from "@/base/origins";
import { DevSettings } from "@/new/photos/components/DevSettings";
@@ -90,26 +89,7 @@ export default function LandingPage() {
if (key && token) {
await router.push(PAGES.GALLERY);
} else if (user?.email) {
- // The user had previously entered their email on the login screen
- // but closed the tab before proceeding (or opened a us in a new tab
- // at this point).
- //
- // In such cases, we'll have an email present.
- //
- // Where to go next depends on whether they have enabled email
- // verification or not.
- //
- // The login page would have fetched and saved SRP attributes, so we
- // can see if they are present and indicate the email verification
- // is not required. Otherwise, move to the verification page.
- const srpAttributes: SRPAttributes = getData(
- LS_KEYS.SRP_ATTRIBUTES,
- );
- if (srpAttributes && !srpAttributes.isEmailMFAEnabled) {
- await router.push(PAGES.CREDENTIALS);
- } else {
- await router.push(PAGES.VERIFY);
- }
+ await router.push(PAGES.VERIFY);
}
await initLocalForage();
setLoading(false);
diff --git a/web/packages/accounts/pages/verify.tsx b/web/packages/accounts/pages/verify.tsx
index 1fd0d094e1..f56aa57d49 100644
--- a/web/packages/accounts/pages/verify.tsx
+++ b/web/packages/accounts/pages/verify.tsx
@@ -42,7 +42,7 @@ import {
import { unstashRedirect } from "../services/redirect";
import { configureSRP } from "../services/srp";
import type { PageProps } from "../types/page";
-import type { SRPSetupAttributes } from "../types/srp";
+import type { SRPAttributes, SRPSetupAttributes } from "../types/srp";
const Page: React.FC = ({ appContext }) => {
const { logout, showNavBar, setDialogBoxAttributesV2 } = appContext;
@@ -69,7 +69,26 @@ const Page: React.FC = ({ appContext }) => {
) {
router.push(PAGES.CREDENTIALS);
} else {
- setEmail(user.email);
+ // The user might have email verification disabled, but after
+ // previously entering their email on the login screen, they
+ // might've closed the tab before proceeding (or opened a us in
+ // a new tab at this point).
+ //
+ // In such cases, we'll end up here with an email present.
+ //
+ // To distinguish this scenario from the normal email
+ // verification flow, we can check to see the SRP attributes
+ // (the login page would've fetched and saved them). If they are
+ // present and indicate that email verification is not required,
+ // redirect to the password verification page.
+ const srpAttributes: SRPAttributes = getData(
+ LS_KEYS.SRP_ATTRIBUTES,
+ );
+ if (srpAttributes && !srpAttributes.isEmailMFAEnabled) {
+ router.push(PAGES.CREDENTIALS);
+ } else {
+ setEmail(user.email);
+ }
}
};
main();
From 1eb5eaece95a060bb742a34f53142d3070db57b6 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Sat, 28 Sep 2024 10:08:11 +0530
Subject: [PATCH 37/61] Freshness check
---
web/packages/accounts/pages/verify.tsx | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/web/packages/accounts/pages/verify.tsx b/web/packages/accounts/pages/verify.tsx
index f56aa57d49..924d210b08 100644
--- a/web/packages/accounts/pages/verify.tsx
+++ b/web/packages/accounts/pages/verify.tsx
@@ -33,6 +33,7 @@ import { t } from "i18next";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { Trans } from "react-i18next";
+import { getSRPAttributes } from "../api/srp";
import { putAttributes, sendOtt, verifyOtt } from "../api/user";
import { PAGES } from "../constants/pages";
import {
@@ -85,7 +86,18 @@ const Page: React.FC = ({ appContext }) => {
LS_KEYS.SRP_ATTRIBUTES,
);
if (srpAttributes && !srpAttributes.isEmailMFAEnabled) {
- router.push(PAGES.CREDENTIALS);
+ // Fetch the latest SRP attributes instead of relying on the
+ // potentially stale stored values. This is an infrequent scenario
+ // path, so extra API calls are fine.
+ const latestSRPAttributes = await getSRPAttributes(email);
+ if (
+ latestSRPAttributes &&
+ !latestSRPAttributes.isEmailMFAEnabled
+ ) {
+ router.push(PAGES.CREDENTIALS);
+ } else {
+ setEmail(user.email);
+ }
} else {
setEmail(user.email);
}
From 3288f3250b0af5d531bc1d2d142d47f6a6e74f30 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Sat, 28 Sep 2024 10:14:49 +0530
Subject: [PATCH 38/61] Extract
---
web/packages/accounts/pages/verify.tsx | 87 ++++++++++++++------------
1 file changed, 46 insertions(+), 41 deletions(-)
diff --git a/web/packages/accounts/pages/verify.tsx b/web/packages/accounts/pages/verify.tsx
index 924d210b08..98a54adf8e 100644
--- a/web/packages/accounts/pages/verify.tsx
+++ b/web/packages/accounts/pages/verify.tsx
@@ -59,48 +59,11 @@ const Page: React.FC = ({ appContext }) => {
useEffect(() => {
const main = async () => {
const user: User = getData(LS_KEYS.USER);
- const keyAttributes: KeyAttributes = getData(
- LS_KEYS.KEY_ATTRIBUTES,
- );
- if (!user?.email) {
- router.push("/");
- } else if (
- keyAttributes?.encryptedKey &&
- (user.token || user.encryptedToken)
- ) {
- router.push(PAGES.CREDENTIALS);
+ const redirect = await redirectionIfNeeded(user);
+ if (redirect) {
+ router.push(redirect);
} else {
- // The user might have email verification disabled, but after
- // previously entering their email on the login screen, they
- // might've closed the tab before proceeding (or opened a us in
- // a new tab at this point).
- //
- // In such cases, we'll end up here with an email present.
- //
- // To distinguish this scenario from the normal email
- // verification flow, we can check to see the SRP attributes
- // (the login page would've fetched and saved them). If they are
- // present and indicate that email verification is not required,
- // redirect to the password verification page.
- const srpAttributes: SRPAttributes = getData(
- LS_KEYS.SRP_ATTRIBUTES,
- );
- if (srpAttributes && !srpAttributes.isEmailMFAEnabled) {
- // Fetch the latest SRP attributes instead of relying on the
- // potentially stale stored values. This is an infrequent scenario
- // path, so extra API calls are fine.
- const latestSRPAttributes = await getSRPAttributes(email);
- if (
- latestSRPAttributes &&
- !latestSRPAttributes.isEmailMFAEnabled
- ) {
- router.push(PAGES.CREDENTIALS);
- } else {
- setEmail(user.email);
- }
- } else {
- setEmail(user.email);
- }
+ setEmail(user.email);
}
};
main();
@@ -284,3 +247,45 @@ const Page: React.FC = ({ appContext }) => {
};
export default Page;
+
+/**
+ * A function called during page load to see if a redirection is required
+ *
+ * @returns The slug to redirect to, if needed.
+ */
+const redirectionIfNeeded = async (user: User | undefined) => {
+ const email = user?.email;
+ if (!email) {
+ return "/";
+ }
+
+ const keyAttributes: KeyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
+
+ if (keyAttributes?.encryptedKey && (user.token || user.encryptedToken)) {
+ return PAGES.CREDENTIALS;
+ }
+
+ // The user might have email verification disabled, but after previously
+ // entering their email on the login screen, they might've closed the tab
+ // before proceeding (or opened a us in a new tab at this point).
+ //
+ // In such cases, we'll end up here with an email present.
+ //
+ // To distinguish this scenario from the normal email verification flow, we
+ // can check to see the SRP attributes (the login page would've fetched and
+ // saved them). If they are present and indicate that email verification is
+ // not required, redirect to the password verification page.
+
+ const srpAttributes: SRPAttributes = getData(LS_KEYS.SRP_ATTRIBUTES);
+ if (srpAttributes && !srpAttributes.isEmailMFAEnabled) {
+ // Fetch the latest SRP attributes instead of relying on the potentially
+ // stale stored values. This is an infrequent scenario path, so extra
+ // API calls are fine.
+ const latestSRPAttributes = await getSRPAttributes(email);
+ if (latestSRPAttributes && !latestSRPAttributes.isEmailMFAEnabled) {
+ return PAGES.CREDENTIALS;
+ }
+ }
+
+ return undefined;
+};
From f255ded0b647631721ffccb4772149a6fe1c8baa Mon Sep 17 00:00:00 2001
From: vishnukvmd
Date: Fri, 27 Sep 2024 23:07:32 -0700
Subject: [PATCH 39/61] [docs] Update ML article
---
docs/docs/photos/features/machine-learning.md | 42 +++++++++----------
1 file changed, 19 insertions(+), 23 deletions(-)
diff --git a/docs/docs/photos/features/machine-learning.md b/docs/docs/photos/features/machine-learning.md
index 1b4355592f..640fe0f535 100644
--- a/docs/docs/photos/features/machine-learning.md
+++ b/docs/docs/photos/features/machine-learning.md
@@ -7,45 +7,41 @@ description:
# Machine learning
-> [!NOTE]
->
-> This document describes a beta feature that will be present in an upcoming
-> release.
-
Ente supports on-device machine learning. This allows you to use the latest
advances in AI in a privacy preserving manner.
-- You can search for your photos by the **faces** of the people in them. Ente
+- You can search for your photos by the **Faces** of the people in them. Ente
will show you all the faces in a photo, and will also try to group similar
faces together to create clusters of people so that you can give them names,
and quickly find all photos with a given person in them.
- You can search for your photos by typing natural language descriptions of
them. For example, you can search for "night", "by the seaside", or "the red
- motorcycle next to a fountain". Within the app, this ability is sometimes
- referred to as **magic search**.
+ motorcycle next to a fountain". Within the app, this ability is referred to
+ as **Magic search**.
-- We will build on this foundation to add more forms of advanced search.
+You can enable face recognition and magic search in the app's preferences on
+either the mobile app or the desktop app.
-You can enable face and magic search in the app's preferences on either the
-mobile app or the desktop app.
+On mobile, this is available under `General > Advanced > Machine learning`.
-If you have a big library, we recommend enabling this on the desktop app first,
-because it can index your existing photos faster (The app needs to download your
-originals to index them which can happen faster over WiFi, and indexing is also
-faster on your computer as compared to your mobile device).
+On desktop, this is available under `Preferences > Machine learning`.
-Once your existing photos have been indexed, then you can use either. The mobile
-app is fast enough to easily and seamlessly index the new photos that you take.
+---
+
+The app needs to download your original photos to index them. This is faster
+over WiFi. Indexing is also faster on your computer as compared to your mobile
+device.
> [!TIP]
>
-> Even for the initial indexing, you don't necessarily need the desktop app, it
-> just will be a bit faster.
+> If you have a large library on Ente, we recommend enabling this feature on the
+> desktop app first, because it can index your existing photos faster. Once your
+> existing photos have been indexed, then you can use either. The mobile app is
+> fast enough to index new photos as they are being backed up.
The indexes are synced across all your devices automatically using the same
-end-to-end encypted security that we use for syncing your photos.
+end-to-end encrypted security that we use for syncing your photos.
-Note that the desktop app does not currently support viewing and modifying the
-automatically generated face groupings, that is only supported by the mobile
-app.
+Note that the desktop app does not currently support modifying the face
+groupings, that is only supported by the mobile app.
From 08cf14a72b4d6fa1467089cce56b01c37d106348 Mon Sep 17 00:00:00 2001
From: Neeraj Gupta <254676+ua741@users.noreply.github.com>
Date: Sat, 28 Sep 2024 14:14:08 +0530
Subject: [PATCH 40/61] [mob] Minor improvement in magicSearch cache refresh
---
mobile/lib/services/machine_learning/ml_service.dart | 3 +--
mobile/lib/services/magic_cache_service.dart | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/mobile/lib/services/machine_learning/ml_service.dart b/mobile/lib/services/machine_learning/ml_service.dart
index c643bd20ff..c09b6874bd 100644
--- a/mobile/lib/services/machine_learning/ml_service.dart
+++ b/mobile/lib/services/machine_learning/ml_service.dart
@@ -117,7 +117,6 @@ class MLService {
try {
if (force) {
_mlControllerStatus = true;
- MagicCacheService.instance.queueUpdate('forced run');
}
if (_cannotRunMLFunction() && !force) return;
_isRunningML = true;
@@ -134,7 +133,7 @@ class MLService {
}
if (_mlControllerStatus == true) {
// refresh discover section
- MagicCacheService.instance.updateCache().ignore();
+ MagicCacheService.instance.updateCache(forced: force).ignore();
}
await indexAllImages();
if ((await MLDataDB.instance.getUnclusteredFaceCount()) > 0) {
diff --git a/mobile/lib/services/magic_cache_service.dart b/mobile/lib/services/magic_cache_service.dart
index 3178e8c4ca..ecb4c4629f 100644
--- a/mobile/lib/services/magic_cache_service.dart
+++ b/mobile/lib/services/magic_cache_service.dart
@@ -205,7 +205,7 @@ class MagicCacheService {
queueUpdate("Prompts data updated");
} else if (lastMagicCacheUpdateTime <
DateTime.now()
- .subtract(const Duration(days: 1))
+ .subtract(const Duration(hours: 12))
.millisecondsSinceEpoch) {
queueUpdate("Cache is old");
}
From 8629212584940fd61956160eea7e8b4cdcd2758e Mon Sep 17 00:00:00 2001
From: Neeraj Gupta <254676+ua741@users.noreply.github.com>
Date: Sat, 28 Sep 2024 14:18:21 +0530
Subject: [PATCH 41/61] [mob] Allow video upload with empty thumbnail
---
mobile/lib/utils/file_uploader_util.dart | 24 +++++++++++++++---------
1 file changed, 15 insertions(+), 9 deletions(-)
diff --git a/mobile/lib/utils/file_uploader_util.dart b/mobile/lib/utils/file_uploader_util.dart
index 6106307fd2..d06234bcf6 100644
--- a/mobile/lib/utils/file_uploader_util.dart
+++ b/mobile/lib/utils/file_uploader_util.dart
@@ -342,7 +342,8 @@ Future _getMediaUploadDataFromAppCache(EnteFile file) async {
Map? dimensions;
if (file.fileType == FileType.image) {
dimensions = await getImageHeightAndWith(imagePath: localPath);
- } else {
+ } else if (thumbnailData != null) {
+ // the thumbnail null check is to ensure that we are able to generate thum
// for video, we need to use the thumbnail data with any max width/height
final thumbnailFilePath = await VideoThumbnail.thumbnailFile(
video: localPath,
@@ -406,14 +407,19 @@ Future getThumbnailFromInAppCacheFile(EnteFile file) async {
return null;
}
if (file.fileType == FileType.video) {
- final thumbnailFilePath = await VideoThumbnail.thumbnailFile(
- video: localFile.path,
- imageFormat: ImageFormat.JPEG,
- thumbnailPath: (await getTemporaryDirectory()).path,
- maxWidth: thumbnailLargeSize,
- quality: 80,
- );
- localFile = File(thumbnailFilePath!);
+ try {
+ final thumbnailFilePath = await VideoThumbnail.thumbnailFile(
+ video: localFile.path,
+ imageFormat: ImageFormat.JPEG,
+ thumbnailPath: (await getTemporaryDirectory()).path,
+ maxWidth: thumbnailLargeSize,
+ quality: 80,
+ );
+ localFile = File(thumbnailFilePath!);
+ } catch (e) {
+ _logger.warning('Failed to generate video thumbnail', e);
+ return null;
+ }
}
var thumbnailData = await localFile.readAsBytes();
int compressionAttempts = 0;
From b6059273fb022a6aa291e9dd8309ebd6ee2dc320 Mon Sep 17 00:00:00 2001
From: Crowdin Bot
Date: Sat, 28 Sep 2024 10:27:04 +0000
Subject: [PATCH 42/61] New Crowdin translations by GitHub Action
---
.../base/locales/da-DK/translation.json | 652 ++++++++++++++++++
.../base/locales/it-IT/translation.json | 2 +-
.../base/locales/pl-PL/translation.json | 2 +-
.../base/locales/pt-BR/translation.json | 2 +-
.../base/locales/zh-CN/translation.json | 2 +-
5 files changed, 656 insertions(+), 4 deletions(-)
create mode 100644 web/packages/base/locales/da-DK/translation.json
diff --git a/web/packages/base/locales/da-DK/translation.json b/web/packages/base/locales/da-DK/translation.json
new file mode 100644
index 0000000000..a2df95ea52
--- /dev/null
+++ b/web/packages/base/locales/da-DK/translation.json
@@ -0,0 +1,652 @@
+{
+ "HERO_SLIDE_1_TITLE": "",
+ "HERO_SLIDE_1": "",
+ "HERO_SLIDE_2_TITLE": "",
+ "HERO_SLIDE_2": "",
+ "HERO_SLIDE_3_TITLE": "",
+ "HERO_SLIDE_3": "",
+ "login": "",
+ "sign_up": "",
+ "NEW_USER": "",
+ "EXISTING_USER": "",
+ "ENTER_NAME": "",
+ "PUBLIC_UPLOADER_NAME_MESSAGE": "",
+ "ENTER_EMAIL": "",
+ "EMAIL_ERROR": "",
+ "REQUIRED": "",
+ "EMAIL_SENT": "",
+ "CHECK_INBOX": "",
+ "ENTER_OTT": "",
+ "RESEND_MAIL": "",
+ "VERIFY": "",
+ "UNKNOWN_ERROR": "",
+ "INVALID_CODE": "",
+ "EXPIRED_CODE": "",
+ "SENDING": "",
+ "SENT": "",
+ "password": "",
+ "link_password_description": "",
+ "unlock": "",
+ "SET_PASSPHRASE": "",
+ "VERIFY_PASSPHRASE": "",
+ "INCORRECT_PASSPHRASE": "",
+ "ENTER_ENC_PASSPHRASE": "",
+ "PASSPHRASE_DISCLAIMER": "",
+ "WELCOME_TO_ENTE_HEADING": "",
+ "WELCOME_TO_ENTE_SUBHEADING": "",
+ "WHERE_YOUR_BEST_PHOTOS_LIVE": "",
+ "KEY_GENERATION_IN_PROGRESS_MESSAGE": "",
+ "PASSPHRASE_HINT": "",
+ "CONFIRM_PASSPHRASE": "",
+ "REFERRAL_CODE_HINT": "",
+ "REFERRAL_INFO": "",
+ "PASSPHRASE_MATCH_ERROR": "",
+ "create_albums": "",
+ "CREATE_COLLECTION": "",
+ "enter_album_name": "",
+ "CLOSE_OPTION": "",
+ "enter_file_name": "",
+ "CLOSE": "",
+ "NO": "",
+ "NOTHING_HERE": "",
+ "upload": "",
+ "import": "",
+ "add_photos": "",
+ "add_more_photos": "",
+ "add_photos_count_one": "",
+ "add_photos_count": "",
+ "select_photos": "",
+ "FILE_UPLOAD": "",
+ "UPLOAD_STAGE_MESSAGE": {
+ "0": "",
+ "1": "",
+ "2": "",
+ "3": "",
+ "4": "",
+ "5": ""
+ },
+ "FILE_NOT_UPLOADED_LIST": "",
+ "INITIAL_LOAD_DELAY_WARNING": "",
+ "USER_DOES_NOT_EXIST": "",
+ "NO_ACCOUNT": "",
+ "ACCOUNT_EXISTS": "",
+ "CREATE": "",
+ "download": "",
+ "download_album": "",
+ "download_favorites": "",
+ "download_uncategorized": "",
+ "download_hidden_items": "",
+ "download_key": "",
+ "copy_key": "",
+ "toggle_fullscreen_key": "",
+ "zoom_in_out_key": "",
+ "previous_key": "",
+ "next_key": "",
+ "title_photos": "",
+ "title_auth": "",
+ "title_accounts": "",
+ "UPLOAD_FIRST_PHOTO": "",
+ "IMPORT_YOUR_FOLDERS": "",
+ "UPLOAD_DROPZONE_MESSAGE": "",
+ "WATCH_FOLDER_DROPZONE_MESSAGE": "",
+ "TRASH_FILES_TITLE": "",
+ "TRASH_FILE_TITLE": "",
+ "DELETE_FILES_TITLE": "",
+ "DELETE_FILES_MESSAGE": "",
+ "DELETE": "",
+ "DELETE_OPTION": "",
+ "FAVORITE_OPTION": "",
+ "UNFAVORITE_OPTION": "",
+ "MULTI_FOLDER_UPLOAD": "",
+ "UPLOAD_STRATEGY_CHOICE": "",
+ "UPLOAD_STRATEGY_SINGLE_COLLECTION": "",
+ "OR": "",
+ "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "",
+ "SESSION_EXPIRED_MESSAGE": "",
+ "SESSION_EXPIRED": "",
+ "PASSWORD_GENERATION_FAILED": "",
+ "CHANGE_PASSWORD": "",
+ "password_changed_elsewhere": "",
+ "password_changed_elsewhere_message": "",
+ "GO_BACK": "",
+ "RECOVERY_KEY": "",
+ "SAVE_LATER": "",
+ "SAVE": "",
+ "RECOVERY_KEY_DESCRIPTION": "",
+ "RECOVER_KEY_GENERATION_FAILED": "",
+ "KEY_NOT_STORED_DISCLAIMER": "",
+ "FORGOT_PASSWORD": "",
+ "RECOVER_ACCOUNT": "",
+ "RECOVERY_KEY_HINT": "",
+ "RECOVER": "",
+ "NO_RECOVERY_KEY": "",
+ "INCORRECT_RECOVERY_KEY": "",
+ "SORRY": "",
+ "NO_RECOVERY_KEY_MESSAGE": "",
+ "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "",
+ "CONTACT_SUPPORT": "",
+ "REQUEST_FEATURE": "",
+ "SUPPORT": "",
+ "CONFIRM": "",
+ "cancel": "",
+ "LOGOUT": "",
+ "delete_account": "",
+ "delete_account_manually_message": "",
+ "LOGOUT_MESSAGE": "",
+ "CHANGE_EMAIL": "",
+ "ok": "",
+ "success": "",
+ "error": "",
+ "OFFLINE_MSG": "",
+ "install": "",
+ "install_mobile_app": "",
+ "download_app": "",
+ "download_app_message": "",
+ "EXPORT": "",
+ "SUBSCRIPTION": "",
+ "SUBSCRIBE": "",
+ "MANAGEMENT_PORTAL": "",
+ "MANAGE_FAMILY_PORTAL": "",
+ "LEAVE_FAMILY_PLAN": "",
+ "LEAVE": "",
+ "LEAVE_FAMILY_CONFIRM": "",
+ "CHOOSE_PLAN": "",
+ "MANAGE_PLAN": "",
+ "CURRENT_USAGE": "",
+ "TWO_MONTHS_FREE": "",
+ "POPULAR": "",
+ "free_plan_option": "",
+ "free_plan_description": "",
+ "active": "",
+ "subscription_info_free": "",
+ "subscription_info_family": "",
+ "subscription_info_expired": "",
+ "subscription_info_renewal_cancelled": "",
+ "subscription_info_storage_quota_exceeded": "",
+ "subscription_status_renewal_active": "",
+ "subscription_status_renewal_cancelled": "",
+ "add_on_valid_till": "",
+ "subscription_expired": "",
+ "storage_quota_exceeded": "",
+ "SUBSCRIPTION_PURCHASE_SUCCESS": "",
+ "SUBSCRIPTION_PURCHASE_CANCELLED": "",
+ "SUBSCRIPTION_PURCHASE_FAILED": "",
+ "SUBSCRIPTION_UPDATE_FAILED": "",
+ "UPDATE_PAYMENT_METHOD_MESSAGE": "",
+ "STRIPE_AUTHENTICATION_FAILED": "",
+ "UPDATE_PAYMENT_METHOD": "",
+ "MONTHLY": "",
+ "YEARLY": "",
+ "MONTH_SHORT": "",
+ "YEAR": "",
+ "update_subscription_title": "",
+ "UPDATE_SUBSCRIPTION_MESSAGE": "",
+ "UPDATE_SUBSCRIPTION": "",
+ "CANCEL_SUBSCRIPTION": "",
+ "CANCEL_SUBSCRIPTION_MESSAGE": "",
+ "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "",
+ "SUBSCRIPTION_CANCEL_FAILED": "",
+ "SUBSCRIPTION_CANCEL_SUCCESS": "",
+ "REACTIVATE_SUBSCRIPTION": "",
+ "REACTIVATE_SUBSCRIPTION_MESSAGE": "",
+ "SUBSCRIPTION_ACTIVATE_SUCCESS": "",
+ "SUBSCRIPTION_ACTIVATE_FAILED": "",
+ "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "",
+ "CANCEL_SUBSCRIPTION_ON_MOBILE": "",
+ "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "",
+ "MAIL_TO_MANAGE_SUBSCRIPTION": "",
+ "rename": "",
+ "rename_file": "",
+ "rename_album": "",
+ "delete_album": "",
+ "delete_album_title": "",
+ "delete_album_message": "",
+ "delete_photos": "",
+ "keep_photos": "",
+ "share_album": "",
+ "SHARE_WITH_SELF": "",
+ "ALREADY_SHARED": "",
+ "SHARING_BAD_REQUEST_ERROR": "",
+ "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "",
+ "CREATE_ALBUM_FAILED": "",
+ "search": "",
+ "search_results": "",
+ "no_results": "",
+ "search_hint": "",
+ "album": "",
+ "date": "",
+ "description": "",
+ "file_type": "",
+ "magic": "",
+ "photos_count_zero": "",
+ "photos_count_one": "",
+ "photos_count": "",
+ "TERMS_AND_CONDITIONS": "",
+ "SELECTED": "",
+ "people": "",
+ "indexing_scheduled": "",
+ "indexing_photos": "",
+ "indexing_fetching": "",
+ "indexing_people": "",
+ "indexing_done": "",
+ "UNIDENTIFIED_FACES": "",
+ "OBJECTS": "",
+ "TEXT": "",
+ "INFO": "",
+ "INFO_OPTION": "",
+ "file_name": "",
+ "CAPTION_PLACEHOLDER": "",
+ "location": "",
+ "SHOW_ON_MAP": "",
+ "MAP": "",
+ "MAP_SETTINGS": "",
+ "ENABLE_MAPS": "",
+ "ENABLE_MAP": "",
+ "DISABLE_MAPS": "",
+ "ENABLE_MAP_DESCRIPTION": "",
+ "DISABLE_MAP_DESCRIPTION": "",
+ "DISABLE_MAP": "",
+ "DETAILS": "",
+ "view_exif": "",
+ "no_exif": "",
+ "exif": "",
+ "ISO": "",
+ "TWO_FACTOR": "",
+ "TWO_FACTOR_AUTHENTICATION": "",
+ "TWO_FACTOR_QR_INSTRUCTION": "",
+ "ENTER_CODE_MANUALLY": "",
+ "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "",
+ "SCAN_QR_CODE": "",
+ "ENABLE_TWO_FACTOR": "",
+ "enable": "",
+ "enabled": "",
+ "LOST_DEVICE": "",
+ "INCORRECT_CODE": "",
+ "TWO_FACTOR_INFO": "",
+ "DISABLE_TWO_FACTOR_LABEL": "",
+ "UPDATE_TWO_FACTOR_LABEL": "",
+ "disable": "",
+ "reconfigure": "",
+ "UPDATE_TWO_FACTOR": "",
+ "UPDATE_TWO_FACTOR_MESSAGE": "",
+ "UPDATE": "",
+ "DISABLE_TWO_FACTOR": "",
+ "DISABLE_TWO_FACTOR_MESSAGE": "",
+ "TWO_FACTOR_DISABLE_FAILED": "",
+ "EXPORT_DATA": "",
+ "select_folder": "",
+ "select_zips": "",
+ "faq": "",
+ "takeout_hint": "",
+ "DESTINATION": "",
+ "START": "",
+ "LAST_EXPORT_TIME": "",
+ "EXPORT_AGAIN": "",
+ "LOCAL_STORAGE_NOT_ACCESSIBLE": "",
+ "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "",
+ "SEND_OTT": "",
+ "EMAIl_ALREADY_OWNED": "",
+ "ETAGS_BLOCKED": "",
+ "LIVE_PHOTOS_DETECTED": "",
+ "RETRY_FAILED": "",
+ "FAILED_UPLOADS": "",
+ "failed_uploads_hint": "",
+ "SKIPPED_FILES": "",
+ "THUMBNAIL_GENERATION_FAILED_UPLOADS": "",
+ "UNSUPPORTED_FILES": "",
+ "SUCCESSFUL_UPLOADS": "",
+ "SKIPPED_INFO": "",
+ "UNSUPPORTED_INFO": "",
+ "BLOCKED_UPLOADS": "",
+ "INPROGRESS_METADATA_EXTRACTION": "",
+ "INPROGRESS_UPLOADS": "",
+ "TOO_LARGE_UPLOADS": "",
+ "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "",
+ "LARGER_THAN_AVAILABLE_STORAGE_INFO": "",
+ "TOO_LARGE_INFO": "",
+ "THUMBNAIL_GENERATION_FAILED_INFO": "",
+ "select_album": "",
+ "upload_to_album": "",
+ "add_to_album": "",
+ "move_to_album": "",
+ "unhide_to_album": "",
+ "restore_to_album": "",
+ "section_all": "",
+ "section_uncategorized": "",
+ "section_archive": "",
+ "section_hidden": "",
+ "section_trash": "",
+ "favorites": "",
+ "archive": "",
+ "archive_album": "",
+ "unarchive": "",
+ "unarchive_album": "",
+ "hide_collection": "",
+ "unhide_collection": "",
+ "MOVE": "",
+ "ADD": "",
+ "REMOVE": "",
+ "YES_REMOVE": "",
+ "REMOVE_FROM_COLLECTION": "",
+ "MOVE_TO_TRASH": "",
+ "TRASH_FILES_MESSAGE": "",
+ "TRASH_FILE_MESSAGE": "",
+ "DELETE_PERMANENTLY": "",
+ "RESTORE": "",
+ "empty_trash": "",
+ "empty_trash_title": "",
+ "empty_trash_message": "",
+ "leave_album": "",
+ "leave_shared_album_title": "",
+ "leave_shared_album_message": "",
+ "leave_shared_album": "",
+ "NOT_FILE_OWNER": "",
+ "CONFIRM_SELF_REMOVE_MESSAGE": "",
+ "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "",
+ "sort_by_creation_time_ascending": "",
+ "sort_by_updation_time_descending": "",
+ "sort_by_name": "",
+ "FIX_CREATION_TIME": "",
+ "FIX_CREATION_TIME_IN_PROGRESS": "",
+ "CREATION_TIME_UPDATED": "",
+ "UPDATE_CREATION_TIME_NOT_STARTED": "",
+ "UPDATE_CREATION_TIME_COMPLETED": "",
+ "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "",
+ "CAPTION_CHARACTER_LIMIT": "",
+ "DATE_TIME_ORIGINAL": "",
+ "DATE_TIME_DIGITIZED": "",
+ "METADATA_DATE": "",
+ "CUSTOM_TIME": "",
+ "REOPEN_PLAN_SELECTOR_MODAL": "",
+ "OPEN_PLAN_SELECTOR_MODAL_FAILED": "",
+ "sharing_details": "",
+ "modify_sharing": "",
+ "ADD_COLLABORATORS": "",
+ "ADD_NEW_EMAIL": "",
+ "shared_with_people_count_zero": "",
+ "shared_with_people_count_one": "",
+ "shared_with_people_count": "",
+ "participants_count_zero": "",
+ "participants_count_one": "",
+ "participants_count": "",
+ "ADD_VIEWERS": "",
+ "CHANGE_PERMISSIONS_TO_VIEWER": "",
+ "CHANGE_PERMISSIONS_TO_COLLABORATOR": "",
+ "CONVERT_TO_VIEWER": "",
+ "CONVERT_TO_COLLABORATOR": "",
+ "CHANGE_PERMISSION": "",
+ "REMOVE_PARTICIPANT": "",
+ "CONFIRM_REMOVE": "",
+ "MANAGE": "",
+ "ADDED_AS": "",
+ "COLLABORATOR_RIGHTS": "",
+ "REMOVE_PARTICIPANT_HEAD": "",
+ "OWNER": "",
+ "COLLABORATORS": "",
+ "ADD_MORE": "",
+ "VIEWERS": "",
+ "OR_ADD_EXISTING": "",
+ "REMOVE_PARTICIPANT_MESSAGE": "",
+ "NOT_FOUND": "",
+ "LINK_EXPIRED": "",
+ "LINK_EXPIRED_MESSAGE": "",
+ "MANAGE_LINK": "",
+ "LINK_TOO_MANY_REQUESTS": "",
+ "FILE_DOWNLOAD": "",
+ "link_password_lock": "",
+ "PUBLIC_COLLECT": "",
+ "LINK_DEVICE_LIMIT": "",
+ "NO_DEVICE_LIMIT": "",
+ "LINK_EXPIRY": "",
+ "NEVER": "",
+ "DISABLE_FILE_DOWNLOAD": "",
+ "DISABLE_FILE_DOWNLOAD_MESSAGE": "",
+ "SHARED_USING": "",
+ "SHARING_REFERRAL_CODE": "",
+ "LIVE": "",
+ "DISABLE_PASSWORD": "",
+ "DISABLE_PASSWORD_MESSAGE": "",
+ "PASSWORD_LOCK": "",
+ "LOCK": "",
+ "file": "",
+ "folder": "",
+ "google_takeout": "",
+ "DEDUPLICATE_FILES": "",
+ "NO_DUPLICATES_FOUND": "",
+ "FILES": "",
+ "EACH": "",
+ "DEDUPLICATE_BASED_ON_SIZE": "",
+ "STOP_ALL_UPLOADS_MESSAGE": "",
+ "STOP_UPLOADS_HEADER": "",
+ "YES_STOP_UPLOADS": "",
+ "STOP_DOWNLOADS_HEADER": "",
+ "YES_STOP_DOWNLOADS": "",
+ "STOP_ALL_DOWNLOADS_MESSAGE": "",
+ "albums": "",
+ "albums_count_one": "",
+ "albums_count": "",
+ "all_albums": "",
+ "all_hidden_albums": "",
+ "hidden_albums": "",
+ "hidden_items": "",
+ "ENTER_TWO_FACTOR_OTP": "",
+ "CREATE_ACCOUNT": "",
+ "COPIED": "",
+ "WATCH_FOLDERS": "",
+ "upgrade_now": "",
+ "renew_now": "",
+ "STORAGE": "",
+ "USED": "",
+ "YOU": "",
+ "FAMILY": "",
+ "FREE": "",
+ "OF": "",
+ "WATCHED_FOLDERS": "",
+ "NO_FOLDERS_ADDED": "",
+ "FOLDERS_AUTOMATICALLY_MONITORED": "",
+ "UPLOAD_NEW_FILES_TO_ENTE": "",
+ "REMOVE_DELETED_FILES_FROM_ENTE": "",
+ "ADD_FOLDER": "",
+ "STOP_WATCHING": "",
+ "STOP_WATCHING_FOLDER": "",
+ "STOP_WATCHING_DIALOG_MESSAGE": "",
+ "YES_STOP": "",
+ "CHANGE_FOLDER": "",
+ "FAMILY_PLAN": "",
+ "debug_logs": "",
+ "DOWNLOAD_LOGS": "",
+ "DOWNLOAD_LOGS_MESSAGE": "",
+ "WEAK_DEVICE": "",
+ "drag_and_drop_hint": "",
+ "AUTHENTICATE": "",
+ "UPLOADED_TO_SINGLE_COLLECTION": "",
+ "UPLOADED_TO_SEPARATE_COLLECTIONS": "",
+ "NEVERMIND": "",
+ "UPDATE_AVAILABLE": "",
+ "UPDATE_INSTALLABLE_MESSAGE": "",
+ "INSTALL_NOW": "",
+ "INSTALL_ON_NEXT_LAUNCH": "",
+ "UPDATE_AVAILABLE_MESSAGE": "",
+ "DOWNLOAD_AND_INSTALL": "",
+ "IGNORE_THIS_VERSION": "",
+ "TODAY": "",
+ "YESTERDAY": "",
+ "NAME_PLACEHOLDER": "",
+ "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "",
+ "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "",
+ "CHOSE_THEME": "",
+ "more_details": "",
+ "ml_search": "",
+ "ml_search_description": "",
+ "ml_search_footnote": "",
+ "indexing": "",
+ "processed": "",
+ "indexing_status_running": "",
+ "indexing_status_fetching": "",
+ "indexing_status_scheduled": "",
+ "indexing_status_done": "",
+ "ml_search_disable": "",
+ "ml_search_disable_confirm": "",
+ "ml_consent": "",
+ "ml_consent_title": "",
+ "ml_consent_description": "",
+ "ml_consent_confirmation": "",
+ "labs": "",
+ "YOURS": "",
+ "passphrase_strength_weak": "",
+ "passphrase_strength_moderate": "",
+ "passphrase_strength_strong": "",
+ "preferences": "",
+ "language": "",
+ "advanced": "",
+ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "",
+ "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "",
+ "SUBSCRIPTION_VERIFICATION_ERROR": "",
+ "storage_unit": {
+ "b": "",
+ "kb": "",
+ "mb": "",
+ "gb": "",
+ "tb": ""
+ },
+ "AFTER_TIME": {
+ "HOUR": "",
+ "DAY": "",
+ "WEEK": "",
+ "MONTH": "",
+ "YEAR": ""
+ },
+ "COPY_LINK": "",
+ "DONE": "",
+ "LINK_SHARE_TITLE": "",
+ "REMOVE_LINK": "",
+ "CREATE_PUBLIC_SHARING": "",
+ "PUBLIC_LINK_CREATED": "",
+ "PUBLIC_LINK_ENABLED": "",
+ "COLLECT_PHOTOS": "",
+ "PUBLIC_COLLECT_SUBTEXT": "",
+ "STOP_EXPORT": "",
+ "EXPORT_PROGRESS": "",
+ "MIGRATING_EXPORT": "",
+ "RENAMING_COLLECTION_FOLDERS": "",
+ "TRASHING_DELETED_FILES": "",
+ "TRASHING_DELETED_COLLECTIONS": "",
+ "CONTINUOUS_EXPORT": "",
+ "PENDING_ITEMS": "",
+ "EXPORT_STARTING": "",
+ "delete_account_reason_label": "",
+ "delete_account_reason_placeholder": "",
+ "delete_reason": {
+ "missing_feature": "",
+ "behaviour": "",
+ "found_another_service": "",
+ "not_listed": ""
+ },
+ "delete_account_feedback_label": "",
+ "delete_account_feedback_placeholder": "",
+ "delete_account_confirm_checkbox_label": "",
+ "delete_account_confirm": "",
+ "delete_account_confirm_message": "",
+ "feedback_required": "",
+ "feedback_required_found_another_service": "",
+ "RECOVER_TWO_FACTOR": "",
+ "at": "",
+ "AUTH_NEXT": "",
+ "AUTH_DOWNLOAD_MOBILE_APP": "",
+ "HIDE": "",
+ "UNHIDE": "",
+ "sort_by": "",
+ "newest_first": "",
+ "oldest_first": "",
+ "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "",
+ "pin_album": "",
+ "unpin_album": "",
+ "DOWNLOAD_COMPLETE": "",
+ "DOWNLOADING_COLLECTION": "",
+ "DOWNLOAD_FAILED": "",
+ "DOWNLOAD_PROGRESS": "",
+ "CHRISTMAS": "",
+ "CHRISTMAS_EVE": "",
+ "NEW_YEAR": "",
+ "NEW_YEAR_EVE": "",
+ "IMAGE": "",
+ "VIDEO": "",
+ "LIVE_PHOTO": "",
+ "editor": {
+ "crop": ""
+ },
+ "CONVERT": "",
+ "CONFIRM_EDITOR_CLOSE_MESSAGE": "",
+ "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "",
+ "BRIGHTNESS": "",
+ "CONTRAST": "",
+ "SATURATION": "",
+ "BLUR": "",
+ "INVERT_COLORS": "",
+ "ASPECT_RATIO": "",
+ "SQUARE": "",
+ "ROTATE_LEFT": "",
+ "ROTATE_RIGHT": "",
+ "FLIP_VERTICALLY": "",
+ "FLIP_HORIZONTALLY": "",
+ "DOWNLOAD_EDITED": "",
+ "SAVE_A_COPY_TO_ENTE": "",
+ "RESTORE_ORIGINAL": "",
+ "TRANSFORM": "",
+ "COLORS": "",
+ "FLIP": "",
+ "ROTATION": "",
+ "RESET": "",
+ "PHOTO_EDITOR": "",
+ "FASTER_UPLOAD": "",
+ "FASTER_UPLOAD_DESCRIPTION": "",
+ "cast_album_to_tv": "",
+ "enter_cast_pin_code": "",
+ "pair_device_to_tv": "",
+ "tv_not_found": "",
+ "cast_auto_pair": "",
+ "cast_auto_pair_description": "",
+ "choose_device_from_browser": "",
+ "cast_auto_pair_failed": "",
+ "pair_with_pin": "",
+ "pair_with_pin_description": "",
+ "visit_cast_url": "",
+ "FREEHAND": "",
+ "APPLY_CROP": "",
+ "PHOTO_EDIT_REQUIRED_TO_SAVE": "",
+ "passkeys": "",
+ "passkey_fetch_failed": "",
+ "manage_passkey": "",
+ "delete_passkey": "",
+ "delete_passkey_confirmation": "",
+ "rename_passkey": "",
+ "add_passkey": "",
+ "enter_passkey_name": "",
+ "passkeys_description": "",
+ "CREATED_AT": "",
+ "passkey_add_failed": "",
+ "passkey_login_failed": "",
+ "passkey_login_invalid_url": "",
+ "passkey_login_already_claimed_session": "",
+ "passkey_login_generic_error": "",
+ "passkey_login_credential_hint": "",
+ "passkeys_not_supported": "",
+ "try_again": "",
+ "check_status": "",
+ "passkey_login_instructions": "",
+ "passkey_login": "",
+ "passkey": "",
+ "passkey_verify_description": "",
+ "waiting_for_verification": "",
+ "verification_still_pending": "",
+ "passkey_verified": "",
+ "redirecting_back_to_app": "",
+ "redirect_close_instructions": "",
+ "redirect_again": "",
+ "autogenerated_first_album_name": "",
+ "autogenerated_default_album_name": "",
+ "developer_settings": "",
+ "server_endpoint": "",
+ "more_information": "",
+ "save": ""
+}
diff --git a/web/packages/base/locales/it-IT/translation.json b/web/packages/base/locales/it-IT/translation.json
index 57596a5920..b00e775e2c 100644
--- a/web/packages/base/locales/it-IT/translation.json
+++ b/web/packages/base/locales/it-IT/translation.json
@@ -48,7 +48,7 @@
"enter_file_name": "Nome del file",
"CLOSE": "Chiudi",
"NO": "No",
- "NOTHING_HERE": "",
+ "NOTHING_HERE": "Non c'e ancora niente qui",
"upload": "Carica",
"import": "Importa",
"add_photos": "Aggiungi foto",
diff --git a/web/packages/base/locales/pl-PL/translation.json b/web/packages/base/locales/pl-PL/translation.json
index 05de0ab560..2ebafcb095 100644
--- a/web/packages/base/locales/pl-PL/translation.json
+++ b/web/packages/base/locales/pl-PL/translation.json
@@ -48,7 +48,7 @@
"enter_file_name": "Nazwa pliku",
"CLOSE": "Zamknij",
"NO": "Nie",
- "NOTHING_HERE": "",
+ "NOTHING_HERE": "Nic tu jeszcze nie ma",
"upload": "Prześlij",
"import": "Importuj",
"add_photos": "Dodaj zdjęcia",
diff --git a/web/packages/base/locales/pt-BR/translation.json b/web/packages/base/locales/pt-BR/translation.json
index af2910ed90..ca8c8230b6 100644
--- a/web/packages/base/locales/pt-BR/translation.json
+++ b/web/packages/base/locales/pt-BR/translation.json
@@ -48,7 +48,7 @@
"enter_file_name": "Nome do arquivo",
"CLOSE": "Fechar",
"NO": "Não",
- "NOTHING_HERE": "",
+ "NOTHING_HERE": "Nada aqui ainda",
"upload": "Enviar",
"import": "Importar",
"add_photos": "Adicionar fotos",
diff --git a/web/packages/base/locales/zh-CN/translation.json b/web/packages/base/locales/zh-CN/translation.json
index 48b90a3d81..fab485ba23 100644
--- a/web/packages/base/locales/zh-CN/translation.json
+++ b/web/packages/base/locales/zh-CN/translation.json
@@ -48,7 +48,7 @@
"enter_file_name": "文件名",
"CLOSE": "关闭",
"NO": "否",
- "NOTHING_HERE": "",
+ "NOTHING_HERE": "这里什么也没有",
"upload": "上传",
"import": "导入",
"add_photos": "添加照片",
From 5d210ab740b1797302704138014ea3e922b1bf74 Mon Sep 17 00:00:00 2001
From: Manav Rathi
Date: Sat, 28 Sep 2024 16:02:41 +0530
Subject: [PATCH 43/61] Rename
---
web/apps/photos/src/components/PhotoList/index.tsx | 2 +-
web/packages/base/locales/ar-SA/translation.json | 2 +-
web/packages/base/locales/bg-BG/translation.json | 2 +-
web/packages/base/locales/ca-ES/translation.json | 2 +-
web/packages/base/locales/da-DK/translation.json | 2 +-
web/packages/base/locales/de-DE/translation.json | 2 +-
web/packages/base/locales/el-GR/translation.json | 2 +-
web/packages/base/locales/en-US/translation.json | 2 +-
web/packages/base/locales/es-ES/translation.json | 2 +-
web/packages/base/locales/et-EE/translation.json | 2 +-
web/packages/base/locales/fa-IR/translation.json | 2 +-
web/packages/base/locales/fi-FI/translation.json | 2 +-
web/packages/base/locales/fr-FR/translation.json | 2 +-
web/packages/base/locales/gu-IN/translation.json | 2 +-
web/packages/base/locales/hi-IN/translation.json | 2 +-
web/packages/base/locales/id-ID/translation.json | 2 +-
web/packages/base/locales/is-IS/translation.json | 2 +-
web/packages/base/locales/it-IT/translation.json | 2 +-
web/packages/base/locales/ja-JP/translation.json | 2 +-
web/packages/base/locales/km-KH/translation.json | 2 +-
web/packages/base/locales/ko-KR/translation.json | 2 +-
web/packages/base/locales/nl-NL/translation.json | 2 +-
web/packages/base/locales/pl-PL/translation.json | 2 +-
web/packages/base/locales/pt-BR/translation.json | 2 +-
web/packages/base/locales/pt-PT/translation.json | 2 +-
web/packages/base/locales/ru-RU/translation.json | 2 +-
web/packages/base/locales/sv-SE/translation.json | 2 +-
web/packages/base/locales/ta-IN/translation.json | 2 +-
web/packages/base/locales/te-IN/translation.json | 2 +-
web/packages/base/locales/th-TH/translation.json | 2 +-
web/packages/base/locales/ti-ER/translation.json | 2 +-
web/packages/base/locales/tr-TR/translation.json | 2 +-
web/packages/base/locales/zh-CN/translation.json | 2 +-
33 files changed, 33 insertions(+), 33 deletions(-)
diff --git a/web/apps/photos/src/components/PhotoList/index.tsx b/web/apps/photos/src/components/PhotoList/index.tsx
index 2b879faf9a..e9777a8166 100644
--- a/web/apps/photos/src/components/PhotoList/index.tsx
+++ b/web/apps/photos/src/components/PhotoList/index.tsx
@@ -510,7 +510,7 @@ export function PhotoList({
itemType: ITEM_TYPE.OTHER,
item: (
-
",
+ "intro_slide_3": "أندرويد، آي أو إس، ويب، سطح المكتب",
"login": "تسجيل الدخول",
"sign_up": "تسجيل",
"NEW_USER": "جديد في Ente",
diff --git a/web/packages/base/locales/bg-BG/translation.json b/web/packages/base/locales/bg-BG/translation.json
index 43a3dc5b5b..0c59e395ba 100644
--- a/web/packages/base/locales/bg-BG/translation.json
+++ b/web/packages/base/locales/bg-BG/translation.json
@@ -1,10 +1,10 @@
{
- "HERO_SLIDE_1_TITLE": "
Личен бекъп
на твоите спомени
",
- "HERO_SLIDE_1": "Криптиран от край до край по подразбиране",
- "HERO_SLIDE_2_TITLE": "",
- "HERO_SLIDE_2": "",
- "HERO_SLIDE_3_TITLE": "",
- "HERO_SLIDE_3": "",
+ "intro_slide_1_title": "