[mob] merge main

This commit is contained in:
ashilkn 2024-04-13 20:05:38 +05:30
commit f2987a82f2
184 changed files with 3035 additions and 19250 deletions

View file

@ -51,6 +51,7 @@ class _HomePageState extends State<HomePage> {
final scaffoldKey = GlobalKey<ScaffoldState>();
final TextEditingController _textController = TextEditingController();
final FocusNode searchInputFocusNode = FocusNode();
bool _showSearchBox = false;
String _searchText = "";
List<Code> _codes = [];
@ -80,6 +81,17 @@ class _HomePageState extends State<HomePage> {
setState(() {});
});
_showSearchBox = PreferenceService.instance.shouldAutoFocusOnSearchBar();
if (_showSearchBox) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
// https://github.com/flutter/flutter/issues/20706#issuecomment-646328652
FocusScope.of(context).unfocus();
Timer(const Duration(milliseconds: 1), () {
FocusScope.of(context).requestFocus(searchInputFocusNode);
});
},
);
}
}
void _loadCodes() {
@ -192,6 +204,7 @@ class _HomePageState extends State<HomePage> {
title: !_showSearchBox
? const Text('Ente Auth')
: TextField(
focusNode: searchInputFocusNode,
autofocus: _searchText.isEmpty,
controller: _textController,
onChanged: (val) {

BIN
desktop/build/icon.icns Normal file

Binary file not shown.

View file

@ -1,5 +1,9 @@
# Dependencies
- [Electron](#electron)
- [Dev dependencies](#dev)
- [Functionality](#functionality)
## Electron
[Electron](https://www.electronjs.org) is a cross-platform (Linux, Windows,
@ -73,7 +77,7 @@ Electron process. This allows us to directly use the output produced by
## Dev
See [web/docs/dependencies#DX](../../web/docs/dependencies.md#dev) for the
See [web/docs/dependencies#dev](../../web/docs/dependencies.md#dev) for the
general development experience related dependencies like TypeScript etc, which
are similar to that in the web code.
@ -88,7 +92,7 @@ Some extra ones specific to the code here are:
## Functionality
### Conversion
### Format conversion
The main tool we use is for arbitrary conversions is FFMPEG. To bundle a
(platform specific) static binary of ffmpeg with our app, we use
@ -104,20 +108,23 @@ resources (`build`) folder. This is used for thumbnail generation on Linux.
On macOS, we use the `sips` CLI tool for conversion, but that is already
available on the host machine, and is not bundled with our app.
### AI/ML
[onnxruntime-node](https://github.com/Microsoft/onnxruntime) is used as the
AI/ML runtime. It powers both natural language searches (using CLIP) and face
detection (using YOLO).
[jpeg-js](https://github.com/jpeg-js/jpeg-js#readme) is used for decoding JPEG
data into raw RGB bytes before passing it to ONNX.
html-entities is used by the bundled clip-bpe-ts tokenizer for CLIP.
### Watch Folders
[chokidar](https://github.com/paulmillr/chokidar) is used as a file system
watcher for the watch folders functionality.
### AI/ML
- [onnxruntime-node](https://github.com/Microsoft/onnxruntime) is used for
natural language searches based on CLIP.
- html-entities is used by the bundled clip-bpe-ts tokenizer.
- [jpeg-js](https://github.com/jpeg-js/jpeg-js#readme) is used for decoding
JPEG data into raw RGB bytes before passing it to ONNX.
## ZIP
### ZIP
[node-stream-zip](https://github.com/antelle/node-stream-zip) is used for
reading of large ZIP files (e.g. during imports of Google Takeout ZIPs).

View file

@ -1,5 +1,15 @@
appId: io.ente.bhari-frame
artifactName: ${productName}-${version}-${arch}.${ext}
files:
- app/**/*
- out
extraFiles:
- from: build
to: resources
win:
target:
- target: nsis
arch: [x64, arm64]
nsis:
deleteAppDataOnUninstall: true
linux:
@ -20,9 +30,3 @@ mac:
category: public.app-category.photography
hardenedRuntime: true
afterSign: electron-builder-notarize
extraFiles:
- from: build
to: resources
files:
- app/**/*
- out

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";
import { setupAutoUpdater } from "./services/app-update";
import { initWatcher } from "./services/chokidar";
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,128 @@ 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
*
* See also: [Note: Caching files].
*/
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 +241,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 +248,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 +268,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 +278,6 @@ const main = () => {
handleDownloads(mainWindow);
handleExternalLinks(mainWindow);
addAllowOriginHeader(mainWindow);
attachEventHandlers(mainWindow);
try {
deleteLegacyDiskCacheDirIfExists();
@ -202,7 +288,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,8 +1,8 @@
import { dialog } from "electron/main";
import path from "node:path";
import { getDirFilePaths, getElectronFile } from "../services/fs";
import { getElectronFilesFromGoogleZip } from "../services/upload";
import type { ElectronFile } from "../types/ipc";
import { getDirFilePaths, getElectronFile } from "./services/fs";
import { getElectronFilesFromGoogleZip } from "./services/upload";
export const selectDirectory = async () => {
const result = await dialog.showOpenDialog({

View file

@ -1,94 +1,7 @@
import { app, BrowserWindow, nativeImage, Tray } from "electron";
import { BrowserWindow, app, shell } from "electron";
import { existsSync } from "node:fs";
import path from "node:path";
import { isAppQuitting, rendererURL } from "../main";
import autoLauncher from "../services/autoLauncher";
import { getHideDockIconPreference } from "../services/userPreference";
import { isPlatform } from "../utils/common/platform";
import log from "./log";
import { createTrayContextMenu } from "./menu";
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) => {
const iconName = isPlatform("mac")
? "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");
tray.setContextMenu(createTrayContextMenu(mainWindow));
};
import { rendererURL } from "../main";
export function handleDownloads(mainWindow: BrowserWindow) {
mainWindow.webContents.session.on("will-download", (_, item) => {
@ -101,7 +14,7 @@ export function handleDownloads(mainWindow: BrowserWindow) {
export function handleExternalLinks(mainWindow: BrowserWindow) {
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (!url.startsWith(rendererURL)) {
require("electron").shell.openExternal(url);
shell.openExternal(url);
return { action: "deny" };
} else {
return { action: "allow" };
@ -129,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

@ -10,37 +10,6 @@
import type { FSWatcher } from "chokidar";
import { ipcMain } from "electron/main";
import {
appVersion,
skipAppUpdate,
updateAndRestart,
updateOnNextRestart,
} from "../services/app-update";
import { clipImageEmbedding, clipTextEmbedding } from "../services/clip";
import { runFFmpegCmd } from "../services/ffmpeg";
import { getDirFiles } from "../services/fs";
import {
convertToJPEG,
generateImageThumbnail,
} from "../services/imageProcessor";
import {
clearStores,
encryptionKey,
saveEncryptionKey,
} from "../services/store";
import {
getElectronFilesFromGoogleZip,
getPendingUploads,
setToUploadCollection,
setToUploadFiles,
} from "../services/upload";
import {
addWatchMapping,
getWatchMappings,
removeWatchMapping,
updateWatchMappingIgnoredFiles,
updateWatchMappingSyncedFiles,
} from "../services/watch";
import type { ElectronFile, FILE_PATH_TYPE, WatchMapping } from "../types/ipc";
import {
selectDirectory,
@ -61,6 +30,38 @@ import {
saveStreamToDisk,
} from "./fs";
import { logToDisk } from "./log";
import {
appVersion,
skipAppUpdate,
updateAndRestart,
updateOnNextRestart,
} from "./services/app-update";
import { runFFmpegCmd } from "./services/ffmpeg";
import { getDirFiles } from "./services/fs";
import {
convertToJPEG,
generateImageThumbnail,
} from "./services/imageProcessor";
import { clipImageEmbedding, clipTextEmbedding } from "./services/ml-clip";
import { detectFaces, faceEmbedding } from "./services/ml-face";
import {
clearStores,
encryptionKey,
saveEncryptionKey,
} from "./services/store";
import {
getElectronFilesFromGoogleZip,
getPendingUploads,
setToUploadCollection,
setToUploadFiles,
} from "./services/upload";
import {
addWatchMapping,
getWatchMappings,
removeWatchMapping,
updateWatchMappingIgnoredFiles,
updateWatchMappingSyncedFiles,
} from "./services/watch";
import { openDirectory, openLogDirectory } from "./util";
/**
@ -146,6 +147,14 @@ export const attachIPCHandlers = () => {
clipTextEmbedding(text),
);
ipcMain.handle("detectFaces", (_, input: Float32Array) =>
detectFaces(input),
);
ipcMain.handle("faceEmbedding", (_, input: Float32Array) =>
faceEmbedding(input),
);
// - File selection
ipcMain.handle("selectDirectory", () => selectDirectory());

View file

@ -5,13 +5,10 @@ import {
MenuItemConstructorOptions,
shell,
} from "electron";
import { setIsAppQuitting } from "../main";
import { forceCheckForAppUpdates } from "../services/app-update";
import autoLauncher from "../services/autoLauncher";
import {
getHideDockIconPreference,
setHideDockIconPreference,
} from "../services/userPreference";
import { allowWindowClose } from "../main";
import { forceCheckForAppUpdates } from "./services/app-update";
import autoLauncher from "./services/autoLauncher";
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;
};
@ -53,7 +52,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
return Menu.buildFromTemplate([
{
label: "ente",
label: "Ente Photos",
submenu: [
...macOSOnly([
{
@ -155,7 +154,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
{ type: "separator" },
{ label: "Bring All to Front", role: "front" },
{ type: "separator" },
{ label: "Ente", role: "window" },
{ label: "Ente Photos", role: "window" },
]),
],
},
@ -196,7 +195,7 @@ export const createTrayContextMenu = (mainWindow: BrowserWindow) => {
};
const handleClose = () => {
setIsAppQuitting(true);
allowWindowClose();
app.quit();
};

View file

@ -0,0 +1,94 @@
import { compareVersions } from "compare-versions";
import { app, BrowserWindow } from "electron";
import { default as electronLog } from "electron-log";
import { autoUpdater } from "electron-updater";
import { allowWindowClose } from "../../main";
import { AppUpdateInfo } from "../../types/ipc";
import log from "../log";
import { userPreferences } from "../stores/user-preferences";
export const setupAutoUpdater = (mainWindow: BrowserWindow) => {
autoUpdater.logger = electronLog;
autoUpdater.autoDownload = false;
const oneDay = 1 * 24 * 60 * 60 * 1000;
setInterval(() => checkForUpdatesAndNotify(mainWindow), oneDay);
checkForUpdatesAndNotify(mainWindow);
};
/**
* Check for app update check ignoring any previously saved skips / mutes.
*/
export const forceCheckForAppUpdates = (mainWindow: BrowserWindow) => {
userPreferences.delete("skipAppVersion");
userPreferences.delete("muteUpdateNotificationVersion");
checkForUpdatesAndNotify(mainWindow);
};
const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => {
const updateCheckResult = await autoUpdater.checkForUpdates();
if (!updateCheckResult) {
log.error("Failed to check for updates");
return;
}
const { version } = updateCheckResult.updateInfo;
log.debug(() => `Update check found version ${version}`);
if (compareVersions(version, app.getVersion()) <= 0) {
log.debug(() => "Skipping update, already at latest version");
return;
}
if (version === userPreferences.get("skipAppVersion")) {
log.info(`User chose to skip version ${version}`);
return;
}
const mutedVersion = userPreferences.get("muteUpdateNotificationVersion");
if (version === mutedVersion) {
log.info(`User has muted update notifications for version ${version}`);
return;
}
const showUpdateDialog = (updateInfo: AppUpdateInfo) =>
mainWindow.webContents.send("appUpdateAvailable", updateInfo);
log.debug(() => "Attempting auto update");
autoUpdater.downloadUpdate();
let timeout: NodeJS.Timeout;
const fiveMinutes = 5 * 60 * 1000;
autoUpdater.on("update-downloaded", () => {
timeout = setTimeout(
() => showUpdateDialog({ autoUpdatable: true, version }),
fiveMinutes,
);
});
autoUpdater.on("error", (error) => {
clearTimeout(timeout);
log.error("Auto update failed", error);
showUpdateDialog({ autoUpdatable: false, version });
});
};
/**
* Return the version of the desktop app
*
* The return value is of the form `v1.2.3`.
*/
export const appVersion = () => `v${app.getVersion()}`;
export const updateAndRestart = () => {
log.info("Restarting the app to apply update");
allowWindowClose();
autoUpdater.quitAndInstall();
};
export const updateOnNextRestart = (version: string) =>
userPreferences.set("muteUpdateNotificationVersion", version);
export const skipAppUpdate = (version: string) =>
userPreferences.set("skipAppVersion", version);

View file

@ -1,5 +1,5 @@
import { AutoLauncherClient } from "../types/main";
import { isPlatform } from "../utils/common/platform";
import { AutoLauncherClient } from "../../types/main";
import { isPlatform } from "../platform";
import linuxAndWinAutoLauncher from "./autoLauncherClients/linuxAndWinAutoLauncher";
import macAutoLauncher from "./autoLauncherClients/macAutoLauncher";

View file

@ -1,6 +1,6 @@
import AutoLaunch from "auto-launch";
import { app } from "electron";
import { AutoLauncherClient } from "../../types/main";
import { AutoLauncherClient } from "../../../types/main";
const LAUNCHED_AS_HIDDEN_FLAG = "hidden";

View file

@ -1,5 +1,5 @@
import { app } from "electron";
import { AutoLauncherClient } from "../../types/main";
import { AutoLauncherClient } from "../../../types/main";
class MacAutoLauncher implements AutoLauncherClient {
async isEnabled() {

View file

@ -1,9 +1,9 @@
import chokidar from "chokidar";
import { BrowserWindow } from "electron";
import path from "path";
import log from "../main/log";
import { getWatchMappings } from "../services/watch";
import log from "../log";
import { getElectronFile } from "./fs";
import { getWatchMappings } from "./watch";
/**
* Convert a file system {@link filePath} that uses the local system specific

View file

@ -1,11 +1,11 @@
import pathToFfmpeg from "ffmpeg-static";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import { writeStream } from "../main/fs";
import log from "../main/log";
import { execAsync } from "../main/util";
import { ElectronFile } from "../types/ipc";
import { generateTempFilePath, getTempDirPath } from "../utils/temp";
import { ElectronFile } from "../../types/ipc";
import { writeStream } from "../fs";
import log from "../log";
import { generateTempFilePath, getTempDirPath } from "../temp";
import { execAsync } from "../util";
const INPUT_PATH_PLACEHOLDER = "INPUT";
const FFMPEG_PLACEHOLDER = "FFMPEG";

View file

@ -2,8 +2,8 @@ import StreamZip from "node-stream-zip";
import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import log from "../main/log";
import { ElectronFile } from "../types/ipc";
import { ElectronFile } from "../../types/ipc";
import log from "../log";
const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024;

View file

@ -1,12 +1,12 @@
import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "path";
import { writeStream } from "../main/fs";
import log from "../main/log";
import { execAsync, isDev } from "../main/util";
import { CustomErrors, ElectronFile } from "../types/ipc";
import { isPlatform } from "../utils/common/platform";
import { generateTempFilePath } from "../utils/temp";
import { CustomErrors, ElectronFile } from "../../types/ipc";
import { writeStream } from "../fs";
import log from "../log";
import { isPlatform } from "../platform";
import { generateTempFilePath } from "../temp";
import { execAsync, isDev } from "../util";
import { deleteTempFile } from "./ffmpeg";
const IMAGE_MAGICK_PLACEHOLDER = "IMAGE_MAGICK";

View file

@ -1,26 +1,26 @@
/**
* @file Compute CLIP embeddings
* @file Compute CLIP embeddings for images and text.
*
* @see `web/apps/photos/src/services/clip-service.ts` for more details. This
* file implements the Node.js implementation of the actual embedding
* computation. By doing it in the Node.js layer, we can use the binary ONNX
* runtimes which are 10-20x faster than the WASM based web ones.
* The embeddings are computed using ONNX runtime, with CLIP as the model.
*
* The embeddings are computed using ONNX runtime. The model itself is not
* shipped with the app but is downloaded on demand.
* @see `web/apps/photos/src/services/clip-service.ts` for more details.
*/
import { app, net } from "electron/main";
import { existsSync } from "fs";
import jpeg from "jpeg-js";
import fs from "node:fs/promises";
import path from "node:path";
import { writeStream } from "../main/fs";
import log from "../main/log";
import { CustomErrors } from "../types/ipc";
import Tokenizer from "../utils/clip-bpe-ts/mod";
import { generateTempFilePath } from "../utils/temp";
import * as ort from "onnxruntime-node";
import Tokenizer from "../../thirdparty/clip-bpe-ts/mod";
import { CustomErrors } from "../../types/ipc";
import { writeStream } from "../fs";
import log from "../log";
import { generateTempFilePath } from "../temp";
import { deleteTempFile } from "./ffmpeg";
const jpeg = require("jpeg-js");
const ort = require("onnxruntime-node");
import {
createInferenceSession,
downloadModel,
modelPathDownloadingIfNeeded,
modelSavePath,
} from "./ml";
const textModelName = "clip-text-vit-32-uint8.onnx";
const textModelByteSize = 64173509; // 61.2 MB
@ -28,55 +28,20 @@ const textModelByteSize = 64173509; // 61.2 MB
const imageModelName = "clip-image-vit-32-float32.onnx";
const imageModelByteSize = 351468764; // 335.2 MB
/** Return the path where the given {@link modelName} is meant to be saved */
const modelSavePath = (modelName: string) =>
path.join(app.getPath("userData"), "models", modelName);
const downloadModel = async (saveLocation: string, name: string) => {
// `mkdir -p` the directory where we want to save the model.
const saveDir = path.dirname(saveLocation);
await fs.mkdir(saveDir, { recursive: true });
// Download
log.info(`Downloading CLIP model from ${name}`);
const url = `https://models.ente.io/${name}`;
const res = await net.fetch(url);
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
// Save
await writeStream(saveLocation, res.body);
log.info(`Downloaded CLIP model ${name}`);
};
let activeImageModelDownload: Promise<void> | undefined;
let activeImageModelDownload: Promise<string> | undefined;
const imageModelPathDownloadingIfNeeded = async () => {
try {
const modelPath = modelSavePath(imageModelName);
if (activeImageModelDownload) {
log.info("Waiting for CLIP image model download to finish");
await activeImageModelDownload;
} else {
if (!existsSync(modelPath)) {
log.info("CLIP image model not found, downloading");
activeImageModelDownload = downloadModel(
modelPath,
imageModelName,
);
await activeImageModelDownload;
} else {
const localFileSize = (await fs.stat(modelPath)).size;
if (localFileSize !== imageModelByteSize) {
log.error(
`CLIP image model size ${localFileSize} does not match the expected size, downloading again`,
);
activeImageModelDownload = downloadModel(
modelPath,
imageModelName,
);
await activeImageModelDownload;
}
}
activeImageModelDownload = modelPathDownloadingIfNeeded(
imageModelName,
imageModelByteSize,
);
return await activeImageModelDownload;
}
return modelPath;
} finally {
activeImageModelDownload = undefined;
}
@ -84,6 +49,8 @@ const imageModelPathDownloadingIfNeeded = async () => {
let textModelDownloadInProgress = false;
/* TODO(MR): use the generic method. Then we can remove the exports for the
internal details functions that we use here */
const textModelPathDownloadingIfNeeded = async () => {
if (textModelDownloadInProgress)
throw Error(CustomErrors.MODEL_DOWNLOAD_PENDING);
@ -123,13 +90,6 @@ const textModelPathDownloadingIfNeeded = async () => {
return modelPath;
};
const createInferenceSession = async (modelPath: string) => {
return await ort.InferenceSession.create(modelPath, {
intraOpNumThreads: 1,
enableCpuMemArena: false,
});
};
let imageSessionPromise: Promise<any> | undefined;
const onnxImageSession = async () => {
@ -174,7 +134,7 @@ const clipImageEmbedding_ = async (jpegFilePath: string) => {
const results = await imageSession.run(feeds);
log.debug(
() =>
`CLIP image embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
`onnx/clip image embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
);
const imageEmbedding = results["output"].data; // Float32Array
return normalizeEmbedding(imageEmbedding);
@ -281,7 +241,7 @@ export const clipTextEmbedding = async (text: string) => {
const results = await imageSession.run(feeds);
log.debug(
() =>
`CLIP text embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
`onnx/clip text embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
);
const textEmbedding = results["output"].data;
return normalizeEmbedding(textEmbedding);

View file

@ -0,0 +1,108 @@
/**
* @file Various face recognition related tasks.
*
* - Face detection with the YOLO model.
* - Face embedding with the MobileFaceNet model.
*
* The runtime used is ONNX.
*/
import * as ort from "onnxruntime-node";
import log from "../log";
import { createInferenceSession, modelPathDownloadingIfNeeded } from "./ml";
const faceDetectionModelName = "yolov5s_face_640_640_dynamic.onnx";
const faceDetectionModelByteSize = 30762872; // 29.3 MB
const faceEmbeddingModelName = "mobilefacenet_opset15.onnx";
const faceEmbeddingModelByteSize = 5286998; // 5 MB
let activeFaceDetectionModelDownload: Promise<string> | undefined;
const faceDetectionModelPathDownloadingIfNeeded = async () => {
try {
if (activeFaceDetectionModelDownload) {
log.info("Waiting for face detection model download to finish");
await activeFaceDetectionModelDownload;
} else {
activeFaceDetectionModelDownload = modelPathDownloadingIfNeeded(
faceDetectionModelName,
faceDetectionModelByteSize,
);
return await activeFaceDetectionModelDownload;
}
} finally {
activeFaceDetectionModelDownload = undefined;
}
};
let _faceDetectionSession: Promise<ort.InferenceSession> | undefined;
const faceDetectionSession = async () => {
if (!_faceDetectionSession) {
_faceDetectionSession =
faceDetectionModelPathDownloadingIfNeeded().then((modelPath) =>
createInferenceSession(modelPath),
);
}
return _faceDetectionSession;
};
let activeFaceEmbeddingModelDownload: Promise<string> | undefined;
const faceEmbeddingModelPathDownloadingIfNeeded = async () => {
try {
if (activeFaceEmbeddingModelDownload) {
log.info("Waiting for face embedding model download to finish");
await activeFaceEmbeddingModelDownload;
} else {
activeFaceEmbeddingModelDownload = modelPathDownloadingIfNeeded(
faceEmbeddingModelName,
faceEmbeddingModelByteSize,
);
return await activeFaceEmbeddingModelDownload;
}
} finally {
activeFaceEmbeddingModelDownload = undefined;
}
};
let _faceEmbeddingSession: Promise<ort.InferenceSession> | undefined;
const faceEmbeddingSession = async () => {
if (!_faceEmbeddingSession) {
_faceEmbeddingSession =
faceEmbeddingModelPathDownloadingIfNeeded().then((modelPath) =>
createInferenceSession(modelPath),
);
}
return _faceEmbeddingSession;
};
export const detectFaces = async (input: Float32Array) => {
const session = await faceDetectionSession();
const t = Date.now();
const feeds = {
input: new ort.Tensor("float32", input, [1, 3, 640, 640]),
};
const results = await session.run(feeds);
log.debug(() => `onnx/yolo face detection took ${Date.now() - t} ms`);
return results["output"].data;
};
export const faceEmbedding = async (input: Float32Array) => {
// Dimension of each face (alias)
const mobileFaceNetFaceSize = 112;
// Smaller alias
const z = mobileFaceNetFaceSize;
// Size of each face's data in the batch
const n = Math.round(input.length / (z * z * 3));
const inputTensor = new ort.Tensor("float32", input, [n, z, z, 3]);
const session = await faceEmbeddingSession();
const t = Date.now();
const feeds = { img_inputs: inputTensor };
const results = await session.run(feeds);
log.debug(() => `onnx/yolo face embedding took ${Date.now() - t} ms`);
// TODO: What's with this type? It works in practice, but double check.
return (results.embeddings as unknown as any)["cpuData"]; // as Float32Array;
};

View file

@ -0,0 +1,79 @@
/**
* @file AI/ML related functionality.
*
* @see also `ml-clip.ts`, `ml-face.ts`.
*
* The ML runtime we use for inference is [ONNX](https://onnxruntime.ai). Models
* for various tasks are not shipped with the app but are downloaded on demand.
*
* The primary reason for doing these tasks in the Node.js layer is so that we
* can use the binary ONNX runtime which is 10-20x faster than the WASM based
* web one.
*/
import { app, net } from "electron/main";
import { existsSync } from "fs";
import fs from "node:fs/promises";
import path from "node:path";
import * as ort from "onnxruntime-node";
import { writeStream } from "../fs";
import log from "../log";
/**
* Download the model named {@link modelName} if we don't already have it.
*
* Also verify that the size of the model we get matches {@expectedByteSize} (if
* not, redownload it).
*
* @returns the path to the model on the local machine.
*/
export const modelPathDownloadingIfNeeded = async (
modelName: string,
expectedByteSize: number,
) => {
const modelPath = modelSavePath(modelName);
if (!existsSync(modelPath)) {
log.info("CLIP image model not found, downloading");
await downloadModel(modelPath, modelName);
} else {
const size = (await fs.stat(modelPath)).size;
if (size !== expectedByteSize) {
log.error(
`The size ${size} of model ${modelName} does not match the expected size, downloading again`,
);
await downloadModel(modelPath, modelName);
}
}
return modelPath;
};
/** Return the path where the given {@link modelName} is meant to be saved */
export const modelSavePath = (modelName: string) =>
path.join(app.getPath("userData"), "models", modelName);
export const downloadModel = async (saveLocation: string, name: string) => {
// `mkdir -p` the directory where we want to save the model.
const saveDir = path.dirname(saveLocation);
await fs.mkdir(saveDir, { recursive: true });
// Download
log.info(`Downloading ML model from ${name}`);
const url = `https://models.ente.io/${name}`;
const res = await net.fetch(url);
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
// Save
await writeStream(saveLocation, res.body);
log.info(`Downloaded CLIP model ${name}`);
};
/**
* Crete an ONNX {@link InferenceSession} with some defaults.
*/
export const createInferenceSession = async (modelPath: string) => {
return await ort.InferenceSession.create(modelPath, {
// Restrict the number of threads to 1
intraOpNumThreads: 1,
// Be more conservative with RAM usage
enableCpuMemArena: false,
});
};

View file

@ -1,10 +1,9 @@
import StreamZip from "node-stream-zip";
import path from "path";
import { getElectronFile } from "../services/fs";
import { ElectronFile, FILE_PATH_TYPE } from "../../types/ipc";
import { FILE_PATH_KEYS } from "../../types/main";
import { uploadStatusStore } from "../stores/upload.store";
import { ElectronFile, FILE_PATH_TYPE } from "../types/ipc";
import { FILE_PATH_KEYS } from "../types/main";
import { getValidPaths, getZipFileStream } from "./fs";
import { getElectronFile, getValidPaths, getZipFileStream } from "./fs";
export const getPendingUploads = async () => {
const filePaths = getSavedFilePaths(FILE_PATH_TYPE.FILES);

View file

@ -1,8 +1,7 @@
import type { FSWatcher } from "chokidar";
import ElectronLog from "electron-log";
import { WatchMapping, WatchStoreType } from "../../types/ipc";
import { watchStore } from "../stores/watch.store";
import { WatchMapping, WatchStoreType } from "../types/ipc";
import { isMappingPresent } from "../utils/watch";
export const addWatchMapping = async (
watcher: FSWatcher,
@ -29,6 +28,13 @@ export const addWatchMapping = async (
setWatchMappings(watchMappings);
};
function isMappingPresent(watchMappings: WatchMapping[], folderPath: string) {
const watchMapping = watchMappings?.find(
(mapping) => mapping.folderPath === folderPath,
);
return !!watchMapping;
}
export const removeWatchMapping = async (
watcher: FSWatcher,
folderPath: string,

View file

@ -1,5 +1,5 @@
import Store, { Schema } from "electron-store";
import type { KeysStoreType } from "../types/main";
import type { KeysStoreType } from "../../types/main";
const keysStoreSchema: Schema<KeysStoreType> = {
AnonymizeUserID: {

View file

@ -1,5 +1,5 @@
import Store, { Schema } from "electron-store";
import type { SafeStorageStoreType } from "../types/main";
import type { SafeStorageStoreType } from "../../types/main";
const safeStorageSchema: Schema<SafeStorageStoreType> = {
encryptionKey: {

View file

@ -1,5 +1,5 @@
import Store, { Schema } from "electron-store";
import type { UploadStoreType } from "../types/main";
import type { UploadStoreType } from "../../types/main";
const uploadStoreSchema: Schema<UploadStoreType> = {
filePaths: {

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,5 +1,5 @@
import Store, { Schema } from "electron-store";
import { WatchStoreType } from "../types/ipc";
import { WatchStoreType } from "../../types/ipc";
const watchStoreSchema: Schema<WatchStoreType> = {
mappings: {

View file

@ -0,0 +1,9 @@
/**
* Types for [onnxruntime-node](https://onnxruntime.ai/docs/api/js/index.html).
*
* Note: these are not the official types but are based on a temporary
* [workaround](https://github.com/microsoft/onnxruntime/issues/17979).
*/
declare module "onnxruntime-node" {
export * from "onnxruntime-common";
}

View file

@ -143,6 +143,12 @@ const clipImageEmbedding = (jpegImageData: Uint8Array): Promise<Float32Array> =>
const clipTextEmbedding = (text: string): Promise<Float32Array> =>
ipcRenderer.invoke("clipTextEmbedding", text);
const detectFaces = (input: Float32Array): Promise<Float32Array> =>
ipcRenderer.invoke("detectFaces", input);
const faceEmbedding = (input: Float32Array): Promise<Float32Array> =>
ipcRenderer.invoke("faceEmbedding", input);
// - File selection
// TODO: Deprecated - use dialogs on the renderer process itself
@ -322,6 +328,8 @@ contextBridge.exposeInMainWorld("electron", {
// - ML
clipImageEmbedding,
clipTextEmbedding,
detectFaces,
faceEmbedding,
// - File selection
selectDirectory,

View file

@ -1,98 +0,0 @@
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 log from "../main/log";
import { userPreferencesStore } from "../stores/user-preferences";
import { AppUpdateInfo } from "../types/ipc";
export const setupAutoUpdater = (mainWindow: BrowserWindow) => {
autoUpdater.logger = electronLog;
autoUpdater.autoDownload = false;
const oneDay = 1 * 24 * 60 * 60 * 1000;
setInterval(() => checkForUpdatesAndNotify(mainWindow), oneDay);
checkForUpdatesAndNotify(mainWindow);
};
/**
* Check for app update check ignoring any previously saved skips / mutes.
*/
export const forceCheckForAppUpdates = (mainWindow: BrowserWindow) => {
userPreferencesStore.delete("skipAppVersion");
userPreferencesStore.delete("muteUpdateNotificationVersion");
checkForUpdatesAndNotify(mainWindow);
};
const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => {
try {
const { updateInfo } = await autoUpdater.checkForUpdates();
const { version } = updateInfo;
log.debug(() => `Checking for updates found version ${version}`);
if (compareVersions(version, app.getVersion()) <= 0) {
log.debug(() => "Skipping update, already at latest version");
return;
}
if (version === userPreferencesStore.get("skipAppVersion")) {
log.info(`User chose to skip version ${version}`);
return;
}
const mutedVersion = userPreferencesStore.get(
"muteUpdateNotificationVersion",
);
if (version === mutedVersion) {
log.info(
`User has muted update notifications for version ${version}`,
);
return;
}
const showUpdateDialog = (updateInfo: AppUpdateInfo) =>
mainWindow.webContents.send("appUpdateAvailable", updateInfo);
log.debug(() => "Attempting auto update");
autoUpdater.downloadUpdate();
let timeout: NodeJS.Timeout;
const fiveMinutes = 5 * 60 * 1000;
autoUpdater.on("update-downloaded", () => {
timeout = setTimeout(
() => showUpdateDialog({ autoUpdatable: true, version }),
fiveMinutes,
);
});
autoUpdater.on("error", (error) => {
clearTimeout(timeout);
log.error("Auto update failed", error);
showUpdateDialog({ autoUpdatable: false, version });
});
setIsUpdateAvailable(true);
} catch (e) {
log.error("checkForUpdateAndNotify failed", e);
}
};
/**
* Return the version of the desktop app
*
* The return value is of the form `v1.2.3`.
*/
export const appVersion = () => `v${app.getVersion()}`;
export const updateAndRestart = () => {
log.info("Restarting the app to apply update");
setIsAppQuitting(true);
autoUpdater.quitAndInstall();
};
export const updateOnNextRestart = (version: string) =>
userPreferencesStore.set("muteUpdateNotificationVersion", version);
export const skipAppUpdate = (version: string) =>
userPreferencesStore.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

@ -1,11 +0,0 @@
import { WatchMapping } from "../types/ipc";
export function isMappingPresent(
watchMappings: WatchMapping[],
folderPath: string,
) {
const watchMapping = watchMappings?.find(
(mapping) => mapping.folderPath === folderPath,
);
return !!watchMapping;
}

View file

@ -139,7 +139,17 @@ export const sidebar = [
text: "Auth",
items: [
{ text: "Introduction", link: "/auth/" },
{ text: "FAQ", link: "/auth/faq/" },
{
text: "FAQ",
collapsed: true,
items: [
{ text: "General", link: "/auth/faq/" },
{
text: "Enteception",
link: "/auth/faq/enteception/",
},
],
},
{
text: "Migration",
collapsed: true,
@ -170,6 +180,10 @@ export const sidebar = [
text: "Connect to custom server",
link: "/self-hosting/guides/custom-server/",
},
{
text: "Hosting the web app",
link: "/self-hosting/guides/web-app",
},
{
text: "Administering your server",
link: "/self-hosting/guides/admin",
@ -197,6 +211,10 @@ export const sidebar = [
text: "Verification code",
link: "/self-hosting/faq/otp",
},
{
text: "Shared albums",
link: "/self-hosting/faq/sharing",
},
],
},
{

View file

@ -0,0 +1,51 @@
---
title: Enteception
description: Using Ente Auth to store 2FA for your Ente account
---
# Enteception
Your 2FA codes are in Ente Auth, but if you enable 2FA for your Ente account
itself, where should the 2FA for your Ente account be stored?
There are multiple answers, none of which are better or worse, they just depend
on your situation and risk tolerance.
If you are using the same account for both Ente Photos and Ente Auth and have
enabled 2FA from the ente Photos app, we recommend that you ensure you store
your recovery key in a safe place (writing it down on a paper is a good idea).
This key can be used to bypass Ente 2FA in case you are locked out.
Another option is to use a separate account for Ente Auth.
Also, taking exporting the encrypted backup is also another good way to reduce
the risk (you can easily import the encrypted backup without signing in).
Finally, we have on our roadmap some features like adding support for
emergency/legacy-contacts, passkeys, and hardware security keys. Beyond other
benefits, all of these would further reduce the risk of users getting locked out
of their accounts.
## Email verification for Ente Auth
There is a related ouroboros scenario where if email verification is enabled in
the Ente Auth app _and_ the 2FA for your email provider is stored in Ente Auth,
then you might need a code from your email to log into Ente Auth, but to log
into your email you needed the Auth code.
To prevent people from accidentally locking themselves out this way, email
verification is disabled by default in the auth app. We also try to show a
warning when you try to enable email verification in the auth app:
<div align="center">
![Warning shown when enabling 2FA in Ente Auth](warning.png){width=400px}
</div>
The solution here are the same as the Ente-in-Ente case.
## TL;DR;
Ideally, you should **note down your recovery key in a safe place (may be on a
paper)**, using which you will be able to by-pass the two factor.

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

View file

@ -31,3 +31,22 @@ You can enable FaceID lock under Settings → Security → Lockscreen.
### Why does the desktop and mobile app displays different code?
Please verify that the time on both your mobile and desktop is same.
### Does ente Authenticator require an account?
Answer: No, ente Authenticator does not require an account. You can choose to
use the app without backups if you prefer.
### Can I use the Ente 2FA app on multiple devices and sync them?
Yes, you can download the Ente app on multiple devices and sync the codes,
end-to-end encrypted.
### What does it mean when I receive a message saying my current device is not powerful enough to verify my password?
This means that the parameters that were used to derive your master-key on your
original device, are incompatible with your current device (likely because it's
less powerful).
If you recover your account via your current device and reset the password, it
will re-generate a key that will be compatible on both devices.

View file

@ -109,3 +109,13 @@ or "dog playing at the beach".
Check the sections within the upload progress bar for "Failed Uploads," "Ignored
Uploads," and "Unsuccessful Uploads."
## How do i keep NAS and Ente photos synced?
Please try using our CLI to pull data into your NAS
https://github.com/ente-io/ente/tree/main/cli#readme .
## Is there a way to view all albums on the map view?
Currently, the Ente mobile app allows you to see a map view of all the albums by
clicking on "Your map" under "Locations" on the search screen.

View file

@ -80,3 +80,10 @@ and is never sent to our servers.
Please note that only users on the paid plan are allowed to share albums. The
receiver just needs a free Ente account.
## Has the Ente Photos app been audited by a credible source?
Yes, Ente Photos has undergone a thorough security audit conducted by Cure53, in
collaboration with Symbolic Software. Cure53 is a prominent German cybersecurity
firm, while Symbolic Software specializes in applied cryptography. Please find
the full report here: https://ente.io/blog/cryptography-audit/

View file

@ -64,6 +64,6 @@ data reflects the latest album states with new files, moves, and deletions.
If you run into any issues during your data export, please reach out to
[support@ente.io](mailto:support@ente.io) and we will be happy to help you!
Note that we also provide a [CLI
tool](https://github.com/ente-io/ente/tree/main/cli#export) to export your data.
Please find more details [here](/photos/faq/export).
Note that we also provide a
[CLI tool](https://github.com/ente-io/ente/tree/main/cli#export) to export your
data. Please find more details [here](/photos/faq/export).

View file

@ -0,0 +1,43 @@
---
title: Album sharing
description: Getting album sharing to work using an self-hosted Ente
---
# Is public sharing available for self-hosted instances?
Yes.
You'll need to run two instances of the web app, one is regular web app, but
another one is the same code but running on a different origin (i.e. on a
different hostname or different port).
Then, you need to tell the regular web app to use your second instance to
service public links. You can do this by setting the
`NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT` to point to your second instance when running
or building the regular web app.
For more details, see
[.env](https://github.com/ente-io/ente/blob/main/web/apps/photos/.env) and
[.env.development](https://github.com/ente-io/ente/blob/main/web/apps/photos/.env.development).
As a concrete example, assuming we have a Ente server running on
`localhost:8080`, we can start two instances of the web app, passing them
`NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT` that points to the origin
("scheme://host[:port]") of the second "albums" instance.
The first one, the normal web app
```sh
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 \
NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=http://localhost:3002 \
yarn dev:photos
```
The second one, the same code but acting as the "albums" app (the only
difference is the port it is running on):
```sh
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 \
NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=http://localhost:3002 \
yarn dev:albums
```

View file

@ -0,0 +1,61 @@
---
title: Hosting the web app
description: Building and hosting Ente's web app, connecting it to your self-hosted server
---
# Web app
The getting started instructions mention using `yarn dev` (which is an alias of
`yarn dev:photos`) to serve your web app.
```sh
cd ente/web
git submodule update --init --recursive
yarn install
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev:photos
```
This is fine for trying this out and verifying that your self-hosted server is
working correctly etc. But if you would like to use the web app for a longer
term, then it is recommended that you use a production build.
To create a production build, you can run the same process, but instead do a
`yarn build` (which is an alias for `yarn build:photos`). For example,
```sh
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn build:photos
```
This creates a production build, which is a static site consisting of a folder
of HTML/CSS/JS files that can then be deployed on any standard web server.
Nginx is a common choice for a web server, and you can then put the generated
static site (from the `web/apps/photos/out` folder) to where nginx would serve
them. Note that there is nothing specific to nginx here - you can use any web
server - the basic gist is that yarn build will produce a web/apps/photos/out
folder that you can then serve with any web server of your choice.
If you're new to web development, you might find the [web app's README], and
some of the documentation it its source code -
[docs/new.md](https://github.com/ente-io/ente/blob/main/web/docs/new.md),
[docs/dev.md](https://github.com/ente-io/ente/blob/main/web/docs/dev.md) -
useful. We've also documented the process we use for our own production
deploypments in
[docs/deploy.md](https://github.com/ente-io/ente/blob/main/web/docs/deploy.md),
though be aware that that is probably overkill for simple cases.
## Using Docker
We currently don't offer pre-built Docker images for the web app, however it is
quite easy to build and deploy the web app in a Docker container without
installing anything extra on your machine. For example, you can use the
dockerfile from this
[discussion](https://github.com/ente-io/ente/discussions/1183), or use the
Dockerfile mentioned in the
[notes](https://help.ente.io/self-hosting/guides/external-s3) created by a
community member.
## Public sharing
If you'd also like to enable public sharing on the web app you're running,
please follow the [step here](https://help.ente.io/self-hosting/faq/sharing).

View file

@ -417,7 +417,7 @@
"pendingItems": "待处理项目",
"clearIndexes": "清空索引",
"selectFoldersForBackup": "选择要备份的文件夹",
"selectedFoldersWillBeEncryptedAndBackedUp": "所选文件夹将被加密备份",
"selectedFoldersWillBeEncryptedAndBackedUp": "所选文件夹将被加密备份",
"unselectAll": "取消全部选择",
"selectAll": "全选",
"skip": "跳过",

View file

@ -27,25 +27,33 @@ class EmbeddingStore {
late SharedPreferences _preferences;
Completer<void>? _syncStatus;
Completer<bool>? _remoteSyncStatus;
Future<void> init() async {
_preferences = await SharedPreferences.getInstance();
}
Future<void> pullEmbeddings(Model model) async {
if (_syncStatus != null) {
return _syncStatus!.future;
Future<bool> pullEmbeddings(Model model) async {
if (_remoteSyncStatus != null) {
return _remoteSyncStatus!.future;
}
_syncStatus = Completer();
var remoteEmbeddings = await _getRemoteEmbeddings(model);
await _storeRemoteEmbeddings(remoteEmbeddings.embeddings);
while (remoteEmbeddings.hasMore) {
remoteEmbeddings = await _getRemoteEmbeddings(model);
_remoteSyncStatus = Completer();
try {
var remoteEmbeddings = await _getRemoteEmbeddings(model);
await _storeRemoteEmbeddings(remoteEmbeddings.embeddings);
while (remoteEmbeddings.hasMore) {
remoteEmbeddings = await _getRemoteEmbeddings(model);
await _storeRemoteEmbeddings(remoteEmbeddings.embeddings);
}
_remoteSyncStatus!.complete(true);
_remoteSyncStatus = null;
return true;
} catch (e, s) {
_logger.severe("failed to fetch & store remote embeddings", e, s);
_remoteSyncStatus!.complete(false);
_remoteSyncStatus = null;
return false;
}
_syncStatus!.complete();
_syncStatus = null;
}
Future<void> pushEmbeddings() async {
@ -132,7 +140,8 @@ class EmbeddingStore {
remoteEmbeddings.add(embedding);
}
} catch (e, s) {
_logger.severe(e, s);
_logger.warning("Fetching embeddings failed", e, s);
rethrow;
}
_logger.info("${remoteEmbeddings.length} embeddings fetched");

View file

@ -49,9 +49,10 @@ class SemanticSearchService {
bool _hasInitialized = false;
bool _isComputingEmbeddings = false;
bool _isSyncing = false;
Future<List<EnteFile>>? _ongoingRequest;
List<Embedding> _cachedEmbeddings = <Embedding>[];
PendingQuery? _nextQuery;
Future<(String, List<EnteFile>)>? _searchScreenRequest;
String? _latestPendingQuery;
Completer<void> _mlController = Completer<void>();
get hasInitialized => _hasInitialized;
@ -125,37 +126,40 @@ class SemanticSearchService {
return;
}
_isSyncing = true;
await EmbeddingStore.instance.pullEmbeddings(_currentModel);
await _backFill();
final fetchCompleted =
await EmbeddingStore.instance.pullEmbeddings(_currentModel);
if (fetchCompleted) {
await _backFill();
}
_isSyncing = false;
}
Future<List<EnteFile>> search(String query) async {
// searchScreenQuery should only be used for the user initiate query on the search screen.
// If there are multiple call tho this method, then for all the calls, the result will be the same as the last query.
Future<(String, List<EnteFile>)> searchScreenQuery(String query) async {
if (!LocalSettings.instance.hasEnabledMagicSearch() ||
!_frameworkInitialization.isCompleted) {
return [];
return (query, <EnteFile>[]);
}
if (_ongoingRequest == null) {
_ongoingRequest = _getMatchingFiles(query).then((result) {
_ongoingRequest = null;
if (_nextQuery != null) {
final next = _nextQuery;
_nextQuery = null;
search(next!.query).then((nextResult) {
next.completer.complete(nextResult);
});
}
return result;
});
return _ongoingRequest!;
// If there's an ongoing request, just update the last query and return its future.
if (_searchScreenRequest != null) {
_latestPendingQuery = query;
return _searchScreenRequest!;
} else {
// If there's an ongoing request, create or replace the nextCompleter.
_logger.info("Queuing query $query");
await _nextQuery?.completer.future
.timeout(const Duration(seconds: 0)); // Cancels the previous future.
_nextQuery = PendingQuery(query, Completer<List<EnteFile>>());
return _nextQuery!.completer.future;
// No ongoing request, start a new search.
_searchScreenRequest = _getMatchingFiles(query).then((result) {
// Search completed, reset the ongoing request.
_searchScreenRequest = null;
// If there was a new query during the last search, start a new search with the last query.
if (_latestPendingQuery != null) {
final String newQuery = _latestPendingQuery!;
_latestPendingQuery = null; // Reset last query.
// Recursively call search with the latest query.
return searchScreenQuery(newQuery);
}
return (query, result);
});
return _searchScreenRequest!;
}
}
@ -431,13 +435,6 @@ class QueryResult {
QueryResult(this.id, this.score);
}
class PendingQuery {
final String query;
final Completer<List<EnteFile>> completer;
PendingQuery(this.query, this.completer);
}
class IndexStatus {
final int indexedItems, pendingItems;

View file

@ -830,8 +830,16 @@ class SearchService {
String query,
) async {
final List<GenericSearchResult> searchResults = [];
final files = await SemanticSearchService.instance.search(query);
if (files.isNotEmpty) {
late List<EnteFile> files;
late String resultForQuery;
try {
(resultForQuery, files) =
await SemanticSearchService.instance.searchScreenQuery(query);
} catch (e, s) {
_logger.severe("Error occurred during magic search", e, s);
return searchResults;
}
if (files.isNotEmpty && resultForQuery == query) {
searchResults.add(GenericSearchResult(ResultType.magic, query, files));
}
return searchResults;

View file

@ -16,7 +16,7 @@ class UpdateService {
static final UpdateService instance = UpdateService._privateConstructor();
static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key";
static const changeLogVersionKey = "update_change_log_key";
static const currentChangeLogVersion = 17;
static const currentChangeLogVersion = 18;
LatestVersionInfo? _latestVersion;
final _logger = Logger("UpdateService");

View file

@ -122,14 +122,18 @@ class _ChangeLogPageState extends State<ChangeLogPage> {
final List<ChangeLogEntry> items = [];
items.addAll([
ChangeLogEntry(
"Share an Album to Multiple Contacts at Once",
'Adding multiple viewers and collaborators just got easier!\n'
'\nYou can now select multiple contacts and add all of them at once.',
"Improved Performance for Large Galleries ✨",
'We\'ve made significant improvements to how quickly galleries load and'
' with less stutter, especially for those with a lot of photos and videos.',
),
ChangeLogEntry(
"Bug Fixes and Performance Improvements",
'Many a bugs were squashed in this release and have improved performance on app start.\n'
'\nIf you run into any bugs, please write to team@ente.io, or let us know on Discord! 🙏',
"Enhanced Functionality for Video Backups",
'Even if video backups are disabled, you can now manually upload individual videos.',
),
ChangeLogEntry(
"Bug Fixes",
'Many a bugs were squashed in this release.\n'
'\nIf you run into any, please write to team@ente.io, or let us know on Discord! 🙏',
),
]);

View file

@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.8.77+597
version: 0.8.79+599
publish_to: none
environment:

View file

@ -52,7 +52,7 @@ func (c *Controller) PaymentUpgradeOrDowngradeCron() {
return
}
if len(bonusPenaltyCandidates) > 0 {
logger.WithField("count", len(bonusPenaltyCandidates)).Error("candidates found for downgrade penalty")
// todo: implement downgrade penalty
logger.WithField("count", len(bonusPenaltyCandidates)).Warn("candidates found for downgrade penalty")
}
}

View file

@ -2,6 +2,8 @@ package user
import (
"context"
"database/sql"
"errors"
"github.com/ente-io/museum/ente"
"github.com/ente-io/museum/pkg/utils/auth"
"github.com/ente-io/stacktrace"
@ -88,7 +90,11 @@ func (c *UserController) UpdateSrpAndKeyAttributes(context *gin.Context,
func (c *UserController) GetSRPAttributes(context *gin.Context, email string) (*ente.GetSRPAttributesResponse, error) {
userID, err := c.UserRepo.GetUserIDWithEmail(email)
if err != nil {
return nil, stacktrace.Propagate(err, "user does not exist")
if errors.Is(err, sql.ErrNoRows) {
return nil, stacktrace.Propagate(ente.ErrNotFound, "user does not exist")
} else {
return nil, stacktrace.Propagate(err, "failed to get user")
}
}
srpAttributes, err := c.UserAuthRepo.GetSRPAttributes(userID)
if err != nil {

View file

@ -30,6 +30,7 @@ func Error(c *gin.Context, err error) {
// echo "GET /ping HTTP/1.0\r\nContent-Length: 300\r\n\r\n" | nc localhost 8080
if errors.Is(err, ente.ErrStorageLimitExceeded) ||
errors.Is(err, ente.ErrNoActiveSubscription) ||
errors.Is(err, ente.ErrInvalidPassword) ||
errors.Is(err, io.ErrUnexpectedEOF) ||
errors.Is(err, syscall.EPIPE) ||
errors.Is(err, syscall.ECONNRESET) {

View file

@ -12,8 +12,14 @@
#NEXT_PUBLIC_ENTE_ENDPOINT = http://localhost:8080
# If you wish to preview how the shared albums work, you can use `yarn
# dev:albums`. The equivalent CLI command using env vars would be
# dev:albums`. You'll need to run two instances.
# The equivalent CLI commands using env vars would be:
#
# # For the normal web app
# NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=http://localhost:3002 yarn dev:photos
#
# # For the albums app
# NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=http://localhost:3002 yarn dev:albums
#NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT = http://localhost:3002

View file

@ -10,14 +10,7 @@
"@ente/shared": "*",
"@mui/x-date-pickers": "^5.0.0-alpha.6",
"@stripe/stripe-js": "^1.13.2",
"@tensorflow-models/coco-ssd": "^2.2.2",
"@tensorflow/tfjs-backend-cpu": "^4.10.0",
"@tensorflow/tfjs-backend-webgl": "^4.9.0",
"@tensorflow/tfjs-converter": "^4.10.0",
"@tensorflow/tfjs-core": "^4.10.0",
"@tensorflow/tfjs-tflite": "0.0.1-alpha.7",
"bip39": "^3.0.4",
"blazeface-back": "^0.0.9",
"bs58": "^5.0.0",
"chrono-node": "^2.2.6",
"date-fns": "^2",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
"use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob==="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}tflite_web_api_ModuleFactory(Module).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,0,0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
"use strict";var Module={};var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};function moduleLoaded(){}self.onmessage=function(e){try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob==="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}tflite_web_api_ModuleFactory(Module).then(function(instance){Module=instance;moduleLoaded()})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,0,0);var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInit();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();initializedJS=true}try{var result=Module["invokeEntryPoint"](e.data.start_routine,e.data.arg);if(Module["keepRuntimeAlive"]()){Module["PThread"].setExitStatus(result)}else{Module["PThread"].threadExit(result)}}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["PThread"].threadExit(ex.status)}}else{Module["PThread"].threadExit(-2);throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(Module["_pthread_self"]()){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);throw ex}};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,32 +0,0 @@
{
"0": "waterfall",
"1": "snow",
"2": "landscape",
"3": "underwater",
"4": "architecture",
"5": "sunset / sunrise",
"6": "blue sky",
"7": "cloudy sky",
"8": "greenery",
"9": "autumn leaves",
"10": "portrait",
"11": "flower",
"12": "night shot",
"13": "stage concert",
"14": "fireworks",
"15": "candle light",
"16": "neon lights",
"17": "indoor",
"18": "backlight",
"19": "text documents",
"20": "qr images",
"21": "group portrait",
"22": "computer screens",
"23": "kids",
"24": "dog",
"25": "cat",
"26": "macro",
"27": "food",
"28": "beach",
"29": "mountain"
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -10,9 +10,9 @@ import {
LinearProgress,
styled,
} from "@mui/material";
import { ExportStage } from "constants/export";
import { t } from "i18next";
import { Trans } from "react-i18next";
import { ExportStage } from "services/export";
import { ExportProgress } from "types/export";
export const ComfySpan = styled("span")`

View file

@ -14,12 +14,11 @@ import {
Switch,
Typography,
} from "@mui/material";
import { ExportStage } from "constants/export";
import { t } from "i18next";
import isElectron from "is-electron";
import { AppContext } from "pages/_app";
import { useContext, useEffect, useState } from "react";
import exportService from "services/export";
import exportService, { ExportStage } from "services/export";
import { ExportProgress, ExportSettings } from "types/export";
import { EnteFile } from "types/file";
import { getExportDirectoryDoesNotExistMessage } from "utils/ui";

Some files were not shown because too many files have changed in this diff Show more