[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]
|
||||
- target: pacman
|
||||
arch: [x64, arm64]
|
||||
icon: ./resources/icon.icns
|
||||
category: Photography
|
||||
mac:
|
||||
target:
|
||||
|
|
|
@ -8,27 +8,26 @@
|
|||
*
|
||||
* https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process
|
||||
*/
|
||||
import log from "electron-log";
|
||||
import { app, BrowserWindow } from "electron/main";
|
||||
import { app, BrowserWindow, Menu } from "electron/main";
|
||||
import serveNextAt from "next-electron-server";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { isDev } from "./main/general";
|
||||
import {
|
||||
addAllowOriginHeader,
|
||||
createWindow,
|
||||
handleDockIconHideOnAutoLaunch,
|
||||
handleDownloads,
|
||||
handleExternalLinks,
|
||||
handleUpdates,
|
||||
logSystemInfo,
|
||||
logStartupBanner,
|
||||
setupMacWindowOnDockIconClick,
|
||||
setupMainMenu,
|
||||
setupTrayItem,
|
||||
} from "./main/init";
|
||||
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";
|
||||
|
||||
let appIsQuitting = false;
|
||||
|
@ -135,8 +134,6 @@ function setupAppEventEmitter(mainWindow: BrowserWindow) {
|
|||
}
|
||||
|
||||
const main = () => {
|
||||
setupLogging(isDev);
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
|
@ -145,6 +142,7 @@ const main = () => {
|
|||
|
||||
let mainWindow: BrowserWindow;
|
||||
|
||||
initLogging();
|
||||
setupRendererServer();
|
||||
handleDockIconHideOnAutoLaunch();
|
||||
increaseDiskCache();
|
||||
|
@ -161,19 +159,19 @@ const main = () => {
|
|||
}
|
||||
});
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
// Emitted once, when Electron has finished initializing.
|
||||
//
|
||||
// Note that some Electron APIs can only be used after this event occurs.
|
||||
app.on("ready", async () => {
|
||||
logSystemInfo();
|
||||
logStartupBanner();
|
||||
mainWindow = await createWindow();
|
||||
const watcher = initWatcher(mainWindow);
|
||||
setupTrayItem(mainWindow);
|
||||
setupMacWindowOnDockIconClick();
|
||||
setupMainMenu(mainWindow);
|
||||
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
|
||||
attachIPCHandlers();
|
||||
attachFSWatchIPCHandlers(watcher);
|
||||
await handleUpdates(mainWindow);
|
||||
if (!isDev) setupAutoUpdater(mainWindow);
|
||||
handleDownloads(mainWindow);
|
||||
handleExternalLinks(mainWindow);
|
||||
addAllowOriginHeader(mainWindow);
|
||||
|
@ -184,7 +182,7 @@ const main = () => {
|
|||
} catch (e) {
|
||||
// Log but otherwise ignore errors during non-critical startup
|
||||
// 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 path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { logError } from "./log";
|
||||
|
||||
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) => {
|
||||
try {
|
||||
const stats = await fs.stat(dirPath);
|
||||
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;
|
||||
}
|
||||
if (!existsSync(dirPath)) return false;
|
||||
const stats = await fs.stat(dirPath);
|
||||
return stats.isDirectory();
|
||||
};
|
||||
|
||||
export const deleteFolder = async (folderPath: string) => {
|
||||
if (!existsSync(folderPath)) {
|
||||
return;
|
||||
}
|
||||
const stat = await fs.stat(folderPath);
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error("Path is not a folder");
|
||||
}
|
||||
// check if folder is empty
|
||||
// Ensure it is folder
|
||||
if (!isFolder(folderPath)) return;
|
||||
|
||||
// Ensure folder is empty
|
||||
const files = await fs.readdir(folderPath);
|
||||
if (files.length > 0) {
|
||||
throw new Error("Folder is not empty");
|
||||
}
|
||||
if (files.length > 0) throw new Error("Folder is not empty");
|
||||
|
||||
// rm -rf it
|
||||
await fs.rmdir(folderPath);
|
||||
};
|
||||
|
||||
export const rename = async (oldPath: string, newPath: string) => {
|
||||
if (!existsSync(oldPath)) {
|
||||
throw new Error("Path does not exist");
|
||||
}
|
||||
if (!existsSync(oldPath)) throw new Error("Path does not exist");
|
||||
await fs.rename(oldPath, newPath);
|
||||
};
|
||||
|
||||
export const deleteFile = async (filePath: string) => {
|
||||
if (!existsSync(filePath)) {
|
||||
return;
|
||||
}
|
||||
// Ensure it exists
|
||||
if (!existsSync(filePath)) return;
|
||||
|
||||
// And is a file
|
||||
const stat = await fs.stat(filePath);
|
||||
if (!stat.isFile()) {
|
||||
throw new Error("Path is not a file");
|
||||
}
|
||||
if (!stat.isFile()) throw new Error("Path is not a file");
|
||||
|
||||
// rm it
|
||||
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 ElectronLog from "electron-log";
|
||||
import { app, BrowserWindow, nativeImage, Tray } from "electron";
|
||||
import { existsSync } from "node:fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { isAppQuitting, rendererURL } from "../main";
|
||||
import { setupAutoUpdater } from "../services/appUpdater";
|
||||
import autoLauncher from "../services/autoLauncher";
|
||||
import { getHideDockIconPreference } from "../services/userPreference";
|
||||
import { isPlatform } from "../utils/common/platform";
|
||||
import { buildContextMenu, buildMenuBar } from "../utils/menu";
|
||||
import { isDev } from "./general";
|
||||
import { logErrorSentry } from "./log";
|
||||
const execAsync = util.promisify(require("child_process").exec);
|
||||
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.
|
||||
|
@ -20,21 +16,15 @@ const execAsync = util.promisify(require("child_process").exec);
|
|||
* This window will show the HTML served from {@link rendererURL}.
|
||||
*/
|
||||
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.
|
||||
const mainWindow = new BrowserWindow({
|
||||
webPreferences: {
|
||||
preload: path.join(app.getAppPath(), "preload.js"),
|
||||
},
|
||||
icon: appIcon,
|
||||
// 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 conditionally depending on `wasAutoLaunched` later
|
||||
// We'll show it conditionally depending on `wasAutoLaunched` later.
|
||||
show: false,
|
||||
});
|
||||
|
||||
|
@ -53,19 +43,14 @@ export const createWindow = async () => {
|
|||
// Open the DevTools automatically when running in dev mode
|
||||
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();
|
||||
logErrorSentry(
|
||||
Error("render-process-gone"),
|
||||
"webContents event render-process-gone",
|
||||
{ details },
|
||||
);
|
||||
ElectronLog.log("webContents event render-process-gone", details);
|
||||
});
|
||||
|
||||
mainWindow.webContents.on("unresponsive", () => {
|
||||
log.error("webContents unresponsive");
|
||||
mainWindow.webContents.forcefullyCrashRenderer();
|
||||
ElectronLog.log("webContents event unresponsive");
|
||||
});
|
||||
|
||||
mainWindow.on("close", function (event) {
|
||||
|
@ -92,12 +77,7 @@ export const createWindow = async () => {
|
|||
return mainWindow;
|
||||
};
|
||||
|
||||
export async function handleUpdates(mainWindow: BrowserWindow) {
|
||||
const isInstalledViaBrew = await checkIfInstalledViaBrew();
|
||||
if (!isDev && !isInstalledViaBrew) {
|
||||
setupAutoUpdater(mainWindow);
|
||||
}
|
||||
}
|
||||
export async function handleUpdates(mainWindow: BrowserWindow) {}
|
||||
|
||||
export const setupTrayItem = (mainWindow: BrowserWindow) => {
|
||||
const iconName = isPlatform("mac")
|
||||
|
@ -110,7 +90,7 @@ export const setupTrayItem = (mainWindow: BrowserWindow) => {
|
|||
const trayIcon = nativeImage.createFromPath(trayImgPath);
|
||||
const tray = new Tray(trayIcon);
|
||||
tray.setToolTip("ente");
|
||||
tray.setContextMenu(buildContextMenu(mainWindow));
|
||||
tray.setContextMenu(createTrayContextMenu(mainWindow));
|
||||
};
|
||||
|
||||
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() {
|
||||
const shouldHideDockIcon = getHideDockIconPreference();
|
||||
const wasAutoLaunched = await autoLauncher.wasAutoLaunched();
|
||||
|
@ -173,27 +149,14 @@ export async function handleDockIconHideOnAutoLaunch() {
|
|||
}
|
||||
}
|
||||
|
||||
export function logSystemInfo() {
|
||||
const systemVersion = process.getSystemVersion();
|
||||
const osName = process.platform;
|
||||
const osRelease = os.release();
|
||||
ElectronLog.info({ osName, osRelease, systemVersion });
|
||||
const appVersion = app.getVersion();
|
||||
ElectronLog.info({ appVersion });
|
||||
}
|
||||
export function logStartupBanner() {
|
||||
const version = isDev ? "dev" : app.getVersion();
|
||||
log.info(`Hello from ente-photos-desktop ${version}`);
|
||||
|
||||
export async function checkIfInstalledViaBrew() {
|
||||
if (!isPlatform("mac")) {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
const platform = process.platform;
|
||||
const osRelease = os.release();
|
||||
const systemVersion = process.getSystemVersion();
|
||||
log.info("Running on", { platform, osRelease, systemVersion });
|
||||
}
|
||||
|
||||
function lowerCaseHeaders(responseHeaders: Record<string, string[]>) {
|
||||
|
|
|
@ -65,8 +65,8 @@ import {
|
|||
saveFileToDisk,
|
||||
saveStreamToDisk,
|
||||
} from "./fs";
|
||||
import { openDirectory, openLogDirectory } from "./general";
|
||||
import { logToDisk } from "./log";
|
||||
import { openDirectory, openLogDirectory } from "./util";
|
||||
|
||||
/**
|
||||
* Listen for IPC events sent/invoked by the renderer process, and route them to
|
||||
|
|
|
@ -1,18 +1,34 @@
|
|||
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.maxSize = 50 * 1024 * 1024; // 50MB;
|
||||
if (!isDev) {
|
||||
log.transports.console.level = false;
|
||||
}
|
||||
log.transports.file.format =
|
||||
"[{y}-{m}-{d}T{h}:{i}:{s}{z}] [{level}]{scope} {text}";
|
||||
}
|
||||
log.transports.file.format = "[{y}-{m}-{d}T{h}:{i}:{s}{z}] {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) => {
|
||||
log.info(message);
|
||||
log.info(`[rndr] ${message}`);
|
||||
};
|
||||
|
||||
export const logError = logErrorSentry;
|
||||
|
@ -32,3 +48,84 @@ export function logErrorSentry(
|
|||
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 { existsSync } from "fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import util from "util";
|
||||
import { CustomErrors } from "../constants/errors";
|
||||
import { writeStream } from "../main/fs";
|
||||
import { isDev } from "../main/general";
|
||||
import { logErrorSentry } from "../main/log";
|
||||
import log, { logErrorSentry } from "../main/log";
|
||||
import { execAsync, isDev } from "../main/util";
|
||||
import { Model } from "../types/ipc";
|
||||
import Tokenizer from "../utils/clip-bpe-ts/mod";
|
||||
import { getPlatform } from "../utils/common/platform";
|
||||
import { generateTempFilePath } from "../utils/temp";
|
||||
import { deleteTempFile } from "./ffmpeg";
|
||||
const shellescape = require("any-shell-escape");
|
||||
const execAsync = util.promisify(require("child_process").exec);
|
||||
const jpeg = require("jpeg-js");
|
||||
|
||||
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;
|
||||
if (localFileSize !== IMAGE_MODEL_SIZE_IN_BYTES[type]) {
|
||||
log.info(
|
||||
"clip image model size mismatch, downloading again got:",
|
||||
localFileSize,
|
||||
`clip image model size mismatch, downloading again got: ${localFileSize}`,
|
||||
);
|
||||
imageModelDownloadInProgress = downloadModel(
|
||||
modelSavePath,
|
||||
|
@ -139,8 +134,7 @@ export async function getClipTextModelPath(type: "ggml" | "onnx") {
|
|||
const localFileSize = (await fs.stat(modelSavePath)).size;
|
||||
if (localFileSize !== TEXT_MODEL_SIZE_IN_BYTES[type]) {
|
||||
log.info(
|
||||
"clip text model size mismatch, downloading again got:",
|
||||
localFileSize,
|
||||
`clip text model size mismatch, downloading again got: ${localFileSize}`,
|
||||
);
|
||||
textModelDownloadInProgress = true;
|
||||
downloadModel(modelSavePath, TEXT_MODEL_DOWNLOAD_URL[type])
|
||||
|
@ -278,11 +272,7 @@ export async function computeGGMLImageEmbedding(
|
|||
}
|
||||
});
|
||||
|
||||
const escapedCmd = shellescape(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);
|
||||
const { stdout } = await execAsync(cmd);
|
||||
// parse stdout and return embedding
|
||||
// get the last line of stdout
|
||||
const lines = stdout.split("\n");
|
||||
|
@ -291,7 +281,7 @@ export async function computeGGMLImageEmbedding(
|
|||
const embeddingArray = new Float32Array(embedding);
|
||||
return embeddingArray;
|
||||
} catch (err) {
|
||||
logErrorSentry(err, "Error in computeGGMLImageEmbedding");
|
||||
log.error("Failed to compute GGML image embedding", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
@ -316,7 +306,7 @@ export async function computeONNXImageEmbedding(
|
|||
const imageEmbedding = results["output"].data; // Float32Array
|
||||
return normalizeEmbedding(imageEmbedding);
|
||||
} catch (err) {
|
||||
logErrorSentry(err, "Error in computeONNXImageEmbedding");
|
||||
log.error("Failed to compute ONNX image embedding", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
@ -367,11 +357,7 @@ export async function computeGGMLTextEmbedding(
|
|||
}
|
||||
});
|
||||
|
||||
const escapedCmd = shellescape(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);
|
||||
const { stdout } = await execAsync(cmd);
|
||||
// parse stdout and return embedding
|
||||
// get the last line of stdout
|
||||
const lines = stdout.split("\n");
|
||||
|
@ -383,7 +369,7 @@ export async function computeGGMLTextEmbedding(
|
|||
if (err.message === CustomErrors.MODEL_DOWNLOAD_PENDING) {
|
||||
log.info(CustomErrors.MODEL_DOWNLOAD_PENDING);
|
||||
} else {
|
||||
logErrorSentry(err, "Error in computeGGMLTextEmbedding");
|
||||
log.error("Failed to compute GGML text embedding", err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
import log from "electron-log";
|
||||
import pathToFfmpeg from "ffmpeg-static";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import util from "util";
|
||||
import { CustomErrors } from "../constants/errors";
|
||||
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 { 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 FFMPEG_PLACEHOLDER = "FFMPEG";
|
||||
const OUTPUT_PATH_PLACEHOLDER = "OUTPUT";
|
||||
|
@ -70,11 +65,7 @@ export async function runFFmpegCmd(
|
|||
return new File([outputFileData], outputFileName);
|
||||
} finally {
|
||||
if (createdTempInputFile) {
|
||||
try {
|
||||
await deleteTempFile(inputFilePath);
|
||||
} catch (e) {
|
||||
logError(e, "failed to deleteTempFile");
|
||||
}
|
||||
await deleteTempFile(inputFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -100,35 +91,23 @@ export async function runFFmpegCmd_(
|
|||
return cmdPart;
|
||||
}
|
||||
});
|
||||
const escapedCmd = shellescape(cmd);
|
||||
log.info("running ffmpeg command", escapedCmd);
|
||||
const startTime = Date.now();
|
||||
|
||||
if (dontTimeout) {
|
||||
await execAsync(escapedCmd);
|
||||
await execAsync(cmd);
|
||||
} else {
|
||||
await promiseWithTimeout(execAsync(escapedCmd), 30 * 1000);
|
||||
await promiseWithTimeout(execAsync(cmd), 30 * 1000);
|
||||
}
|
||||
|
||||
if (!existsSync(tempOutputFilePath)) {
|
||||
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);
|
||||
return new Uint8Array(outputFile);
|
||||
} catch (e) {
|
||||
logErrorSentry(e, "ffmpeg run command error");
|
||||
log.error("FFMPEG command failed", e);
|
||||
throw e;
|
||||
} finally {
|
||||
try {
|
||||
await fs.rm(tempOutputFilePath, { force: true });
|
||||
} catch (e) {
|
||||
logErrorSentry(e, "failed to remove tempOutputFile");
|
||||
}
|
||||
await deleteTempFile(tempOutputFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,16 +132,12 @@ export async function writeTempFile(fileStream: Uint8Array, fileName: string) {
|
|||
|
||||
export async function deleteTempFile(tempFilePath: string) {
|
||||
const tempDirPath = await getTempDirPath();
|
||||
if (!tempFilePath.startsWith(tempDirPath)) {
|
||||
logErrorSentry(
|
||||
Error("not a temp file"),
|
||||
"tried to delete a non temp file",
|
||||
);
|
||||
}
|
||||
if (!tempFilePath.startsWith(tempDirPath))
|
||||
log.error("Attempting to delete a non-temp file ${tempFilePath}");
|
||||
await fs.rm(tempFilePath, { force: true });
|
||||
}
|
||||
|
||||
export const promiseWithTimeout = async <T>(
|
||||
const promiseWithTimeout = async <T>(
|
||||
request: Promise<T>,
|
||||
timeout: number,
|
||||
): Promise<T> => {
|
||||
|
|
|
@ -1,22 +1,15 @@
|
|||
import { exec } from "child_process";
|
||||
import log from "electron-log";
|
||||
import { existsSync } from "fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "path";
|
||||
import util from "util";
|
||||
import { CustomErrors } from "../constants/errors";
|
||||
import { writeStream } from "../main/fs";
|
||||
import { isDev } from "../main/general";
|
||||
import { logError, logErrorSentry } from "../main/log";
|
||||
import { execAsync, isDev } from "../main/util";
|
||||
import { ElectronFile } from "../types/ipc";
|
||||
import { isPlatform } from "../utils/common/platform";
|
||||
import { generateTempFilePath } from "../utils/temp";
|
||||
import { deleteTempFile } from "./ffmpeg";
|
||||
|
||||
const shellescape = require("any-shell-escape");
|
||||
|
||||
const asyncExec = util.promisify(exec);
|
||||
|
||||
const IMAGE_MAGICK_PLACEHOLDER = "IMAGE_MAGICK";
|
||||
const MAX_DIMENSION_PLACEHOLDER = "MAX_DIMENSION";
|
||||
const SAMPLE_SIZE_PLACEHOLDER = "SAMPLE_SIZE";
|
||||
|
@ -104,7 +97,9 @@ async function convertToJPEG_(
|
|||
|
||||
await fs.writeFile(tempInputFilePath, fileData);
|
||||
|
||||
await runConvertCommand(tempInputFilePath, tempOutputFilePath);
|
||||
await execAsync(
|
||||
constructConvertCommand(tempInputFilePath, tempOutputFilePath),
|
||||
);
|
||||
|
||||
return new Uint8Array(await fs.readFile(tempOutputFilePath));
|
||||
} 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(
|
||||
tempInputFilePath: string,
|
||||
tempOutputFilePath: string,
|
||||
|
@ -222,13 +204,14 @@ async function generateImageThumbnail_(
|
|||
tempOutputFilePath = await generateTempFilePath("thumb.jpeg");
|
||||
let thumbnail: Uint8Array;
|
||||
do {
|
||||
await runThumbnailGenerationCommand(
|
||||
inputFilePath,
|
||||
tempOutputFilePath,
|
||||
width,
|
||||
quality,
|
||||
await execAsync(
|
||||
constructThumbnailGenerationCommand(
|
||||
inputFilePath,
|
||||
tempOutputFilePath,
|
||||
width,
|
||||
quality,
|
||||
),
|
||||
);
|
||||
|
||||
thumbnail = new Uint8Array(await fs.readFile(tempOutputFilePath));
|
||||
quality -= 10;
|
||||
} 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(
|
||||
inputFilePath: 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
|
||||
version of Node.js bundled with Electron.
|
||||
|
||||
Currently, with Electron 25, this is Node.js 18
|
||||
https://www.electronjs.org/blog/electron-25-0
|
||||
Currently, with Electron 29, this is Node.js 20.9
|
||||
https://www.electronjs.org/blog/electron-29-0
|
||||
|
||||
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
|
||||
understand what's going on here), that breaks our compilation since
|
||||
|
|
Loading…
Reference in a new issue