[mob] merge main
This commit is contained in:
commit
f2987a82f2
184 changed files with 3035 additions and 19250 deletions
|
@ -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
BIN
desktop/build/icon.icns
Normal file
Binary file not shown.
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
94
desktop/src/main/services/app-update.ts
Normal file
94
desktop/src/main/services/app-update.ts
Normal 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);
|
|
@ -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";
|
||||
|
|
@ -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";
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { app } from "electron";
|
||||
import { AutoLauncherClient } from "../../types/main";
|
||||
import { AutoLauncherClient } from "../../../types/main";
|
||||
|
||||
class MacAutoLauncher implements AutoLauncherClient {
|
||||
async isEnabled() {
|
|
@ -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
|
|
@ -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";
|
|
@ -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;
|
||||
|
|
@ -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";
|
|
@ -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);
|
108
desktop/src/main/services/ml-face.ts
Normal file
108
desktop/src/main/services/ml-face.ts
Normal 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;
|
||||
};
|
79
desktop/src/main/services/ml.ts
Normal file
79
desktop/src/main/services/ml.ts
Normal 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,
|
||||
});
|
||||
};
|
|
@ -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);
|
|
@ -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,
|
|
@ -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: {
|
|
@ -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: {
|
|
@ -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: {
|
|
@ -18,7 +18,7 @@ const userPreferencesSchema: Schema<UserPreferencesSchema> = {
|
|||
},
|
||||
};
|
||||
|
||||
export const userPreferencesStore = new Store({
|
||||
export const userPreferences = new Store({
|
||||
name: "userPreferences",
|
||||
schema: userPreferencesSchema,
|
||||
});
|
|
@ -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: {
|
9
desktop/src/main/types/onnx-runtime.d.ts
vendored
Normal file
9
desktop/src/main/types/onnx-runtime.d.ts
vendored
Normal 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";
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
51
docs/docs/auth/faq/enteception/index.md
Normal file
51
docs/docs/auth/faq/enteception/index.md
Normal 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">
|
||||
|
||||
{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.
|
BIN
docs/docs/auth/faq/enteception/warning.png
Normal file
BIN
docs/docs/auth/faq/enteception/warning.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 516 KiB |
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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).
|
||||
|
|
43
docs/docs/self-hosting/faq/sharing.md
Normal file
43
docs/docs/self-hosting/faq/sharing.md
Normal 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
|
||||
```
|
61
docs/docs/self-hosting/guides/web-app.md
Normal file
61
docs/docs/self-hosting/guides/web-app.md
Normal 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).
|
|
@ -417,7 +417,7 @@
|
|||
"pendingItems": "待处理项目",
|
||||
"clearIndexes": "清空索引",
|
||||
"selectFoldersForBackup": "选择要备份的文件夹",
|
||||
"selectedFoldersWillBeEncryptedAndBackedUp": "所选文件夹将被加密和备份",
|
||||
"selectedFoldersWillBeEncryptedAndBackedUp": "所选文件夹将被加密并备份",
|
||||
"unselectAll": "取消全部选择",
|
||||
"selectAll": "全选",
|
||||
"skip": "跳过",
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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! 🙏',
|
||||
),
|
||||
]);
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -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
Binary file not shown.
|
@ -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}};
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
|
@ -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"
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
|
@ -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")`
|
||||
|
|
|
@ -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
Loading…
Add table
Reference in a new issue