[desktop] More fixes leading on to the release (#1632)
This commit is contained in:
commit
c4756fb847
15 changed files with 224 additions and 48 deletions
70
desktop/.github/workflows/desktop-draft-release.yml
vendored
Normal file
70
desktop/.github/workflows/desktop-draft-release.yml
vendored
Normal file
|
@ -0,0 +1,70 @@
|
|||
name: "Draft release"
|
||||
|
||||
# Build the desktop/draft-release branch and update the existing draft release
|
||||
# with the resultant artifacts.
|
||||
#
|
||||
# This is meant for doing tests that require the app to be signed and packaged.
|
||||
# Such releases should not be published to end users.
|
||||
#
|
||||
# Workflow:
|
||||
#
|
||||
# 1. Push your changes to the "desktop/draft-release" branch on
|
||||
# https://github.com/ente-io/ente.
|
||||
#
|
||||
# 2. Create a draft release with tag equal to the version in the `package.json`.
|
||||
#
|
||||
# 3. Trigger this workflow. You can trigger it multiple times, each time it'll
|
||||
# just update the artifacts attached to the same draft.
|
||||
#
|
||||
# 4. Once testing is done delete the draft.
|
||||
|
||||
on:
|
||||
# Trigger manually or `gh workflow run desktop-draft-release.yml`.
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: macos-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: desktop
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ente-io/ente
|
||||
ref: desktop/draft-release
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Build
|
||||
uses: ente-io/action-electron-builder@v1.0.0
|
||||
with:
|
||||
package_root: desktop
|
||||
|
||||
# GitHub token, automatically provided to the action
|
||||
# (No need to define this secret in the repo settings)
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# If the commit is tagged with a version (e.g. "v1.0.0"),
|
||||
# release the app after building.
|
||||
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
||||
mac_certs: ${{ secrets.MAC_CERTS }}
|
||||
mac_certs_password: ${{ secrets.MAC_CERTS_PASSWORD }}
|
||||
env:
|
||||
# macOS notarization credentials key details
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD:
|
||||
${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
USE_HARD_LINKS: false
|
|
@ -29,5 +29,4 @@ mac:
|
|||
arch: [universal]
|
||||
category: public.app-category.photography
|
||||
hardenedRuntime: true
|
||||
notarize: true
|
||||
afterSign: electron-builder-notarize
|
||||
|
|
|
@ -142,7 +142,7 @@ const createMainWindow = () => {
|
|||
// Create the main window. This'll show our web content.
|
||||
const window = new BrowserWindow({
|
||||
webPreferences: {
|
||||
preload: path.join(app.getAppPath(), "preload.js"),
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
sandbox: true,
|
||||
},
|
||||
// The color to show in the window until the web content gets loaded.
|
||||
|
@ -287,13 +287,29 @@ const setupTrayItem = (mainWindow: BrowserWindow) => {
|
|||
|
||||
/**
|
||||
* Older versions of our app used to maintain a cache dir using the main
|
||||
* process. This has been deprecated in favor of using a normal web cache.
|
||||
* process. This has been removed in favor of cache on the web layer.
|
||||
*
|
||||
* Delete the old cache dir if it exists. This code was added March 2024, and
|
||||
* can be removed after some time once most people have upgraded to newer
|
||||
* versions.
|
||||
* Delete the old cache dir if it exists.
|
||||
*
|
||||
* This will happen in two phases. The cache had three subdirectories:
|
||||
*
|
||||
* - Two of them, "thumbs" and "files", will be removed now (v1.7.0, May 2024).
|
||||
*
|
||||
* - The third one, "face-crops" will be removed once we finish the face search
|
||||
* changes. See: [Note: Legacy face crops].
|
||||
*
|
||||
* This migration code can be removed after some time once most people have
|
||||
* upgraded to newer versions.
|
||||
*/
|
||||
const deleteLegacyDiskCacheDirIfExists = async () => {
|
||||
const removeIfExists = async (dirPath: string) => {
|
||||
if (existsSync(dirPath)) {
|
||||
log.info(`Removing legacy disk cache from ${dirPath}`);
|
||||
await fs.rm(dirPath, { recursive: true });
|
||||
}
|
||||
};
|
||||
// [Note: Getting the cache path]
|
||||
//
|
||||
// The existing code was passing "cache" as a parameter to getPath.
|
||||
//
|
||||
// However, "cache" is not a valid parameter to getPath. It works! (for
|
||||
|
@ -309,8 +325,8 @@ const deleteLegacyDiskCacheDirIfExists = async () => {
|
|||
// @ts-expect-error "cache" works but is not part of the public API.
|
||||
const cacheDir = path.join(app.getPath("cache"), "ente");
|
||||
if (existsSync(cacheDir)) {
|
||||
log.info(`Removing legacy disk cache from ${cacheDir}`);
|
||||
await fs.rm(cacheDir, { recursive: true });
|
||||
await removeIfExists(path.join(cacheDir, "thumbs"));
|
||||
await removeIfExists(path.join(cacheDir, "files"));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -375,7 +391,7 @@ const main = () => {
|
|||
// Continue on with the rest of the startup sequence.
|
||||
Menu.setApplicationMenu(await createApplicationMenu(mainWindow));
|
||||
setupTrayItem(mainWindow);
|
||||
if (!isDev) setupAutoUpdater(mainWindow);
|
||||
setupAutoUpdater(mainWindow);
|
||||
|
||||
try {
|
||||
await deleteLegacyDiskCacheDirIfExists();
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
updateOnNextRestart,
|
||||
} from "./services/app-update";
|
||||
import {
|
||||
legacyFaceCrop,
|
||||
openDirectory,
|
||||
openLogDirectory,
|
||||
selectDirectory,
|
||||
|
@ -198,6 +199,10 @@ export const attachIPCHandlers = () => {
|
|||
faceEmbedding(input),
|
||||
);
|
||||
|
||||
ipcMain.handle("legacyFaceCrop", (_, faceID: string) =>
|
||||
legacyFaceCrop(faceID),
|
||||
);
|
||||
|
||||
// - Upload
|
||||
|
||||
ipcMain.handle("listZipItems", (_, zipPath: string) =>
|
||||
|
|
|
@ -10,7 +10,6 @@ import { forceCheckForAppUpdates } from "./services/app-update";
|
|||
import autoLauncher from "./services/auto-launcher";
|
||||
import { openLogDirectory } from "./services/dir";
|
||||
import { userPreferences } from "./stores/user-preferences";
|
||||
import { isDev } from "./utils/electron";
|
||||
|
||||
/** Create and return the entries in the app's main menu bar */
|
||||
export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
|
||||
|
@ -24,9 +23,6 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
|
|||
const macOSOnly = (options: MenuItemConstructorOptions[]) =>
|
||||
process.platform == "darwin" ? options : [];
|
||||
|
||||
const devOnly = (options: MenuItemConstructorOptions[]) =>
|
||||
isDev ? options : [];
|
||||
|
||||
const handleCheckForUpdates = () => forceCheckForAppUpdates(mainWindow);
|
||||
|
||||
const handleViewChangelog = () =>
|
||||
|
@ -130,11 +126,11 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
|
|||
submenu: [
|
||||
{
|
||||
role: "startSpeaking",
|
||||
label: "start speaking",
|
||||
label: "Start Speaking",
|
||||
},
|
||||
{
|
||||
role: "stopSpeaking",
|
||||
label: "stop speaking",
|
||||
label: "Stop Speaking",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -145,9 +141,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
|
|||
label: "View",
|
||||
submenu: [
|
||||
{ label: "Reload", role: "reload" },
|
||||
...devOnly([
|
||||
{ label: "Toggle Dev Tools", role: "toggleDevTools" },
|
||||
]),
|
||||
{ label: "Toggle Dev Tools", role: "toggleDevTools" },
|
||||
{ type: "separator" },
|
||||
{ label: "Toggle Full Screen", role: "togglefullscreen" },
|
||||
],
|
||||
|
|
|
@ -6,11 +6,20 @@ import { allowWindowClose } from "../../main";
|
|||
import { AppUpdate } from "../../types/ipc";
|
||||
import log from "../log";
|
||||
import { userPreferences } from "../stores/user-preferences";
|
||||
import { isDev } from "../utils/electron";
|
||||
|
||||
export const setupAutoUpdater = (mainWindow: BrowserWindow) => {
|
||||
autoUpdater.logger = electronLog;
|
||||
autoUpdater.autoDownload = false;
|
||||
|
||||
// Skip checking for updates automatically in dev builds. Installing an
|
||||
// update would fail anyway since (at least on macOS), the auto update
|
||||
// process requires signed builds.
|
||||
//
|
||||
// Even though this is skipped on app start, we can still use the "Check for
|
||||
// updates..." menu option to trigger the update if we wish in dev builds.
|
||||
if (isDev) return;
|
||||
|
||||
const oneDay = 1 * 24 * 60 * 60 * 1000;
|
||||
setInterval(() => void checkForUpdatesAndNotify(mainWindow), oneDay);
|
||||
void checkForUpdatesAndNotify(mainWindow);
|
||||
|
|
|
@ -27,14 +27,14 @@ class AutoLauncher {
|
|||
}
|
||||
|
||||
async toggleAutoLaunch() {
|
||||
const isEnabled = await this.isEnabled();
|
||||
const wasEnabled = await this.isEnabled();
|
||||
const autoLaunch = this.autoLaunch;
|
||||
if (autoLaunch) {
|
||||
if (isEnabled) await autoLaunch.disable();
|
||||
if (wasEnabled) await autoLaunch.disable();
|
||||
else await autoLaunch.enable();
|
||||
} else {
|
||||
if (isEnabled) app.setLoginItemSettings({ openAtLogin: false });
|
||||
else app.setLoginItemSettings({ openAtLogin: true });
|
||||
const openAtLogin = !wasEnabled;
|
||||
app.setLoginItemSettings({ openAtLogin });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,8 +42,7 @@ class AutoLauncher {
|
|||
if (this.autoLaunch) {
|
||||
return app.commandLine.hasSwitch("hidden");
|
||||
} else {
|
||||
// TODO(MR): This apparently doesn't work anymore.
|
||||
return app.getLoginItemSettings().wasOpenedAtLogin;
|
||||
return app.getLoginItemSettings().openAtLogin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { shell } from "electron/common";
|
||||
import { app, dialog } from "electron/main";
|
||||
import { existsSync } from "fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { posixPath } from "../utils/electron";
|
||||
|
||||
|
@ -38,14 +40,50 @@ export const openLogDirectory = () => openDirectory(logDirectoryPath());
|
|||
*
|
||||
* [Note: Electron app paths]
|
||||
*
|
||||
* By default, these paths are at the following locations:
|
||||
* There are three paths we need to be aware of usually.
|
||||
*
|
||||
* - macOS: `~/Library/Application Support/ente`
|
||||
* First is the "appData". We can obtain this with `app.getPath("appData")`.
|
||||
* This is per-user application data directory. This is usually the following:
|
||||
*
|
||||
* - Windows: `%APPDATA%`, e.g. `C:\Users\<username>\AppData\Local`
|
||||
* - Linux: `~/.config`
|
||||
* - macOS: `~/Library/Application Support`
|
||||
*
|
||||
* Now, if we suffix the app's name onto the appData directory, we get the
|
||||
* "userData" directory. This is the **primary** place applications are meant to
|
||||
* store user's data, e.g. various configuration files and saved state.
|
||||
*
|
||||
* During development, our app name is "Electron", so this'd be, for example,
|
||||
* `~/Library/Application Support/Electron` if we run using `yarn dev`. For the
|
||||
* packaged production app, our app name is "ente", so this would be:
|
||||
*
|
||||
* - Windows: `%APPDATA%\ente`, e.g. `C:\Users\<username>\AppData\Local\ente`
|
||||
* - Linux: `~/.config/ente`
|
||||
* - Windows: `%APPDATA%`, e.g. `C:\Users\<username>\AppData\Local\ente`
|
||||
* - Windows: C:\Users\<you>\AppData\Local\<Your App Name>
|
||||
* - macOS: `~/Library/Application Support/ente`
|
||||
*
|
||||
* Note that Chromium also stores the browser state, e.g. localStorage or disk
|
||||
* caches, in userData.
|
||||
*
|
||||
* Finally, there is the "logs" directory. This is not within "appData" but has
|
||||
* a slightly different OS specific path. Since our log file is named
|
||||
* "ente.log", it can be found at:
|
||||
*
|
||||
* - macOS: ~/Library/Logs/ente/ente.log (production)
|
||||
* - macOS: ~/Library/Logs/Electron/ente.log (dev)
|
||||
*
|
||||
* https://www.electronjs.org/docs/latest/api/app
|
||||
*
|
||||
*/
|
||||
const logDirectoryPath = () => app.getPath("logs");
|
||||
|
||||
/**
|
||||
* See: [Note: Legacy face crops]
|
||||
*/
|
||||
export const legacyFaceCrop = async (
|
||||
faceID: string,
|
||||
): Promise<Uint8Array | undefined> => {
|
||||
// See: [Note: Getting the cache path]
|
||||
// @ts-expect-error "cache" works but is not part of the public API.
|
||||
const cacheDir = path.join(app.getPath("cache"), "ente");
|
||||
const filePath = path.join(cacheDir, "face-crops", faceID);
|
||||
return existsSync(filePath) ? await fs.readFile(filePath) : undefined;
|
||||
};
|
||||
|
|
|
@ -14,6 +14,15 @@ export const clearStores = () => {
|
|||
watchStore.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* [Note: Safe storage keys]
|
||||
*
|
||||
* On macOS, `safeStorage` stores our data under a Keychain entry named
|
||||
* "<app-name> Safe Storage". Which resolves to:
|
||||
*
|
||||
* - Electron Safe Storage (dev)
|
||||
* - ente Safe Storage (prod)
|
||||
*/
|
||||
export const saveEncryptionKey = (encryptionKey: string) => {
|
||||
const encryptedKey = safeStorage.encryptString(encryptionKey);
|
||||
const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64");
|
||||
|
|
|
@ -164,6 +164,9 @@ const detectFaces = (input: Float32Array) =>
|
|||
const faceEmbedding = (input: Float32Array) =>
|
||||
ipcRenderer.invoke("faceEmbedding", input);
|
||||
|
||||
const legacyFaceCrop = (faceID: string) =>
|
||||
ipcRenderer.invoke("legacyFaceCrop", faceID);
|
||||
|
||||
// - Watch
|
||||
|
||||
const watchGet = () => ipcRenderer.invoke("watchGet");
|
||||
|
@ -341,6 +344,7 @@ contextBridge.exposeInMainWorld("electron", {
|
|||
clipTextEmbeddingIfAvailable,
|
||||
detectFaces,
|
||||
faceEmbedding,
|
||||
legacyFaceCrop,
|
||||
|
||||
// - Watch
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
getFaceSearchEnabledStatus,
|
||||
updateFaceSearchEnabledStatus,
|
||||
} from "services/userService";
|
||||
import { isInternalUser } from "utils/user";
|
||||
import { isInternalUserForML } from "utils/user";
|
||||
|
||||
export const MLSearchSettings = ({ open, onClose, onRootClose }) => {
|
||||
const {
|
||||
|
@ -280,7 +280,7 @@ function EnableMLSearch({ onClose, enableMlSearch, onRootClose }) {
|
|||
</p>
|
||||
</Typography>
|
||||
</Box>
|
||||
{isInternalUser() && (
|
||||
{isInternalUserForML() && (
|
||||
<Stack px={"8px"} spacing={"8px"}>
|
||||
<Button
|
||||
color={"accent"}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import { cachedOrNew } from "@/next/blob-cache";
|
||||
import { ensureLocalUser } from "@/next/local-user";
|
||||
import log from "@/next/log";
|
||||
import { Skeleton, styled } from "@mui/material";
|
||||
import { Legend } from "components/PhotoViewer/styledComponents/Legend";
|
||||
import { t } from "i18next";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import machineLearningService from "services/machineLearning/machineLearningService";
|
||||
import { EnteFile } from "types/file";
|
||||
import { Face, Person } from "types/machineLearning";
|
||||
import { getPeopleList, getUnidentifiedFaces } from "utils/machineLearning";
|
||||
|
@ -61,7 +58,7 @@ export const PeopleList = React.memo((props: PeopleListProps) => {
|
|||
}
|
||||
>
|
||||
<FaceCropImageView
|
||||
faceId={person.displayFaceId}
|
||||
faceID={person.displayFaceId}
|
||||
cacheKey={person.faceCropCacheKey}
|
||||
/>
|
||||
</FaceChip>
|
||||
|
@ -140,7 +137,7 @@ export function UnidentifiedFaces(props: {
|
|||
faces.map((face, index) => (
|
||||
<FaceChip key={index}>
|
||||
<FaceCropImageView
|
||||
faceId={face.id}
|
||||
faceID={face.id}
|
||||
cacheKey={face.crop?.cacheKey}
|
||||
/>
|
||||
</FaceChip>
|
||||
|
@ -151,20 +148,24 @@ export function UnidentifiedFaces(props: {
|
|||
}
|
||||
|
||||
interface FaceCropImageViewProps {
|
||||
faceId: string;
|
||||
faceID: string;
|
||||
cacheKey?: string;
|
||||
}
|
||||
|
||||
const FaceCropImageView: React.FC<FaceCropImageViewProps> = ({
|
||||
faceId,
|
||||
faceID,
|
||||
cacheKey,
|
||||
}) => {
|
||||
const [objectURL, setObjectURL] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
let didCancel = false;
|
||||
const electron = globalThis.electron;
|
||||
|
||||
if (cacheKey) {
|
||||
if (faceID && electron) {
|
||||
electron
|
||||
.legacyFaceCrop(faceID)
|
||||
/*
|
||||
cachedOrNew("face-crops", cacheKey, async () => {
|
||||
const user = await ensureLocalUser();
|
||||
return machineLearningService.regenerateFaceCrop(
|
||||
|
@ -172,16 +173,20 @@ const FaceCropImageView: React.FC<FaceCropImageViewProps> = ({
|
|||
user.id,
|
||||
faceId,
|
||||
);
|
||||
}).then((blob) => {
|
||||
if (!didCancel) setObjectURL(URL.createObjectURL(blob));
|
||||
});
|
||||
})*/
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
const blob = new Blob([data]);
|
||||
if (!didCancel) setObjectURL(URL.createObjectURL(blob));
|
||||
}
|
||||
});
|
||||
} else setObjectURL(undefined);
|
||||
|
||||
return () => {
|
||||
didCancel = true;
|
||||
if (objectURL) URL.revokeObjectURL(objectURL);
|
||||
};
|
||||
}, [faceId, cacheKey]);
|
||||
}, [faceID, cacheKey]);
|
||||
|
||||
return objectURL ? (
|
||||
<img src={objectURL} />
|
||||
|
|
|
@ -10,7 +10,7 @@ import mlIDbStorage, {
|
|||
ML_SYNC_CONFIG_NAME,
|
||||
ML_SYNC_JOB_CONFIG_NAME,
|
||||
} from "utils/storage/mlIDbStorage";
|
||||
import { isInternalUser } from "utils/user";
|
||||
import { isInternalUserForML } from "utils/user";
|
||||
|
||||
export async function getMLSyncJobConfig() {
|
||||
return mlIDbStorage.getConfig(
|
||||
|
@ -24,7 +24,7 @@ export async function getMLSyncConfig() {
|
|||
}
|
||||
|
||||
export async function getMLSearchConfig() {
|
||||
if (isInternalUser()) {
|
||||
if (isInternalUserForML()) {
|
||||
return mlIDbStorage.getConfig(
|
||||
ML_SEARCH_CONFIG_NAME,
|
||||
DEFAULT_ML_SEARCH_CONFIG,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { getData, LS_KEYS } from "@ente/shared/storage/localStorage";
|
||||
import type { User } from "@ente/shared/user/types";
|
||||
import { UserDetails } from "types/user";
|
||||
|
||||
export function getLocalUserDetails(): UserDetails {
|
||||
|
@ -9,7 +10,12 @@ export const isInternalUser = () => {
|
|||
const userEmail = getData(LS_KEYS.USER)?.email;
|
||||
if (!userEmail) return false;
|
||||
|
||||
return (
|
||||
userEmail.endsWith("@ente.io") || userEmail === "kr.anand619@gmail.com"
|
||||
);
|
||||
return userEmail.endsWith("@ente.io");
|
||||
};
|
||||
|
||||
export const isInternalUserForML = () => {
|
||||
const userId = (getData(LS_KEYS.USER) as User)?.id;
|
||||
if (userId == 1) return true;
|
||||
|
||||
return isInternalUser();
|
||||
};
|
||||
|
|
|
@ -346,6 +346,28 @@ export interface Electron {
|
|||
*/
|
||||
faceEmbedding: (input: Float32Array) => Promise<Float32Array>;
|
||||
|
||||
/**
|
||||
* Return a face crop stored by a previous version of ML.
|
||||
*
|
||||
* [Note: Legacy face crops]
|
||||
*
|
||||
* Older versions of ML generated and stored face crops in a "face-crops"
|
||||
* cache directory on the Electron side. For the time being, we have
|
||||
* disabled the face search whilst we put finishing touches to it. However,
|
||||
* it'll be nice to still show the existing faces that have been clustered
|
||||
* for people who opted in to the older beta.
|
||||
*
|
||||
* So we retain the older "face-crops" disk cache, and use this method to
|
||||
* serve faces from it when needed.
|
||||
*
|
||||
* @param faceID An identifier corresponding to which the face crop had been
|
||||
* stored by the older version of our app.
|
||||
*
|
||||
* @returns the JPEG data of the face crop if a file is found for the given
|
||||
* {@link faceID}, otherwise undefined.
|
||||
*/
|
||||
legacyFaceCrop: (faceID: string) => Promise<Uint8Array | undefined>;
|
||||
|
||||
// - Watch
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Reference in a new issue