[photos-desktop] Various changes, bringing the code up to speed (#1227)
See commit titles for a gist.
This commit is contained in:
commit
3213fe0d26
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 1 KiB |
|
@ -12,7 +12,6 @@ linux:
|
||||||
arch: [x64, arm64]
|
arch: [x64, arm64]
|
||||||
- target: pacman
|
- target: pacman
|
||||||
arch: [x64, arm64]
|
arch: [x64, arm64]
|
||||||
icon: ./resources/icon.icns
|
|
||||||
category: Photography
|
category: Photography
|
||||||
mac:
|
mac:
|
||||||
target:
|
target:
|
||||||
|
|
|
@ -8,27 +8,26 @@
|
||||||
*
|
*
|
||||||
* https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process
|
* https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process
|
||||||
*/
|
*/
|
||||||
import log from "electron-log";
|
import { app, BrowserWindow, Menu } from "electron/main";
|
||||||
import { app, BrowserWindow } from "electron/main";
|
|
||||||
import serveNextAt from "next-electron-server";
|
import serveNextAt from "next-electron-server";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { isDev } from "./main/general";
|
|
||||||
import {
|
import {
|
||||||
addAllowOriginHeader,
|
addAllowOriginHeader,
|
||||||
createWindow,
|
createWindow,
|
||||||
handleDockIconHideOnAutoLaunch,
|
handleDockIconHideOnAutoLaunch,
|
||||||
handleDownloads,
|
handleDownloads,
|
||||||
handleExternalLinks,
|
handleExternalLinks,
|
||||||
handleUpdates,
|
logStartupBanner,
|
||||||
logSystemInfo,
|
|
||||||
setupMacWindowOnDockIconClick,
|
setupMacWindowOnDockIconClick,
|
||||||
setupMainMenu,
|
|
||||||
setupTrayItem,
|
setupTrayItem,
|
||||||
} from "./main/init";
|
} from "./main/init";
|
||||||
import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc";
|
import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc";
|
||||||
import { logErrorSentry, setupLogging } from "./main/log";
|
import log, { initLogging } from "./main/log";
|
||||||
|
import { createApplicationMenu } from "./main/menu";
|
||||||
|
import { isDev } from "./main/util";
|
||||||
|
import { setupAutoUpdater } from "./services/appUpdater";
|
||||||
import { initWatcher } from "./services/chokidar";
|
import { initWatcher } from "./services/chokidar";
|
||||||
|
|
||||||
let appIsQuitting = false;
|
let appIsQuitting = false;
|
||||||
|
@ -135,8 +134,6 @@ function setupAppEventEmitter(mainWindow: BrowserWindow) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const main = () => {
|
const main = () => {
|
||||||
setupLogging(isDev);
|
|
||||||
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
if (!gotTheLock) {
|
if (!gotTheLock) {
|
||||||
app.quit();
|
app.quit();
|
||||||
|
@ -145,6 +142,7 @@ const main = () => {
|
||||||
|
|
||||||
let mainWindow: BrowserWindow;
|
let mainWindow: BrowserWindow;
|
||||||
|
|
||||||
|
initLogging();
|
||||||
setupRendererServer();
|
setupRendererServer();
|
||||||
handleDockIconHideOnAutoLaunch();
|
handleDockIconHideOnAutoLaunch();
|
||||||
increaseDiskCache();
|
increaseDiskCache();
|
||||||
|
@ -161,19 +159,19 @@ const main = () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// Emitted once, when Electron has finished initializing.
|
||||||
// initialization and is ready to create browser windows.
|
//
|
||||||
// Some APIs can only be used after this event occurs.
|
// Note that some Electron APIs can only be used after this event occurs.
|
||||||
app.on("ready", async () => {
|
app.on("ready", async () => {
|
||||||
logSystemInfo();
|
logStartupBanner();
|
||||||
mainWindow = await createWindow();
|
mainWindow = await createWindow();
|
||||||
const watcher = initWatcher(mainWindow);
|
const watcher = initWatcher(mainWindow);
|
||||||
setupTrayItem(mainWindow);
|
setupTrayItem(mainWindow);
|
||||||
setupMacWindowOnDockIconClick();
|
setupMacWindowOnDockIconClick();
|
||||||
setupMainMenu(mainWindow);
|
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
|
||||||
attachIPCHandlers();
|
attachIPCHandlers();
|
||||||
attachFSWatchIPCHandlers(watcher);
|
attachFSWatchIPCHandlers(watcher);
|
||||||
await handleUpdates(mainWindow);
|
if (!isDev) setupAutoUpdater(mainWindow);
|
||||||
handleDownloads(mainWindow);
|
handleDownloads(mainWindow);
|
||||||
handleExternalLinks(mainWindow);
|
handleExternalLinks(mainWindow);
|
||||||
addAllowOriginHeader(mainWindow);
|
addAllowOriginHeader(mainWindow);
|
||||||
|
@ -184,7 +182,7 @@ const main = () => {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Log but otherwise ignore errors during non-critical startup
|
// Log but otherwise ignore errors during non-critical startup
|
||||||
// actions
|
// actions
|
||||||
logErrorSentry(e, "Ignoring startup error");
|
log.error("Ignoring startup error", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { createWriteStream, existsSync } from "node:fs";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { Readable } from "node:stream";
|
import { Readable } from "node:stream";
|
||||||
import { logError } from "./log";
|
|
||||||
|
|
||||||
export const fsExists = (path: string) => existsSync(path);
|
export const fsExists = (path: string) => existsSync(path);
|
||||||
|
|
||||||
|
@ -99,54 +98,36 @@ export const moveFile = async (sourcePath: string, destinationPath: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isFolder = async (dirPath: string) => {
|
export const isFolder = async (dirPath: string) => {
|
||||||
try {
|
if (!existsSync(dirPath)) return false;
|
||||||
const stats = await fs.stat(dirPath);
|
const stats = await fs.stat(dirPath);
|
||||||
return stats.isDirectory();
|
return stats.isDirectory();
|
||||||
} catch (e) {
|
|
||||||
let err = e;
|
|
||||||
// if code is defined, it's an error from fs.stat
|
|
||||||
if (typeof e.code !== "undefined") {
|
|
||||||
// ENOENT means the file does not exist
|
|
||||||
if (e.code === "ENOENT") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
err = Error(`fs error code: ${e.code}`);
|
|
||||||
}
|
|
||||||
logError(err, "isFolder failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteFolder = async (folderPath: string) => {
|
export const deleteFolder = async (folderPath: string) => {
|
||||||
if (!existsSync(folderPath)) {
|
// Ensure it is folder
|
||||||
return;
|
if (!isFolder(folderPath)) return;
|
||||||
}
|
|
||||||
const stat = await fs.stat(folderPath);
|
// Ensure folder is empty
|
||||||
if (!stat.isDirectory()) {
|
|
||||||
throw new Error("Path is not a folder");
|
|
||||||
}
|
|
||||||
// check if folder is empty
|
|
||||||
const files = await fs.readdir(folderPath);
|
const files = await fs.readdir(folderPath);
|
||||||
if (files.length > 0) {
|
if (files.length > 0) throw new Error("Folder is not empty");
|
||||||
throw new Error("Folder is not empty");
|
|
||||||
}
|
// rm -rf it
|
||||||
await fs.rmdir(folderPath);
|
await fs.rmdir(folderPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rename = async (oldPath: string, newPath: string) => {
|
export const rename = async (oldPath: string, newPath: string) => {
|
||||||
if (!existsSync(oldPath)) {
|
if (!existsSync(oldPath)) throw new Error("Path does not exist");
|
||||||
throw new Error("Path does not exist");
|
|
||||||
}
|
|
||||||
await fs.rename(oldPath, newPath);
|
await fs.rename(oldPath, newPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteFile = async (filePath: string) => {
|
export const deleteFile = async (filePath: string) => {
|
||||||
if (!existsSync(filePath)) {
|
// Ensure it exists
|
||||||
return;
|
if (!existsSync(filePath)) return;
|
||||||
}
|
|
||||||
|
// And is a file
|
||||||
const stat = await fs.stat(filePath);
|
const stat = await fs.stat(filePath);
|
||||||
if (!stat.isFile()) {
|
if (!stat.isFile()) throw new Error("Path is not a file");
|
||||||
throw new Error("Path is not a file");
|
|
||||||
}
|
// rm it
|
||||||
return fs.rm(filePath);
|
return fs.rm(filePath);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
import { shell } from "electron"; /* TODO(MR): Why is this not in /main? */
|
|
||||||
import { app } from "electron/main";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
/** `true` if the app is running in development mode. */
|
|
||||||
export const isDev = !app.isPackaged;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the given {@link dirPath} in the system's folder viewer.
|
|
||||||
*
|
|
||||||
* For example, on macOS this'll open {@link dirPath} in Finder.
|
|
||||||
*/
|
|
||||||
export const openDirectory = async (dirPath: string) => {
|
|
||||||
const res = await shell.openPath(path.normalize(dirPath));
|
|
||||||
// shell.openPath resolves with a string containing the error message
|
|
||||||
// corresponding to the failure if a failure occurred, otherwise "".
|
|
||||||
if (res) throw new Error(`Failed to open directory ${dirPath}: res`);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the path where the logs for the app are saved.
|
|
||||||
*
|
|
||||||
* [Note: Electron app paths]
|
|
||||||
*
|
|
||||||
* By default, these paths are at the following locations:
|
|
||||||
*
|
|
||||||
* - macOS: `~/Library/Application Support/ente`
|
|
||||||
* - Linux: `~/.config/ente`
|
|
||||||
* - Windows: `%APPDATA%`, e.g. `C:\Users\<username>\AppData\Local\ente`
|
|
||||||
* - Windows: C:\Users\<you>\AppData\Local\<Your App Name>
|
|
||||||
*
|
|
||||||
* https://www.electronjs.org/docs/latest/api/app
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const logDirectoryPath = () => app.getPath("logs");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open the app's log directory in the system's folder viewer.
|
|
||||||
*
|
|
||||||
* @see {@link openDirectory}
|
|
||||||
*/
|
|
||||||
export const openLogDirectory = () => openDirectory(logDirectoryPath());
|
|
|
@ -1,18 +1,14 @@
|
||||||
import { app, BrowserWindow, Menu, nativeImage, Tray } from "electron";
|
import { app, BrowserWindow, nativeImage, Tray } from "electron";
|
||||||
import ElectronLog from "electron-log";
|
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import os from "os";
|
import os from "node:os";
|
||||||
import path from "path";
|
import path from "node:path";
|
||||||
import util from "util";
|
|
||||||
import { isAppQuitting, rendererURL } from "../main";
|
import { isAppQuitting, rendererURL } from "../main";
|
||||||
import { setupAutoUpdater } from "../services/appUpdater";
|
|
||||||
import autoLauncher from "../services/autoLauncher";
|
import autoLauncher from "../services/autoLauncher";
|
||||||
import { getHideDockIconPreference } from "../services/userPreference";
|
import { getHideDockIconPreference } from "../services/userPreference";
|
||||||
import { isPlatform } from "../utils/common/platform";
|
import { isPlatform } from "../utils/common/platform";
|
||||||
import { buildContextMenu, buildMenuBar } from "../utils/menu";
|
import log from "./log";
|
||||||
import { isDev } from "./general";
|
import { createTrayContextMenu } from "./menu";
|
||||||
import { logErrorSentry } from "./log";
|
import { isDev } from "./util";
|
||||||
const execAsync = util.promisify(require("child_process").exec);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an return the {@link BrowserWindow} that will form our app's UI.
|
* Create an return the {@link BrowserWindow} that will form our app's UI.
|
||||||
|
@ -20,21 +16,15 @@ const execAsync = util.promisify(require("child_process").exec);
|
||||||
* This window will show the HTML served from {@link rendererURL}.
|
* This window will show the HTML served from {@link rendererURL}.
|
||||||
*/
|
*/
|
||||||
export const createWindow = async () => {
|
export const createWindow = async () => {
|
||||||
const appImgPath = isDev
|
|
||||||
? "../build/window-icon.png"
|
|
||||||
: path.join(process.resourcesPath, "window-icon.png");
|
|
||||||
const appIcon = nativeImage.createFromPath(appImgPath);
|
|
||||||
|
|
||||||
// Create the main window. This'll show our web content.
|
// Create the main window. This'll show our web content.
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(app.getAppPath(), "preload.js"),
|
preload: path.join(app.getAppPath(), "preload.js"),
|
||||||
},
|
},
|
||||||
icon: appIcon,
|
|
||||||
// The color to show in the window until the web content gets loaded.
|
// 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
|
// See: https://www.electronjs.org/docs/latest/api/browser-window#setting-the-backgroundcolor-property
|
||||||
backgroundColor: "black",
|
backgroundColor: "black",
|
||||||
// We'll show conditionally depending on `wasAutoLaunched` later
|
// We'll show it conditionally depending on `wasAutoLaunched` later.
|
||||||
show: false,
|
show: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -53,19 +43,14 @@ export const createWindow = async () => {
|
||||||
// Open the DevTools automatically when running in dev mode
|
// Open the DevTools automatically when running in dev mode
|
||||||
if (isDev) mainWindow.webContents.openDevTools();
|
if (isDev) mainWindow.webContents.openDevTools();
|
||||||
|
|
||||||
mainWindow.webContents.on("render-process-gone", (event, details) => {
|
mainWindow.webContents.on("render-process-gone", (_, details) => {
|
||||||
|
log.error(`render-process-gone: ${details}`);
|
||||||
mainWindow.webContents.reload();
|
mainWindow.webContents.reload();
|
||||||
logErrorSentry(
|
|
||||||
Error("render-process-gone"),
|
|
||||||
"webContents event render-process-gone",
|
|
||||||
{ details },
|
|
||||||
);
|
|
||||||
ElectronLog.log("webContents event render-process-gone", details);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.on("unresponsive", () => {
|
mainWindow.webContents.on("unresponsive", () => {
|
||||||
|
log.error("webContents unresponsive");
|
||||||
mainWindow.webContents.forcefullyCrashRenderer();
|
mainWindow.webContents.forcefullyCrashRenderer();
|
||||||
ElectronLog.log("webContents event unresponsive");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.on("close", function (event) {
|
mainWindow.on("close", function (event) {
|
||||||
|
@ -92,12 +77,7 @@ export const createWindow = async () => {
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function handleUpdates(mainWindow: BrowserWindow) {
|
export async function handleUpdates(mainWindow: BrowserWindow) {}
|
||||||
const isInstalledViaBrew = await checkIfInstalledViaBrew();
|
|
||||||
if (!isDev && !isInstalledViaBrew) {
|
|
||||||
setupAutoUpdater(mainWindow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setupTrayItem = (mainWindow: BrowserWindow) => {
|
export const setupTrayItem = (mainWindow: BrowserWindow) => {
|
||||||
const iconName = isPlatform("mac")
|
const iconName = isPlatform("mac")
|
||||||
|
@ -110,7 +90,7 @@ export const setupTrayItem = (mainWindow: BrowserWindow) => {
|
||||||
const trayIcon = nativeImage.createFromPath(trayImgPath);
|
const trayIcon = nativeImage.createFromPath(trayImgPath);
|
||||||
const tray = new Tray(trayIcon);
|
const tray = new Tray(trayIcon);
|
||||||
tray.setToolTip("ente");
|
tray.setToolTip("ente");
|
||||||
tray.setContextMenu(buildContextMenu(mainWindow));
|
tray.setContextMenu(createTrayContextMenu(mainWindow));
|
||||||
};
|
};
|
||||||
|
|
||||||
export function handleDownloads(mainWindow: BrowserWindow) {
|
export function handleDownloads(mainWindow: BrowserWindow) {
|
||||||
|
@ -160,10 +140,6 @@ export function setupMacWindowOnDockIconClick() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupMainMenu(mainWindow: BrowserWindow) {
|
|
||||||
Menu.setApplicationMenu(await buildMenuBar(mainWindow));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleDockIconHideOnAutoLaunch() {
|
export async function handleDockIconHideOnAutoLaunch() {
|
||||||
const shouldHideDockIcon = getHideDockIconPreference();
|
const shouldHideDockIcon = getHideDockIconPreference();
|
||||||
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
|
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
|
||||||
|
@ -173,27 +149,14 @@ export async function handleDockIconHideOnAutoLaunch() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logSystemInfo() {
|
export function logStartupBanner() {
|
||||||
const systemVersion = process.getSystemVersion();
|
const version = isDev ? "dev" : app.getVersion();
|
||||||
const osName = process.platform;
|
log.info(`Hello from ente-photos-desktop ${version}`);
|
||||||
const osRelease = os.release();
|
|
||||||
ElectronLog.info({ osName, osRelease, systemVersion });
|
|
||||||
const appVersion = app.getVersion();
|
|
||||||
ElectronLog.info({ appVersion });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkIfInstalledViaBrew() {
|
const platform = process.platform;
|
||||||
if (!isPlatform("mac")) {
|
const osRelease = os.release();
|
||||||
return false;
|
const systemVersion = process.getSystemVersion();
|
||||||
}
|
log.info("Running on", { platform, osRelease, systemVersion });
|
||||||
try {
|
|
||||||
await execAsync("brew list --cask ente");
|
|
||||||
ElectronLog.info("ente installed via brew");
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
ElectronLog.info("ente not installed via brew");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function lowerCaseHeaders(responseHeaders: Record<string, string[]>) {
|
function lowerCaseHeaders(responseHeaders: Record<string, string[]>) {
|
||||||
|
|
|
@ -65,8 +65,8 @@ import {
|
||||||
saveFileToDisk,
|
saveFileToDisk,
|
||||||
saveStreamToDisk,
|
saveStreamToDisk,
|
||||||
} from "./fs";
|
} from "./fs";
|
||||||
import { openDirectory, openLogDirectory } from "./general";
|
|
||||||
import { logToDisk } from "./log";
|
import { logToDisk } from "./log";
|
||||||
|
import { openDirectory, openLogDirectory } from "./util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen for IPC events sent/invoked by the renderer process, and route them to
|
* Listen for IPC events sent/invoked by the renderer process, and route them to
|
||||||
|
|
|
@ -1,18 +1,34 @@
|
||||||
import log from "electron-log";
|
import log from "electron-log";
|
||||||
import { isDev } from "./general";
|
import util from "node:util";
|
||||||
|
import { isDev } from "./util";
|
||||||
|
|
||||||
export function setupLogging(isDev?: boolean) {
|
/**
|
||||||
|
* Initialize logging in the main process.
|
||||||
|
*
|
||||||
|
* This will set our underlying logger up to log to a file named `ente.log`,
|
||||||
|
*
|
||||||
|
* - on Linux at ~/.config/ente/logs/main.log
|
||||||
|
* - on macOS at ~/Library/Logs/ente/main.log
|
||||||
|
* - on Windows at %USERPROFILE%\AppData\Roaming\ente\logs\main.log
|
||||||
|
*
|
||||||
|
* On dev builds, it will also log to the console.
|
||||||
|
*/
|
||||||
|
export const initLogging = () => {
|
||||||
log.transports.file.fileName = "ente.log";
|
log.transports.file.fileName = "ente.log";
|
||||||
log.transports.file.maxSize = 50 * 1024 * 1024; // 50MB;
|
log.transports.file.maxSize = 50 * 1024 * 1024; // 50MB;
|
||||||
if (!isDev) {
|
log.transports.file.format = "[{y}-{m}-{d}T{h}:{i}:{s}{z}] {text}";
|
||||||
log.transports.console.level = false;
|
|
||||||
}
|
|
||||||
log.transports.file.format =
|
|
||||||
"[{y}-{m}-{d}T{h}:{i}:{s}{z}] [{level}]{scope} {text}";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
log.transports.console.level = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a {@link message} to the on-disk log.
|
||||||
|
*
|
||||||
|
* This is used by the renderer process (via the contextBridge) to add entries
|
||||||
|
* in the log that is saved on disk.
|
||||||
|
*/
|
||||||
export const logToDisk = (message: string) => {
|
export const logToDisk = (message: string) => {
|
||||||
log.info(message);
|
log.info(`[rndr] ${message}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logError = logErrorSentry;
|
export const logError = logErrorSentry;
|
||||||
|
@ -32,3 +48,84 @@ export function logErrorSentry(
|
||||||
console.log(error, { msg, info });
|
console.log(error, { msg, info });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logError1 = (message: string, e?: unknown) => {
|
||||||
|
if (!e) {
|
||||||
|
logError_(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let es: string;
|
||||||
|
if (e instanceof Error) {
|
||||||
|
// In practice, we expect ourselves to be called with Error objects, so
|
||||||
|
// this is the happy path so to say.
|
||||||
|
es = `${e.name}: ${e.message}\n${e.stack}`;
|
||||||
|
} else {
|
||||||
|
// For the rest rare cases, use the default string serialization of e.
|
||||||
|
es = String(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
logError_(`${message}: ${es}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const logError_ = (message: string) => {
|
||||||
|
log.error(`[main] [error] ${message}`);
|
||||||
|
if (isDev) console.error(`[error] ${message}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const logInfo = (...params: any[]) => {
|
||||||
|
const message = params
|
||||||
|
.map((p) => (typeof p == "string" ? p : util.inspect(p)))
|
||||||
|
.join(" ");
|
||||||
|
log.info(`[main] ${message}`);
|
||||||
|
if (isDev) console.log(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const logDebug = (param: () => any) => {
|
||||||
|
if (isDev) console.log(`[debug] ${util.inspect(param())}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ente's logger.
|
||||||
|
*
|
||||||
|
* This is an object that provides three functions to log at the corresponding
|
||||||
|
* levels - error, info or debug.
|
||||||
|
*
|
||||||
|
* {@link initLogging} needs to be called once before using any of these.
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Log an error message with an optional associated error object.
|
||||||
|
*
|
||||||
|
* {@link e} is generally expected to be an `instanceof Error` but it can be
|
||||||
|
* any arbitrary object that we obtain, say, when in a try-catch handler.
|
||||||
|
*
|
||||||
|
* The log is written to disk. In development builds, the log is also
|
||||||
|
* printed to the (Node.js process') console.
|
||||||
|
*/
|
||||||
|
error: logError1,
|
||||||
|
/**
|
||||||
|
* Log a message.
|
||||||
|
*
|
||||||
|
* This is meant as a replacement of {@link console.log}, and takes an
|
||||||
|
* arbitrary number of arbitrary parameters that it then serializes.
|
||||||
|
*
|
||||||
|
* The log is written to disk. In development builds, the log is also
|
||||||
|
* printed to the (Node.js process') console.
|
||||||
|
*/
|
||||||
|
info: logInfo,
|
||||||
|
/**
|
||||||
|
* Log a debug message.
|
||||||
|
*
|
||||||
|
* To avoid running unnecessary code in release builds, this takes a
|
||||||
|
* function to call to get the log message instead of directly taking the
|
||||||
|
* message. The provided function will only be called in development builds.
|
||||||
|
*
|
||||||
|
* The function can return an arbitrary value which is serialied before
|
||||||
|
* being logged.
|
||||||
|
*
|
||||||
|
* This log is not written to disk. It is printed to the (Node.js process')
|
||||||
|
* console only on development builds.
|
||||||
|
*/
|
||||||
|
debug: logDebug,
|
||||||
|
};
|
||||||
|
|
214
desktop/src/main/menu.ts
Normal file
214
desktop/src/main/menu.ts
Normal file
|
@ -0,0 +1,214 @@
|
||||||
|
import {
|
||||||
|
app,
|
||||||
|
BrowserWindow,
|
||||||
|
Menu,
|
||||||
|
MenuItemConstructorOptions,
|
||||||
|
shell,
|
||||||
|
} from "electron";
|
||||||
|
import { setIsAppQuitting } from "../main";
|
||||||
|
import { forceCheckForUpdateAndNotify } from "../services/appUpdater";
|
||||||
|
import autoLauncher from "../services/autoLauncher";
|
||||||
|
import {
|
||||||
|
getHideDockIconPreference,
|
||||||
|
setHideDockIconPreference,
|
||||||
|
} from "../services/userPreference";
|
||||||
|
import { openLogDirectory } from "./util";
|
||||||
|
|
||||||
|
/** Create and return the entries in the app's main menu bar */
|
||||||
|
export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
|
||||||
|
// The state of checkboxes
|
||||||
|
//
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
const macOSOnly = (options: MenuItemConstructorOptions[]) =>
|
||||||
|
process.platform == "darwin" ? options : [];
|
||||||
|
|
||||||
|
const handleCheckForUpdates = () =>
|
||||||
|
forceCheckForUpdateAndNotify(mainWindow);
|
||||||
|
|
||||||
|
const handleViewChangelog = () =>
|
||||||
|
shell.openExternal(
|
||||||
|
"https://github.com/ente-io/ente/blob/main/desktop/CHANGELOG.md",
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleAutoLaunch = () => {
|
||||||
|
autoLauncher.toggleAutoLaunch();
|
||||||
|
isAutoLaunchEnabled = !isAutoLaunchEnabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleHideDockIcon = () => {
|
||||||
|
setHideDockIconPreference(!shouldHideDockIcon);
|
||||||
|
shouldHideDockIcon = !shouldHideDockIcon;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHelp = () => shell.openExternal("https://help.ente.io/photos/");
|
||||||
|
|
||||||
|
const handleSupport = () => shell.openExternal("mailto:support@ente.io");
|
||||||
|
|
||||||
|
const handleBlog = () => shell.openExternal("https://ente.io/blog/");
|
||||||
|
|
||||||
|
const handleViewLogs = openLogDirectory;
|
||||||
|
|
||||||
|
return Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: "ente",
|
||||||
|
submenu: [
|
||||||
|
...macOSOnly([
|
||||||
|
{
|
||||||
|
label: "About Ente",
|
||||||
|
role: "about",
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label: "Check for Updates...",
|
||||||
|
click: handleCheckForUpdates,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "View Changelog",
|
||||||
|
click: handleViewChangelog,
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
|
||||||
|
{
|
||||||
|
label: "Preferences",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Open Ente on Startup",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: isAutoLaunchEnabled,
|
||||||
|
click: toggleAutoLaunch,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Hide Dock Icon",
|
||||||
|
type: "checkbox",
|
||||||
|
checked: shouldHideDockIcon,
|
||||||
|
click: toggleHideDockIcon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{ type: "separator" },
|
||||||
|
...macOSOnly([
|
||||||
|
{
|
||||||
|
label: "Hide Ente",
|
||||||
|
role: "hide",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Hide Others",
|
||||||
|
role: "hideOthers",
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
label: "Quit",
|
||||||
|
role: "quit",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
submenu: [
|
||||||
|
{ label: "Undo", role: "undo" },
|
||||||
|
{ label: "Redo", role: "redo" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ label: "Cut", role: "cut" },
|
||||||
|
{ label: "Copy", role: "copy" },
|
||||||
|
{ label: "Paste", role: "paste" },
|
||||||
|
{ label: "Select All", role: "selectAll" },
|
||||||
|
...macOSOnly([
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label: "Speech",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
role: "startSpeaking",
|
||||||
|
label: "start speaking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "stopSpeaking",
|
||||||
|
label: "stop speaking",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "View",
|
||||||
|
submenu: [
|
||||||
|
{ label: "Reload", role: "reload" },
|
||||||
|
{ label: "Toggle Dev Tools", role: "toggleDevTools" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ label: "Toggle Full Screen", role: "togglefullscreen" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Window",
|
||||||
|
submenu: [
|
||||||
|
{ label: "Minimize", role: "minimize" },
|
||||||
|
{ label: "Zoom", role: "zoom" },
|
||||||
|
{ label: "Close", role: "close" },
|
||||||
|
...macOSOnly([
|
||||||
|
{ type: "separator" },
|
||||||
|
{ label: "Bring All to Front", role: "front" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ label: "Ente", role: "window" },
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Help",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Ente Help",
|
||||||
|
click: handleHelp,
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label: "Support",
|
||||||
|
click: handleSupport,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Product Updates",
|
||||||
|
click: handleBlog,
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label: "View Logs",
|
||||||
|
click: handleViewLogs,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and return a {@link Menu} that is shown when the user clicks on our
|
||||||
|
* system tray icon (e.g. the icon list at the top right of the screen on macOS)
|
||||||
|
*/
|
||||||
|
export const createTrayContextMenu = (mainWindow: BrowserWindow) => {
|
||||||
|
const handleOpen = () => {
|
||||||
|
mainWindow.maximize();
|
||||||
|
mainWindow.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsAppQuitting(true);
|
||||||
|
app.quit();
|
||||||
|
};
|
||||||
|
|
||||||
|
return Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: "Open Ente",
|
||||||
|
click: handleOpen,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Quit Ente",
|
||||||
|
click: handleClose,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
81
desktop/src/main/util.ts
Normal file
81
desktop/src/main/util.ts
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import shellescape from "any-shell-escape";
|
||||||
|
import { shell } from "electron"; /* TODO(MR): Why is this not in /main? */
|
||||||
|
import { app } from "electron/main";
|
||||||
|
import { exec } from "node:child_process";
|
||||||
|
import path from "node:path";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
import log from "./log";
|
||||||
|
|
||||||
|
/** `true` if the app is running in development mode. */
|
||||||
|
export const isDev = !app.isPackaged;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a shell command asynchronously.
|
||||||
|
*
|
||||||
|
* This is a convenience promisified version of child_process.exec. It runs the
|
||||||
|
* command asynchronously and returns its stdout and stderr if there were no
|
||||||
|
* errors.
|
||||||
|
*
|
||||||
|
* If the command is passed as a string, then it will be executed verbatim.
|
||||||
|
*
|
||||||
|
* If the command is passed as an array, then the first argument will be treated
|
||||||
|
* as the executable and the remaining (optional) items as the command line
|
||||||
|
* parameters. This function will shellescape and join the array to form the
|
||||||
|
* command that finally gets executed.
|
||||||
|
*
|
||||||
|
* > Note: This is not a 1-1 replacement of child_process.exec - if you're
|
||||||
|
* > trying to run a trivial shell command, say something that produces a lot of
|
||||||
|
* > output, this might not be the best option and it might be better to use the
|
||||||
|
* > underlying functions.
|
||||||
|
*/
|
||||||
|
export const execAsync = (command: string | string[]) => {
|
||||||
|
const escapedCommand = Array.isArray(command)
|
||||||
|
? shellescape(command)
|
||||||
|
: command;
|
||||||
|
const startTime = Date.now();
|
||||||
|
log.debug(() => `Running shell command: ${escapedCommand}`);
|
||||||
|
const result = execAsync_(escapedCommand);
|
||||||
|
log.debug(
|
||||||
|
() =>
|
||||||
|
`Completed in ${Math.round(Date.now() - startTime)} ms (${escapedCommand})`,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const execAsync_ = promisify(exec);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the given {@link dirPath} in the system's folder viewer.
|
||||||
|
*
|
||||||
|
* For example, on macOS this'll open {@link dirPath} in Finder.
|
||||||
|
*/
|
||||||
|
export const openDirectory = async (dirPath: string) => {
|
||||||
|
const res = await shell.openPath(path.normalize(dirPath));
|
||||||
|
// shell.openPath resolves with a string containing the error message
|
||||||
|
// corresponding to the failure if a failure occurred, otherwise "".
|
||||||
|
if (res) throw new Error(`Failed to open directory ${dirPath}: res`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the path where the logs for the app are saved.
|
||||||
|
*
|
||||||
|
* [Note: Electron app paths]
|
||||||
|
*
|
||||||
|
* By default, these paths are at the following locations:
|
||||||
|
*
|
||||||
|
* - macOS: `~/Library/Application Support/ente`
|
||||||
|
* - Linux: `~/.config/ente`
|
||||||
|
* - Windows: `%APPDATA%`, e.g. `C:\Users\<username>\AppData\Local\ente`
|
||||||
|
* - Windows: C:\Users\<you>\AppData\Local\<Your App Name>
|
||||||
|
*
|
||||||
|
* https://www.electronjs.org/docs/latest/api/app
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const logDirectoryPath = () => app.getPath("logs");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the app's log directory in the system's folder viewer.
|
||||||
|
*
|
||||||
|
* @see {@link openDirectory}
|
||||||
|
*/
|
||||||
|
export const openLogDirectory = () => openDirectory(logDirectoryPath());
|
|
@ -1,20 +1,16 @@
|
||||||
import log from "electron-log";
|
|
||||||
import { app, net } from "electron/main";
|
import { app, net } from "electron/main";
|
||||||
import { existsSync } from "fs";
|
import { existsSync } from "fs";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import util from "util";
|
|
||||||
import { CustomErrors } from "../constants/errors";
|
import { CustomErrors } from "../constants/errors";
|
||||||
import { writeStream } from "../main/fs";
|
import { writeStream } from "../main/fs";
|
||||||
import { isDev } from "../main/general";
|
import log, { logErrorSentry } from "../main/log";
|
||||||
import { logErrorSentry } from "../main/log";
|
import { execAsync, isDev } from "../main/util";
|
||||||
import { Model } from "../types/ipc";
|
import { Model } from "../types/ipc";
|
||||||
import Tokenizer from "../utils/clip-bpe-ts/mod";
|
import Tokenizer from "../utils/clip-bpe-ts/mod";
|
||||||
import { getPlatform } from "../utils/common/platform";
|
import { getPlatform } from "../utils/common/platform";
|
||||||
import { generateTempFilePath } from "../utils/temp";
|
import { generateTempFilePath } from "../utils/temp";
|
||||||
import { deleteTempFile } from "./ffmpeg";
|
import { deleteTempFile } from "./ffmpeg";
|
||||||
const shellescape = require("any-shell-escape");
|
|
||||||
const execAsync = util.promisify(require("child_process").exec);
|
|
||||||
const jpeg = require("jpeg-js");
|
const jpeg = require("jpeg-js");
|
||||||
|
|
||||||
const CLIP_MODEL_PATH_PLACEHOLDER = "CLIP_MODEL";
|
const CLIP_MODEL_PATH_PLACEHOLDER = "CLIP_MODEL";
|
||||||
|
@ -100,8 +96,7 @@ export async function getClipImageModelPath(type: "ggml" | "onnx") {
|
||||||
const localFileSize = (await fs.stat(modelSavePath)).size;
|
const localFileSize = (await fs.stat(modelSavePath)).size;
|
||||||
if (localFileSize !== IMAGE_MODEL_SIZE_IN_BYTES[type]) {
|
if (localFileSize !== IMAGE_MODEL_SIZE_IN_BYTES[type]) {
|
||||||
log.info(
|
log.info(
|
||||||
"clip image model size mismatch, downloading again got:",
|
`clip image model size mismatch, downloading again got: ${localFileSize}`,
|
||||||
localFileSize,
|
|
||||||
);
|
);
|
||||||
imageModelDownloadInProgress = downloadModel(
|
imageModelDownloadInProgress = downloadModel(
|
||||||
modelSavePath,
|
modelSavePath,
|
||||||
|
@ -139,8 +134,7 @@ export async function getClipTextModelPath(type: "ggml" | "onnx") {
|
||||||
const localFileSize = (await fs.stat(modelSavePath)).size;
|
const localFileSize = (await fs.stat(modelSavePath)).size;
|
||||||
if (localFileSize !== TEXT_MODEL_SIZE_IN_BYTES[type]) {
|
if (localFileSize !== TEXT_MODEL_SIZE_IN_BYTES[type]) {
|
||||||
log.info(
|
log.info(
|
||||||
"clip text model size mismatch, downloading again got:",
|
`clip text model size mismatch, downloading again got: ${localFileSize}`,
|
||||||
localFileSize,
|
|
||||||
);
|
);
|
||||||
textModelDownloadInProgress = true;
|
textModelDownloadInProgress = true;
|
||||||
downloadModel(modelSavePath, TEXT_MODEL_DOWNLOAD_URL[type])
|
downloadModel(modelSavePath, TEXT_MODEL_DOWNLOAD_URL[type])
|
||||||
|
@ -278,11 +272,7 @@ export async function computeGGMLImageEmbedding(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const escapedCmd = shellescape(cmd);
|
const { stdout } = await execAsync(cmd);
|
||||||
log.info("running clip command", escapedCmd);
|
|
||||||
const startTime = Date.now();
|
|
||||||
const { stdout } = await execAsync(escapedCmd);
|
|
||||||
log.info("clip command execution time ", Date.now() - startTime);
|
|
||||||
// parse stdout and return embedding
|
// parse stdout and return embedding
|
||||||
// get the last line of stdout
|
// get the last line of stdout
|
||||||
const lines = stdout.split("\n");
|
const lines = stdout.split("\n");
|
||||||
|
@ -291,7 +281,7 @@ export async function computeGGMLImageEmbedding(
|
||||||
const embeddingArray = new Float32Array(embedding);
|
const embeddingArray = new Float32Array(embedding);
|
||||||
return embeddingArray;
|
return embeddingArray;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logErrorSentry(err, "Error in computeGGMLImageEmbedding");
|
log.error("Failed to compute GGML image embedding", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -316,7 +306,7 @@ export async function computeONNXImageEmbedding(
|
||||||
const imageEmbedding = results["output"].data; // Float32Array
|
const imageEmbedding = results["output"].data; // Float32Array
|
||||||
return normalizeEmbedding(imageEmbedding);
|
return normalizeEmbedding(imageEmbedding);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logErrorSentry(err, "Error in computeONNXImageEmbedding");
|
log.error("Failed to compute ONNX image embedding", err);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -367,11 +357,7 @@ export async function computeGGMLTextEmbedding(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const escapedCmd = shellescape(cmd);
|
const { stdout } = await execAsync(cmd);
|
||||||
log.info("running clip command", escapedCmd);
|
|
||||||
const startTime = Date.now();
|
|
||||||
const { stdout } = await execAsync(escapedCmd);
|
|
||||||
log.info("clip command execution time ", Date.now() - startTime);
|
|
||||||
// parse stdout and return embedding
|
// parse stdout and return embedding
|
||||||
// get the last line of stdout
|
// get the last line of stdout
|
||||||
const lines = stdout.split("\n");
|
const lines = stdout.split("\n");
|
||||||
|
@ -383,7 +369,7 @@ export async function computeGGMLTextEmbedding(
|
||||||
if (err.message === CustomErrors.MODEL_DOWNLOAD_PENDING) {
|
if (err.message === CustomErrors.MODEL_DOWNLOAD_PENDING) {
|
||||||
log.info(CustomErrors.MODEL_DOWNLOAD_PENDING);
|
log.info(CustomErrors.MODEL_DOWNLOAD_PENDING);
|
||||||
} else {
|
} else {
|
||||||
logErrorSentry(err, "Error in computeGGMLTextEmbedding");
|
log.error("Failed to compute GGML text embedding", err);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
import log from "electron-log";
|
|
||||||
import pathToFfmpeg from "ffmpeg-static";
|
import pathToFfmpeg from "ffmpeg-static";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import util from "util";
|
|
||||||
import { CustomErrors } from "../constants/errors";
|
import { CustomErrors } from "../constants/errors";
|
||||||
import { writeStream } from "../main/fs";
|
import { writeStream } from "../main/fs";
|
||||||
import { logError, logErrorSentry } from "../main/log";
|
import log from "../main/log";
|
||||||
|
import { execAsync } from "../main/util";
|
||||||
import { ElectronFile } from "../types/ipc";
|
import { ElectronFile } from "../types/ipc";
|
||||||
import { generateTempFilePath, getTempDirPath } from "../utils/temp";
|
import { generateTempFilePath, getTempDirPath } from "../utils/temp";
|
||||||
|
|
||||||
const shellescape = require("any-shell-escape");
|
|
||||||
|
|
||||||
const execAsync = util.promisify(require("child_process").exec);
|
|
||||||
|
|
||||||
const INPUT_PATH_PLACEHOLDER = "INPUT";
|
const INPUT_PATH_PLACEHOLDER = "INPUT";
|
||||||
const FFMPEG_PLACEHOLDER = "FFMPEG";
|
const FFMPEG_PLACEHOLDER = "FFMPEG";
|
||||||
const OUTPUT_PATH_PLACEHOLDER = "OUTPUT";
|
const OUTPUT_PATH_PLACEHOLDER = "OUTPUT";
|
||||||
|
@ -70,11 +65,7 @@ export async function runFFmpegCmd(
|
||||||
return new File([outputFileData], outputFileName);
|
return new File([outputFileData], outputFileName);
|
||||||
} finally {
|
} finally {
|
||||||
if (createdTempInputFile) {
|
if (createdTempInputFile) {
|
||||||
try {
|
await deleteTempFile(inputFilePath);
|
||||||
await deleteTempFile(inputFilePath);
|
|
||||||
} catch (e) {
|
|
||||||
logError(e, "failed to deleteTempFile");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,35 +91,23 @@ export async function runFFmpegCmd_(
|
||||||
return cmdPart;
|
return cmdPart;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const escapedCmd = shellescape(cmd);
|
|
||||||
log.info("running ffmpeg command", escapedCmd);
|
|
||||||
const startTime = Date.now();
|
|
||||||
if (dontTimeout) {
|
if (dontTimeout) {
|
||||||
await execAsync(escapedCmd);
|
await execAsync(cmd);
|
||||||
} else {
|
} else {
|
||||||
await promiseWithTimeout(execAsync(escapedCmd), 30 * 1000);
|
await promiseWithTimeout(execAsync(cmd), 30 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existsSync(tempOutputFilePath)) {
|
if (!existsSync(tempOutputFilePath)) {
|
||||||
throw new Error("ffmpeg output file not found");
|
throw new Error("ffmpeg output file not found");
|
||||||
}
|
}
|
||||||
log.info(
|
|
||||||
"ffmpeg command execution time ",
|
|
||||||
escapedCmd,
|
|
||||||
Date.now() - startTime,
|
|
||||||
"ms",
|
|
||||||
);
|
|
||||||
|
|
||||||
const outputFile = await fs.readFile(tempOutputFilePath);
|
const outputFile = await fs.readFile(tempOutputFilePath);
|
||||||
return new Uint8Array(outputFile);
|
return new Uint8Array(outputFile);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logErrorSentry(e, "ffmpeg run command error");
|
log.error("FFMPEG command failed", e);
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
await deleteTempFile(tempOutputFilePath);
|
||||||
await fs.rm(tempOutputFilePath, { force: true });
|
|
||||||
} catch (e) {
|
|
||||||
logErrorSentry(e, "failed to remove tempOutputFile");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,16 +132,12 @@ export async function writeTempFile(fileStream: Uint8Array, fileName: string) {
|
||||||
|
|
||||||
export async function deleteTempFile(tempFilePath: string) {
|
export async function deleteTempFile(tempFilePath: string) {
|
||||||
const tempDirPath = await getTempDirPath();
|
const tempDirPath = await getTempDirPath();
|
||||||
if (!tempFilePath.startsWith(tempDirPath)) {
|
if (!tempFilePath.startsWith(tempDirPath))
|
||||||
logErrorSentry(
|
log.error("Attempting to delete a non-temp file ${tempFilePath}");
|
||||||
Error("not a temp file"),
|
|
||||||
"tried to delete a non temp file",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
await fs.rm(tempFilePath, { force: true });
|
await fs.rm(tempFilePath, { force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const promiseWithTimeout = async <T>(
|
const promiseWithTimeout = async <T>(
|
||||||
request: Promise<T>,
|
request: Promise<T>,
|
||||||
timeout: number,
|
timeout: number,
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
import { exec } from "child_process";
|
|
||||||
import log from "electron-log";
|
|
||||||
import { existsSync } from "fs";
|
import { existsSync } from "fs";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import util from "util";
|
|
||||||
import { CustomErrors } from "../constants/errors";
|
import { CustomErrors } from "../constants/errors";
|
||||||
import { writeStream } from "../main/fs";
|
import { writeStream } from "../main/fs";
|
||||||
import { isDev } from "../main/general";
|
|
||||||
import { logError, logErrorSentry } from "../main/log";
|
import { logError, logErrorSentry } from "../main/log";
|
||||||
|
import { execAsync, isDev } from "../main/util";
|
||||||
import { ElectronFile } from "../types/ipc";
|
import { ElectronFile } from "../types/ipc";
|
||||||
import { isPlatform } from "../utils/common/platform";
|
import { isPlatform } from "../utils/common/platform";
|
||||||
import { generateTempFilePath } from "../utils/temp";
|
import { generateTempFilePath } from "../utils/temp";
|
||||||
import { deleteTempFile } from "./ffmpeg";
|
import { deleteTempFile } from "./ffmpeg";
|
||||||
|
|
||||||
const shellescape = require("any-shell-escape");
|
|
||||||
|
|
||||||
const asyncExec = util.promisify(exec);
|
|
||||||
|
|
||||||
const IMAGE_MAGICK_PLACEHOLDER = "IMAGE_MAGICK";
|
const IMAGE_MAGICK_PLACEHOLDER = "IMAGE_MAGICK";
|
||||||
const MAX_DIMENSION_PLACEHOLDER = "MAX_DIMENSION";
|
const MAX_DIMENSION_PLACEHOLDER = "MAX_DIMENSION";
|
||||||
const SAMPLE_SIZE_PLACEHOLDER = "SAMPLE_SIZE";
|
const SAMPLE_SIZE_PLACEHOLDER = "SAMPLE_SIZE";
|
||||||
|
@ -104,7 +97,9 @@ async function convertToJPEG_(
|
||||||
|
|
||||||
await fs.writeFile(tempInputFilePath, fileData);
|
await fs.writeFile(tempInputFilePath, fileData);
|
||||||
|
|
||||||
await runConvertCommand(tempInputFilePath, tempOutputFilePath);
|
await execAsync(
|
||||||
|
constructConvertCommand(tempInputFilePath, tempOutputFilePath),
|
||||||
|
);
|
||||||
|
|
||||||
return new Uint8Array(await fs.readFile(tempOutputFilePath));
|
return new Uint8Array(await fs.readFile(tempOutputFilePath));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -124,19 +119,6 @@ async function convertToJPEG_(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runConvertCommand(
|
|
||||||
tempInputFilePath: string,
|
|
||||||
tempOutputFilePath: string,
|
|
||||||
) {
|
|
||||||
const convertCmd = constructConvertCommand(
|
|
||||||
tempInputFilePath,
|
|
||||||
tempOutputFilePath,
|
|
||||||
);
|
|
||||||
const escapedCmd = shellescape(convertCmd);
|
|
||||||
log.info("running convert command: " + escapedCmd);
|
|
||||||
await asyncExec(escapedCmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
function constructConvertCommand(
|
function constructConvertCommand(
|
||||||
tempInputFilePath: string,
|
tempInputFilePath: string,
|
||||||
tempOutputFilePath: string,
|
tempOutputFilePath: string,
|
||||||
|
@ -222,13 +204,14 @@ async function generateImageThumbnail_(
|
||||||
tempOutputFilePath = await generateTempFilePath("thumb.jpeg");
|
tempOutputFilePath = await generateTempFilePath("thumb.jpeg");
|
||||||
let thumbnail: Uint8Array;
|
let thumbnail: Uint8Array;
|
||||||
do {
|
do {
|
||||||
await runThumbnailGenerationCommand(
|
await execAsync(
|
||||||
inputFilePath,
|
constructThumbnailGenerationCommand(
|
||||||
tempOutputFilePath,
|
inputFilePath,
|
||||||
width,
|
tempOutputFilePath,
|
||||||
quality,
|
width,
|
||||||
|
quality,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
thumbnail = new Uint8Array(await fs.readFile(tempOutputFilePath));
|
thumbnail = new Uint8Array(await fs.readFile(tempOutputFilePath));
|
||||||
quality -= 10;
|
quality -= 10;
|
||||||
} while (thumbnail.length > maxSize && quality > MIN_QUALITY);
|
} while (thumbnail.length > maxSize && quality > MIN_QUALITY);
|
||||||
|
@ -245,23 +228,6 @@ async function generateImageThumbnail_(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runThumbnailGenerationCommand(
|
|
||||||
inputFilePath: string,
|
|
||||||
tempOutputFilePath: string,
|
|
||||||
maxDimension: number,
|
|
||||||
quality: number,
|
|
||||||
) {
|
|
||||||
const thumbnailGenerationCmd: string[] =
|
|
||||||
constructThumbnailGenerationCommand(
|
|
||||||
inputFilePath,
|
|
||||||
tempOutputFilePath,
|
|
||||||
maxDimension,
|
|
||||||
quality,
|
|
||||||
);
|
|
||||||
const escapedCmd = shellescape(thumbnailGenerationCmd);
|
|
||||||
log.info("running thumbnail generation command: " + escapedCmd);
|
|
||||||
await asyncExec(escapedCmd);
|
|
||||||
}
|
|
||||||
function constructThumbnailGenerationCommand(
|
function constructThumbnailGenerationCommand(
|
||||||
inputFilePath: string,
|
inputFilePath: string,
|
||||||
tempOutputFilePath: string,
|
tempOutputFilePath: string,
|
||||||
|
|
25
desktop/src/types/any-shell-escape.d.ts
vendored
Normal file
25
desktop/src/types/any-shell-escape.d.ts
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* Escape and stringify an array of arguments to be executed on the shell.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* const shellescape = require('any-shell-escape');
|
||||||
|
*
|
||||||
|
* const args = ['curl', '-v', '-H', 'Location;', '-H', "User-Agent: FooBar's so-called \"Browser\"", 'http://www.daveeddy.com/?name=dave&age=24'];
|
||||||
|
*
|
||||||
|
* const escaped = shellescape(args);
|
||||||
|
* console.log(escaped);
|
||||||
|
*
|
||||||
|
* yields (on POSIX shells):
|
||||||
|
*
|
||||||
|
* curl -v -H 'Location;' -H 'User-Agent: FoorBar'"'"'s so-called "Browser"' 'http://www.daveeddy.com/?name=dave&age=24'
|
||||||
|
*
|
||||||
|
* or (on Windows):
|
||||||
|
*
|
||||||
|
* curl -v -H "Location;" -H "User-Agent: FooBar's so-called ""Browser""" "http://www.daveeddy.com/?name=dave&age=24"
|
||||||
|
Which is suitable for being executed by the shell.
|
||||||
|
*/
|
||||||
|
declare module "any-shell-escape" {
|
||||||
|
declare const shellescape: (args: readonly string | string[]) => string;
|
||||||
|
export default shellescape;
|
||||||
|
}
|
|
@ -1,215 +0,0 @@
|
||||||
import {
|
|
||||||
app,
|
|
||||||
BrowserWindow,
|
|
||||||
Menu,
|
|
||||||
MenuItemConstructorOptions,
|
|
||||||
shell,
|
|
||||||
} from "electron";
|
|
||||||
import ElectronLog from "electron-log";
|
|
||||||
import { setIsAppQuitting } from "../main";
|
|
||||||
import { openDirectory, openLogDirectory } from "../main/general";
|
|
||||||
import { forceCheckForUpdateAndNotify } from "../services/appUpdater";
|
|
||||||
import autoLauncher from "../services/autoLauncher";
|
|
||||||
import {
|
|
||||||
getHideDockIconPreference,
|
|
||||||
setHideDockIconPreference,
|
|
||||||
} from "../services/userPreference";
|
|
||||||
import { isPlatform } from "./common/platform";
|
|
||||||
|
|
||||||
export function buildContextMenu(mainWindow: BrowserWindow): Menu {
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
|
||||||
{
|
|
||||||
label: "Open ente",
|
|
||||||
click: function () {
|
|
||||||
mainWindow.maximize();
|
|
||||||
mainWindow.show();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Quit ente",
|
|
||||||
click: function () {
|
|
||||||
ElectronLog.log("user quit the app");
|
|
||||||
setIsAppQuitting(true);
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return contextMenu;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function buildMenuBar(mainWindow: BrowserWindow): Promise<Menu> {
|
|
||||||
let isAutoLaunchEnabled = await autoLauncher.isEnabled();
|
|
||||||
const isMac = isPlatform("mac");
|
|
||||||
let shouldHideDockIcon = getHideDockIconPreference();
|
|
||||||
const template: MenuItemConstructorOptions[] = [
|
|
||||||
{
|
|
||||||
label: "ente",
|
|
||||||
submenu: [
|
|
||||||
...((isMac
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: "About ente",
|
|
||||||
role: "about",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []) as MenuItemConstructorOptions[]),
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
|
||||||
label: "Check for updates...",
|
|
||||||
click: () => {
|
|
||||||
forceCheckForUpdateAndNotify(mainWindow);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "View Changelog",
|
|
||||||
click: () => {
|
|
||||||
shell.openExternal(
|
|
||||||
"https://github.com/ente-io/ente/blob/main/desktop/CHANGELOG.md",
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
|
|
||||||
{
|
|
||||||
label: "Preferences",
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: "Open ente on startup",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: isAutoLaunchEnabled,
|
|
||||||
click: () => {
|
|
||||||
autoLauncher.toggleAutoLaunch();
|
|
||||||
isAutoLaunchEnabled = !isAutoLaunchEnabled;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Hide dock icon",
|
|
||||||
type: "checkbox",
|
|
||||||
checked: shouldHideDockIcon,
|
|
||||||
click: () => {
|
|
||||||
setHideDockIconPreference(!shouldHideDockIcon);
|
|
||||||
shouldHideDockIcon = !shouldHideDockIcon;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
{ type: "separator" },
|
|
||||||
...((isMac
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: "Hide ente",
|
|
||||||
role: "hide",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Hide others",
|
|
||||||
role: "hideOthers",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []) as MenuItemConstructorOptions[]),
|
|
||||||
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
|
||||||
label: "Quit ente",
|
|
||||||
role: "quit",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Edit",
|
|
||||||
submenu: [
|
|
||||||
{ role: "undo", label: "Undo" },
|
|
||||||
{ role: "redo", label: "Redo" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "cut", label: "Cut" },
|
|
||||||
{ role: "copy", label: "Copy" },
|
|
||||||
{ role: "paste", label: "Paste" },
|
|
||||||
...((isMac
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
role: "pasteAndMatchStyle",
|
|
||||||
label: "Paste and match style",
|
|
||||||
},
|
|
||||||
{ role: "delete", label: "Delete" },
|
|
||||||
{ role: "selectAll", label: "Select all" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
|
||||||
label: "Speech",
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
role: "startSpeaking",
|
|
||||||
label: "start speaking",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "stopSpeaking",
|
|
||||||
label: "stop speaking",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "selectAll", label: "Select all" },
|
|
||||||
]) as MenuItemConstructorOptions[]),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "View",
|
|
||||||
submenu: [
|
|
||||||
{ role: "reload", label: "Reload" },
|
|
||||||
{ role: "forceReload", label: "Force reload" },
|
|
||||||
{ role: "toggleDevTools", label: "Toggle dev tools" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "resetZoom", label: "Reset zoom" },
|
|
||||||
{ role: "zoomIn", label: "Zoom in" },
|
|
||||||
{ role: "zoomOut", label: "Zoom out" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "togglefullscreen", label: "Toggle fullscreen" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Window",
|
|
||||||
submenu: [
|
|
||||||
{ role: "close", label: "Close" },
|
|
||||||
{ role: "minimize", label: "Minimize" },
|
|
||||||
...((isMac
|
|
||||||
? [
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "front", label: "Bring to front" },
|
|
||||||
{ type: "separator" },
|
|
||||||
{ role: "window", label: "ente" },
|
|
||||||
]
|
|
||||||
: []) as MenuItemConstructorOptions[]),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Help",
|
|
||||||
submenu: [
|
|
||||||
{
|
|
||||||
label: "Ente Help",
|
|
||||||
click: () => shell.openExternal("https://help.ente.io/photos/"),
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
|
||||||
label: "Support",
|
|
||||||
click: () => shell.openExternal("mailto:support@ente.io"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Product updates",
|
|
||||||
click: () => shell.openExternal("https://ente.io/blog/"),
|
|
||||||
},
|
|
||||||
{ type: "separator" },
|
|
||||||
{
|
|
||||||
label: "View crash reports",
|
|
||||||
click: () => openDirectory(app.getPath("crashDumps")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "View logs",
|
|
||||||
click: openLogDirectory,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return Menu.buildFromTemplate(template);
|
|
||||||
}
|
|
|
@ -9,12 +9,12 @@
|
||||||
/* Recommended target, lib and other settings for code running in the
|
/* Recommended target, lib and other settings for code running in the
|
||||||
version of Node.js bundled with Electron.
|
version of Node.js bundled with Electron.
|
||||||
|
|
||||||
Currently, with Electron 25, this is Node.js 18
|
Currently, with Electron 29, this is Node.js 20.9
|
||||||
https://www.electronjs.org/blog/electron-25-0
|
https://www.electronjs.org/blog/electron-29-0
|
||||||
|
|
||||||
Note that we cannot do
|
Note that we cannot do
|
||||||
|
|
||||||
"extends": "@tsconfig/node18/tsconfig.json",
|
"extends": "@tsconfig/node20/tsconfig.json",
|
||||||
|
|
||||||
because that sets "lib": ["es2023"]. However (and I don't fully
|
because that sets "lib": ["es2023"]. However (and I don't fully
|
||||||
understand what's going on here), that breaks our compilation since
|
understand what's going on here), that breaks our compilation since
|
||||||
|
|
Loading…
Reference in a new issue