[web] Various changes, moving towards fixing desktop caching (#1424)

The overall aim was to get the caching layer trimmed down to a point
where we can plug in OPFS into it for desktop. This PR doesn't have that
specific change, but it's just me gradually changing things, working
towards that.
This commit is contained in:
Manav Rathi 2024-04-12 16:28:59 +05:30 committed by GitHub
commit 65c7cd2c05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 691 additions and 900 deletions

View file

@ -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",

View file

@ -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";
@ -16,45 +17,47 @@ import os from "node:os";
import path from "node:path";
import {
addAllowOriginHeader,
createWindow,
handleDockIconHideOnAutoLaunch,
handleDownloads,
handleExternalLinks,
setupMacWindowOnDockIconClick,
setupTrayItem,
} 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";
import { userPreferences } from "./main/stores/user-preferences";
import { isDev } from "./main/util";
let appIsQuitting = false;
let updateIsAvailable = false;
export const isAppQuitting = (): boolean => {
return appIsQuitting;
};
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.
*/
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 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 shouldAllowWindowClose = false;
export const allowWindowClose = (): void => {
shouldAllowWindowClose = true;
};
/**
* 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
@ -68,9 +71,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.
@ -87,29 +88,126 @@ 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]
*
* 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
* 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 = () => {
app.commandLine.appendSwitch("disk-cache-size", "5368709120");
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.
*
* This window will show the HTML served from {@link rendererURL}.
*/
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"),
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
backgroundColor: "black",
// We'll show it conditionally depending on `wasAutoLaunched` later.
show: false,
});
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
if (wasAutoLaunched) {
// 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) otherwise.
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(
"Main window's webContents are unresponsive, will restart the renderer process",
);
window.webContents.forcefullyCrashRenderer();
});
window.on("close", (event) => {
if (!shouldAllowWindowClose) {
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();
});
// 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;
};
/**
* 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.
//
// 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));
};
/**
@ -141,14 +239,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) {
@ -156,22 +246,18 @@ const main = () => {
return;
}
let mainWindow: BrowserWindow;
let mainWindow: BrowserWindow | undefined;
initLogging();
setupRendererServer();
logStartupBanner();
handleDockIconHideOnAutoLaunch();
increaseDiskCache();
enableSharedArrayBufferSupport();
app.on("second-instance", () => {
// 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();
}
});
@ -180,10 +266,9 @@ 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();
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
attachIPCHandlers();
attachFSWatchIPCHandlers(watcher);
@ -191,7 +276,6 @@ const main = () => {
handleDownloads(mainWindow);
handleExternalLinks(mainWindow);
addAllowOriginHeader(mainWindow);
attachEventHandlers(mainWindow);
try {
deleteLegacyDiskCacheDirIfExists();
@ -202,7 +286,11 @@ const main = () => {
}
});
app.on("before-quit", () => setIsAppQuitting(true));
// 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", allowWindowClose);
};
main();

View file

@ -1,102 +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 { 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.
*
* 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));
};
import { rendererURL } from "../main";
export function handleDownloads(mainWindow: BrowserWindow) {
mainWindow.webContents.session.on("will-download", (_, item) => {
@ -137,23 +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();
});
}
export async function handleDockIconHideOnAutoLaunch() {
const shouldHideDockIcon = getHideDockIconPreference();
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
if (isPlatform("mac") && shouldHideDockIcon && wasAutoLaunched) {
app.dock.hide();
}
}
function lowerCaseHeaders(responseHeaders: Record<string, string[]>) {
const headers: Record<string, string[]> = {};
for (const key of Object.keys(responseHeaders)) {

View file

@ -5,13 +5,10 @@ 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 {
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;
};
@ -196,7 +195,7 @@ export const createTrayContextMenu = (mainWindow: BrowserWindow) => {
};
const handleClose = () => {
setIsAppQuitting(true);
allowWindowClose();
app.quit();
};

View file

@ -2,10 +2,10 @@ 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 { allowWindowClose } 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;
@ -74,8 +72,6 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => {
log.error("Auto update failed", error);
showUpdateDialog({ autoUpdatable: false, version });
});
setIsUpdateAvailable(true);
};
/**
@ -87,12 +83,12 @@ export const appVersion = () => `v${app.getVersion()}`;
export const updateAndRestart = () => {
log.info("Restarting the app to apply update");
setIsAppQuitting(true);
allowWindowClose();
autoUpdater.quitAndInstall();
};
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);

View file

@ -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);
}

View file

@ -18,7 +18,7 @@ const userPreferencesSchema: Schema<UserPreferencesSchema> = {
},
};
export const userPreferencesStore = new Store({
export const userPreferences = new Store({
name: "userPreferences",
schema: userPreferencesSchema,
});

View file

@ -1,126 +0,0 @@
import log from "@/next/log";
import { cached } from "@ente/shared/storage/cacheStorage/helpers";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import { User } from "@ente/shared/user/types";
import { Skeleton, styled } 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: {
url: string;
cacheName: string;
faceID: string;
}) {
const [imageBlob, setImageBlob] = useState<Blob>();
useEffect(() => {
let didCancel = false;
async function loadImage() {
try {
const user: User = getData(LS_KEYS.USER);
let blob: Blob;
if (!props.url || !props.cacheName || !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,
);
}
},
);
}
!didCancel && setImageBlob(blob);
} catch (e) {
log.error("ImageCacheView useEffect failed", e);
}
}
loadImage();
return () => {
didCancel = true;
};
}, [props.url, props.cacheName]);
return (
<>
<ImageBlobView blob={imageBlob}></ImageBlobView>
</>
);
}
export function ImageBitmapView(props: { image: ImageBitmap }) {
const [imageBlob, setImageBlob] = useState<Blob>();
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 (
<>
<ImageBlobView blob={imageBlob}></ImageBlobView>
</>
);
}
export function ImageBlobView(props: { blob: Blob }) {
const [imgUrl, setImgUrl] = useState<string>();
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 ? (
<img src={imgUrl} />
) : (
<Skeleton variant="circular" height={120} width={120} />
);
}

View file

@ -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 (
<EnteDrawer
transitionDuration={0}
open={open}
onClose={handleDrawerClose}
BackdropProps={{
sx: { "&&&": { backgroundColor: "transparent" } },
}}
>
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
title={t("ENABLE_FACE_SEARCH_TITLE")}
onRootClose={handleRootClose}
/>
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
<Typography color="text.muted" px={"8px"}>
<Trans
i18nKey={"ENABLE_FACE_SEARCH_DESCRIPTION"}
components={{
a: (
<Link
target="_blank"
href="https://ente.io/privacy#8-biometric-information-privacy-policy"
underline="always"
sx={{
color: "inherit",
textDecorationColor: "inherit",
}}
/>
),
}}
/>
</Typography>
<FormGroup sx={{ width: "100%" }}>
<FormControlLabel
sx={{
color: "text.muted",
ml: 0,
mt: 2,
}}
control={
<Checkbox
size="small"
checked={acceptTerms}
onChange={(e) =>
setAcceptTerms(e.target.checked)
}
/>
}
label={t("FACE_SEARCH_CONFIRMATION")}
/>
</FormGroup>
<Stack px={"8px"} spacing={"8px"}>
<Button
color={"accent"}
size="large"
disabled={!acceptTerms}
onClick={enableFaceSearch}
>
{t("ENABLE_FACE_SEARCH")}
</Button>
<Button
color={"secondary"}
size="large"
onClick={onClose}
>
{t("CANCEL")}
</Button>
</Stack>
</Stack>
</Stack>
</EnteDrawer>
);
}

View file

@ -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 (
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
title={t("ML_SEARCH")}
onRootClose={onRootClose}
/>
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
<Box px={"8px"}>
{" "}
<Typography color="text.muted">
<Trans i18nKey={"ENABLE_ML_SEARCH_DESCRIPTION"} />
</Typography>
</Box>
<Stack px={"8px"} spacing={"8px"}>
<Button
color={"accent"}
size="large"
onClick={enableMlSearch}
>
{t("ENABLE")}
</Button>
<Button
color="secondary"
size="large"
onClick={showDetails}
>
{t("ML_MORE_DETAILS")}
</Button>
</Stack>
</Stack>
</Stack>
);
}

View file

@ -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: (
<Typography>
<Trans i18nKey={"DISABLE_FACE_SEARCH_DESCRIPTION"} />
</Typography>
),
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 (
<Box>
<EnteDrawer
anchor="left"
transitionDuration={0}
open={open}
onClose={handleDrawerClose}
BackdropProps={{
sx: { "&&&": { backgroundColor: "transparent" } },
}}
>
{mlSearchEnabled ? (
<ManageMLSearch
onClose={onClose}
disableMlSearch={disableMlSearch}
handleDisableFaceSearch={confirmDisableFaceSearch}
onRootClose={handleRootClose}
/>
) : (
<EnableMLSearch
onClose={onClose}
enableMlSearch={enableMlSearch}
onRootClose={handleRootClose}
/>
)}
</EnteDrawer>
<EnableFaceSearch
open={enableFaceSearchView}
onClose={closeEnableFaceSearch}
enableFaceSearch={enableFaceSearch}
onRootClose={handleRootClose}
/>
</Box>
);
};
export default MLSearchSettings;

View file

@ -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 (
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
title={t("ML_SEARCH")}
onRootClose={onRootClose}
/>
<Box px={"16px"}>
<Stack py={"20px"} spacing={"24px"}>
<MenuItemGroup>
<EnteMenuItem
onClick={disableMlSearch}
label={t("DISABLE_BETA")}
/>
</MenuItemGroup>
<MenuItemGroup>
<EnteMenuItem
onClick={handleDisableFaceSearch}
label={t("DISABLE_FACE_SEARCH")}
/>
</MenuItemGroup>
</Stack>
</Box>
</Stack>
);
}

View file

@ -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";

View file

@ -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";

View file

@ -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";

View file

@ -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: (
<Typography>
<Trans i18nKey={"DISABLE_FACE_SEARCH_DESCRIPTION"} />
</Typography>
),
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 (
<Box>
<EnteDrawer
anchor="left"
transitionDuration={0}
open={open}
onClose={handleDrawerClose}
BackdropProps={{
sx: { "&&&": { backgroundColor: "transparent" } },
}}
>
{mlSearchEnabled ? (
<ManageMLSearch
onClose={onClose}
disableMlSearch={disableMlSearch}
handleDisableFaceSearch={confirmDisableFaceSearch}
onRootClose={handleRootClose}
/>
) : (
<EnableMLSearch
onClose={onClose}
enableMlSearch={enableMlSearch}
onRootClose={handleRootClose}
/>
)}
</EnteDrawer>
<EnableFaceSearch
open={enableFaceSearchView}
onClose={closeEnableFaceSearch}
enableFaceSearch={enableFaceSearch}
onRootClose={handleRootClose}
/>
</Box>
);
};
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 (
<EnteDrawer
transitionDuration={0}
open={open}
onClose={handleDrawerClose}
BackdropProps={{
sx: { "&&&": { backgroundColor: "transparent" } },
}}
>
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
title={t("ENABLE_FACE_SEARCH_TITLE")}
onRootClose={handleRootClose}
/>
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
<Typography color="text.muted" px={"8px"}>
<Trans
i18nKey={"ENABLE_FACE_SEARCH_DESCRIPTION"}
components={{
a: (
<Link
target="_blank"
href="https://ente.io/privacy#8-biometric-information-privacy-policy"
underline="always"
sx={{
color: "inherit",
textDecorationColor: "inherit",
}}
/>
),
}}
/>
</Typography>
<FormGroup sx={{ width: "100%" }}>
<FormControlLabel
sx={{
color: "text.muted",
ml: 0,
mt: 2,
}}
control={
<Checkbox
size="small"
checked={acceptTerms}
onChange={(e) =>
setAcceptTerms(e.target.checked)
}
/>
}
label={t("FACE_SEARCH_CONFIRMATION")}
/>
</FormGroup>
<Stack px={"8px"} spacing={"8px"}>
<Button
color={"accent"}
size="large"
disabled={!acceptTerms}
onClick={enableFaceSearch}
>
{t("ENABLE_FACE_SEARCH")}
</Button>
<Button
color={"secondary"}
size="large"
onClick={onClose}
>
{t("CANCEL")}
</Button>
</Stack>
</Stack>
</Stack>
</EnteDrawer>
);
}
function EnableMLSearch({ onClose, enableMlSearch, onRootClose }) {
const showDetails = () =>
openLink("https://ente.io/blog/desktop-ml-beta", true);
return (
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
title={t("ML_SEARCH")}
onRootClose={onRootClose}
/>
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
<Box px={"8px"}>
{" "}
<Typography color="text.muted">
<Trans i18nKey={"ENABLE_ML_SEARCH_DESCRIPTION"} />
</Typography>
</Box>
<Stack px={"8px"} spacing={"8px"}>
<Button
color={"accent"}
size="large"
onClick={enableMlSearch}
>
{t("ENABLE")}
</Button>
<Button
color="secondary"
size="large"
onClick={showDetails}
>
{t("ML_MORE_DETAILS")}
</Button>
</Stack>
</Stack>
</Stack>
);
}
function ManageMLSearch({
onClose,
disableMlSearch,
handleDisableFaceSearch,
onRootClose,
}) {
return (
<Stack spacing={"4px"} py={"12px"}>
<Titlebar
onClose={onClose}
title={t("ML_SEARCH")}
onRootClose={onRootClose}
/>
<Box px={"16px"}>
<Stack py={"20px"} spacing={"24px"}>
<MenuItemGroup>
<EnteMenuItem
onClick={disableMlSearch}
label={t("DISABLE_BETA")}
/>
</MenuItemGroup>
<MenuItemGroup>
<EnteMenuItem
onClick={handleDisableFaceSearch}
label={t("DISABLE_FACE_SEARCH")}
/>
</MenuItemGroup>
</Stack>
</Box>
</Stack>
);
}

View file

@ -1,17 +1,14 @@
import { ensureLocalUser } from "@/next/local-user";
import log from "@/next/log";
import { CACHES } from "@ente/shared/storage/cacheStorage/constants";
import { styled } from "@mui/material";
import { cached } from "@ente/shared/storage/cache";
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 {
getAllPeople,
getPeopleList,
getUnidentifiedFaces,
} from "utils/machineLearning";
import { ImageCacheView } from "./ImageViews";
import { getPeopleList, getUnidentifiedFaces } from "utils/machineLearning";
const FaceChipContainer = styled("div")`
display: flex;
@ -63,10 +60,9 @@ export const PeopleList = React.memo((props: PeopleListProps) => {
props.onSelect && props.onSelect(person, index)
}
>
<ImageCacheView
<FaceCropImageView
url={person.displayImageUrl}
cacheName={CACHES.FACE_CROPS}
faceID={person.displayFaceId}
faceId={person.displayFaceId}
/>
</FaceChip>
))}
@ -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<Array<Person>>([]);
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 <PeopleList people={people} onSelect={props.onSelect}></PeopleList>;
}
export function UnidentifiedFaces(props: {
file: EnteFile;
updateMLDataIndex: number;
@ -173,10 +139,9 @@ export function UnidentifiedFaces(props: {
{faces &&
faces.map((face, index) => (
<FaceChip key={index}>
<ImageCacheView
faceID={face.id}
<FaceCropImageView
faceId={face.id}
url={face.crop?.imageUrl}
cacheName={CACHES.FACE_CROPS}
/>
</FaceChip>
))}
@ -184,3 +149,62 @@ export function UnidentifiedFaces(props: {
</>
);
}
interface FaceCropImageViewProps {
url: string;
faceId: string;
}
const FaceCropImageView: React.FC<FaceCropImageViewProps> = ({
url,
faceId,
}) => {
const [objectURL, setObjectURL] = useState<string | undefined>();
useEffect(() => {
let didCancel = false;
async function loadImage() {
let blob: Blob;
if (!url) {
blob = undefined;
} else {
const user = await ensureLocalUser();
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(blob ? URL.createObjectURL(blob) : undefined);
}
loadImage();
return () => {
didCancel = true;
if (objectURL) URL.revokeObjectURL(objectURL);
};
}, [url, faceId]);
return objectURL ? (
<img src={objectURL} />
) : (
<Skeleton variant="circular" height={120} width={120} />
);
};

View file

@ -4,9 +4,10 @@ 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 {
CacheStorageService,
type LimitedCache,
} from "@ente/shared/storage/cache";
import { Remote } from "comlink";
import { FILE_TYPE } from "constants/file";
import isElectron from "is-electron";
@ -516,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()) {
@ -532,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()) {

View file

@ -1,5 +1,4 @@
import { CacheStorageService } from "@ente/shared/storage/cacheStorage";
import { CACHES } from "@ente/shared/storage/cacheStorage/constants";
import { CacheStorageService } from "@ente/shared/storage/cache";
import { BlobOptions } from "types/image";
import {
FaceAlignment,
@ -55,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,

View file

@ -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 { cached } from "@ente/shared/storage/cache";
import { FILE_TYPE } from "constants/file";
import PQueue from "p-queue";
import DownloadManager from "services/download";
@ -152,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 {

View file

@ -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);
}

View file

@ -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<typeof localUserSchema>;
/**
* 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<LocalUser | undefined> => {
// 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<LocalUser> => {
const user = await localUser();
if (!user)
throw new Error("Attempting to access user data when not logged in");
return user;
};

View file

@ -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<T extends new () => InstanceType<T>> {
public remote: Promise<Remote<InstanceType<T>>>;
@ -35,29 +36,20 @@ export class ComlinkWorker<T extends new () => InstanceType<T>> {
}
}
// 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),

View file

@ -0,0 +1,66 @@
const cacheNames = [
"thumbs",
"face-crops",
// Desktop app only
"files",
] as const;
/** Namespaces into which our caches data is divided */
export type CacheName = (typeof cacheNames)[number];
export interface LimitedCache {
match: (
key: string,
options?: { sizeInBytes?: number },
) => Promise<Response>;
put: (key: string, data: Response) => Promise<void>;
delete: (key: string) => Promise<boolean>;
}
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: CacheName,
id: string,
get: () => Promise<Blob>,
): Promise<Blob> {
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 Promise.all(cacheNames.map((name) => caches.delete(name)));
};

View file

@ -1,6 +0,0 @@
export enum CACHES {
THUMBS = "thumbs",
FACE_CROPS = "face-crops",
// Desktop app only
FILES = "files",
}

View file

@ -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),
};
}

View file

@ -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<Blob>,
): Promise<Blob> {
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<Blob> {
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
}
}

View file

@ -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 };

View file

@ -1,16 +0,0 @@
export interface LimitedCacheStorage {
open: (
cacheName: string,
cacheLimitInBytes?: number,
) => Promise<LimitedCache>;
delete: (cacheName: string) => Promise<boolean>;
}
export interface LimitedCache {
match: (
key: string,
options?: { sizeInBytes?: number },
) => Promise<Response>;
put: (key: string, data: Response) => Promise<void>;
delete: (key: string) => Promise<boolean>;
}