From 7fa9e924ebb58a7cf163d52553a0af0787d0b0d9 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 11 Apr 2024 20:54:53 +0530 Subject: [PATCH 01/25] Consolidate --- .../components/MachineLearning/ImageViews.tsx | 2 +- .../components/MachineLearning/PeopleList.tsx | 2 +- .../photos/src/services/download/index.ts | 8 +- .../src/utils/machineLearning/faceCrop.ts | 3 +- .../photos/src/utils/machineLearning/index.ts | 3 +- web/packages/accounts/services/user.ts | 4 +- web/packages/shared/storage/cache.ts | 123 ++++++++++++++++++ .../shared/storage/cacheStorage/constants.ts | 6 - .../shared/storage/cacheStorage/factory.ts | 28 ---- .../shared/storage/cacheStorage/helpers.ts | 55 -------- .../shared/storage/cacheStorage/index.ts | 36 ----- .../shared/storage/cacheStorage/types.ts | 16 --- 12 files changed, 134 insertions(+), 152 deletions(-) create mode 100644 web/packages/shared/storage/cache.ts delete mode 100644 web/packages/shared/storage/cacheStorage/constants.ts delete mode 100644 web/packages/shared/storage/cacheStorage/factory.ts delete mode 100644 web/packages/shared/storage/cacheStorage/helpers.ts delete mode 100644 web/packages/shared/storage/cacheStorage/index.ts delete mode 100644 web/packages/shared/storage/cacheStorage/types.ts diff --git a/web/apps/photos/src/components/MachineLearning/ImageViews.tsx b/web/apps/photos/src/components/MachineLearning/ImageViews.tsx index 72800271b1..4605cafe89 100644 --- a/web/apps/photos/src/components/MachineLearning/ImageViews.tsx +++ b/web/apps/photos/src/components/MachineLearning/ImageViews.tsx @@ -1,5 +1,5 @@ import log from "@/next/log"; -import { cached } from "@ente/shared/storage/cacheStorage/helpers"; +import { cached } from "@ente/shared/storage/cache"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { User } from "@ente/shared/user/types"; import { Skeleton, styled } from "@mui/material"; diff --git a/web/apps/photos/src/components/MachineLearning/PeopleList.tsx b/web/apps/photos/src/components/MachineLearning/PeopleList.tsx index 0e358ce337..dc5f0fd435 100644 --- a/web/apps/photos/src/components/MachineLearning/PeopleList.tsx +++ b/web/apps/photos/src/components/MachineLearning/PeopleList.tsx @@ -1,5 +1,5 @@ import log from "@/next/log"; -import { CACHES } from "@ente/shared/storage/cacheStorage/constants"; +import { CACHES } from "@ente/shared/storage/cache"; import { styled } from "@mui/material"; import { Legend } from "components/PhotoViewer/styledComponents/Legend"; import { t } from "i18next"; diff --git a/web/apps/photos/src/services/download/index.ts b/web/apps/photos/src/services/download/index.ts index d262c74aac..64930989ac 100644 --- a/web/apps/photos/src/services/download/index.ts +++ b/web/apps/photos/src/services/download/index.ts @@ -4,9 +4,11 @@ import ComlinkCryptoWorker from "@ente/shared/crypto"; import { DedicatedCryptoWorker } from "@ente/shared/crypto/internal/crypto.worker"; import { CustomError } from "@ente/shared/error"; import { Events, eventBus } from "@ente/shared/events"; -import { CacheStorageService } from "@ente/shared/storage/cacheStorage"; -import { CACHES } from "@ente/shared/storage/cacheStorage/constants"; -import { LimitedCache } from "@ente/shared/storage/cacheStorage/types"; +import { + CACHES, + CacheStorageService, + type LimitedCache, +} from "@ente/shared/storage/cache"; import { Remote } from "comlink"; import { FILE_TYPE } from "constants/file"; import isElectron from "is-electron"; diff --git a/web/apps/photos/src/utils/machineLearning/faceCrop.ts b/web/apps/photos/src/utils/machineLearning/faceCrop.ts index 5486c64488..e681443263 100644 --- a/web/apps/photos/src/utils/machineLearning/faceCrop.ts +++ b/web/apps/photos/src/utils/machineLearning/faceCrop.ts @@ -1,5 +1,4 @@ -import { CacheStorageService } from "@ente/shared/storage/cacheStorage"; -import { CACHES } from "@ente/shared/storage/cacheStorage/constants"; +import { CACHES, CacheStorageService } from "@ente/shared/storage/cache"; import { BlobOptions } from "types/image"; import { FaceAlignment, diff --git a/web/apps/photos/src/utils/machineLearning/index.ts b/web/apps/photos/src/utils/machineLearning/index.ts index d0eacf1a6e..0157f66c1c 100644 --- a/web/apps/photos/src/utils/machineLearning/index.ts +++ b/web/apps/photos/src/utils/machineLearning/index.ts @@ -1,6 +1,5 @@ import log from "@/next/log"; -import { CACHES } from "@ente/shared/storage/cacheStorage/constants"; -import { cached } from "@ente/shared/storage/cacheStorage/helpers"; +import { CACHES, cached } from "@ente/shared/storage/cache"; import { FILE_TYPE } from "constants/file"; import PQueue from "p-queue"; import DownloadManager from "services/download"; diff --git a/web/packages/accounts/services/user.ts b/web/packages/accounts/services/user.ts index 87a320e361..777709c3a3 100644 --- a/web/packages/accounts/services/user.ts +++ b/web/packages/accounts/services/user.ts @@ -1,7 +1,7 @@ import log from "@/next/log"; import { Events, eventBus } from "@ente/shared/events"; import InMemoryStore from "@ente/shared/storage/InMemoryStore"; -import { deleteAllCache } from "@ente/shared/storage/cacheStorage/helpers"; +import { clearCaches } from "@ente/shared/storage/cache"; import { clearFiles } from "@ente/shared/storage/localForage/helpers"; import { clearData } from "@ente/shared/storage/localStorage"; import { clearKeys } from "@ente/shared/storage/sessionStorage"; @@ -31,7 +31,7 @@ export const logoutUser = async () => { log.error("Ignoring error when clearing data", e); } try { - await deleteAllCache(); + await clearCaches(); } catch (e) { log.error("Ignoring error when clearing caches", e); } diff --git a/web/packages/shared/storage/cache.ts b/web/packages/shared/storage/cache.ts new file mode 100644 index 0000000000..035d029c1d --- /dev/null +++ b/web/packages/shared/storage/cache.ts @@ -0,0 +1,123 @@ +import log from "@/next/log"; + +export enum CACHES { + THUMBS = "thumbs", + FACE_CROPS = "face-crops", + // Desktop app only + FILES = "files", +} + +export interface LimitedCacheStorage { + open: ( + cacheName: string, + cacheLimitInBytes?: number, + ) => Promise; + delete: (cacheName: string) => Promise; +} + +export interface LimitedCache { + match: ( + key: string, + options?: { sizeInBytes?: number }, + ) => Promise; + put: (key: string, data: Response) => Promise; + delete: (key: string) => Promise; +} + +class cacheStorageFactory { + getCacheStorage(): LimitedCacheStorage { + return transformBrowserCacheStorageToLimitedCacheStorage(caches); + } +} + +export const CacheStorageFactory = new cacheStorageFactory(); + +function transformBrowserCacheStorageToLimitedCacheStorage( + caches: CacheStorage, +): LimitedCacheStorage { + return { + async open(cacheName) { + const cache = await caches.open(cacheName); + return { + match: (key) => { + // options are not supported in the browser + return cache.match(key); + }, + put: cache.put.bind(cache), + delete: cache.delete.bind(cache), + }; + }, + delete: caches.delete.bind(caches), + }; +} + +const SecurityError = "SecurityError"; +const INSECURE_OPERATION = "The operation is insecure."; + +async function openCache(cacheName: string, cacheLimit?: number) { + try { + return await CacheStorageFactory.getCacheStorage().open( + cacheName, + cacheLimit, + ); + } catch (e) { + // ignoring insecure operation error, as it is thrown in incognito mode in firefox + if (e.name === SecurityError && e.message === INSECURE_OPERATION) { + // no-op + } else { + // log and ignore, we don't want to break the caller flow, when cache is not available + log.error("openCache failed", e); + } + } +} +async function deleteCache(cacheName: string) { + try { + return await CacheStorageFactory.getCacheStorage().delete(cacheName); + } catch (e) { + // ignoring insecure operation error, as it is thrown in incognito mode in firefox + if (e.name === SecurityError && e.message === INSECURE_OPERATION) { + // no-op + } else { + // log and ignore, we don't want to break the caller flow, when cache is not available + log.error("deleteCache failed", e); + } + } +} + +export const CacheStorageService = { open: openCache, delete: deleteCache }; + +export async function cached( + cacheName: string, + id: string, + get: () => Promise, +): Promise { + const cache = await CacheStorageService.open(cacheName); + const cacheResponse = await cache.match(id); + + let result: Blob; + if (cacheResponse) { + result = await cacheResponse.blob(); + } else { + result = await get(); + + try { + await cache.put(id, new Response(result)); + } catch (e) { + // TODO: handle storage full exception. + console.error("Error while storing file to cache: ", id); + } + } + + return result; +} + +/** + * Delete all cached data. + * + * Meant for use during logout, to reset the state of the user's account. + */ +export const clearCaches = async () => { + await CacheStorageService.delete(CACHES.THUMBS); + await CacheStorageService.delete(CACHES.FACE_CROPS); + await CacheStorageService.delete(CACHES.FILES); +}; diff --git a/web/packages/shared/storage/cacheStorage/constants.ts b/web/packages/shared/storage/cacheStorage/constants.ts deleted file mode 100644 index 4a31b78972..0000000000 --- a/web/packages/shared/storage/cacheStorage/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum CACHES { - THUMBS = "thumbs", - FACE_CROPS = "face-crops", - // Desktop app only - FILES = "files", -} diff --git a/web/packages/shared/storage/cacheStorage/factory.ts b/web/packages/shared/storage/cacheStorage/factory.ts deleted file mode 100644 index 0de07aeec0..0000000000 --- a/web/packages/shared/storage/cacheStorage/factory.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { LimitedCacheStorage } from "./types"; - -class cacheStorageFactory { - getCacheStorage(): LimitedCacheStorage { - return transformBrowserCacheStorageToLimitedCacheStorage(caches); - } -} - -export const CacheStorageFactory = new cacheStorageFactory(); - -function transformBrowserCacheStorageToLimitedCacheStorage( - caches: CacheStorage, -): LimitedCacheStorage { - return { - async open(cacheName) { - const cache = await caches.open(cacheName); - return { - match: (key) => { - // options are not supported in the browser - return cache.match(key); - }, - put: cache.put.bind(cache), - delete: cache.delete.bind(cache), - }; - }, - delete: caches.delete.bind(caches), - }; -} diff --git a/web/packages/shared/storage/cacheStorage/helpers.ts b/web/packages/shared/storage/cacheStorage/helpers.ts deleted file mode 100644 index b306d56837..0000000000 --- a/web/packages/shared/storage/cacheStorage/helpers.ts +++ /dev/null @@ -1,55 +0,0 @@ -import log from "@/next/log"; -import { CacheStorageService } from "."; -import { CACHES } from "./constants"; -import { LimitedCache } from "./types"; - -export async function cached( - cacheName: string, - id: string, - get: () => Promise, -): Promise { - const cache = await CacheStorageService.open(cacheName); - const cacheResponse = await cache.match(id); - - let result: Blob; - if (cacheResponse) { - result = await cacheResponse.blob(); - } else { - result = await get(); - - try { - await cache.put(id, new Response(result)); - } catch (e) { - // TODO: handle storage full exception. - console.error("Error while storing file to cache: ", id); - } - } - - return result; -} - -let thumbCache: LimitedCache; - -export async function getBlobFromCache( - cacheName: string, - url: string, -): Promise { - if (!thumbCache) { - thumbCache = await CacheStorageService.open(cacheName); - } - const response = await thumbCache.match(url); - if (!response) { - return undefined; - } - return response.blob(); -} - -export async function deleteAllCache() { - try { - await CacheStorageService.delete(CACHES.THUMBS); - await CacheStorageService.delete(CACHES.FACE_CROPS); - await CacheStorageService.delete(CACHES.FILES); - } catch (e) { - log.error("deleteAllCache failed", e); // log and ignore - } -} diff --git a/web/packages/shared/storage/cacheStorage/index.ts b/web/packages/shared/storage/cacheStorage/index.ts deleted file mode 100644 index a70bf09272..0000000000 --- a/web/packages/shared/storage/cacheStorage/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import log from "@/next/log"; -import { CacheStorageFactory } from "./factory"; - -const SecurityError = "SecurityError"; -const INSECURE_OPERATION = "The operation is insecure."; -async function openCache(cacheName: string, cacheLimit?: number) { - try { - return await CacheStorageFactory.getCacheStorage().open( - cacheName, - cacheLimit, - ); - } catch (e) { - // ignoring insecure operation error, as it is thrown in incognito mode in firefox - if (e.name === SecurityError && e.message === INSECURE_OPERATION) { - // no-op - } else { - // log and ignore, we don't want to break the caller flow, when cache is not available - log.error("openCache failed", e); - } - } -} -async function deleteCache(cacheName: string) { - try { - return await CacheStorageFactory.getCacheStorage().delete(cacheName); - } catch (e) { - // ignoring insecure operation error, as it is thrown in incognito mode in firefox - if (e.name === SecurityError && e.message === INSECURE_OPERATION) { - // no-op - } else { - // log and ignore, we don't want to break the caller flow, when cache is not available - log.error("deleteCache failed", e); - } - } -} - -export const CacheStorageService = { open: openCache, delete: deleteCache }; diff --git a/web/packages/shared/storage/cacheStorage/types.ts b/web/packages/shared/storage/cacheStorage/types.ts deleted file mode 100644 index d520559224..0000000000 --- a/web/packages/shared/storage/cacheStorage/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface LimitedCacheStorage { - open: ( - cacheName: string, - cacheLimitInBytes?: number, - ) => Promise; - delete: (cacheName: string) => Promise; -} - -export interface LimitedCache { - match: ( - key: string, - options?: { sizeInBytes?: number }, - ) => Promise; - put: (key: string, data: Response) => Promise; - delete: (key: string) => Promise; -} From 9c786da73bc7be054c03fb6a184e882fda2860c6 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 09:38:38 +0530 Subject: [PATCH 02/25] Tweak --- desktop/src/main.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 68b2cc51cc..93c85c92a5 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -96,12 +96,11 @@ function enableSharedArrayBufferSupport() { * * Set the "disk-cache-size" command line flag to ask the Chromium process to * use a larger size for the caches that it keeps on disk. This allows us to use - * the same web-native caching mechanism on both the web and the desktop app, - * just ask the embedded Chromium to be a bit more generous in disk usage when + * the web based caching mechanisms on both the web and the desktop app, just + * ask the embedded Chromium to be a bit more generous in disk usage when * running as the desktop app. * - * The size we provide is in bytes. We set it to a large value, 5 GB (5 * 1024 * - * 1024 * 1024 = 5368709120) + * The size we provide is in bytes. * https://www.electronjs.org/docs/latest/api/command-line-switches#--disk-cache-sizesize * * Note that increasing the disk cache size does not guarantee that Chromium @@ -109,7 +108,10 @@ function enableSharedArrayBufferSupport() { * https://superuser.com/questions/378991/what-is-chrome-default-cache-size-limit/1577693#1577693 */ const increaseDiskCache = () => { - app.commandLine.appendSwitch("disk-cache-size", "5368709120"); + app.commandLine.appendSwitch( + "disk-cache-size", + `${5 * 1024 * 1024 * 1024}`, // 5 GB + ); }; /** From 4d867898f7cc8d1ddbca687e77fe3b83dbe0dbf6 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 09:43:52 +0530 Subject: [PATCH 03/25] Remove the sharedarraybuffer flag From my understanding, this was needed for the wasm FFMPEG, which we are no longer using when running as the desktop app. --- desktop/src/main.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 93c85c92a5..e8bf1fee05 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -87,10 +87,6 @@ const logStartupBanner = () => { log.info("Running on", { platform, osRelease, systemVersion }); }; -function enableSharedArrayBufferSupport() { - app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); -} - /** * [Note: Increased disk cache for the desktop app] * From aace18aba048e2f85852d3a68a192ac698eaf968 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 09:51:08 +0530 Subject: [PATCH 04/25] Inline and rename --- desktop/src/main.ts | 15 ++++++++++++--- desktop/src/main/init.ts | 12 ++---------- desktop/src/main/menu.ts | 11 +++++------ desktop/src/main/services/app-update.ts | 16 +++++++--------- desktop/src/main/services/userPreference.ts | 9 --------- desktop/src/main/stores/user-preferences.ts | 2 +- 6 files changed, 27 insertions(+), 38 deletions(-) delete mode 100644 desktop/src/main/services/userPreference.ts diff --git a/desktop/src/main.ts b/desktop/src/main.ts index e8bf1fee05..ca88194026 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -17,7 +17,6 @@ import path from "node:path"; import { addAllowOriginHeader, createWindow, - handleDockIconHideOnAutoLaunch, handleDownloads, handleExternalLinks, setupMacWindowOnDockIconClick, @@ -27,7 +26,9 @@ import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc"; import log, { initLogging } from "./main/log"; import { createApplicationMenu } from "./main/menu"; import { setupAutoUpdater } from "./main/services/app-update"; +import autoLauncher from "./main/services/autoLauncher"; import { initWatcher } from "./main/services/chokidar"; +import { userPreferences } from "./main/stores/user-preferences"; import { isDev } from "./main/util"; let appIsQuitting = false; @@ -110,6 +111,15 @@ const increaseDiskCache = () => { ); }; +const hideDockIconIfNeeded = async () => { + const shouldHideDockIcon = userPreferences.get("hideDockIcon"); + const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); + + if (process.platform == "darwin" && shouldHideDockIcon && wasAutoLaunched) { + app.dock.hide(); + } +}; + /** * Older versions of our app used to maintain a cache dir using the main * process. This has been deprecated in favor of using a normal web cache. @@ -159,9 +169,8 @@ const main = () => { initLogging(); setupRendererServer(); logStartupBanner(); - handleDockIconHideOnAutoLaunch(); + hideDockIconIfNeeded(); increaseDiskCache(); - enableSharedArrayBufferSupport(); app.on("second-instance", () => { // Someone tried to run a second instance, we should focus our window. diff --git a/desktop/src/main/init.ts b/desktop/src/main/init.ts index 2bea075a33..0b73f35f91 100644 --- a/desktop/src/main/init.ts +++ b/desktop/src/main/init.ts @@ -4,11 +4,12 @@ import path from "node:path"; import { isAppQuitting, rendererURL } from "../main"; import log from "./log"; import { createTrayContextMenu } from "./menu"; -import { isPlatform } from "./platform"; import autoLauncher from "./services/autoLauncher"; import { getHideDockIconPreference } from "./services/userPreference"; import { isDev } from "./util"; + + /** * Create an return the {@link BrowserWindow} that will form our app's UI. * @@ -145,15 +146,6 @@ export function setupMacWindowOnDockIconClick() { }); } -export async function handleDockIconHideOnAutoLaunch() { - const shouldHideDockIcon = getHideDockIconPreference(); - const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); - - if (isPlatform("mac") && shouldHideDockIcon && wasAutoLaunched) { - app.dock.hide(); - } -} - function lowerCaseHeaders(responseHeaders: Record) { const headers: Record = {}; for (const key of Object.keys(responseHeaders)) { diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts index 7801e77aec..bc51f6c349 100644 --- a/desktop/src/main/menu.ts +++ b/desktop/src/main/menu.ts @@ -8,10 +8,7 @@ import { import { setIsAppQuitting } from "../main"; import { forceCheckForAppUpdates } from "./services/app-update"; import autoLauncher from "./services/autoLauncher"; -import { - getHideDockIconPreference, - setHideDockIconPreference, -} from "./services/userPreference"; +import { userPreferences } from "./stores/user-preferences"; import { openLogDirectory } from "./util"; /** Create and return the entries in the app's main menu bar */ @@ -21,7 +18,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { // Whenever the menu is redrawn the current value of these variables is used // to set the checked state for the various settings checkboxes. let isAutoLaunchEnabled = await autoLauncher.isEnabled(); - let shouldHideDockIcon = getHideDockIconPreference(); + let shouldHideDockIcon = userPreferences.get("hideDockIcon"); const macOSOnly = (options: MenuItemConstructorOptions[]) => process.platform == "darwin" ? options : []; @@ -39,7 +36,9 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { }; const toggleHideDockIcon = () => { - setHideDockIconPreference(!shouldHideDockIcon); + // Persist + userPreferences.set("hideDockIcon", !shouldHideDockIcon); + // And update the in-memory state shouldHideDockIcon = !shouldHideDockIcon; }; diff --git a/desktop/src/main/services/app-update.ts b/desktop/src/main/services/app-update.ts index 272a1ca6c6..996f736333 100644 --- a/desktop/src/main/services/app-update.ts +++ b/desktop/src/main/services/app-update.ts @@ -5,7 +5,7 @@ import { autoUpdater } from "electron-updater"; import { setIsAppQuitting, setIsUpdateAvailable } from "../../main"; import { AppUpdateInfo } from "../../types/ipc"; import log from "../log"; -import { userPreferencesStore } from "../stores/user-preferences"; +import { userPreferences } from "../stores/user-preferences"; export const setupAutoUpdater = (mainWindow: BrowserWindow) => { autoUpdater.logger = electronLog; @@ -20,8 +20,8 @@ export const setupAutoUpdater = (mainWindow: BrowserWindow) => { * Check for app update check ignoring any previously saved skips / mutes. */ export const forceCheckForAppUpdates = (mainWindow: BrowserWindow) => { - userPreferencesStore.delete("skipAppVersion"); - userPreferencesStore.delete("muteUpdateNotificationVersion"); + userPreferences.delete("skipAppVersion"); + userPreferences.delete("muteUpdateNotificationVersion"); checkForUpdatesAndNotify(mainWindow); }; @@ -41,14 +41,12 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => { return; } - if (version === userPreferencesStore.get("skipAppVersion")) { + if (version === userPreferences.get("skipAppVersion")) { log.info(`User chose to skip version ${version}`); return; } - const mutedVersion = userPreferencesStore.get( - "muteUpdateNotificationVersion", - ); + const mutedVersion = userPreferences.get("muteUpdateNotificationVersion"); if (version === mutedVersion) { log.info(`User has muted update notifications for version ${version}`); return; @@ -92,7 +90,7 @@ export const updateAndRestart = () => { }; export const updateOnNextRestart = (version: string) => - userPreferencesStore.set("muteUpdateNotificationVersion", version); + userPreferences.set("muteUpdateNotificationVersion", version); export const skipAppUpdate = (version: string) => - userPreferencesStore.set("skipAppVersion", version); + userPreferences.set("skipAppVersion", version); diff --git a/desktop/src/main/services/userPreference.ts b/desktop/src/main/services/userPreference.ts deleted file mode 100644 index c20657aa9c..0000000000 --- a/desktop/src/main/services/userPreference.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { userPreferencesStore } from "../stores/user-preferences"; - -export function getHideDockIconPreference() { - return userPreferencesStore.get("hideDockIcon"); -} - -export function setHideDockIconPreference(shouldHideDockIcon: boolean) { - userPreferencesStore.set("hideDockIcon", shouldHideDockIcon); -} diff --git a/desktop/src/main/stores/user-preferences.ts b/desktop/src/main/stores/user-preferences.ts index 396e7a86c7..a305f1a99b 100644 --- a/desktop/src/main/stores/user-preferences.ts +++ b/desktop/src/main/stores/user-preferences.ts @@ -18,7 +18,7 @@ const userPreferencesSchema: Schema = { }, }; -export const userPreferencesStore = new Store({ +export const userPreferences = new Store({ name: "userPreferences", schema: userPreferencesSchema, }); From 664cf2157ba94cd57bcbdd27ff3aa5a506bd0ea3 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 10:00:04 +0530 Subject: [PATCH 05/25] Reword --- desktop/src/main.ts | 19 +++++++++---------- desktop/src/main/init.ts | 3 --- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index ca88194026..81b6dda2d3 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -69,9 +69,7 @@ export const rendererURL = "next://app"; * For more details, see this comparison: * https://github.com/HaNdTriX/next-electron-server/issues/5 */ -const setupRendererServer = () => { - serveNextAt(rendererURL); -}; +const setupRendererServer = () => serveNextAt(rendererURL); /** * Log a standard startup banner. @@ -111,13 +109,16 @@ const increaseDiskCache = () => { ); }; +/** + * Hide the dock icon on macOS if the user wants it hidden and we were + * auto-launched on login. + */ const hideDockIconIfNeeded = async () => { + if (process.platform != "darwin") return; + const shouldHideDockIcon = userPreferences.get("hideDockIcon"); const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); - - if (process.platform == "darwin" && shouldHideDockIcon && wasAutoLaunched) { - app.dock.hide(); - } + if (shouldHideDockIcon && wasAutoLaunched) app.dock.hide(); }; /** @@ -176,9 +177,7 @@ const main = () => { // Someone tried to run a second instance, we should focus our window. if (mainWindow) { mainWindow.show(); - if (mainWindow.isMinimized()) { - mainWindow.restore(); - } + if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); } }); diff --git a/desktop/src/main/init.ts b/desktop/src/main/init.ts index 0b73f35f91..4568aee66a 100644 --- a/desktop/src/main/init.ts +++ b/desktop/src/main/init.ts @@ -5,11 +5,8 @@ import { isAppQuitting, rendererURL } from "../main"; import log from "./log"; import { createTrayContextMenu } from "./menu"; import autoLauncher from "./services/autoLauncher"; -import { getHideDockIconPreference } from "./services/userPreference"; import { isDev } from "./util"; - - /** * Create an return the {@link BrowserWindow} that will form our app's UI. * From e651e9d9c78d0ebdfac41bf975a6d12996df8f79 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 10:11:38 +0530 Subject: [PATCH 06/25] Inline --- desktop/src/main.ts | 92 +++++++++++++++++++++++++++++++++++++++- desktop/src/main/init.ts | 88 -------------------------------------- 2 files changed, 90 insertions(+), 90 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 81b6dda2d3..e18d74ed7c 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -8,7 +8,8 @@ * * https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process */ -import { app, BrowserWindow, Menu } from "electron/main"; +import { nativeImage } from "electron"; +import { app, BrowserWindow, Menu, Tray } from "electron/main"; import serveNextAt from "next-electron-server"; import { existsSync } from "node:fs"; import fs from "node:fs/promises"; @@ -24,7 +25,7 @@ import { } from "./main/init"; import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc"; import log, { initLogging } from "./main/log"; -import { createApplicationMenu } from "./main/menu"; +import { createApplicationMenu, createTrayContextMenu } from "./main/menu"; import { setupAutoUpdater } from "./main/services/app-update"; import autoLauncher from "./main/services/autoLauncher"; import { initWatcher } from "./main/services/chokidar"; @@ -121,6 +122,93 @@ const hideDockIconIfNeeded = async () => { if (shouldHideDockIcon && wasAutoLaunched) app.dock.hide(); }; +/** + * Create an return the {@link BrowserWindow} that will form our app's UI. + * + * This window will show the HTML served from {@link rendererURL}. + */ +export const createMainWindow = async () => { + // Create the main window. This'll show our web content. + const window = new BrowserWindow({ + webPreferences: { + preload: path.join(app.getAppPath(), "preload.js"), + }, + // The color to show in the window until the web content gets loaded. + // See: https://www.electronjs.org/docs/latest/api/browser-window#setting-the-backgroundcolor-property + backgroundColor: "black", + // We'll show it conditionally depending on `wasAutoLaunched` later. + show: false, + }); + + const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); + if (wasAutoLaunched) { + // Keep the macOS dock icon hidden if we were auto launched. + if (process.platform == "darwin") app.dock.hide(); + } else { + // Show our window (maximizing it) if this is not an auto-launch on + // login. + window.maximize(); + } + + window.loadURL(rendererURL); + + // Open the DevTools automatically when running in dev mode + if (isDev) window.webContents.openDevTools(); + + window.webContents.on("render-process-gone", (_, details) => { + log.error(`render-process-gone: ${details}`); + window.webContents.reload(); + }); + + window.webContents.on("unresponsive", () => { + log.error("webContents unresponsive"); + window.webContents.forcefullyCrashRenderer(); + }); + + window.on("close", function (event) { + if (!isAppQuitting()) { + event.preventDefault(); + window.hide(); + } + return false; + }); + + window.on("hide", () => { + // On macOS, when hiding the window also hide the app's icon in the dock + // if the user has selected the Settings > Hide dock icon checkbox. + if (process.platform == "darwin" && userPreferences.get("hideDockIcon")) + app.dock.hide(); + }); + + window.on("show", () => { + if (process.platform == "darwin") app.dock.show(); + }); + + return window; +}; + +const setupTrayItem = (mainWindow: BrowserWindow) => { + // There are a total of 6 files corresponding to this tray icon. + // + // On macOS, use template images (filename needs to end with "Template.ext") + // https://www.electronjs.org/docs/latest/api/native-image#template-image-macos + // + // And for each (template or otherwise), there are 3 "retina" variants + // https://www.electronjs.org/docs/latest/api/native-image#high-resolution-image + const iconName = + process.platform == "darwin" + ? "taskbar-icon-Template.png" + : "taskbar-icon.png"; + const trayImgPath = path.join( + isDev ? "build" : process.resourcesPath, + iconName, + ); + const trayIcon = nativeImage.createFromPath(trayImgPath); + const tray = new Tray(trayIcon); + tray.setToolTip("Ente Photos"); + tray.setContextMenu(createTrayContextMenu(mainWindow)); +}; + /** * Older versions of our app used to maintain a cache dir using the main * process. This has been deprecated in favor of using a normal web cache. diff --git a/desktop/src/main/init.ts b/desktop/src/main/init.ts index 4568aee66a..6210dcdddd 100644 --- a/desktop/src/main/init.ts +++ b/desktop/src/main/init.ts @@ -7,94 +7,6 @@ import { createTrayContextMenu } from "./menu"; import autoLauncher from "./services/autoLauncher"; import { isDev } from "./util"; -/** - * Create an return the {@link BrowserWindow} that will form our app's UI. - * - * This window will show the HTML served from {@link rendererURL}. - */ -export const createWindow = async () => { - // Create the main window. This'll show our web content. - const mainWindow = new BrowserWindow({ - webPreferences: { - preload: path.join(app.getAppPath(), "preload.js"), - }, - // The color to show in the window until the web content gets loaded. - // See: https://www.electronjs.org/docs/latest/api/browser-window#setting-the-backgroundcolor-property - backgroundColor: "black", - // We'll show it conditionally depending on `wasAutoLaunched` later. - show: false, - }); - - const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); - if (wasAutoLaunched) { - // Keep the macOS dock icon hidden if we were auto launched. - if (process.platform == "darwin") app.dock.hide(); - } else { - // Show our window (maximizing it) if this is not an auto-launch on - // login. - mainWindow.maximize(); - } - - mainWindow.loadURL(rendererURL); - - // Open the DevTools automatically when running in dev mode - if (isDev) mainWindow.webContents.openDevTools(); - - mainWindow.webContents.on("render-process-gone", (_, details) => { - log.error(`render-process-gone: ${details}`); - mainWindow.webContents.reload(); - }); - - mainWindow.webContents.on("unresponsive", () => { - log.error("webContents unresponsive"); - mainWindow.webContents.forcefullyCrashRenderer(); - }); - - mainWindow.on("close", function (event) { - if (!isAppQuitting()) { - event.preventDefault(); - mainWindow.hide(); - } - return false; - }); - - mainWindow.on("hide", () => { - // On macOS, also hide the app's icon in the dock if the user has - // selected the Settings > Hide dock icon checkbox. - const shouldHideDockIcon = getHideDockIconPreference(); - if (process.platform == "darwin" && shouldHideDockIcon) { - app.dock.hide(); - } - }); - - mainWindow.on("show", () => { - if (process.platform == "darwin") app.dock.show(); - }); - - return mainWindow; -}; - -export const setupTrayItem = (mainWindow: BrowserWindow) => { - // There are a total of 6 files corresponding to this tray icon. - // - // On macOS, use template images (filename needs to end with "Template.ext") - // https://www.electronjs.org/docs/latest/api/native-image#template-image-macos - // - // And for each (template or otherwise), there are 3 "retina" variants - // https://www.electronjs.org/docs/latest/api/native-image#high-resolution-image - const iconName = - process.platform == "darwin" - ? "taskbar-icon-Template.png" - : "taskbar-icon.png"; - const trayImgPath = path.join( - isDev ? "build" : process.resourcesPath, - iconName, - ); - const trayIcon = nativeImage.createFromPath(trayImgPath); - const tray = new Tray(trayIcon); - tray.setToolTip("Ente Photos"); - tray.setContextMenu(createTrayContextMenu(mainWindow)); -}; export function handleDownloads(mainWindow: BrowserWindow) { mainWindow.webContents.session.on("will-download", (_, item) => { From a71ca936962cbb1b795fa132f329b83ad9c5385c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 10:15:08 +0530 Subject: [PATCH 07/25] Inline --- desktop/src/main.ts | 12 ++++++++---- desktop/src/main/init.ts | 9 ++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index e18d74ed7c..950c985b08 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -17,11 +17,9 @@ import os from "node:os"; import path from "node:path"; import { addAllowOriginHeader, - createWindow, handleDownloads, handleExternalLinks, setupMacWindowOnDockIconClick, - setupTrayItem, } from "./main/init"; import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc"; import log, { initLogging } from "./main/log"; @@ -127,7 +125,7 @@ const hideDockIconIfNeeded = async () => { * * This window will show the HTML served from {@link rendererURL}. */ -export const createMainWindow = async () => { +const createMainWindow = async () => { // Create the main window. This'll show our web content. const window = new BrowserWindow({ webPreferences: { @@ -187,6 +185,12 @@ export const createMainWindow = async () => { return window; }; +/** + * Add an icon for our app in the system tray. + * + * For example, these are the small icons that appear on the top right of the + * screen in the main menu bar on macOS. + */ const setupTrayItem = (mainWindow: BrowserWindow) => { // There are a total of 6 files corresponding to this tray icon. // @@ -274,7 +278,7 @@ const main = () => { // // Note that some Electron APIs can only be used after this event occurs. app.on("ready", async () => { - mainWindow = await createWindow(); + mainWindow = await createMainWindow(); const watcher = initWatcher(mainWindow); setupTrayItem(mainWindow); setupMacWindowOnDockIconClick(); diff --git a/desktop/src/main/init.ts b/desktop/src/main/init.ts index 6210dcdddd..baa6444fd2 100644 --- a/desktop/src/main/init.ts +++ b/desktop/src/main/init.ts @@ -1,12 +1,7 @@ -import { BrowserWindow, Tray, app, nativeImage, shell } from "electron"; +import { BrowserWindow, app, shell } from "electron"; import { existsSync } from "node:fs"; import path from "node:path"; -import { isAppQuitting, rendererURL } from "../main"; -import log from "./log"; -import { createTrayContextMenu } from "./menu"; -import autoLauncher from "./services/autoLauncher"; -import { isDev } from "./util"; - +import { rendererURL } from "../main"; export function handleDownloads(mainWindow: BrowserWindow) { mainWindow.webContents.session.on("will-download", (_, item) => { From 87905a39f8857293824ae54dc4420e35fb690418 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 10:22:04 +0530 Subject: [PATCH 08/25] Dedup --- desktop/src/main.ts | 36 ++++++++++-------------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 950c985b08..3a802988f3 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -108,18 +108,6 @@ const increaseDiskCache = () => { ); }; -/** - * Hide the dock icon on macOS if the user wants it hidden and we were - * auto-launched on login. - */ -const hideDockIconIfNeeded = async () => { - if (process.platform != "darwin") return; - - const shouldHideDockIcon = userPreferences.get("hideDockIcon"); - const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); - if (shouldHideDockIcon && wasAutoLaunched) app.dock.hide(); -}; - /** * Create an return the {@link BrowserWindow} that will form our app's UI. * @@ -140,11 +128,11 @@ const createMainWindow = async () => { const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); if (wasAutoLaunched) { - // Keep the macOS dock icon hidden if we were auto launched. + // Don't automatically show the app's window if we were auto-launched. + // On macOS, also hide the dock icon on macOS. if (process.platform == "darwin") app.dock.hide(); } else { - // Show our window (maximizing it) if this is not an auto-launch on - // login. + // Show our window (maximizing it) otherwise. window.maximize(); } @@ -159,7 +147,9 @@ const createMainWindow = async () => { }); window.webContents.on("unresponsive", () => { - log.error("webContents unresponsive"); + log.error( + "Main window's webContents are unresponsive, will restart the renderer process", + ); window.webContents.forcefullyCrashRenderer(); }); @@ -182,6 +172,10 @@ const createMainWindow = async () => { if (process.platform == "darwin") app.dock.show(); }); + // Let ipcRenderer know when mainWindow is in the foreground so that it can + // in turn inform the renderer process. + window.on("focus", () => window.webContents.send("mainWindowFocus")); + return window; }; @@ -242,14 +236,6 @@ const deleteLegacyDiskCacheDirIfExists = async () => { } }; -const attachEventHandlers = (mainWindow: BrowserWindow) => { - // Let ipcRenderer know when mainWindow is in the foreground so that it can - // in turn inform the renderer process. - mainWindow.on("focus", () => - mainWindow.webContents.send("mainWindowFocus"), - ); -}; - const main = () => { const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { @@ -262,7 +248,6 @@ const main = () => { initLogging(); setupRendererServer(); logStartupBanner(); - hideDockIconIfNeeded(); increaseDiskCache(); app.on("second-instance", () => { @@ -289,7 +274,6 @@ const main = () => { handleDownloads(mainWindow); handleExternalLinks(mainWindow); addAllowOriginHeader(mainWindow); - attachEventHandlers(mainWindow); try { deleteLegacyDiskCacheDirIfExists(); From 98c3e43b70ec4c47ef36cbf18ced3ae0a46f130a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 10:28:23 +0530 Subject: [PATCH 09/25] Inline --- desktop/src/main.ts | 8 +++++--- desktop/src/main/init.ts | 8 -------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 3a802988f3..b76b37f968 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -19,7 +19,6 @@ import { addAllowOriginHeader, handleDownloads, handleExternalLinks, - setupMacWindowOnDockIconClick, } from "./main/init"; import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc"; import log, { initLogging } from "./main/log"; @@ -243,7 +242,7 @@ const main = () => { return; } - let mainWindow: BrowserWindow; + let mainWindow: BrowserWindow | undefined; initLogging(); setupRendererServer(); @@ -266,7 +265,6 @@ const main = () => { mainWindow = await createMainWindow(); const watcher = initWatcher(mainWindow); setupTrayItem(mainWindow); - setupMacWindowOnDockIconClick(); Menu.setApplicationMenu(await createApplicationMenu(mainWindow)); attachIPCHandlers(); attachFSWatchIPCHandlers(watcher); @@ -284,6 +282,10 @@ const main = () => { } }); + // This is a macOS only event. Show our window when the user activates the + // app, e.g. by clicking on its dock icon. + app.on("activate", () => mainWindow?.show()); + app.on("before-quit", () => setIsAppQuitting(true)); }; diff --git a/desktop/src/main/init.ts b/desktop/src/main/init.ts index baa6444fd2..d3e9b28b4b 100644 --- a/desktop/src/main/init.ts +++ b/desktop/src/main/init.ts @@ -42,14 +42,6 @@ export function getUniqueSavePath(filename: string, directory: string): string { return uniqueFileSavePath; } -export function setupMacWindowOnDockIconClick() { - app.on("activate", function () { - const windows = BrowserWindow.getAllWindows(); - // we allow only one window - windows[0].show(); - }); -} - function lowerCaseHeaders(responseHeaders: Record) { const headers: Record = {}; for (const key of Object.keys(responseHeaders)) { From cfec6ca52edb955bc2e69a22b9f44b430c4f8698 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 10:39:00 +0530 Subject: [PATCH 10/25] Enable the Chromium sandbox --- desktop/src/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index b76b37f968..360b04ff6d 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -100,12 +100,11 @@ const logStartupBanner = () => { * will respect in verbatim, it uses its own heuristics atop this hint. * https://superuser.com/questions/378991/what-is-chrome-default-cache-size-limit/1577693#1577693 */ -const increaseDiskCache = () => { +const increaseDiskCache = () => app.commandLine.appendSwitch( "disk-cache-size", `${5 * 1024 * 1024 * 1024}`, // 5 GB ); -}; /** * Create an return the {@link BrowserWindow} that will form our app's UI. @@ -117,6 +116,7 @@ const createMainWindow = async () => { const window = new BrowserWindow({ webPreferences: { preload: path.join(app.getAppPath(), "preload.js"), + sandbox: true, }, // The color to show in the window until the web content gets loaded. // See: https://www.electronjs.org/docs/latest/api/browser-window#setting-the-backgroundcolor-property From 39912d9b352cc9181b19a75a4bd64d136a6fae86 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 13:25:48 +0530 Subject: [PATCH 11/25] Remove unused code --- desktop/src/main.ts | 10 ---------- desktop/src/main/services/app-update.ts | 4 +--- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 360b04ff6d..a3cb665383 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -31,8 +31,6 @@ import { isDev } from "./main/util"; let appIsQuitting = false; -let updateIsAvailable = false; - export const isAppQuitting = (): boolean => { return appIsQuitting; }; @@ -41,14 +39,6 @@ export const setIsAppQuitting = (value: boolean): void => { appIsQuitting = value; }; -export const isUpdateAvailable = (): boolean => { - return updateIsAvailable; -}; - -export const setIsUpdateAvailable = (value: boolean): void => { - updateIsAvailable = value; -}; - /** * The URL where the renderer HTML is being served from. */ diff --git a/desktop/src/main/services/app-update.ts b/desktop/src/main/services/app-update.ts index 996f736333..6590bea26e 100644 --- a/desktop/src/main/services/app-update.ts +++ b/desktop/src/main/services/app-update.ts @@ -2,7 +2,7 @@ import { compareVersions } from "compare-versions"; import { app, BrowserWindow } from "electron"; import { default as electronLog } from "electron-log"; import { autoUpdater } from "electron-updater"; -import { setIsAppQuitting, setIsUpdateAvailable } from "../../main"; +import { setIsAppQuitting } from "../../main"; import { AppUpdateInfo } from "../../types/ipc"; import log from "../log"; import { userPreferences } from "../stores/user-preferences"; @@ -72,8 +72,6 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => { log.error("Auto update failed", error); showUpdateDialog({ autoUpdatable: false, version }); }); - - setIsUpdateAvailable(true); }; /** From d1d9cd9518cc6ac5b962ba83e2ae2abebde35cae Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 13:48:16 +0530 Subject: [PATCH 12/25] Document --- desktop/src/main.ts | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index a3cb665383..503ad5aee2 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -29,21 +29,33 @@ import { initWatcher } from "./main/services/chokidar"; import { userPreferences } from "./main/stores/user-preferences"; import { isDev } from "./main/util"; -let appIsQuitting = false; - -export const isAppQuitting = (): boolean => { - return appIsQuitting; -}; - -export const setIsAppQuitting = (value: boolean): void => { - appIsQuitting = value; -}; - /** * The URL where the renderer HTML is being served from. */ export const rendererURL = "next://app"; +/** + * We want to hide our window instead of closing it when the user presses the + * cross button on the window (this is because there is 1. a perceptible initial + * window creation time for our app, and 2. because the long running processes + * like export and watch folders are tied to the lifetime of the window and + * otherwise won't run in the background. + * + * Intercepting the window close event and using that to instead hide it is + * easy, however that prevents the actual app quit to stop working (since the + * window never gets closed). + * + * So to achieve our original goal (hide window instead of closing) without + * disabling expected app quits, we keep this `allowWindowClose` flag. It is off + * by default, but in the cases where we *do* want the app to quit, we set it to + * true beforehand before calling the actual process that'll do the quitting. + */ +let allowWindowClose = false; + +export const setIsAppQuitting = (value: boolean): void => { + allowWindowClose = value; +}; + /** * next-electron-server allows up to directly use the output of `next build` in * production mode and `next dev` in development mode, whilst keeping the rest @@ -143,7 +155,7 @@ const createMainWindow = async () => { }); window.on("close", function (event) { - if (!isAppQuitting()) { + if (!allowWindowClose) { event.preventDefault(); window.hide(); } From 8045bd1e5ac46155aaef4a1ddcd4c5e31b9bb221 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 14:00:35 +0530 Subject: [PATCH 13/25] Rename and prune --- desktop/src/main.ts | 28 +++++++++++++------------ desktop/src/main/menu.ts | 4 ++-- desktop/src/main/services/app-update.ts | 4 ++-- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 503ad5aee2..809b153b8b 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -36,24 +36,26 @@ export const rendererURL = "next://app"; /** * We want to hide our window instead of closing it when the user presses the - * cross button on the window (this is because there is 1. a perceptible initial - * window creation time for our app, and 2. because the long running processes - * like export and watch folders are tied to the lifetime of the window and - * otherwise won't run in the background. + * cross button on the window. + * + * > This is because there is 1. a perceptible initial window creation time for + * > our app, and 2. because the long running processes like export and watch + * > folders are tied to the lifetime of the window and otherwise won't run in + * > the background. * * Intercepting the window close event and using that to instead hide it is * easy, however that prevents the actual app quit to stop working (since the * window never gets closed). * * So to achieve our original goal (hide window instead of closing) without - * disabling expected app quits, we keep this `allowWindowClose` flag. It is off - * by default, but in the cases where we *do* want the app to quit, we set it to - * true beforehand before calling the actual process that'll do the quitting. + * disabling expected app quits, we keep a flag, and we turn it on when we're + * part of the quit sequence. When this flag is on, we bypass the code that + * prevents the window from being closed. */ -let allowWindowClose = false; +let shouldAllowWindowClose = false; -export const setIsAppQuitting = (value: boolean): void => { - allowWindowClose = value; +export const allowWindowClose = (): void => { + shouldAllowWindowClose = true; }; /** @@ -154,8 +156,8 @@ const createMainWindow = async () => { window.webContents.forcefullyCrashRenderer(); }); - window.on("close", function (event) { - if (!allowWindowClose) { + window.on("close", (event) => { + if (!shouldAllowWindowClose) { event.preventDefault(); window.hide(); } @@ -288,7 +290,7 @@ const main = () => { // app, e.g. by clicking on its dock icon. app.on("activate", () => mainWindow?.show()); - app.on("before-quit", () => setIsAppQuitting(true)); + app.on("before-quit", allowWindowClose); }; main(); diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts index bc51f6c349..3441f3f2ab 100644 --- a/desktop/src/main/menu.ts +++ b/desktop/src/main/menu.ts @@ -5,7 +5,7 @@ import { MenuItemConstructorOptions, shell, } from "electron"; -import { setIsAppQuitting } from "../main"; +import { allowWindowClose } from "../main"; import { forceCheckForAppUpdates } from "./services/app-update"; import autoLauncher from "./services/autoLauncher"; import { userPreferences } from "./stores/user-preferences"; @@ -195,7 +195,7 @@ export const createTrayContextMenu = (mainWindow: BrowserWindow) => { }; const handleClose = () => { - setIsAppQuitting(true); + allowWindowClose(); app.quit(); }; diff --git a/desktop/src/main/services/app-update.ts b/desktop/src/main/services/app-update.ts index 6590bea26e..b474485016 100644 --- a/desktop/src/main/services/app-update.ts +++ b/desktop/src/main/services/app-update.ts @@ -2,7 +2,7 @@ import { compareVersions } from "compare-versions"; import { app, BrowserWindow } from "electron"; import { default as electronLog } from "electron-log"; import { autoUpdater } from "electron-updater"; -import { setIsAppQuitting } from "../../main"; +import { allowWindowClose } from "../../main"; import { AppUpdateInfo } from "../../types/ipc"; import log from "../log"; import { userPreferences } from "../stores/user-preferences"; @@ -83,7 +83,7 @@ export const appVersion = () => `v${app.getVersion()}`; export const updateAndRestart = () => { log.info("Restarting the app to apply update"); - setIsAppQuitting(true); + allowWindowClose(); autoUpdater.quitAndInstall(); }; From 02763e3be448771ef6673310d9628ce875087622 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 14:03:22 +0530 Subject: [PATCH 14/25] Kill the renderer when we quit the app --- desktop/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/package.json b/desktop/package.json index 16ba23eb96..032953d8d0 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -11,7 +11,7 @@ "build-main:quick": "tsc && electron-builder --dir --config.compression=store --config.mac.identity=null", "build-renderer": "cd ../web && yarn install && yarn build:photos && cd ../desktop && shx rm -f out && shx ln -sf ../web/apps/photos/out out", "build:quick": "yarn build-renderer && yarn build-main:quick", - "dev": "concurrently --names 'main,rndr' \"yarn dev-main\" \"yarn dev-renderer\"", + "dev": "concurrently --kill-others --success first --names 'main,rndr' \"yarn dev-main\" \"yarn dev-renderer\"", "dev-main": "tsc && electron app/main.js", "dev-renderer": "cd ../web && yarn install && yarn dev:photos", "postinstall": "electron-builder install-app-deps", From bdadc839e0081828d662901c21e871f3257c2d4c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 14:23:56 +0530 Subject: [PATCH 15/25] Remove firefox specific workaround In a quick (but possibly incomplete) test I wasn't able to reproduce this in an incognito window. Throwing the error from here has a potential for breaking things though, I'll try to verify this as I go through the flows individually. --- web/packages/shared/storage/cache.ts | 71 ++++++++-------------------- 1 file changed, 20 insertions(+), 51 deletions(-) diff --git a/web/packages/shared/storage/cache.ts b/web/packages/shared/storage/cache.ts index 035d029c1d..3b585116e2 100644 --- a/web/packages/shared/storage/cache.ts +++ b/web/packages/shared/storage/cache.ts @@ -1,5 +1,3 @@ -import log from "@/next/log"; - export enum CACHES { THUMBS = "thumbs", FACE_CROPS = "face-crops", @@ -7,7 +5,7 @@ export enum CACHES { FILES = "files", } -export interface LimitedCacheStorage { +interface LimitedCacheStorage { open: ( cacheName: string, cacheLimitInBytes?: number, @@ -26,62 +24,33 @@ export interface LimitedCache { class cacheStorageFactory { getCacheStorage(): LimitedCacheStorage { - return transformBrowserCacheStorageToLimitedCacheStorage(caches); + return { + async open(cacheName) { + const cache = await caches.open(cacheName); + return { + match: (key) => { + // options are not supported in the browser + return cache.match(key); + }, + put: cache.put.bind(cache), + delete: cache.delete.bind(cache), + }; + }, + delete: caches.delete.bind(caches), + }; } } export const CacheStorageFactory = new cacheStorageFactory(); -function transformBrowserCacheStorageToLimitedCacheStorage( - caches: CacheStorage, -): LimitedCacheStorage { - return { - async open(cacheName) { - const cache = await caches.open(cacheName); - return { - match: (key) => { - // options are not supported in the browser - return cache.match(key); - }, - put: cache.put.bind(cache), - delete: cache.delete.bind(cache), - }; - }, - delete: caches.delete.bind(caches), - }; -} - -const SecurityError = "SecurityError"; -const INSECURE_OPERATION = "The operation is insecure."; - async function openCache(cacheName: string, cacheLimit?: number) { - try { - return await CacheStorageFactory.getCacheStorage().open( - cacheName, - cacheLimit, - ); - } catch (e) { - // ignoring insecure operation error, as it is thrown in incognito mode in firefox - if (e.name === SecurityError && e.message === INSECURE_OPERATION) { - // no-op - } else { - // log and ignore, we don't want to break the caller flow, when cache is not available - log.error("openCache failed", e); - } - } + return await CacheStorageFactory.getCacheStorage().open( + cacheName, + cacheLimit, + ); } async function deleteCache(cacheName: string) { - try { - return await CacheStorageFactory.getCacheStorage().delete(cacheName); - } catch (e) { - // ignoring insecure operation error, as it is thrown in incognito mode in firefox - if (e.name === SecurityError && e.message === INSECURE_OPERATION) { - // no-op - } else { - // log and ignore, we don't want to break the caller flow, when cache is not available - log.error("deleteCache failed", e); - } - } + return await CacheStorageFactory.getCacheStorage().delete(cacheName); } export const CacheStorageService = { open: openCache, delete: deleteCache }; From b447dedf83815880cba13e13ed3870a15b558525 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 14:26:41 +0530 Subject: [PATCH 16/25] Cache limit is usused --- web/packages/shared/storage/cache.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/web/packages/shared/storage/cache.ts b/web/packages/shared/storage/cache.ts index 3b585116e2..e2f35b7b94 100644 --- a/web/packages/shared/storage/cache.ts +++ b/web/packages/shared/storage/cache.ts @@ -6,10 +6,7 @@ export enum CACHES { } interface LimitedCacheStorage { - open: ( - cacheName: string, - cacheLimitInBytes?: number, - ) => Promise; + open: (cacheName: string) => Promise; delete: (cacheName: string) => Promise; } @@ -43,11 +40,8 @@ class cacheStorageFactory { export const CacheStorageFactory = new cacheStorageFactory(); -async function openCache(cacheName: string, cacheLimit?: number) { - return await CacheStorageFactory.getCacheStorage().open( - cacheName, - cacheLimit, - ); +async function openCache(cacheName: string) { + return await CacheStorageFactory.getCacheStorage().open(cacheName); } async function deleteCache(cacheName: string) { return await CacheStorageFactory.getCacheStorage().delete(cacheName); From cd3ff6f878191d38cf6df28b9a3a40c83972977f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 14:32:50 +0530 Subject: [PATCH 17/25] Delete is not needed outside --- web/packages/shared/storage/cache.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/web/packages/shared/storage/cache.ts b/web/packages/shared/storage/cache.ts index e2f35b7b94..c863590872 100644 --- a/web/packages/shared/storage/cache.ts +++ b/web/packages/shared/storage/cache.ts @@ -7,7 +7,6 @@ export enum CACHES { interface LimitedCacheStorage { open: (cacheName: string) => Promise; - delete: (cacheName: string) => Promise; } export interface LimitedCache { @@ -33,7 +32,6 @@ class cacheStorageFactory { delete: cache.delete.bind(cache), }; }, - delete: caches.delete.bind(caches), }; } } @@ -43,11 +41,8 @@ export const CacheStorageFactory = new cacheStorageFactory(); async function openCache(cacheName: string) { return await CacheStorageFactory.getCacheStorage().open(cacheName); } -async function deleteCache(cacheName: string) { - return await CacheStorageFactory.getCacheStorage().delete(cacheName); -} -export const CacheStorageService = { open: openCache, delete: deleteCache }; +export const CacheStorageService = { open: openCache }; export async function cached( cacheName: string, @@ -80,7 +75,9 @@ export async function cached( * Meant for use during logout, to reset the state of the user's account. */ export const clearCaches = async () => { - await CacheStorageService.delete(CACHES.THUMBS); - await CacheStorageService.delete(CACHES.FACE_CROPS); - await CacheStorageService.delete(CACHES.FILES); + await Promise.all([ + caches.delete(CACHES.THUMBS), + caches.delete(CACHES.FACE_CROPS), + caches.delete(CACHES.FILES), + ]); }; From 495ff99874b312795b1a299af46d1193d9c02fad Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 14:41:33 +0530 Subject: [PATCH 18/25] Replace enum with TS type --- .../components/MachineLearning/PeopleList.tsx | 5 ++--- .../photos/src/services/download/index.ts | 5 ++--- .../src/utils/machineLearning/faceCrop.ts | 4 ++-- .../photos/src/utils/machineLearning/index.ts | 4 ++-- web/packages/shared/storage/cache.ts | 19 +++++++++---------- 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/web/apps/photos/src/components/MachineLearning/PeopleList.tsx b/web/apps/photos/src/components/MachineLearning/PeopleList.tsx index dc5f0fd435..c28b5790da 100644 --- a/web/apps/photos/src/components/MachineLearning/PeopleList.tsx +++ b/web/apps/photos/src/components/MachineLearning/PeopleList.tsx @@ -1,5 +1,4 @@ import log from "@/next/log"; -import { CACHES } from "@ente/shared/storage/cache"; import { styled } from "@mui/material"; import { Legend } from "components/PhotoViewer/styledComponents/Legend"; import { t } from "i18next"; @@ -65,7 +64,7 @@ export const PeopleList = React.memo((props: PeopleListProps) => { > @@ -176,7 +175,7 @@ export function UnidentifiedFaces(props: { ))} diff --git a/web/apps/photos/src/services/download/index.ts b/web/apps/photos/src/services/download/index.ts index 64930989ac..1c9919fbee 100644 --- a/web/apps/photos/src/services/download/index.ts +++ b/web/apps/photos/src/services/download/index.ts @@ -5,7 +5,6 @@ import { DedicatedCryptoWorker } from "@ente/shared/crypto/internal/crypto.worke import { CustomError } from "@ente/shared/error"; import { Events, eventBus } from "@ente/shared/events"; import { - CACHES, CacheStorageService, type LimitedCache, } from "@ente/shared/storage/cache"; @@ -518,7 +517,7 @@ export default DownloadManager; async function openThumbnailCache() { try { - return await CacheStorageService.open(CACHES.THUMBS); + return await CacheStorageService.open("thumbs"); } catch (e) { log.error("Failed to open thumbnail cache", e); if (isInternalUser()) { @@ -534,7 +533,7 @@ async function openDiskFileCache() { if (!isElectron()) { throw Error(CustomError.NOT_AVAILABLE_ON_WEB); } - return await CacheStorageService.open(CACHES.FILES); + return await CacheStorageService.open("files"); } catch (e) { log.error("Failed to open file cache", e); if (isInternalUser()) { diff --git a/web/apps/photos/src/utils/machineLearning/faceCrop.ts b/web/apps/photos/src/utils/machineLearning/faceCrop.ts index e681443263..177b2a8ec1 100644 --- a/web/apps/photos/src/utils/machineLearning/faceCrop.ts +++ b/web/apps/photos/src/utils/machineLearning/faceCrop.ts @@ -1,4 +1,4 @@ -import { CACHES, CacheStorageService } from "@ente/shared/storage/cache"; +import { CacheStorageService } from "@ente/shared/storage/cache"; import { BlobOptions } from "types/image"; import { FaceAlignment, @@ -54,7 +54,7 @@ async function storeFaceCropForBlob( ) { const faceCropUrl = `/${faceId}`; const faceCropResponse = new Response(faceCropBlob); - const faceCropCache = await CacheStorageService.open(CACHES.FACE_CROPS); + const faceCropCache = await CacheStorageService.open("face-crops"); await faceCropCache.put(faceCropUrl, faceCropResponse); return { imageUrl: faceCropUrl, diff --git a/web/apps/photos/src/utils/machineLearning/index.ts b/web/apps/photos/src/utils/machineLearning/index.ts index 0157f66c1c..95e1018c9c 100644 --- a/web/apps/photos/src/utils/machineLearning/index.ts +++ b/web/apps/photos/src/utils/machineLearning/index.ts @@ -1,5 +1,5 @@ import log from "@/next/log"; -import { CACHES, cached } from "@ente/shared/storage/cache"; +import { cached } from "@ente/shared/storage/cache"; import { FILE_TYPE } from "constants/file"; import PQueue from "p-queue"; import DownloadManager from "services/download"; @@ -151,7 +151,7 @@ export async function getOriginalImageBitmap( let fileBlob; if (useCache) { - fileBlob = await cached(CACHES.FILES, file.id.toString(), () => { + fileBlob = await cached("files", file.id.toString(), () => { return getOriginalConvertedFile(file, queue); }); } else { diff --git a/web/packages/shared/storage/cache.ts b/web/packages/shared/storage/cache.ts index c863590872..be135c1b92 100644 --- a/web/packages/shared/storage/cache.ts +++ b/web/packages/shared/storage/cache.ts @@ -1,9 +1,12 @@ -export enum CACHES { - THUMBS = "thumbs", - FACE_CROPS = "face-crops", +const cacheNames = [ + "thumbs", + "face-crops", // Desktop app only - FILES = "files", -} + "files", +] as const; + +/** Namespaces into which our caches data is divided */ +export type CacheName = (typeof cacheNames)[number]; interface LimitedCacheStorage { open: (cacheName: string) => Promise; @@ -75,9 +78,5 @@ export async function cached( * Meant for use during logout, to reset the state of the user's account. */ export const clearCaches = async () => { - await Promise.all([ - caches.delete(CACHES.THUMBS), - caches.delete(CACHES.FACE_CROPS), - caches.delete(CACHES.FILES), - ]); + await Promise.all(cacheNames.map((name) => caches.delete(name))); }; From c83dc87d5dc0ba17ebaa060e949490ff5c40e39a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 14:45:03 +0530 Subject: [PATCH 19/25] Inline the factory --- web/packages/shared/storage/cache.ts | 40 +++++++++------------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/web/packages/shared/storage/cache.ts b/web/packages/shared/storage/cache.ts index be135c1b92..ba26019b94 100644 --- a/web/packages/shared/storage/cache.ts +++ b/web/packages/shared/storage/cache.ts @@ -8,10 +8,6 @@ const cacheNames = [ /** Namespaces into which our caches data is divided */ export type CacheName = (typeof cacheNames)[number]; -interface LimitedCacheStorage { - open: (cacheName: string) => Promise; -} - export interface LimitedCache { match: ( key: string, @@ -21,34 +17,22 @@ export interface LimitedCache { delete: (key: string) => Promise; } -class cacheStorageFactory { - getCacheStorage(): LimitedCacheStorage { - return { - async open(cacheName) { - const cache = await caches.open(cacheName); - return { - match: (key) => { - // options are not supported in the browser - return cache.match(key); - }, - put: cache.put.bind(cache), - delete: cache.delete.bind(cache), - }; - }, - }; - } -} - -export const CacheStorageFactory = new cacheStorageFactory(); - -async function openCache(cacheName: string) { - return await CacheStorageFactory.getCacheStorage().open(cacheName); -} +const openCache = async (name: CacheName) => { + const cache = await caches.open(name); + return { + match: (key) => { + // options are not supported in the browser + return cache.match(key); + }, + put: cache.put.bind(cache), + delete: cache.delete.bind(cache), + }; +}; export const CacheStorageService = { open: openCache }; export async function cached( - cacheName: string, + cacheName: CacheName, id: string, get: () => Promise, ): Promise { From 15b601b0fff31b96318831d4fe87321ad61333f3 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 14:53:11 +0530 Subject: [PATCH 20/25] Inline and prune --- .../components/MachineLearning/ImageViews.tsx | 109 ++++++------------ .../components/MachineLearning/PeopleList.tsx | 8 +- 2 files changed, 36 insertions(+), 81 deletions(-) diff --git a/web/apps/photos/src/components/MachineLearning/ImageViews.tsx b/web/apps/photos/src/components/MachineLearning/ImageViews.tsx index 4605cafe89..ccc79b4307 100644 --- a/web/apps/photos/src/components/MachineLearning/ImageViews.tsx +++ b/web/apps/photos/src/components/MachineLearning/ImageViews.tsx @@ -1,31 +1,20 @@ import log from "@/next/log"; import { cached } from "@ente/shared/storage/cache"; -import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; +import { getData, LS_KEYS } from "@ente/shared/storage/localStorage"; import { User } from "@ente/shared/user/types"; -import { Skeleton, styled } from "@mui/material"; +import { Skeleton } from "@mui/material"; import { useEffect, useState } from "react"; import machineLearningService from "services/machineLearning/machineLearningService"; -import { imageBitmapToBlob } from "utils/image"; -export const FaceCropsRow = styled("div")` - & > img { - width: 256px; - height: 256px; - } -`; - -export const FaceImagesRow = styled("div")` - & > img { - width: 112px; - height: 112px; - } -`; - -export function ImageCacheView(props: { +interface FaceCropImageViewProps { url: string; - cacheName: string; faceID: string; -}) { +} + +export const FaceCropImageView: React.FC = ({ + url, + faceID, +}) => { const [imageBlob, setImageBlob] = useState(); useEffect(() => { @@ -34,31 +23,27 @@ export function ImageCacheView(props: { try { const user: User = getData(LS_KEYS.USER); let blob: Blob; - if (!props.url || !props.cacheName || !user) { + if (!url || !user) { blob = undefined; } else { - blob = await cached( - props.cacheName, - props.url, - async () => { - try { - log.debug( - () => - `ImageCacheView: regenerate face crop for ${props.faceID}`, - ); - return machineLearningService.regenerateFaceCrop( - user.token, - user.id, - props.faceID, - ); - } catch (e) { - log.error( - "ImageCacheView: regenerate face crop failed", - e, - ); - } - }, - ); + blob = await cached("face-crops", url, async () => { + try { + log.debug( + () => + `ImageCacheView: regenerate face crop for ${faceID}`, + ); + return machineLearningService.regenerateFaceCrop( + user.token, + user.id, + faceID, + ); + } catch (e) { + log.error( + "ImageCacheView: regenerate face crop failed", + e, + ); + } + }); } !didCancel && setImageBlob(blob); @@ -70,40 +55,12 @@ export function ImageCacheView(props: { return () => { didCancel = true; }; - }, [props.url, props.cacheName]); + }, [url, faceID]); - return ( - <> - - - ); -} + return ; +}; -export function ImageBitmapView(props: { image: ImageBitmap }) { - const [imageBlob, setImageBlob] = useState(); - - useEffect(() => { - let didCancel = false; - - async function loadImage() { - const blob = props.image && (await imageBitmapToBlob(props.image)); - !didCancel && setImageBlob(blob); - } - - loadImage(); - return () => { - didCancel = true; - }; - }, [props.image]); - - return ( - <> - - - ); -} - -export function ImageBlobView(props: { blob: Blob }) { +const ImageBlobView = (props: { blob: Blob }) => { const [imgUrl, setImgUrl] = useState(); useEffect(() => { @@ -123,4 +80,4 @@ export function ImageBlobView(props: { blob: Blob }) { ) : ( ); -} +}; diff --git a/web/apps/photos/src/components/MachineLearning/PeopleList.tsx b/web/apps/photos/src/components/MachineLearning/PeopleList.tsx index c28b5790da..7198aa267d 100644 --- a/web/apps/photos/src/components/MachineLearning/PeopleList.tsx +++ b/web/apps/photos/src/components/MachineLearning/PeopleList.tsx @@ -10,7 +10,7 @@ import { getPeopleList, getUnidentifiedFaces, } from "utils/machineLearning"; -import { ImageCacheView } from "./ImageViews"; +import { FaceCropImageView } from "./ImageViews"; const FaceChipContainer = styled("div")` display: flex; @@ -62,9 +62,8 @@ export const PeopleList = React.memo((props: PeopleListProps) => { props.onSelect && props.onSelect(person, index) } > - @@ -172,10 +171,9 @@ export function UnidentifiedFaces(props: { {faces && faces.map((face, index) => ( - ))} From 158186290c0eb7789b014116bd5d72f1ae53fcdb Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 15:02:06 +0530 Subject: [PATCH 21/25] Inline --- .../MachineLearning/FaceCropImageView.tsx | 66 +++++++++++++++ .../components/MachineLearning/ImageViews.tsx | 83 ------------------- .../components/MachineLearning/PeopleList.tsx | 2 +- 3 files changed, 67 insertions(+), 84 deletions(-) create mode 100644 web/apps/photos/src/components/MachineLearning/FaceCropImageView.tsx delete mode 100644 web/apps/photos/src/components/MachineLearning/ImageViews.tsx diff --git a/web/apps/photos/src/components/MachineLearning/FaceCropImageView.tsx b/web/apps/photos/src/components/MachineLearning/FaceCropImageView.tsx new file mode 100644 index 0000000000..d5b364cc97 --- /dev/null +++ b/web/apps/photos/src/components/MachineLearning/FaceCropImageView.tsx @@ -0,0 +1,66 @@ +import log from "@/next/log"; +import { cached } from "@ente/shared/storage/cache"; +import { getData, LS_KEYS } from "@ente/shared/storage/localStorage"; +import { User } from "@ente/shared/user/types"; +import { Skeleton } from "@mui/material"; +import { useEffect, useState } from "react"; +import machineLearningService from "services/machineLearning/machineLearningService"; + +interface FaceCropImageViewProps { + url: string; + faceID: string; +} + +export const FaceCropImageView: React.FC = ({ + url, + faceID, +}) => { + const [objectURL, setObjectURL] = useState(); + + useEffect(() => { + let didCancel = false; + + async function loadImage() { + const user: User = getData(LS_KEYS.USER); + let blob: Blob; + if (!url || !user) { + blob = undefined; + } else { + blob = await cached("face-crops", url, async () => { + try { + log.debug( + () => + `ImageCacheView: regenerate face crop for ${faceID}`, + ); + return machineLearningService.regenerateFaceCrop( + user.token, + user.id, + faceID, + ); + } catch (e) { + log.error( + "ImageCacheView: regenerate face crop failed", + e, + ); + } + }); + } + + if (didCancel) return; + setObjectURL(URL.createObjectURL(blob)); + } + + loadImage(); + + return () => { + didCancel = true; + if (objectURL) URL.revokeObjectURL(objectURL); + }; + }, [url, faceID]); + + return objectURL ? ( + + ) : ( + + ); +}; diff --git a/web/apps/photos/src/components/MachineLearning/ImageViews.tsx b/web/apps/photos/src/components/MachineLearning/ImageViews.tsx deleted file mode 100644 index ccc79b4307..0000000000 --- a/web/apps/photos/src/components/MachineLearning/ImageViews.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import log from "@/next/log"; -import { cached } from "@ente/shared/storage/cache"; -import { getData, LS_KEYS } from "@ente/shared/storage/localStorage"; -import { User } from "@ente/shared/user/types"; -import { Skeleton } from "@mui/material"; -import { useEffect, useState } from "react"; -import machineLearningService from "services/machineLearning/machineLearningService"; - -interface FaceCropImageViewProps { - url: string; - faceID: string; -} - -export const FaceCropImageView: React.FC = ({ - url, - faceID, -}) => { - const [imageBlob, setImageBlob] = useState(); - - useEffect(() => { - let didCancel = false; - async function loadImage() { - try { - const user: User = getData(LS_KEYS.USER); - let blob: Blob; - if (!url || !user) { - blob = undefined; - } else { - blob = await cached("face-crops", url, async () => { - try { - log.debug( - () => - `ImageCacheView: regenerate face crop for ${faceID}`, - ); - return machineLearningService.regenerateFaceCrop( - user.token, - user.id, - faceID, - ); - } catch (e) { - log.error( - "ImageCacheView: regenerate face crop failed", - e, - ); - } - }); - } - - !didCancel && setImageBlob(blob); - } catch (e) { - log.error("ImageCacheView useEffect failed", e); - } - } - loadImage(); - return () => { - didCancel = true; - }; - }, [url, faceID]); - - return ; -}; - -const ImageBlobView = (props: { blob: Blob }) => { - const [imgUrl, setImgUrl] = useState(); - - useEffect(() => { - try { - setImgUrl(props.blob && URL.createObjectURL(props.blob)); - } catch (e) { - console.error( - "ImageBlobView: can not create object url for blob: ", - props.blob, - e, - ); - } - }, [props.blob]); - - return imgUrl ? ( - - ) : ( - - ); -}; diff --git a/web/apps/photos/src/components/MachineLearning/PeopleList.tsx b/web/apps/photos/src/components/MachineLearning/PeopleList.tsx index 7198aa267d..397227693e 100644 --- a/web/apps/photos/src/components/MachineLearning/PeopleList.tsx +++ b/web/apps/photos/src/components/MachineLearning/PeopleList.tsx @@ -10,7 +10,7 @@ import { getPeopleList, getUnidentifiedFaces, } from "utils/machineLearning"; -import { FaceCropImageView } from "./ImageViews"; +import { FaceCropImageView } from "./FaceCropImageView"; const FaceChipContainer = styled("div")` display: flex; From b36175a779938f1ccbd2ffce985b2500bab34772 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 15:33:36 +0530 Subject: [PATCH 22/25] user --- web/packages/next/local-user.ts | 42 ++++++++++++++++++++++ web/packages/next/worker/comlink-worker.ts | 24 +++++-------- 2 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 web/packages/next/local-user.ts diff --git a/web/packages/next/local-user.ts b/web/packages/next/local-user.ts new file mode 100644 index 0000000000..d20bb7781d --- /dev/null +++ b/web/packages/next/local-user.ts @@ -0,0 +1,42 @@ +// TODO: This file belongs to the accounts package +import * as yup from "yup"; + +const localUserSchema = yup.object({ + /** The user's ID. */ + id: yup.number().required(), + /** The user's email. */ + email: yup.string().required(), + /** + * The user's (plaintext) auth token. + * + * It is used for making API calls on their behalf. + */ + token: yup.string().required(), +}); + +/** Locally available data for the logged in user's */ +export type LocalUser = yup.InferType; + +/** + * Return the logged-in user (if someone is indeed logged in). + * + * The user's data is stored in the browser's localStorage. + */ +export const localUser = async (): Promise => { + // TODO(MR): duplicate of LS_KEYS.USER + const s = localStorage.getItem("user"); + if (!s) return undefined; + return await localUserSchema.validate(JSON.parse(s), { + strict: true, + }); +}; + +/** + * A wrapper over {@link localUser} with that throws if no one is logged in. + */ +export const ensureLocalUser = async (): Promise => { + const user = await localUser(); + if (!user) + throw new Error("Attempting to access user data when not logged in"); + return user; +}; diff --git a/web/packages/next/worker/comlink-worker.ts b/web/packages/next/worker/comlink-worker.ts index f082ac1145..a5237fccc5 100644 --- a/web/packages/next/worker/comlink-worker.ts +++ b/web/packages/next/worker/comlink-worker.ts @@ -1,6 +1,7 @@ import { ensureElectron } from "@/next/electron"; import log, { logToDisk } from "@/next/log"; import { expose, wrap, type Remote } from "comlink"; +import { ensureLocalUser } from "../local-user"; export class ComlinkWorker InstanceType> { public remote: Promise>>; @@ -35,29 +36,20 @@ export class ComlinkWorker InstanceType> { } } -// TODO(MR): Temporary method to forward auth tokens to workers -const getAuthToken = () => { - // LS_KEYS.USER - const userJSONString = localStorage.getItem("user"); - if (!userJSONString) return undefined; - const json: unknown = JSON.parse(userJSONString); - if (!json || typeof json != "object" || !("token" in json)) - return undefined; - const token = json.token; - if (typeof token != "string") return undefined; - return token; -}; - /** - * A minimal set of utility functions that we expose to all workers that we - * create. + * A set of utility functions that we expose to all workers that we create. * * Inside the worker's code, this can be accessed by using the sibling * `workerBridge` object after importing it from `worker-bridge.ts`. + * + * Not all workers need access to all these functions, and this can indeed be + * done in a more fine-grained, per-worker, manner if needed. */ const workerBridge = { + // Needed: generally (presumably) logToDisk, - getAuthToken, + // Needed by ML worker + getAuthToken: () => ensureLocalUser().then((user) => user.token), convertToJPEG: (inputFileData: Uint8Array, filename: string) => ensureElectron().convertToJPEG(inputFileData, filename), detectFaces: (input: Float32Array) => ensureElectron().detectFaces(input), From 401f879424528493fb23c591e45649d07f6890fe Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 16:08:36 +0530 Subject: [PATCH 23/25] Consolidate --- .../MachineLearning/FaceCropImageView.tsx | 66 ---- .../MLSearchSettings/enableFaceSearch.tsx | 114 ------ .../MLSearchSettings/enableMLSearch.tsx | 48 --- .../MLSearchSettings/index.tsx | 151 -------- .../MLSearchSettings/manageMLSearch.tsx | 38 -- .../components/PhotoViewer/FileInfo/index.tsx | 5 +- .../SearchBar/searchInput/MenuWithPeople.tsx | 2 +- .../components/Sidebar/AdvancedSettings.tsx | 2 +- .../src/components/ml/MLSearchSettings.tsx | 327 ++++++++++++++++++ .../{MachineLearning => ml}/PeopleList.tsx | 66 +++- 10 files changed, 394 insertions(+), 425 deletions(-) delete mode 100644 web/apps/photos/src/components/MachineLearning/FaceCropImageView.tsx delete mode 100644 web/apps/photos/src/components/MachineLearning/MLSearchSettings/enableFaceSearch.tsx delete mode 100644 web/apps/photos/src/components/MachineLearning/MLSearchSettings/enableMLSearch.tsx delete mode 100644 web/apps/photos/src/components/MachineLearning/MLSearchSettings/index.tsx delete mode 100644 web/apps/photos/src/components/MachineLearning/MLSearchSettings/manageMLSearch.tsx create mode 100644 web/apps/photos/src/components/ml/MLSearchSettings.tsx rename web/apps/photos/src/components/{MachineLearning => ml}/PeopleList.tsx (71%) diff --git a/web/apps/photos/src/components/MachineLearning/FaceCropImageView.tsx b/web/apps/photos/src/components/MachineLearning/FaceCropImageView.tsx deleted file mode 100644 index d5b364cc97..0000000000 --- a/web/apps/photos/src/components/MachineLearning/FaceCropImageView.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import log from "@/next/log"; -import { cached } from "@ente/shared/storage/cache"; -import { getData, LS_KEYS } from "@ente/shared/storage/localStorage"; -import { User } from "@ente/shared/user/types"; -import { Skeleton } from "@mui/material"; -import { useEffect, useState } from "react"; -import machineLearningService from "services/machineLearning/machineLearningService"; - -interface FaceCropImageViewProps { - url: string; - faceID: string; -} - -export const FaceCropImageView: React.FC = ({ - url, - faceID, -}) => { - const [objectURL, setObjectURL] = useState(); - - useEffect(() => { - let didCancel = false; - - async function loadImage() { - const user: User = getData(LS_KEYS.USER); - let blob: Blob; - if (!url || !user) { - blob = undefined; - } else { - blob = await cached("face-crops", url, async () => { - try { - log.debug( - () => - `ImageCacheView: regenerate face crop for ${faceID}`, - ); - return machineLearningService.regenerateFaceCrop( - user.token, - user.id, - faceID, - ); - } catch (e) { - log.error( - "ImageCacheView: regenerate face crop failed", - e, - ); - } - }); - } - - if (didCancel) return; - setObjectURL(URL.createObjectURL(blob)); - } - - loadImage(); - - return () => { - didCancel = true; - if (objectURL) URL.revokeObjectURL(objectURL); - }; - }, [url, faceID]); - - return objectURL ? ( - - ) : ( - - ); -}; diff --git a/web/apps/photos/src/components/MachineLearning/MLSearchSettings/enableFaceSearch.tsx b/web/apps/photos/src/components/MachineLearning/MLSearchSettings/enableFaceSearch.tsx deleted file mode 100644 index a007cb3988..0000000000 --- a/web/apps/photos/src/components/MachineLearning/MLSearchSettings/enableFaceSearch.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { - Button, - Checkbox, - DialogProps, - FormControlLabel, - FormGroup, - Link, - Stack, - Typography, -} from "@mui/material"; -import { EnteDrawer } from "components/EnteDrawer"; -import Titlebar from "components/Titlebar"; -import { t } from "i18next"; -import { useEffect, useState } from "react"; -import { Trans } from "react-i18next"; -export default function EnableFaceSearch({ - open, - onClose, - enableFaceSearch, - onRootClose, -}) { - const [acceptTerms, setAcceptTerms] = useState(false); - - useEffect(() => { - setAcceptTerms(false); - }, [open]); - - const handleRootClose = () => { - onClose(); - onRootClose(); - }; - - const handleDrawerClose: DialogProps["onClose"] = (_, reason) => { - if (reason === "backdropClick") { - handleRootClose(); - } else { - onClose(); - } - }; - return ( - - - - - - - ), - }} - /> - - - - setAcceptTerms(e.target.checked) - } - /> - } - label={t("FACE_SEARCH_CONFIRMATION")} - /> - - - - - - - - - ); -} diff --git a/web/apps/photos/src/components/MachineLearning/MLSearchSettings/enableMLSearch.tsx b/web/apps/photos/src/components/MachineLearning/MLSearchSettings/enableMLSearch.tsx deleted file mode 100644 index 1cd0a3b3f7..0000000000 --- a/web/apps/photos/src/components/MachineLearning/MLSearchSettings/enableMLSearch.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Box, Button, Stack, Typography } from "@mui/material"; -import Titlebar from "components/Titlebar"; -import { t } from "i18next"; -import { Trans } from "react-i18next"; -import { openLink } from "utils/common"; - -export default function EnableMLSearch({ - onClose, - enableMlSearch, - onRootClose, -}) { - const showDetails = () => - openLink("https://ente.io/blog/desktop-ml-beta", true); - - return ( - - - - - {" "} - - - - - - - - - - - ); -} diff --git a/web/apps/photos/src/components/MachineLearning/MLSearchSettings/index.tsx b/web/apps/photos/src/components/MachineLearning/MLSearchSettings/index.tsx deleted file mode 100644 index 9b33a984a1..0000000000 --- a/web/apps/photos/src/components/MachineLearning/MLSearchSettings/index.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import log from "@/next/log"; -import { Box, DialogProps, Typography } from "@mui/material"; -import { EnteDrawer } from "components/EnteDrawer"; -import { t } from "i18next"; -import { AppContext } from "pages/_app"; -import { useContext, useState } from "react"; -import { Trans } from "react-i18next"; -import { - getFaceSearchEnabledStatus, - updateFaceSearchEnabledStatus, -} from "services/userService"; -import EnableFaceSearch from "./enableFaceSearch"; -import EnableMLSearch from "./enableMLSearch"; -import ManageMLSearch from "./manageMLSearch"; - -const MLSearchSettings = ({ open, onClose, onRootClose }) => { - const { - updateMlSearchEnabled, - mlSearchEnabled, - setDialogMessage, - somethingWentWrong, - startLoading, - finishLoading, - } = useContext(AppContext); - - const [enableFaceSearchView, setEnableFaceSearchView] = useState(false); - - const openEnableFaceSearch = () => { - setEnableFaceSearchView(true); - }; - const closeEnableFaceSearch = () => { - setEnableFaceSearchView(false); - }; - - const enableMlSearch = async () => { - try { - const hasEnabledFaceSearch = await getFaceSearchEnabledStatus(); - if (!hasEnabledFaceSearch) { - openEnableFaceSearch(); - } else { - updateMlSearchEnabled(true); - } - } catch (e) { - log.error("Enable ML search failed", e); - somethingWentWrong(); - } - }; - - const enableFaceSearch = async () => { - try { - startLoading(); - await updateFaceSearchEnabledStatus(true); - updateMlSearchEnabled(true); - closeEnableFaceSearch(); - finishLoading(); - } catch (e) { - log.error("Enable face search failed", e); - somethingWentWrong(); - } - }; - - const disableMlSearch = async () => { - try { - await updateMlSearchEnabled(false); - onClose(); - } catch (e) { - log.error("Disable ML search failed", e); - somethingWentWrong(); - } - }; - - const disableFaceSearch = async () => { - try { - startLoading(); - await updateFaceSearchEnabledStatus(false); - await disableMlSearch(); - finishLoading(); - } catch (e) { - log.error("Disable face search failed", e); - somethingWentWrong(); - } - }; - - const confirmDisableFaceSearch = () => { - setDialogMessage({ - title: t("DISABLE_FACE_SEARCH_TITLE"), - content: ( - - - - ), - close: { text: t("CANCEL") }, - proceed: { - variant: "primary", - text: t("DISABLE_FACE_SEARCH"), - action: disableFaceSearch, - }, - }); - }; - - const handleRootClose = () => { - onClose(); - onRootClose(); - }; - - const handleDrawerClose: DialogProps["onClose"] = (_, reason) => { - if (reason === "backdropClick") { - handleRootClose(); - } else { - onClose(); - } - }; - - return ( - - - {mlSearchEnabled ? ( - - ) : ( - - )} - - - - - ); -}; - -export default MLSearchSettings; diff --git a/web/apps/photos/src/components/MachineLearning/MLSearchSettings/manageMLSearch.tsx b/web/apps/photos/src/components/MachineLearning/MLSearchSettings/manageMLSearch.tsx deleted file mode 100644 index 15dacd7b25..0000000000 --- a/web/apps/photos/src/components/MachineLearning/MLSearchSettings/manageMLSearch.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Box, Stack } from "@mui/material"; -import { EnteMenuItem } from "components/Menu/EnteMenuItem"; -import { MenuItemGroup } from "components/Menu/MenuItemGroup"; -import Titlebar from "components/Titlebar"; -import { t } from "i18next"; - -export default function ManageMLSearch({ - onClose, - disableMlSearch, - handleDisableFaceSearch, - onRootClose, -}) { - return ( - - - - - - - - - - - - - - ); -} diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx index 24de200c04..34fdb8e34a 100644 --- a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx @@ -10,11 +10,8 @@ import TextSnippetOutlined from "@mui/icons-material/TextSnippetOutlined"; import { Box, DialogProps, Link, Stack, styled } from "@mui/material"; import { Chip } from "components/Chip"; import { EnteDrawer } from "components/EnteDrawer"; -import { - PhotoPeopleList, - UnidentifiedFaces, -} from "components/MachineLearning/PeopleList"; import Titlebar from "components/Titlebar"; +import { PhotoPeopleList, UnidentifiedFaces } from "components/ml/PeopleList"; import LinkButton from "components/pages/gallery/LinkButton"; import { t } from "i18next"; import { AppContext } from "pages/_app"; diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx index 89bdce56ac..6ebc0d9422 100644 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx @@ -1,6 +1,6 @@ import { Row } from "@ente/shared/components/Container"; import { Box, styled } from "@mui/material"; -import { PeopleList } from "components/MachineLearning/PeopleList"; +import { PeopleList } from "components/ml/PeopleList"; import { t } from "i18next"; import { AppContext } from "pages/_app"; import { useContext } from "react"; diff --git a/web/apps/photos/src/components/Sidebar/AdvancedSettings.tsx b/web/apps/photos/src/components/Sidebar/AdvancedSettings.tsx index 817aecb2bf..6972cc1613 100644 --- a/web/apps/photos/src/components/Sidebar/AdvancedSettings.tsx +++ b/web/apps/photos/src/components/Sidebar/AdvancedSettings.tsx @@ -3,9 +3,9 @@ import ChevronRight from "@mui/icons-material/ChevronRight"; import ScienceIcon from "@mui/icons-material/Science"; import { Box, DialogProps, Stack, Typography } from "@mui/material"; import { EnteDrawer } from "components/EnteDrawer"; -import MLSearchSettings from "components/MachineLearning/MLSearchSettings"; import MenuSectionTitle from "components/Menu/MenuSectionTitle"; import Titlebar from "components/Titlebar"; +import { MLSearchSettings } from "components/ml/MLSearchSettings"; import { t } from "i18next"; import { useContext, useEffect, useState } from "react"; diff --git a/web/apps/photos/src/components/ml/MLSearchSettings.tsx b/web/apps/photos/src/components/ml/MLSearchSettings.tsx new file mode 100644 index 0000000000..583b79529c --- /dev/null +++ b/web/apps/photos/src/components/ml/MLSearchSettings.tsx @@ -0,0 +1,327 @@ +import log from "@/next/log"; +import { + Box, + Button, + Checkbox, + DialogProps, + FormControlLabel, + FormGroup, + Link, + Stack, + Typography, +} from "@mui/material"; +import { EnteDrawer } from "components/EnteDrawer"; +import { EnteMenuItem } from "components/Menu/EnteMenuItem"; +import { MenuItemGroup } from "components/Menu/MenuItemGroup"; +import Titlebar from "components/Titlebar"; +import { t } from "i18next"; +import { AppContext } from "pages/_app"; +import { useContext, useEffect, useState } from "react"; +import { Trans } from "react-i18next"; +import { + getFaceSearchEnabledStatus, + updateFaceSearchEnabledStatus, +} from "services/userService"; +import { openLink } from "utils/common"; + +export const MLSearchSettings = ({ open, onClose, onRootClose }) => { + const { + updateMlSearchEnabled, + mlSearchEnabled, + setDialogMessage, + somethingWentWrong, + startLoading, + finishLoading, + } = useContext(AppContext); + + const [enableFaceSearchView, setEnableFaceSearchView] = useState(false); + + const openEnableFaceSearch = () => { + setEnableFaceSearchView(true); + }; + const closeEnableFaceSearch = () => { + setEnableFaceSearchView(false); + }; + + const enableMlSearch = async () => { + try { + const hasEnabledFaceSearch = await getFaceSearchEnabledStatus(); + if (!hasEnabledFaceSearch) { + openEnableFaceSearch(); + } else { + updateMlSearchEnabled(true); + } + } catch (e) { + log.error("Enable ML search failed", e); + somethingWentWrong(); + } + }; + + const enableFaceSearch = async () => { + try { + startLoading(); + await updateFaceSearchEnabledStatus(true); + updateMlSearchEnabled(true); + closeEnableFaceSearch(); + finishLoading(); + } catch (e) { + log.error("Enable face search failed", e); + somethingWentWrong(); + } + }; + + const disableMlSearch = async () => { + try { + await updateMlSearchEnabled(false); + onClose(); + } catch (e) { + log.error("Disable ML search failed", e); + somethingWentWrong(); + } + }; + + const disableFaceSearch = async () => { + try { + startLoading(); + await updateFaceSearchEnabledStatus(false); + await disableMlSearch(); + finishLoading(); + } catch (e) { + log.error("Disable face search failed", e); + somethingWentWrong(); + } + }; + + const confirmDisableFaceSearch = () => { + setDialogMessage({ + title: t("DISABLE_FACE_SEARCH_TITLE"), + content: ( + + + + ), + close: { text: t("CANCEL") }, + proceed: { + variant: "primary", + text: t("DISABLE_FACE_SEARCH"), + action: disableFaceSearch, + }, + }); + }; + + const handleRootClose = () => { + onClose(); + onRootClose(); + }; + + const handleDrawerClose: DialogProps["onClose"] = (_, reason) => { + if (reason === "backdropClick") { + handleRootClose(); + } else { + onClose(); + } + }; + + return ( + + + {mlSearchEnabled ? ( + + ) : ( + + )} + + + + + ); +}; + +function EnableFaceSearch({ open, onClose, enableFaceSearch, onRootClose }) { + const [acceptTerms, setAcceptTerms] = useState(false); + + useEffect(() => { + setAcceptTerms(false); + }, [open]); + + const handleRootClose = () => { + onClose(); + onRootClose(); + }; + + const handleDrawerClose: DialogProps["onClose"] = (_, reason) => { + if (reason === "backdropClick") { + handleRootClose(); + } else { + onClose(); + } + }; + return ( + + + + + + + ), + }} + /> + + + + setAcceptTerms(e.target.checked) + } + /> + } + label={t("FACE_SEARCH_CONFIRMATION")} + /> + + + + + + + + + ); +} + +function EnableMLSearch({ onClose, enableMlSearch, onRootClose }) { + const showDetails = () => + openLink("https://ente.io/blog/desktop-ml-beta", true); + + return ( + + + + + {" "} + + + + + + + + + + + ); +} + +function ManageMLSearch({ + onClose, + disableMlSearch, + handleDisableFaceSearch, + onRootClose, +}) { + return ( + + + + + + + + + + + + + + ); +} diff --git a/web/apps/photos/src/components/MachineLearning/PeopleList.tsx b/web/apps/photos/src/components/ml/PeopleList.tsx similarity index 71% rename from web/apps/photos/src/components/MachineLearning/PeopleList.tsx rename to web/apps/photos/src/components/ml/PeopleList.tsx index 397227693e..ebe3ad443c 100644 --- a/web/apps/photos/src/components/MachineLearning/PeopleList.tsx +++ b/web/apps/photos/src/components/ml/PeopleList.tsx @@ -1,8 +1,12 @@ import log from "@/next/log"; -import { styled } from "@mui/material"; +import { cached } from "@ente/shared/storage/cache"; +import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; +import { User } from "@ente/shared/user/types"; +import { Skeleton, styled } from "@mui/material"; import { Legend } from "components/PhotoViewer/styledComponents/Legend"; import { t } from "i18next"; import React, { useEffect, useState } from "react"; +import machineLearningService from "services/machineLearning/machineLearningService"; import { EnteFile } from "types/file"; import { Face, Person } from "types/machineLearning"; import { @@ -10,7 +14,6 @@ import { getPeopleList, getUnidentifiedFaces, } from "utils/machineLearning"; -import { FaceCropImageView } from "./FaceCropImageView"; const FaceChipContainer = styled("div")` display: flex; @@ -181,3 +184,62 @@ export function UnidentifiedFaces(props: { ); } + +interface FaceCropImageViewProps { + url: string; + faceID: string; +} + +export const FaceCropImageView: React.FC = ({ + url, + faceID, +}) => { + const [objectURL, setObjectURL] = useState(); + + useEffect(() => { + let didCancel = false; + + async function loadImage() { + const user: User = getData(LS_KEYS.USER); + let blob: Blob; + if (!url || !user) { + blob = undefined; + } else { + blob = await cached("face-crops", url, async () => { + try { + log.debug( + () => + `ImageCacheView: regenerate face crop for ${faceID}`, + ); + return machineLearningService.regenerateFaceCrop( + user.token, + user.id, + faceID, + ); + } catch (e) { + log.error( + "ImageCacheView: regenerate face crop failed", + e, + ); + } + }); + } + + if (didCancel) return; + setObjectURL(URL.createObjectURL(blob)); + } + + loadImage(); + + return () => { + didCancel = true; + if (objectURL) URL.revokeObjectURL(objectURL); + }; + }, [url, faceID]); + + return objectURL ? ( + + ) : ( + + ); +}; From 55632fff64104f4a0b8721f9d9bd56fd053a6649 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 16:10:31 +0530 Subject: [PATCH 24/25] Use new method --- web/apps/photos/src/components/ml/PeopleList.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/web/apps/photos/src/components/ml/PeopleList.tsx b/web/apps/photos/src/components/ml/PeopleList.tsx index ebe3ad443c..4ba08a6d08 100644 --- a/web/apps/photos/src/components/ml/PeopleList.tsx +++ b/web/apps/photos/src/components/ml/PeopleList.tsx @@ -1,7 +1,6 @@ +import { ensureLocalUser } from "@/next/local-user"; import log from "@/next/log"; import { cached } from "@ente/shared/storage/cache"; -import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; -import { User } from "@ente/shared/user/types"; import { Skeleton, styled } from "@mui/material"; import { Legend } from "components/PhotoViewer/styledComponents/Legend"; import { t } from "i18next"; @@ -190,7 +189,7 @@ interface FaceCropImageViewProps { faceID: string; } -export const FaceCropImageView: React.FC = ({ +const FaceCropImageView: React.FC = ({ url, faceID, }) => { @@ -200,11 +199,11 @@ export const FaceCropImageView: React.FC = ({ let didCancel = false; async function loadImage() { - const user: User = getData(LS_KEYS.USER); let blob: Blob; - if (!url || !user) { + if (!url) { blob = undefined; } else { + const user = await ensureLocalUser(); blob = await cached("face-crops", url, async () => { try { log.debug( @@ -226,7 +225,7 @@ export const FaceCropImageView: React.FC = ({ } if (didCancel) return; - setObjectURL(URL.createObjectURL(blob)); + setObjectURL(blob ? URL.createObjectURL(blob) : undefined); } loadImage(); From b061b618e74c746f7cd6b3cf38bea53d58e4108d Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Fri, 12 Apr 2024 16:16:02 +0530 Subject: [PATCH 25/25] Tweaks --- .../photos/src/components/ml/PeopleList.tsx | 50 +++---------------- 1 file changed, 8 insertions(+), 42 deletions(-) diff --git a/web/apps/photos/src/components/ml/PeopleList.tsx b/web/apps/photos/src/components/ml/PeopleList.tsx index 4ba08a6d08..e92b02680a 100644 --- a/web/apps/photos/src/components/ml/PeopleList.tsx +++ b/web/apps/photos/src/components/ml/PeopleList.tsx @@ -8,11 +8,7 @@ import React, { useEffect, useState } from "react"; import machineLearningService from "services/machineLearning/machineLearningService"; import { EnteFile } from "types/file"; import { Face, Person } from "types/machineLearning"; -import { - getAllPeople, - getPeopleList, - getUnidentifiedFaces, -} from "utils/machineLearning"; +import { getPeopleList, getUnidentifiedFaces } from "utils/machineLearning"; const FaceChipContainer = styled("div")` display: flex; @@ -66,7 +62,7 @@ export const PeopleList = React.memo((props: PeopleListProps) => { > ))} @@ -111,36 +107,6 @@ export function PhotoPeopleList(props: PhotoPeopleListProps) { ); } -export interface AllPeopleListProps extends PeopleListPropsBase { - limit?: number; -} - -export function AllPeopleList(props: AllPeopleListProps) { - const [people, setPeople] = useState>([]); - - useEffect(() => { - let didCancel = false; - - async function updateFaceImages() { - try { - let people = await getAllPeople(); - if (props.limit) { - people = people.slice(0, props.limit); - } - !didCancel && setPeople(people); - } catch (e) { - log.error("updateFaceImages failed", e); - } - } - updateFaceImages(); - return () => { - didCancel = true; - }; - }, [props.limit]); - - return ; -} - export function UnidentifiedFaces(props: { file: EnteFile; updateMLDataIndex: number; @@ -174,7 +140,7 @@ export function UnidentifiedFaces(props: { faces.map((face, index) => ( @@ -186,12 +152,12 @@ export function UnidentifiedFaces(props: { interface FaceCropImageViewProps { url: string; - faceID: string; + faceId: string; } const FaceCropImageView: React.FC = ({ url, - faceID, + faceId, }) => { const [objectURL, setObjectURL] = useState(); @@ -208,12 +174,12 @@ const FaceCropImageView: React.FC = ({ try { log.debug( () => - `ImageCacheView: regenerate face crop for ${faceID}`, + `ImageCacheView: regenerate face crop for ${faceId}`, ); return machineLearningService.regenerateFaceCrop( user.token, user.id, - faceID, + faceId, ); } catch (e) { log.error( @@ -234,7 +200,7 @@ const FaceCropImageView: React.FC = ({ didCancel = true; if (objectURL) URL.revokeObjectURL(objectURL); }; - }, [url, faceID]); + }, [url, faceId]); return objectURL ? (