Merge branch 'main' into sepa-fix
This commit is contained in:
commit
628565cfaf
45 changed files with 1113 additions and 1773 deletions
|
@ -1 +0,0 @@
|
|||
ui/*
|
|
@ -1,55 +0,0 @@
|
|||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"google",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"indent": "off",
|
||||
"class-methods-use-this": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/display-name": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error"],
|
||||
"require-jsdoc": "off",
|
||||
"valid-jsdoc": "off",
|
||||
"max-len": "off",
|
||||
"new-cap": "off",
|
||||
"no-invalid-this": "off",
|
||||
"eqeqeq": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"space-before-function-paren": "off",
|
||||
"operator-linebreak": [
|
||||
"error",
|
||||
"after",
|
||||
{ "overrides": { "?": "before", ":": "before" } }
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"JSX": "readonly",
|
||||
"NodeJS": "readonly",
|
||||
"ReadableStreamDefaultController": "readonly"
|
||||
}
|
||||
}
|
31
desktop/.eslintrc.js
Normal file
31
desktop/.eslintrc.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
/* eslint-env node */
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
/* What we really want eventually */
|
||||
// "plugin:@typescript-eslint/strict-type-checked",
|
||||
// "plugin:@typescript-eslint/stylistic-type-checked",
|
||||
],
|
||||
/* Temporarily disable some rules
|
||||
Enhancement: Remove me */
|
||||
rules: {
|
||||
"no-unused-vars": "off",
|
||||
},
|
||||
/* Temporarily add a global
|
||||
Enhancement: Remove me */
|
||||
globals: {
|
||||
NodeJS: "readonly",
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
root: true,
|
||||
ignorePatterns: [".eslintrc.js", "app", "out", "dist"],
|
||||
env: {
|
||||
es2022: true,
|
||||
node: true,
|
||||
},
|
||||
};
|
3
desktop/.gitignore
vendored
3
desktop/.gitignore
vendored
|
@ -14,7 +14,8 @@ node_modules/
|
|||
# tsc transpiles src/**/*.ts and emits the generated JS into app
|
||||
app/
|
||||
|
||||
# out is a symlink to the photos web app's dir
|
||||
# out is a symlink to the photos web app's out dir, which contains the built up
|
||||
# photos app.
|
||||
out
|
||||
|
||||
# electron-builder
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"proseWrap": "always",
|
||||
"plugins": [
|
||||
"prettier-plugin-organize-imports",
|
||||
"prettier-plugin-packagejson"
|
||||
|
|
|
@ -131,7 +131,8 @@
|
|||
|
||||
### Photo Editor
|
||||
|
||||
Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/) to know about feature and functionalities.
|
||||
Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/)
|
||||
to know about feature and functionalities.
|
||||
|
||||
## v1.6.47
|
||||
|
||||
|
@ -146,15 +147,19 @@ Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/)
|
|||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixes OOM crashes during file upload [#1379](https://github.com/ente-io/photos-web/pull/1379)
|
||||
- Fixes OOM crashes during file upload
|
||||
[#1379](https://github.com/ente-io/photos-web/pull/1379)
|
||||
|
||||
## v1.6.45
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fixed app keeps reloading issue [#235](https://github.com/ente-io/photos-desktop/pull/235)
|
||||
- Fixed dng and arw preview issue [#1378](https://github.com/ente-io/photos-web/pull/1378)
|
||||
- Added view crash report option (help menu) for user to share electron crash report locally
|
||||
- Fixed app keeps reloading issue
|
||||
[#235](https://github.com/ente-io/photos-desktop/pull/235)
|
||||
- Fixed dng and arw preview issue
|
||||
[#1378](https://github.com/ente-io/photos-web/pull/1378)
|
||||
- Added view crash report option (help menu) for user to share electron crash
|
||||
report locally
|
||||
|
||||
## v1.6.44
|
||||
|
||||
|
@ -166,23 +171,28 @@ Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/)
|
|||
|
||||
- #### Check for update and changelog option
|
||||
|
||||
Added options to check for update manually and a view changelog via the app menubar
|
||||
Added options to check for update manually and a view changelog via the app
|
||||
menubar
|
||||
|
||||
- #### Opt out of crash reporting
|
||||
|
||||
Added option to out of a crash reporting, it can accessed from the settings -> preferences -> disable crash reporting
|
||||
Added option to out of a crash reporting, it can accessed from the settings
|
||||
-> preferences -> disable crash reporting
|
||||
|
||||
- #### Type search
|
||||
|
||||
Added new search option to search files based on file type i.e, image, video, live-photo.
|
||||
Added new search option to search files based on file type i.e, image,
|
||||
video, live-photo.
|
||||
|
||||
- #### Manual Convert Button
|
||||
|
||||
In case the video is not playable, Now there is a convert button which can be used to trigger conversion of the video to supported format.
|
||||
In case the video is not playable, Now there is a convert button which can
|
||||
be used to trigger conversion of the video to supported format.
|
||||
|
||||
- #### File Download Progress
|
||||
|
||||
The file loader now also shows the exact percentage download progress, instead of just a simple loader.
|
||||
The file loader now also shows the exact percentage download progress,
|
||||
instead of just a simple loader.
|
||||
|
||||
- #### Bug fixes & other enhancements
|
||||
|
||||
|
@ -198,16 +208,19 @@ Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/)
|
|||
|
||||
- #### Email verification
|
||||
|
||||
We have now made email verification optional, so you can sign in with just your email address and password, without waiting for a verification code.
|
||||
We have now made email verification optional, so you can sign in with just
|
||||
your email address and password, without waiting for a verification code.
|
||||
|
||||
You can opt in / out of email verification from Settings > Security.
|
||||
|
||||
- #### Download Album
|
||||
|
||||
You can now chose the download location for downloading albums. Along with that we have also added progress bar for album download.
|
||||
You can now chose the download location for downloading albums. Along with
|
||||
that we have also added progress bar for album download.
|
||||
|
||||
- #### Bug fixes & other enhancements
|
||||
|
||||
We have squashed a few pesky bugs that were reported by our community
|
||||
|
||||
If you would like to help us improve ente, come join the party @ ente.io/community!
|
||||
If you would like to help us improve ente, come join the party @
|
||||
ente.io/community!
|
||||
|
|
|
@ -2,14 +2,20 @@
|
|||
|
||||
The sweetness of Ente Photos, right on your computer. Linux, Windows and macOS.
|
||||
|
||||
You can [**download** a pre-built binary from
|
||||
releases](https://github.com/ente-io/photos-desktop/releases/latest).
|
||||
You can
|
||||
[**download** a pre-built binary from releases](https://github.com/ente-io/photos-desktop/releases/latest).
|
||||
|
||||
To know more about Ente, see [our main README](../README.md) or visit
|
||||
[ente.io](https://ente.io).
|
||||
|
||||
## Building from source
|
||||
|
||||
Fetch submodules
|
||||
|
||||
```sh
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Install dependencies
|
||||
|
||||
```sh
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ente Photos</title>
|
||||
</head>
|
||||
|
||||
<body style="background-color: black">
|
||||
<div
|
||||
style="
|
||||
height: 95vh;
|
||||
width: 96vw;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: white;
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<div style="margin-bottom: 10px">
|
||||
Site unreachable, please try again later
|
||||
</div>
|
||||
<button onClick="window[`ElectronAPIs`].reloadWindow()">
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,14 +1,61 @@
|
|||
# Dependencies
|
||||
|
||||
See [web/docs/dependencies.md](../../web/docs/dependencies.md) for general web
|
||||
specific dependencies. See [electron.md](electron.md) for our main dependency,
|
||||
Electron. The rest of this document describes the remaining, desktop specific
|
||||
dependencies that are used by the Photos desktop app.
|
||||
## Electron
|
||||
|
||||
## Electron related
|
||||
[Electron](https://www.electronjs.org) is a cross-platform (Linux, Windows,
|
||||
macOS) way for creating desktop apps using TypeScript.
|
||||
|
||||
Electron embeds Chromium and Node.js in the generated app's binary. The
|
||||
generated app thus consists of two separate processes - the _main_ process, and
|
||||
a _renderer_ process.
|
||||
|
||||
- The _main_ process is runs the embedded node. This process can deal with the
|
||||
host OS - it is conceptually like a `node` repl running on your machine. In
|
||||
our case, the TypeScript code (in the `src/` directory) gets transpiled by
|
||||
`tsc` into JavaScript in the `build/app/` directory, which gets bundled in
|
||||
the generated app's binary and is loaded by the node (main) process when the
|
||||
app starts.
|
||||
|
||||
- The _renderer_ process is a regular web app that gets loaded into the
|
||||
embedded Chromium. When the main process starts, it creates a new "window"
|
||||
that shows this embedded Chromium. In our case, we build and bundle a static
|
||||
export of the [Photos web app](../web/README.md) in the generated app. This
|
||||
gets loaded by the embedded Chromium at runtime, acting as the app's UI.
|
||||
|
||||
There is also a third environment that gets temporarily created:
|
||||
|
||||
- The [preload script](../src/preload.ts) acts as a gateway between the _main_
|
||||
and the _renderer_ process. It runs in its own isolated environment.
|
||||
|
||||
### electron-builder
|
||||
|
||||
[Electron Builder](https://www.electron.build) is used for packaging the app for
|
||||
distribution.
|
||||
|
||||
During the build it uses
|
||||
[electron-builder-notarize](https://github.com/karaggeorge/electron-builder-notarize)
|
||||
to notarize the macOS binary.
|
||||
|
||||
### next-electron-server
|
||||
|
||||
This spins up a server for serving files using a protocol handler inside our
|
||||
Electron process. This allows us to directly use the output produced by `next
|
||||
build` for loading into our renderer process.
|
||||
Electron process. This allows us to directly use the output produced by
|
||||
`next build` for loading into our renderer process.
|
||||
|
||||
### electron-reload
|
||||
|
||||
Reloads contents of the BrowserWindow (renderer process) when source files are
|
||||
changed.
|
||||
|
||||
* TODO (MR): Do we need this? Isn't the next-electron-server HMR covering this?
|
||||
|
||||
## DX
|
||||
|
||||
See [web/docs/dependencies#DX](../../web/docs/dependencies.md#dx) for the
|
||||
general development experience related dependencies like TypeScript etc, which
|
||||
are similar to that in the web code.
|
||||
|
||||
Some extra ones specific to the code here are:
|
||||
|
||||
* [concurrently](https://github.com/open-cli-tools/concurrently) for spawning
|
||||
parallel tasks when we do `yarn dev`.
|
||||
|
|
|
@ -1,4 +1,41 @@
|
|||
# Development tips
|
||||
# Development
|
||||
|
||||
- `yarn build:quick` is a variant of `yarn build` that uses the
|
||||
`--config.compression=store` flag to (slightly) speed up electron-builder.
|
||||
## Yarn commands
|
||||
|
||||
### yarn dev
|
||||
|
||||
Launch the app in development mode
|
||||
|
||||
- Runs a development server for the renderer, with HMR.
|
||||
|
||||
- Starts tsc in watch mode to recompile the JS files used by the main process.
|
||||
|
||||
- Starts the main process, reloading it on changes to the the TS files in
|
||||
`src/`.
|
||||
|
||||
### yarn build
|
||||
|
||||
Build a binary for your current platform.
|
||||
|
||||
Note that our actual releases use a
|
||||
[GitHub workflow](../.github/workflows/desktop-release.yml) that is similar to
|
||||
this, except it builds binaries for all the supported OSes and uses production
|
||||
signing credentials.
|
||||
|
||||
During development, you might find `yarn build:quick` helpful. It is a variant
|
||||
of `yarn build` that omits some steps to build a binary quicker, something that
|
||||
can be useful during development.
|
||||
|
||||
### postinstall
|
||||
|
||||
When using native node modules (those written in C/C++), we need to ensure they
|
||||
are built against `electron`'s packaged `node` version. We use
|
||||
[electron-builder](https://www.electron.build/cli)'s `install-app-deps` command
|
||||
to rebuild those modules automatically after each `yarn install` by invoking it
|
||||
in as the `postinstall` step in our package.json.
|
||||
|
||||
### lint and lint-fix
|
||||
|
||||
Use `yarn lint` to check that your code formatting is as expected, and that
|
||||
there are no linter errors. Use `yarn lint-fix` to try and automatically fix the
|
||||
issues.
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
# Electron
|
||||
|
||||
[Electron](https://www.electronjs.org) is a cross-platform (Linux, Windows,
|
||||
macOS) way for creating desktop apps using TypeScript.
|
||||
|
||||
Electron embeds Chromium and Node.js in the generated app's binary. The
|
||||
generated app thus consists of two separate processes - the _main_ process, and
|
||||
a _renderer_ process.
|
||||
|
||||
- The _main_ process is runs the embedded node. This process can deal with the
|
||||
host OS - it is conceptually like a `node` repl running on your machine. In our
|
||||
case, the TypeScript code (in the `src/` directory) gets transpiled by `tsc`
|
||||
into JavaScript in the `build/app/` directory, which gets bundled in the
|
||||
generated app's binary and is loaded by the node (main) process when the app
|
||||
starts.
|
||||
|
||||
- The _renderer_ process is a regular web app that gets loaded into the embedded
|
||||
Chromium. When the main process starts, it creates a new "window" that shows
|
||||
this embedded Chromium. In our case, we build and bundle a static export of
|
||||
the [Photos web app](../web/README.md) in the generated app. This gets loaded
|
||||
by the embedded Chromium at runtime, acting as the app's UI.
|
|
@ -20,11 +20,11 @@ So the process for doing a release would be.
|
|||
|
||||
4. Commit and push to remote
|
||||
|
||||
```sh
|
||||
git add package.json && git commit -m 'Release v1.x.x'
|
||||
git tag v1.x.x
|
||||
git push && git push --tags
|
||||
```
|
||||
```sh
|
||||
git add package.json && git commit -m 'Release v1.x.x'
|
||||
git tag v1.x.x
|
||||
git push && git push --tags
|
||||
```
|
||||
|
||||
This by itself will already trigger a new release. The GitHub action will create
|
||||
a new draft release that can then be used as descibed below.
|
||||
|
@ -42,9 +42,9 @@ To wrap up, we also need to merge back these changes into main. So for that,
|
|||
The GitHub Action runs on Windows, Linux and macOS. It produces the artifacts
|
||||
defined in the `build` value in `package.json`.
|
||||
|
||||
* Windows - An NSIS installer.
|
||||
* Linux - An AppImage, and 3 other packages (`.rpm`, `.deb`, `.pacman`)
|
||||
* macOS - A universal DMG
|
||||
- Windows - An NSIS installer.
|
||||
- Linux - An AppImage, and 3 other packages (`.rpm`, `.deb`, `.pacman`)
|
||||
- macOS - A universal DMG
|
||||
|
||||
Additionally, the GitHub action notarizes the macOS DMG. For this it needs
|
||||
credentials provided via GitHub secrets.
|
||||
|
@ -70,19 +70,19 @@ If everything goes well, we'll have a release on GitHub, and the corresponding
|
|||
source maps for the renderer process uploaded to Sentry. There isn't anything
|
||||
else to do:
|
||||
|
||||
* The website automatically redirects to the latest release on GitHub when
|
||||
people try to download.
|
||||
- The website automatically redirects to the latest release on GitHub when
|
||||
people try to download.
|
||||
|
||||
* The file formats with support auto update (Windows `exe`, the Linux AppImage
|
||||
and the macOS DMG) also check the latest GitHub release automatically to
|
||||
download and apply the update (the rest of the formats don't support auto
|
||||
updates).
|
||||
- The file formats with support auto update (Windows `exe`, the Linux AppImage
|
||||
and the macOS DMG) also check the latest GitHub release automatically to
|
||||
download and apply the update (the rest of the formats don't support auto
|
||||
updates).
|
||||
|
||||
* We're not putting the desktop app in other stores currently. It is available
|
||||
as a `brew cask`, but we only had to open a PR to add the initial formula, now
|
||||
their maintainers automatically bump the SHA, version number and the (derived
|
||||
from the version) URL in the formula when their tools notice a new release on
|
||||
our GitHub.
|
||||
- We're not putting the desktop app in other stores currently. It is available
|
||||
as a `brew cask`, but we only had to open a PR to add the initial formula,
|
||||
now their maintainers automatically bump the SHA, version number and the
|
||||
(derived from the version) URL in the formula when their tools notice a new
|
||||
release on our GitHub.
|
||||
|
||||
We can also publish the draft releases by checking the "pre-release" option.
|
||||
Such releases don't cause any of the channels (our website, or the desktop app
|
||||
|
|
43
desktop/electron-builder.yml
Normal file
43
desktop/electron-builder.yml
Normal file
|
@ -0,0 +1,43 @@
|
|||
appId: io.ente.bhari-frame
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
nsis:
|
||||
deleteAppDataOnUninstall: true
|
||||
linux:
|
||||
target:
|
||||
- target: AppImage
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: deb
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: rpm
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: pacman
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
icon: ./resources/icon.icns
|
||||
category: Photography
|
||||
mac:
|
||||
target:
|
||||
target: default
|
||||
arch:
|
||||
- universal
|
||||
category: public.app-category.photography
|
||||
hardenedRuntime: true
|
||||
x64ArchFiles: Contents/Resources/ggmlclip-mac
|
||||
afterSign: electron-builder-notarize
|
||||
asarUnpack:
|
||||
- node_modules/ffmpeg-static/bin/${os}/${arch}/ffmpeg
|
||||
- node_modules/ffmpeg-static/index.js
|
||||
- node_modules/ffmpeg-static/package.json
|
||||
extraFiles:
|
||||
- from: build
|
||||
to: resources
|
||||
files:
|
||||
- app/**/*
|
||||
- out
|
|
@ -8,7 +8,7 @@
|
|||
"scripts": {
|
||||
"build": "yarn build-renderer && yarn build-main",
|
||||
"build-main": "tsc && electron-builder",
|
||||
"build-main:quick": "tsc && electron-builder --config.compression=store",
|
||||
"build-main:quick": "tsc && electron-builder --dir --config.compression=store --config.mac.identity=null",
|
||||
"build-renderer": "cd ../web && yarn install && yarn build:photos && cd ../desktop && rm -f out && ln -sf ../web/apps/photos/out",
|
||||
"build:quick": "yarn build-renderer && yarn build-main:quick",
|
||||
"dev": "concurrently --names 'main,rndr,tscw' \"yarn dev-main\" \"yarn dev-renderer\" \"yarn dev-main-watch\"",
|
||||
|
@ -16,8 +16,8 @@
|
|||
"dev-main-watch": "tsc --watch --preserveWatchOutput",
|
||||
"dev-renderer": "cd ../web && yarn install && yarn dev:photos",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"lint": "yarn prettier --check . && eslint \"src/**/*.ts\"",
|
||||
"lint-fix": "yarn prettier --write . && eslint --fix src"
|
||||
"lint": "yarn prettier --check . && eslint --ext .ts src",
|
||||
"lint-fix": "yarn prettier --write . && eslint --fix --ext .ts src"
|
||||
},
|
||||
"dependencies": {
|
||||
"any-shell-escape": "^0.1.1",
|
||||
|
@ -42,94 +42,19 @@
|
|||
"@types/auto-launch": "^5.0.2",
|
||||
"@types/ffmpeg-static": "^3.0.1",
|
||||
"@types/get-folder-size": "^2.0.0",
|
||||
"@types/node": "18.15.0",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/promise-fs": "^2.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.28.0",
|
||||
"@typescript-eslint/parser": "^5.28.0",
|
||||
"concurrently": "^7.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7",
|
||||
"@typescript-eslint/parser": "^7",
|
||||
"concurrently": "^8",
|
||||
"electron": "^25.8.4",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-builder-notarize": "^1.2.0",
|
||||
"electron-download": "^4.1.1",
|
||||
"eslint": "^7.23.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint": "^8",
|
||||
"prettier": "^3",
|
||||
"prettier-plugin-organize-imports": "^3.2",
|
||||
"prettier-plugin-packagejson": "^2.4",
|
||||
"typescript": "^4.2.3"
|
||||
"typescript": "^5"
|
||||
},
|
||||
"build": {
|
||||
"appId": "io.ente.bhari-frame",
|
||||
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
||||
"nsis": {
|
||||
"deleteAppDataOnUninstall": true
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
{
|
||||
"target": "AppImage",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "deb",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "rpm",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "pacman",
|
||||
"arch": [
|
||||
"x64",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"icon": "./resources/icon.icns",
|
||||
"category": "Photography"
|
||||
},
|
||||
"mac": {
|
||||
"target": {
|
||||
"target": "default",
|
||||
"arch": [
|
||||
"universal"
|
||||
]
|
||||
},
|
||||
"category": "public.app-category.photography",
|
||||
"hardenedRuntime": true,
|
||||
"x64ArchFiles": "Contents/Resources/ggmlclip-mac"
|
||||
},
|
||||
"afterSign": "electron-builder-notarize",
|
||||
"asarUnpack": [
|
||||
"node_modules/ffmpeg-static/bin/${os}/${arch}/ffmpeg",
|
||||
"node_modules/ffmpeg-static/index.js",
|
||||
"node_modules/ffmpeg-static/package.json"
|
||||
],
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "build",
|
||||
"to": "resources"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"app/**/*",
|
||||
"out"
|
||||
]
|
||||
},
|
||||
"productName": "ente",
|
||||
"standard": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
"productName": "ente"
|
||||
}
|
||||
|
|
|
@ -5,11 +5,7 @@ import { DiskCache } from "../services/diskCache";
|
|||
|
||||
const ENTE_CACHE_DIR_NAME = "ente";
|
||||
|
||||
export const getCacheDirectory = async () => {
|
||||
const customCacheDir = await getCustomCacheDirectory();
|
||||
if (customCacheDir && existsSync(customCacheDir)) {
|
||||
return customCacheDir;
|
||||
}
|
||||
const getCacheDirectory = async () => {
|
||||
const defaultSystemCacheDir = await ipcRenderer.invoke("get-path", "cache");
|
||||
return path.join(defaultSystemCacheDir, ENTE_CACHE_DIR_NAME);
|
||||
};
|
||||
|
@ -40,13 +36,3 @@ export async function deleteDiskCache(cacheName: string) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function setCustomCacheDirectory(
|
||||
directory: string,
|
||||
): Promise<void> {
|
||||
await ipcRenderer.invoke("set-custom-cache-directory", directory);
|
||||
}
|
||||
|
||||
async function getCustomCacheDirectory(): Promise<string> {
|
||||
return await ipcRenderer.invoke("get-custom-cache-directory");
|
||||
}
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
import { ipcRenderer } from "electron";
|
||||
import { writeStream } from "../services/fs";
|
||||
import { Model } from "../types";
|
||||
import { isExecError, parseExecError } from "../utils/error";
|
||||
|
||||
export async function computeImageEmbedding(
|
||||
model: Model,
|
||||
imageData: Uint8Array,
|
||||
): Promise<Float32Array> {
|
||||
let tempInputFilePath = null;
|
||||
try {
|
||||
tempInputFilePath = await ipcRenderer.invoke("get-temp-file-path", "");
|
||||
const imageStream = new Response(imageData.buffer).body;
|
||||
await writeStream(tempInputFilePath, imageStream);
|
||||
const embedding = await ipcRenderer.invoke(
|
||||
"compute-image-embedding",
|
||||
model,
|
||||
tempInputFilePath,
|
||||
);
|
||||
return embedding;
|
||||
} catch (err) {
|
||||
if (isExecError(err)) {
|
||||
const parsedExecError = parseExecError(err);
|
||||
throw Error(parsedExecError);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
if (tempInputFilePath) {
|
||||
await ipcRenderer.invoke("remove-temp-file", tempInputFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function computeTextEmbedding(
|
||||
model: Model,
|
||||
text: string,
|
||||
): Promise<Float32Array> {
|
||||
try {
|
||||
const embedding = await ipcRenderer.invoke(
|
||||
"compute-text-embedding",
|
||||
model,
|
||||
text,
|
||||
);
|
||||
return embedding;
|
||||
} catch (err) {
|
||||
if (isExecError(err)) {
|
||||
const parsedExecError = parseExecError(err);
|
||||
throw Error(parsedExecError);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,39 +1,5 @@
|
|||
import { ipcRenderer } from "electron/renderer";
|
||||
import { logError } from "../services/logging";
|
||||
|
||||
export const selectDirectory = async (): Promise<string> => {
|
||||
try {
|
||||
return await ipcRenderer.invoke("select-dir");
|
||||
} catch (e) {
|
||||
logError(e, "error while selecting root directory");
|
||||
}
|
||||
};
|
||||
|
||||
export const getAppVersion = async (): Promise<string> => {
|
||||
try {
|
||||
return await ipcRenderer.invoke("get-app-version");
|
||||
} catch (e) {
|
||||
logError(e, "failed to get release version");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const openDirectory = async (dirPath: string): Promise<void> => {
|
||||
try {
|
||||
await ipcRenderer.invoke("open-dir", dirPath);
|
||||
} catch (e) {
|
||||
logError(e, "error while opening directory");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getPlatform = async (): Promise<"mac" | "windows" | "linux"> => {
|
||||
try {
|
||||
return await ipcRenderer.invoke("get-platform");
|
||||
} catch (e) {
|
||||
logError(e, "failed to get platform");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export { logToDisk, openLogDirectory } from "../services/logging";
|
||||
|
|
|
@ -5,11 +5,3 @@ export async function getDirFiles(dirPath: string) {
|
|||
const electronFiles = await Promise.all(files.map(getElectronFile));
|
||||
return electronFiles;
|
||||
}
|
||||
export {
|
||||
deleteFile,
|
||||
deleteFolder,
|
||||
isFolder,
|
||||
moveFile,
|
||||
readTextFile,
|
||||
rename,
|
||||
} from "../services/fs";
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
import { ipcRenderer } from "electron";
|
||||
import { AppUpdateInfo } from "../types";
|
||||
|
||||
export const sendNotification = (content: string) => {
|
||||
ipcRenderer.send("send-notification", content);
|
||||
};
|
||||
export const reloadWindow = () => {
|
||||
ipcRenderer.send("reload-window");
|
||||
};
|
||||
|
||||
export const registerUpdateEventListener = (
|
||||
showUpdateDialog: (updateInfo: AppUpdateInfo) => void,
|
||||
) => {
|
||||
|
@ -23,15 +16,3 @@ export const registerForegroundEventListener = (onForeground: () => void) => {
|
|||
onForeground();
|
||||
});
|
||||
};
|
||||
|
||||
export const updateAndRestart = () => {
|
||||
ipcRenderer.send("update-and-restart");
|
||||
};
|
||||
|
||||
export const skipAppUpdate = (version: string) => {
|
||||
ipcRenderer.send("skip-app-update", version);
|
||||
};
|
||||
|
||||
export const muteUpdateNotification = (version: string) => {
|
||||
ipcRenderer.send("mute-update-notification", version);
|
||||
};
|
||||
|
|
|
@ -19,7 +19,6 @@ import {
|
|||
setupMainMenu,
|
||||
setupTrayItem,
|
||||
} from "./utils/main";
|
||||
import { setupMainProcessStatsLogger } from "./utils/processStats";
|
||||
|
||||
let mainWindow: BrowserWindow;
|
||||
|
||||
|
@ -104,7 +103,6 @@ if (!gotTheLock) {
|
|||
// Some APIs can only be used after this event occurs.
|
||||
app.on("ready", async () => {
|
||||
logSystemInfo();
|
||||
setupMainProcessStatsLogger();
|
||||
mainWindow = await createWindow();
|
||||
const tray = setupTrayItem(mainWindow);
|
||||
const watcher = initWatcher(mainWindow);
|
||||
|
|
|
@ -1,19 +1,39 @@
|
|||
import {
|
||||
deleteDiskCache,
|
||||
getCacheDirectory,
|
||||
openDiskCache,
|
||||
setCustomCacheDirectory,
|
||||
} from "./api/cache";
|
||||
import { computeImageEmbedding, computeTextEmbedding } from "./api/clip";
|
||||
import {
|
||||
getAppVersion,
|
||||
getPlatform,
|
||||
logToDisk,
|
||||
openDirectory,
|
||||
openLogDirectory,
|
||||
selectDirectory,
|
||||
} from "./api/common";
|
||||
import { clearElectronStore } from "./api/electronStore";
|
||||
/**
|
||||
* @file The preload script
|
||||
*
|
||||
* The preload script runs in a renderer process before its web contents begin
|
||||
* loading. During their execution they have access to a subset of Node.js APIs
|
||||
* and imports. Its purpose is to expose the relevant imports and other
|
||||
* functions as an object on the DOM, so that the renderer process can invoke
|
||||
* functions that live in the main (Node.js) process if needed.
|
||||
*
|
||||
* Note that this script cannot import other code from `src/` - conceptually it
|
||||
* can be thought of as running in a separate, third, process different from
|
||||
* both the main or a renderer process (technically, it runs in a BrowserWindow
|
||||
* context that runs prior to the renderer process).
|
||||
*
|
||||
* That said, this can be split into multiple files if we wished. However,
|
||||
* that'd require us setting up a bundler to package it back up into a single JS
|
||||
* file that can be used at runtime.
|
||||
*
|
||||
* > Since enabling the sandbox disables Node.js integration in your preload
|
||||
* > scripts, you can no longer use require("../my-script"). In other words,
|
||||
* > your preload script needs to be a single file.
|
||||
* >
|
||||
* > https://www.electronjs.org/blog/breach-to-barrier
|
||||
*
|
||||
* Since most of this is just boilerplate code providing a bridge between the
|
||||
* main and renderer, we avoid introducing another moving part into the mix and
|
||||
* just keep the entire preload setup in this single file.
|
||||
*/
|
||||
|
||||
import { contextBridge, ipcRenderer } from "electron";
|
||||
import { existsSync } from "fs";
|
||||
import path from "path";
|
||||
import * as fs from "promise-fs";
|
||||
import { Readable } from "stream";
|
||||
import { deleteDiskCache, openDiskCache } from "./api/cache";
|
||||
import { logToDisk, openLogDirectory } from "./api/common";
|
||||
import {
|
||||
checkExistsAndCreateDir,
|
||||
exists,
|
||||
|
@ -21,25 +41,12 @@ import {
|
|||
saveStreamToDisk,
|
||||
} from "./api/export";
|
||||
import { runFFmpegCmd } from "./api/ffmpeg";
|
||||
import {
|
||||
deleteFile,
|
||||
deleteFolder,
|
||||
getDirFiles,
|
||||
isFolder,
|
||||
moveFile,
|
||||
readTextFile,
|
||||
rename,
|
||||
} from "./api/fs";
|
||||
import { getDirFiles } from "./api/fs";
|
||||
import { convertToJPEG, generateImageThumbnail } from "./api/imageProcessor";
|
||||
import { getEncryptionKey, setEncryptionKey } from "./api/safeStorage";
|
||||
import {
|
||||
muteUpdateNotification,
|
||||
registerForegroundEventListener,
|
||||
registerUpdateEventListener,
|
||||
reloadWindow,
|
||||
sendNotification,
|
||||
skipAppUpdate,
|
||||
updateAndRestart,
|
||||
} from "./api/system";
|
||||
import {
|
||||
getElectronFilesFromGoogleZip,
|
||||
|
@ -59,25 +66,318 @@ import {
|
|||
updateWatchMappingSyncedFiles,
|
||||
} from "./api/watch";
|
||||
import { setupLogging } from "./utils/logging";
|
||||
import {
|
||||
logRendererProcessMemoryUsage,
|
||||
setupRendererProcessStatsLogger,
|
||||
} from "./utils/processStats";
|
||||
|
||||
/* Some of the code below has been duplicated to make this file self contained.
|
||||
Enhancement: consider alternatives */
|
||||
|
||||
/* preload: duplicated logError */
|
||||
export function logError(error: Error, message: string, info?: string): void {
|
||||
ipcRenderer.invoke("log-error", error, message, info);
|
||||
}
|
||||
|
||||
// -
|
||||
|
||||
export const convertBrowserStreamToNode = (
|
||||
fileStream: ReadableStream<Uint8Array>,
|
||||
) => {
|
||||
const reader = fileStream.getReader();
|
||||
const rs = new Readable();
|
||||
|
||||
rs._read = async () => {
|
||||
try {
|
||||
const result = await reader.read();
|
||||
|
||||
if (!result.done) {
|
||||
rs.push(Buffer.from(result.value));
|
||||
} else {
|
||||
rs.push(null);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
rs.emit("error", e);
|
||||
}
|
||||
};
|
||||
|
||||
return rs;
|
||||
};
|
||||
|
||||
export async function writeNodeStream(
|
||||
filePath: string,
|
||||
fileStream: NodeJS.ReadableStream,
|
||||
) {
|
||||
const writeable = fs.createWriteStream(filePath);
|
||||
|
||||
fileStream.on("error", (error) => {
|
||||
writeable.destroy(error); // Close the writable stream with an error
|
||||
});
|
||||
|
||||
fileStream.pipe(writeable);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writeable.on("finish", resolve);
|
||||
writeable.on("error", async (e) => {
|
||||
if (existsSync(filePath)) {
|
||||
await fs.unlink(filePath);
|
||||
}
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* preload: duplicated writeStream */
|
||||
export async function writeStream(
|
||||
filePath: string,
|
||||
fileStream: ReadableStream<Uint8Array>,
|
||||
) {
|
||||
const readable = convertBrowserStreamToNode(fileStream);
|
||||
await writeNodeStream(filePath, readable);
|
||||
}
|
||||
|
||||
// -
|
||||
|
||||
async function readTextFile(filePath: string) {
|
||||
if (!existsSync(filePath)) {
|
||||
throw new Error("File does not exist");
|
||||
}
|
||||
return await fs.readFile(filePath, "utf-8");
|
||||
}
|
||||
|
||||
async function moveFile(
|
||||
sourcePath: string,
|
||||
destinationPath: string,
|
||||
): Promise<void> {
|
||||
if (!existsSync(sourcePath)) {
|
||||
throw new Error("File does not exist");
|
||||
}
|
||||
if (existsSync(destinationPath)) {
|
||||
throw new Error("Destination file already exists");
|
||||
}
|
||||
// check if destination folder exists
|
||||
const destinationFolder = path.dirname(destinationPath);
|
||||
if (!existsSync(destinationFolder)) {
|
||||
await fs.mkdir(destinationFolder, { recursive: true });
|
||||
}
|
||||
await fs.rename(sourcePath, destinationPath);
|
||||
}
|
||||
|
||||
export async function isFolder(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;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFolder(folderPath: string): Promise<void> {
|
||||
if (!existsSync(folderPath)) {
|
||||
return;
|
||||
}
|
||||
if (!fs.statSync(folderPath).isDirectory()) {
|
||||
throw new Error("Path is not a folder");
|
||||
}
|
||||
// check if folder is empty
|
||||
const files = await fs.readdir(folderPath);
|
||||
if (files.length > 0) {
|
||||
throw new Error("Folder is not empty");
|
||||
}
|
||||
await fs.rmdir(folderPath);
|
||||
}
|
||||
|
||||
async function rename(oldPath: string, newPath: string) {
|
||||
if (!existsSync(oldPath)) {
|
||||
throw new Error("Path does not exist");
|
||||
}
|
||||
await fs.rename(oldPath, newPath);
|
||||
}
|
||||
|
||||
function deleteFile(filePath: string): void {
|
||||
if (!existsSync(filePath)) {
|
||||
return;
|
||||
}
|
||||
if (!fs.statSync(filePath).isFile()) {
|
||||
throw new Error("Path is not a file");
|
||||
}
|
||||
fs.rmSync(filePath);
|
||||
}
|
||||
|
||||
// -
|
||||
|
||||
/* preload: duplicated Model */
|
||||
export enum Model {
|
||||
GGML_CLIP = "ggml-clip",
|
||||
ONNX_CLIP = "onnx-clip",
|
||||
}
|
||||
|
||||
const computeImageEmbedding = async (
|
||||
model: Model,
|
||||
imageData: Uint8Array,
|
||||
): Promise<Float32Array> => {
|
||||
let tempInputFilePath = null;
|
||||
try {
|
||||
tempInputFilePath = await ipcRenderer.invoke("get-temp-file-path", "");
|
||||
const imageStream = new Response(imageData.buffer).body;
|
||||
await writeStream(tempInputFilePath, imageStream);
|
||||
const embedding = await ipcRenderer.invoke(
|
||||
"compute-image-embedding",
|
||||
model,
|
||||
tempInputFilePath,
|
||||
);
|
||||
return embedding;
|
||||
} catch (err) {
|
||||
if (isExecError(err)) {
|
||||
const parsedExecError = parseExecError(err);
|
||||
throw Error(parsedExecError);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
if (tempInputFilePath) {
|
||||
await ipcRenderer.invoke("remove-temp-file", tempInputFilePath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function computeTextEmbedding(
|
||||
model: Model,
|
||||
text: string,
|
||||
): Promise<Float32Array> {
|
||||
try {
|
||||
const embedding = await ipcRenderer.invoke(
|
||||
"compute-text-embedding",
|
||||
model,
|
||||
text,
|
||||
);
|
||||
return embedding;
|
||||
} catch (err) {
|
||||
if (isExecError(err)) {
|
||||
const parsedExecError = parseExecError(err);
|
||||
throw Error(parsedExecError);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -
|
||||
|
||||
/**
|
||||
* [Note: Custom errors across Electron/Renderer boundary]
|
||||
*
|
||||
* We need to use the `message` field to disambiguate between errors thrown by
|
||||
* the main process when invoked from the renderer process. This is because:
|
||||
*
|
||||
* > Errors thrown throw `handle` in the main process are not transparent as
|
||||
* > they are serialized and only the `message` property from the original error
|
||||
* > is provided to the renderer process.
|
||||
* >
|
||||
* > - https://www.electronjs.org/docs/latest/tutorial/ipc
|
||||
* >
|
||||
* > Ref: https://github.com/electron/electron/issues/24427
|
||||
*/
|
||||
/* preload: duplicated CustomErrors */
|
||||
const CustomErrorsP = {
|
||||
WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED:
|
||||
"Windows native image processing is not supported",
|
||||
INVALID_OS: (os: string) => `Invalid OS - ${os}`,
|
||||
WAIT_TIME_EXCEEDED: "Wait time exceeded",
|
||||
UNSUPPORTED_PLATFORM: (platform: string, arch: string) =>
|
||||
`Unsupported platform - ${platform} ${arch}`,
|
||||
MODEL_DOWNLOAD_PENDING:
|
||||
"Model download pending, skipping clip search request",
|
||||
INVALID_FILE_PATH: "Invalid file path",
|
||||
INVALID_CLIP_MODEL: (model: string) => `Invalid Clip model - ${model}`,
|
||||
};
|
||||
|
||||
const isExecError = (err: any) => {
|
||||
return err.message.includes("Command failed:");
|
||||
};
|
||||
|
||||
const parseExecError = (err: any) => {
|
||||
const errMessage = err.message;
|
||||
if (errMessage.includes("Bad CPU type in executable")) {
|
||||
return CustomErrorsP.UNSUPPORTED_PLATFORM(
|
||||
process.platform,
|
||||
process.arch,
|
||||
);
|
||||
} else {
|
||||
return errMessage;
|
||||
}
|
||||
};
|
||||
|
||||
// -
|
||||
|
||||
const selectDirectory = async (): Promise<string> => {
|
||||
try {
|
||||
return await ipcRenderer.invoke("select-dir");
|
||||
} catch (e) {
|
||||
logError(e, "error while selecting root directory");
|
||||
}
|
||||
};
|
||||
|
||||
const getAppVersion = async (): Promise<string> => {
|
||||
try {
|
||||
return await ipcRenderer.invoke("get-app-version");
|
||||
} catch (e) {
|
||||
logError(e, "failed to get release version");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const openDirectory = async (dirPath: string): Promise<void> => {
|
||||
try {
|
||||
await ipcRenderer.invoke("open-dir", dirPath);
|
||||
} catch (e) {
|
||||
logError(e, "error while opening directory");
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
// -
|
||||
|
||||
const clearElectronStore = () => {
|
||||
ipcRenderer.send("clear-electron-store");
|
||||
};
|
||||
|
||||
// -
|
||||
|
||||
const updateAndRestart = () => {
|
||||
ipcRenderer.send("update-and-restart");
|
||||
};
|
||||
|
||||
const skipAppUpdate = (version: string) => {
|
||||
ipcRenderer.send("skip-app-update", version);
|
||||
};
|
||||
|
||||
const muteUpdateNotification = (version: string) => {
|
||||
ipcRenderer.send("mute-update-notification", version);
|
||||
};
|
||||
|
||||
// -
|
||||
|
||||
setupLogging();
|
||||
setupRendererProcessStatsLogger();
|
||||
|
||||
const windowObject: any = window;
|
||||
|
||||
windowObject["ElectronAPIs"] = {
|
||||
// These objects exposed here will become available to the JS code in our
|
||||
// renderer (the web/ code) as `window.ElectronAPIs.*`
|
||||
//
|
||||
// https://www.electronjs.org/docs/latest/tutorial/tutorial-preload
|
||||
contextBridge.exposeInMainWorld("ElectronAPIs", {
|
||||
exists,
|
||||
checkExistsAndCreateDir,
|
||||
saveStreamToDisk,
|
||||
saveFileToDisk,
|
||||
selectDirectory,
|
||||
clearElectronStore,
|
||||
sendNotification,
|
||||
reloadWindow,
|
||||
readTextFile,
|
||||
showUploadFilesDialog,
|
||||
showUploadDirsDialog,
|
||||
|
@ -108,7 +408,6 @@ windowObject["ElectronAPIs"] = {
|
|||
runFFmpegCmd,
|
||||
muteUpdateNotification,
|
||||
generateImageThumbnail,
|
||||
logRendererProcessMemoryUsage,
|
||||
registerForegroundEventListener,
|
||||
openDirectory,
|
||||
moveFile,
|
||||
|
@ -117,7 +416,4 @@ windowObject["ElectronAPIs"] = {
|
|||
deleteFile,
|
||||
computeImageEmbedding,
|
||||
computeTextEmbedding,
|
||||
getPlatform,
|
||||
getCacheDirectory,
|
||||
setCustomCacheDirectory,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -184,25 +184,6 @@ export const getZipFileStream = async (
|
|||
return readableStream;
|
||||
};
|
||||
|
||||
export async function isFolder(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;
|
||||
}
|
||||
}
|
||||
|
||||
export const convertBrowserStreamToNode = (
|
||||
fileStream: ReadableStream<Uint8Array>,
|
||||
) => {
|
||||
|
@ -257,60 +238,3 @@ export async function writeStream(
|
|||
const readable = convertBrowserStreamToNode(fileStream);
|
||||
await writeNodeStream(filePath, readable);
|
||||
}
|
||||
|
||||
export async function readTextFile(filePath: string) {
|
||||
if (!existsSync(filePath)) {
|
||||
throw new Error("File does not exist");
|
||||
}
|
||||
return await fs.readFile(filePath, "utf-8");
|
||||
}
|
||||
|
||||
export async function moveFile(
|
||||
sourcePath: string,
|
||||
destinationPath: string,
|
||||
): Promise<void> {
|
||||
if (!existsSync(sourcePath)) {
|
||||
throw new Error("File does not exist");
|
||||
}
|
||||
if (existsSync(destinationPath)) {
|
||||
throw new Error("Destination file already exists");
|
||||
}
|
||||
// check if destination folder exists
|
||||
const destinationFolder = path.dirname(destinationPath);
|
||||
if (!existsSync(destinationFolder)) {
|
||||
await fs.mkdir(destinationFolder, { recursive: true });
|
||||
}
|
||||
await fs.rename(sourcePath, destinationPath);
|
||||
}
|
||||
|
||||
export async function deleteFolder(folderPath: string): Promise<void> {
|
||||
if (!existsSync(folderPath)) {
|
||||
return;
|
||||
}
|
||||
if (!fs.statSync(folderPath).isDirectory()) {
|
||||
throw new Error("Path is not a folder");
|
||||
}
|
||||
// check if folder is empty
|
||||
const files = await fs.readdir(folderPath);
|
||||
if (files.length > 0) {
|
||||
throw new Error("Folder is not empty");
|
||||
}
|
||||
await fs.rmdir(folderPath);
|
||||
}
|
||||
|
||||
export async function rename(oldPath: string, newPath: string) {
|
||||
if (!existsSync(oldPath)) {
|
||||
throw new Error("Path does not exist");
|
||||
}
|
||||
await fs.rename(oldPath, newPath);
|
||||
}
|
||||
|
||||
export function deleteFile(filePath: string): void {
|
||||
if (!existsSync(filePath)) {
|
||||
return;
|
||||
}
|
||||
if (!fs.statSync(filePath).isFile()) {
|
||||
throw new Error("Path is not a file");
|
||||
}
|
||||
fs.rmSync(filePath);
|
||||
}
|
||||
|
|
|
@ -31,11 +31,3 @@ export function clearSkipAppVersion() {
|
|||
export function clearMuteUpdateNotificationVersion() {
|
||||
userPreferencesStore.delete("muteUpdateNotificationVersion");
|
||||
}
|
||||
|
||||
export function setCustomCacheDirectory(directory: string) {
|
||||
userPreferencesStore.set("customCacheDirectory", directory);
|
||||
}
|
||||
|
||||
export function getCustomCacheDirectory(): string {
|
||||
return userPreferencesStore.get("customCacheDirectory");
|
||||
}
|
||||
|
|
|
@ -11,9 +11,6 @@ const userPreferencesSchema: Schema<UserPreferencesType> = {
|
|||
muteUpdateNotificationVersion: {
|
||||
type: "string",
|
||||
},
|
||||
customCacheDirectory: {
|
||||
type: "string",
|
||||
},
|
||||
};
|
||||
|
||||
export const userPreferencesStore = new Store({
|
||||
|
|
|
@ -58,7 +58,6 @@ export interface UserPreferencesType {
|
|||
hideDockIcon: boolean;
|
||||
skipAppVersion: string;
|
||||
muteUpdateNotificationVersion: string;
|
||||
customCacheDirectory: string;
|
||||
}
|
||||
|
||||
export interface AppUpdateInfo {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# CLIP Byte Pair Encoding JavaScript Port
|
||||
|
||||
A JavaScript port of [OpenAI's CLIP byte-pair-encoding tokenizer](https://github.com/openai/CLIP/blob/3bee28119e6b28e75b82b811b87b56935314e6a5/clip/simple_tokenizer.py).
|
||||
A JavaScript port of
|
||||
[OpenAI's CLIP byte-pair-encoding tokenizer](https://github.com/openai/CLIP/blob/3bee28119e6b28e75b82b811b87b56935314e6a5/clip/simple_tokenizer.py).
|
||||
|
||||
```js
|
||||
import Tokenizer from "https://deno.land/x/clip_bpe@v0.0.6/mod.js";
|
||||
|
@ -18,10 +19,22 @@ t.encode("hello world!"); // [3306, 1002, 256]
|
|||
t.encodeForCLIP("hello world!"); // [49406,3306,1002,256,49407,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
|
||||
```
|
||||
|
||||
This encoder/decoder behaves differently to the the GPT-2/3 tokenizer (JavaScript version of that [here](https://github.com/latitudegames/GPT-3-Encoder)). For example, it doesn't preserve capital letters, as shown above.
|
||||
This encoder/decoder behaves differently to the the GPT-2/3 tokenizer
|
||||
(JavaScript version of that
|
||||
[here](https://github.com/latitudegames/GPT-3-Encoder)). For example, it doesn't
|
||||
preserve capital letters, as shown above.
|
||||
|
||||
The [Python version](https://github.com/openai/CLIP/blob/3bee28119e6b28e75b82b811b87b56935314e6a5/clip/simple_tokenizer.py) of this tokenizer uses the `ftfy` module to clean up the text before encoding it. I didn't include that module by default because currently the only version available in JavaScript is [this one](https://github.com/josephrocca/ftfy-pyodide), which requires importing a full Python runtime as a WebAssembly module. If you want the `ftfy` cleaning, just import it and clean your text with it before passing it to the `.encode()` method.
|
||||
The
|
||||
[Python version](https://github.com/openai/CLIP/blob/3bee28119e6b28e75b82b811b87b56935314e6a5/clip/simple_tokenizer.py)
|
||||
of this tokenizer uses the `ftfy` module to clean up the text before encoding
|
||||
it. I didn't include that module by default because currently the only version
|
||||
available in JavaScript is
|
||||
[this one](https://github.com/josephrocca/ftfy-pyodide), which requires
|
||||
importing a full Python runtime as a WebAssembly module. If you want the `ftfy`
|
||||
cleaning, just import it and clean your text with it before passing it to the
|
||||
`.encode()` method.
|
||||
|
||||
# License
|
||||
|
||||
To the extent that there is any original work in this repo, it is MIT Licensed, just like [openai/CLIP](https://github.com/openai/CLIP).
|
||||
To the extent that there is any original work in this repo, it is MIT Licensed,
|
||||
just like [openai/CLIP](https://github.com/openai/CLIP).
|
||||
|
|
|
@ -8,7 +8,12 @@ import { getHideDockIconPreference } from "../services/userPreference";
|
|||
import { isDev } from "./common";
|
||||
import { isPlatform } from "./common/platform";
|
||||
|
||||
export async function createWindow(): Promise<BrowserWindow> {
|
||||
/**
|
||||
* Create an return the {@link BrowserWindow} that will form our app's UI.
|
||||
*
|
||||
* This window will show the HTML served from {@link rendererURL}.
|
||||
*/
|
||||
export const createWindow = async () => {
|
||||
const appImgPath = isDev
|
||||
? "resources/window-icon.png"
|
||||
: path.join(process.resourcesPath, "window-icon.png");
|
||||
|
@ -16,9 +21,7 @@ export async function createWindow(): Promise<BrowserWindow> {
|
|||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
webPreferences: {
|
||||
sandbox: false,
|
||||
preload: path.join(__dirname, "../preload.js"),
|
||||
contextIsolation: false,
|
||||
},
|
||||
icon: appIcon,
|
||||
show: false, // don't show the main window on load,
|
||||
|
@ -49,16 +52,6 @@ export async function createWindow(): Promise<BrowserWindow> {
|
|||
);
|
||||
mainWindow.loadURL(rendererURL);
|
||||
}
|
||||
mainWindow.webContents.on("did-fail-load", () => {
|
||||
splash.close();
|
||||
isDev
|
||||
? mainWindow.loadFile(`../resources/error.html`)
|
||||
: splash.loadURL(
|
||||
`file://${path.join(process.resourcesPath, "error.html")}`,
|
||||
);
|
||||
mainWindow.maximize();
|
||||
mainWindow.show();
|
||||
});
|
||||
mainWindow.once("ready-to-show", async () => {
|
||||
try {
|
||||
splash.destroy();
|
||||
|
@ -114,4 +107,4 @@ export async function createWindow(): Promise<BrowserWindow> {
|
|||
}
|
||||
});
|
||||
return mainWindow;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import { CustomErrors } from "../constants/errors";
|
||||
|
||||
export const isExecError = (err: any) => {
|
||||
return err.message.includes("Command failed:");
|
||||
};
|
||||
|
||||
export const parseExecError = (err: any) => {
|
||||
const errMessage = err.message;
|
||||
if (errMessage.includes("Bad CPU type in executable")) {
|
||||
return CustomErrors.UNSUPPORTED_PLATFORM(
|
||||
process.platform,
|
||||
process.arch,
|
||||
);
|
||||
} else {
|
||||
return errMessage;
|
||||
}
|
||||
};
|
|
@ -4,12 +4,12 @@ import {
|
|||
BrowserWindow,
|
||||
dialog,
|
||||
ipcMain,
|
||||
Notification,
|
||||
safeStorage,
|
||||
shell,
|
||||
Tray,
|
||||
} from "electron";
|
||||
import path from "path";
|
||||
import { clearElectronStore } from "../api/electronStore";
|
||||
import {
|
||||
getAppVersion,
|
||||
muteUpdateNotification,
|
||||
|
@ -27,12 +27,6 @@ import {
|
|||
generateImageThumbnail,
|
||||
} from "../services/imageProcessor";
|
||||
import { logErrorSentry } from "../services/sentry";
|
||||
import {
|
||||
getCustomCacheDirectory,
|
||||
setCustomCacheDirectory,
|
||||
} from "../services/userPreference";
|
||||
import { getPlatform } from "./common/platform";
|
||||
import { createWindow } from "./createWindow";
|
||||
import { generateTempFilePath } from "./temp";
|
||||
|
||||
export default function setupIpcComs(
|
||||
|
@ -49,19 +43,6 @@ export default function setupIpcComs(
|
|||
}
|
||||
});
|
||||
|
||||
ipcMain.on("send-notification", (_, args) => {
|
||||
const notification = {
|
||||
title: "ente",
|
||||
body: args,
|
||||
};
|
||||
new Notification(notification).show();
|
||||
});
|
||||
ipcMain.on("reload-window", async () => {
|
||||
const secondWindow = await createWindow();
|
||||
mainWindow.destroy();
|
||||
mainWindow = secondWindow;
|
||||
});
|
||||
|
||||
ipcMain.handle("show-upload-files-dialog", async () => {
|
||||
const files = await dialog.showOpenDialog({
|
||||
properties: ["openFile", "multiSelections"],
|
||||
|
@ -110,6 +91,10 @@ export default function setupIpcComs(
|
|||
return safeStorage.decryptString(message);
|
||||
});
|
||||
|
||||
ipcMain.on("clear-electron-store", () => {
|
||||
clearElectronStore();
|
||||
});
|
||||
|
||||
ipcMain.handle("get-path", (_, message) => {
|
||||
// By default, these paths are at the following locations:
|
||||
//
|
||||
|
@ -180,15 +165,4 @@ export default function setupIpcComs(
|
|||
ipcMain.handle("compute-text-embedding", (_, model, text) => {
|
||||
return computeTextEmbedding(model, text);
|
||||
});
|
||||
ipcMain.handle("get-platform", () => {
|
||||
return getPlatform();
|
||||
});
|
||||
|
||||
ipcMain.handle("set-custom-cache-directory", (_, directory: string) => {
|
||||
setCustomCacheDirectory(directory);
|
||||
});
|
||||
|
||||
ipcMain.handle("get-custom-cache-directory", async () => {
|
||||
return getCustomCacheDirectory();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,16 +9,3 @@ export function setupLogging(isDev?: boolean) {
|
|||
log.transports.file.format =
|
||||
"[{y}-{m}-{d}T{h}:{i}:{s}{z}] [{level}]{scope} {text}";
|
||||
}
|
||||
|
||||
export function convertBytesToHumanReadable(
|
||||
bytes: number,
|
||||
precision = 2,
|
||||
): string {
|
||||
if (bytes === 0 || isNaN(bytes)) {
|
||||
return "0 MB";
|
||||
}
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
return (bytes / Math.pow(1024, i)).toFixed(precision) + " " + sizes[i];
|
||||
}
|
||||
|
|
|
@ -1,295 +0,0 @@
|
|||
import ElectronLog from "electron-log";
|
||||
import { webFrame } from "electron/renderer";
|
||||
import { convertBytesToHumanReadable } from "./logging";
|
||||
|
||||
const LOGGING_INTERVAL_IN_MICROSECONDS = 30 * 1000; // 30 seconds
|
||||
|
||||
const SPIKE_DETECTION_INTERVAL_IN_MICROSECONDS = 1 * 1000; // 1 seconds
|
||||
|
||||
const MAIN_MEMORY_USAGE_DIFF_IN_KILOBYTES_CONSIDERED_AS_SPIKE = 50 * 1024; // 50 MB
|
||||
|
||||
const HIGH_MAIN_MEMORY_USAGE_THRESHOLD_IN_KILOBYTES = 200 * 1024; // 200 MB
|
||||
|
||||
const RENDERER_MEMORY_USAGE_DIFF_IN_KILOBYTES_CONSIDERED_AS_SPIKE = 200 * 1024; // 200 MB
|
||||
|
||||
const HIGH_RENDERER_MEMORY_USAGE_THRESHOLD_IN_KILOBYTES = 1024 * 1024; // 1 GB
|
||||
|
||||
async function logMainProcessStats() {
|
||||
const processMemoryInfo = await getNormalizedProcessMemoryInfo(
|
||||
await process.getProcessMemoryInfo(),
|
||||
);
|
||||
const cpuUsage = process.getCPUUsage();
|
||||
const heapStatistics = getNormalizedHeapStatistics(
|
||||
process.getHeapStatistics(),
|
||||
);
|
||||
|
||||
ElectronLog.log("main process stats", {
|
||||
processMemoryInfo,
|
||||
heapStatistics,
|
||||
cpuUsage,
|
||||
});
|
||||
}
|
||||
|
||||
let previousMainProcessMemoryInfo: Electron.ProcessMemoryInfo = {
|
||||
private: 0,
|
||||
shared: 0,
|
||||
residentSet: 0,
|
||||
};
|
||||
|
||||
let mainProcessUsingHighMemory = false;
|
||||
|
||||
async function logSpikeMainMemoryUsage() {
|
||||
const processMemoryInfo = await process.getProcessMemoryInfo();
|
||||
const currentMemoryUsage = Math.max(
|
||||
processMemoryInfo.residentSet ?? 0,
|
||||
processMemoryInfo.private,
|
||||
);
|
||||
const previousMemoryUsage = Math.max(
|
||||
previousMainProcessMemoryInfo.residentSet ?? 0,
|
||||
previousMainProcessMemoryInfo.private,
|
||||
);
|
||||
const isSpiking =
|
||||
currentMemoryUsage - previousMemoryUsage >=
|
||||
MAIN_MEMORY_USAGE_DIFF_IN_KILOBYTES_CONSIDERED_AS_SPIKE;
|
||||
|
||||
const isHighMemoryUsage =
|
||||
currentMemoryUsage >= HIGH_MAIN_MEMORY_USAGE_THRESHOLD_IN_KILOBYTES;
|
||||
|
||||
const shouldReport =
|
||||
(isHighMemoryUsage && !mainProcessUsingHighMemory) ||
|
||||
(!isHighMemoryUsage && mainProcessUsingHighMemory);
|
||||
|
||||
if (isSpiking || shouldReport) {
|
||||
const normalizedCurrentProcessMemoryInfo =
|
||||
await getNormalizedProcessMemoryInfo(processMemoryInfo);
|
||||
const normalizedPreviousProcessMemoryInfo =
|
||||
await getNormalizedProcessMemoryInfo(previousMainProcessMemoryInfo);
|
||||
const cpuUsage = process.getCPUUsage();
|
||||
const heapStatistics = getNormalizedHeapStatistics(
|
||||
process.getHeapStatistics(),
|
||||
);
|
||||
|
||||
ElectronLog.log("reporting main memory usage spike", {
|
||||
currentProcessMemoryInfo: normalizedCurrentProcessMemoryInfo,
|
||||
previousProcessMemoryInfo: normalizedPreviousProcessMemoryInfo,
|
||||
heapStatistics,
|
||||
cpuUsage,
|
||||
});
|
||||
}
|
||||
previousMainProcessMemoryInfo = processMemoryInfo;
|
||||
if (shouldReport) {
|
||||
mainProcessUsingHighMemory = !mainProcessUsingHighMemory;
|
||||
}
|
||||
}
|
||||
|
||||
let previousRendererProcessMemoryInfo: Electron.ProcessMemoryInfo = {
|
||||
private: 0,
|
||||
shared: 0,
|
||||
residentSet: 0,
|
||||
};
|
||||
|
||||
let rendererUsingHighMemory = false;
|
||||
|
||||
async function logSpikeRendererMemoryUsage() {
|
||||
const processMemoryInfo = await process.getProcessMemoryInfo();
|
||||
const currentMemoryUsage = Math.max(
|
||||
processMemoryInfo.residentSet ?? 0,
|
||||
processMemoryInfo.private,
|
||||
);
|
||||
|
||||
const previousMemoryUsage = Math.max(
|
||||
previousRendererProcessMemoryInfo.private,
|
||||
previousRendererProcessMemoryInfo.residentSet ?? 0,
|
||||
);
|
||||
const isSpiking =
|
||||
currentMemoryUsage - previousMemoryUsage >=
|
||||
RENDERER_MEMORY_USAGE_DIFF_IN_KILOBYTES_CONSIDERED_AS_SPIKE;
|
||||
|
||||
const isHighMemoryUsage =
|
||||
currentMemoryUsage >= HIGH_RENDERER_MEMORY_USAGE_THRESHOLD_IN_KILOBYTES;
|
||||
|
||||
const shouldReport =
|
||||
(isHighMemoryUsage && !rendererUsingHighMemory) ||
|
||||
(!isHighMemoryUsage && rendererUsingHighMemory);
|
||||
|
||||
if (isSpiking || shouldReport) {
|
||||
const normalizedCurrentProcessMemoryInfo =
|
||||
await getNormalizedProcessMemoryInfo(processMemoryInfo);
|
||||
const normalizedPreviousProcessMemoryInfo =
|
||||
await getNormalizedProcessMemoryInfo(
|
||||
previousRendererProcessMemoryInfo,
|
||||
);
|
||||
const cpuUsage = process.getCPUUsage();
|
||||
const heapStatistics = getNormalizedHeapStatistics(
|
||||
process.getHeapStatistics(),
|
||||
);
|
||||
|
||||
ElectronLog.log("reporting renderer memory usage spike", {
|
||||
currentProcessMemoryInfo: normalizedCurrentProcessMemoryInfo,
|
||||
previousProcessMemoryInfo: normalizedPreviousProcessMemoryInfo,
|
||||
heapStatistics,
|
||||
cpuUsage,
|
||||
});
|
||||
}
|
||||
previousRendererProcessMemoryInfo = processMemoryInfo;
|
||||
if (shouldReport) {
|
||||
rendererUsingHighMemory = !rendererUsingHighMemory;
|
||||
}
|
||||
}
|
||||
|
||||
async function logRendererProcessStats() {
|
||||
const blinkMemoryInfo = getNormalizedBlinkMemoryInfo();
|
||||
const heapStatistics = getNormalizedHeapStatistics(
|
||||
process.getHeapStatistics(),
|
||||
);
|
||||
const webFrameResourceUsage = getNormalizedWebFrameResourceUsage();
|
||||
const processMemoryInfo = await getNormalizedProcessMemoryInfo(
|
||||
await process.getProcessMemoryInfo(),
|
||||
);
|
||||
ElectronLog.log("renderer process stats", {
|
||||
blinkMemoryInfo,
|
||||
heapStatistics,
|
||||
processMemoryInfo,
|
||||
webFrameResourceUsage,
|
||||
});
|
||||
}
|
||||
|
||||
export function setupMainProcessStatsLogger() {
|
||||
setInterval(
|
||||
logSpikeMainMemoryUsage,
|
||||
SPIKE_DETECTION_INTERVAL_IN_MICROSECONDS,
|
||||
);
|
||||
setInterval(logMainProcessStats, LOGGING_INTERVAL_IN_MICROSECONDS);
|
||||
}
|
||||
|
||||
export function setupRendererProcessStatsLogger() {
|
||||
setInterval(
|
||||
logSpikeRendererMemoryUsage,
|
||||
SPIKE_DETECTION_INTERVAL_IN_MICROSECONDS,
|
||||
);
|
||||
setInterval(logRendererProcessStats, LOGGING_INTERVAL_IN_MICROSECONDS);
|
||||
}
|
||||
|
||||
export async function logRendererProcessMemoryUsage(message: string) {
|
||||
const processMemoryInfo = await process.getProcessMemoryInfo();
|
||||
const processMemory = Math.max(
|
||||
processMemoryInfo.private,
|
||||
processMemoryInfo.residentSet ?? 0,
|
||||
);
|
||||
ElectronLog.log(
|
||||
"renderer ProcessMemory",
|
||||
message,
|
||||
convertBytesToHumanReadable(processMemory * 1024),
|
||||
);
|
||||
}
|
||||
|
||||
const getNormalizedProcessMemoryInfo = async (
|
||||
processMemoryInfo: Electron.ProcessMemoryInfo,
|
||||
) => {
|
||||
return {
|
||||
residentSet: convertBytesToHumanReadable(
|
||||
processMemoryInfo.residentSet * 1024,
|
||||
),
|
||||
private: convertBytesToHumanReadable(processMemoryInfo.private * 1024),
|
||||
shared: convertBytesToHumanReadable(processMemoryInfo.shared * 1024),
|
||||
};
|
||||
};
|
||||
|
||||
const getNormalizedBlinkMemoryInfo = () => {
|
||||
const blinkMemoryInfo = process.getBlinkMemoryInfo();
|
||||
return {
|
||||
allocated: convertBytesToHumanReadable(
|
||||
blinkMemoryInfo.allocated * 1024,
|
||||
),
|
||||
total: convertBytesToHumanReadable(blinkMemoryInfo.total * 1024),
|
||||
};
|
||||
};
|
||||
|
||||
const getNormalizedHeapStatistics = (
|
||||
heapStatistics: Electron.HeapStatistics,
|
||||
) => {
|
||||
return {
|
||||
totalHeapSize: convertBytesToHumanReadable(
|
||||
heapStatistics.totalHeapSize * 1024,
|
||||
),
|
||||
totalHeapSizeExecutable: convertBytesToHumanReadable(
|
||||
heapStatistics.totalHeapSizeExecutable * 1024,
|
||||
),
|
||||
totalPhysicalSize: convertBytesToHumanReadable(
|
||||
heapStatistics.totalPhysicalSize * 1024,
|
||||
),
|
||||
totalAvailableSize: convertBytesToHumanReadable(
|
||||
heapStatistics.totalAvailableSize * 1024,
|
||||
),
|
||||
usedHeapSize: convertBytesToHumanReadable(
|
||||
heapStatistics.usedHeapSize * 1024,
|
||||
),
|
||||
|
||||
heapSizeLimit: convertBytesToHumanReadable(
|
||||
heapStatistics.heapSizeLimit * 1024,
|
||||
),
|
||||
mallocedMemory: convertBytesToHumanReadable(
|
||||
heapStatistics.mallocedMemory * 1024,
|
||||
),
|
||||
peakMallocedMemory: convertBytesToHumanReadable(
|
||||
heapStatistics.peakMallocedMemory * 1024,
|
||||
),
|
||||
doesZapGarbage: heapStatistics.doesZapGarbage,
|
||||
};
|
||||
};
|
||||
|
||||
const getNormalizedWebFrameResourceUsage = () => {
|
||||
const webFrameResourceUsage = webFrame.getResourceUsage();
|
||||
return {
|
||||
images: {
|
||||
count: webFrameResourceUsage.images.count,
|
||||
size: convertBytesToHumanReadable(
|
||||
webFrameResourceUsage.images.size,
|
||||
),
|
||||
liveSize: convertBytesToHumanReadable(
|
||||
webFrameResourceUsage.images.liveSize,
|
||||
),
|
||||
},
|
||||
scripts: {
|
||||
count: webFrameResourceUsage.scripts.count,
|
||||
size: convertBytesToHumanReadable(
|
||||
webFrameResourceUsage.scripts.size,
|
||||
),
|
||||
liveSize: convertBytesToHumanReadable(
|
||||
webFrameResourceUsage.scripts.liveSize,
|
||||
),
|
||||
},
|
||||
cssStyleSheets: {
|
||||
count: webFrameResourceUsage.cssStyleSheets.count,
|
||||
size: convertBytesToHumanReadable(
|
||||
webFrameResourceUsage.cssStyleSheets.size,
|
||||
),
|
||||
liveSize: convertBytesToHumanReadable(
|
||||
webFrameResourceUsage.cssStyleSheets.liveSize,
|
||||
),
|
||||
},
|
||||
xslStyleSheets: {
|
||||
count: webFrameResourceUsage.xslStyleSheets.count,
|
||||
size: convertBytesToHumanReadable(
|
||||
webFrameResourceUsage.xslStyleSheets.size,
|
||||
),
|
||||
liveSize: convertBytesToHumanReadable(
|
||||
webFrameResourceUsage.xslStyleSheets.liveSize,
|
||||
),
|
||||
},
|
||||
fonts: {
|
||||
count: webFrameResourceUsage.fonts.count,
|
||||
size: convertBytesToHumanReadable(webFrameResourceUsage.fonts.size),
|
||||
liveSize: convertBytesToHumanReadable(
|
||||
webFrameResourceUsage.fonts.liveSize,
|
||||
),
|
||||
},
|
||||
other: {
|
||||
count: webFrameResourceUsage.other.count,
|
||||
size: convertBytesToHumanReadable(webFrameResourceUsage.other.size),
|
||||
liveSize: convertBytesToHumanReadable(
|
||||
webFrameResourceUsage.other.liveSize,
|
||||
),
|
||||
},
|
||||
};
|
||||
};
|
|
@ -1,17 +1,82 @@
|
|||
{
|
||||
/* TSConfig for a set of vanilla TypeScript files that need to be transpiled
|
||||
into JavaScript that'll then be loaded and run by the main (node) process
|
||||
of our Electron app. */
|
||||
|
||||
/* TSConfig docs: https://aka.ms/tsconfig.json */
|
||||
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"module": "commonjs",
|
||||
/* 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
|
||||
|
||||
Note that we cannot do
|
||||
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
|
||||
because that sets "lib": ["es2023"]. However (and I don't fully
|
||||
understand what's going on here), that breaks our compilation since
|
||||
tsc can then not find type definitions of things like ReadableStream.
|
||||
|
||||
Adding "dom" to "lib" (e.g. `"lib": ["es2023", "dom"]`) fixes the
|
||||
issue, but that doesn't sound correct - the main Electron process
|
||||
isn't running in a browser context.
|
||||
|
||||
It is possible that we're using some of the types incorrectly. For
|
||||
now, we just omit the "lib" definition and rely on the defaults for
|
||||
the "target" we've chosen. This is also what the current
|
||||
electron-forge starter does:
|
||||
|
||||
yarn create electron-app electron-forge-starter -- --template=webpack-typescript
|
||||
|
||||
Enhancement: Can revisit this later.
|
||||
|
||||
Refs:
|
||||
- https://github.com/electron/electron/issues/27092
|
||||
- https://github.com/electron/electron/issues/16146
|
||||
*/
|
||||
|
||||
"target": "es2022",
|
||||
"module": "node16",
|
||||
|
||||
/* Enable various workarounds to play better with CJS libraries */
|
||||
"esModuleInterop": true,
|
||||
/* Emit the generated JS into app */
|
||||
/* Speed things up by not type checking `node_modules` */
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Emit the generated JS into `app/` */
|
||||
"outDir": "app",
|
||||
"noImplicitAny": true,
|
||||
/* Generate source maps */
|
||||
"sourceMap": true,
|
||||
/* Allow absolute imports starting with src as root */
|
||||
"baseUrl": "src",
|
||||
/* Allow imports of paths from node_modules */
|
||||
"paths": {
|
||||
"*": ["node_modules/*"]
|
||||
}
|
||||
},
|
||||
|
||||
/* Temporary overrides to get things to compile with the older config */
|
||||
"strict": false,
|
||||
"noImplicitAny": true
|
||||
|
||||
/* Below is the state we want */
|
||||
/* Enable these one by one */
|
||||
// "strict": true,
|
||||
|
||||
/* Require the `type` modifier when importing types */
|
||||
// "verbatimModuleSyntax": true
|
||||
|
||||
/* Stricter than strict */
|
||||
// "noImplicitReturns": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
/* e.g. makes array indexing returns undefined */
|
||||
// "noUncheckedIndexedAccess": true,
|
||||
// "exactOptionalPropertyTypes": true,
|
||||
},
|
||||
/* Transpile all ts files in src/ */
|
||||
/* Transpile all `.ts` files in `src/` */
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
|
|
1106
desktop/yarn.lock
1106
desktop/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -95,10 +95,19 @@ sudo journalctl --follow --unit example
|
|||
|
||||
## Logging
|
||||
|
||||
Services should log to files in `/var/logs` within the container. This should be
|
||||
mounted to `/root/var/logs` on the instance (using the `-v` flag in the service
|
||||
file which launches the Docker container or the Docker compose cluster).
|
||||
Simple services can log to their standard output: these are captured by Docker,
|
||||
and by default promtail is setup to injest Docker logs and send them to Grafana.
|
||||
|
||||
If these logs need to be sent to Grafana, then ensure that there is an entry for
|
||||
this log file in the `promtail/promtail.yaml` on that instance. The logs will
|
||||
then get scraped by Promtail and sent over to Grafana.
|
||||
One issue with the above simple setup is that we cannot attach job names.
|
||||
|
||||
If the service needs to to attach a specific job name, or if the service wants
|
||||
more control over the log retention etc, then then services can log to to its
|
||||
own files.
|
||||
|
||||
* Such files should be in `/var/logs` within the container, and this should be
|
||||
mounted to `/root/var/logs` on the instance (using the `-v` flag in the
|
||||
service file which launches the Docker container or the Docker compose cluster).
|
||||
|
||||
* There should be entry for this log file in the `promtail/promtail.yaml` on
|
||||
that instance. The logs will then get scraped by Promtail and sent over to
|
||||
Grafana.
|
||||
|
|
|
@ -3,16 +3,6 @@
|
|||
*
|
||||
* This worker receives webhooks from GitHub, filters out the ones we don't
|
||||
* need, and forwards them to a Discord webhook.
|
||||
*
|
||||
* [Note: GitHub specific Discord Webhooks]
|
||||
*
|
||||
* By appending `/github` to the end of the webhook URL, we can get Discord to
|
||||
* automatically parse the payload sent by GitHub.
|
||||
* https://discord.com/developers/docs/resources/webhook#execute-githubcompatible-webhook
|
||||
*
|
||||
* Note that this doesn't work for all events. And sadly, the events it doesn't
|
||||
* work for get silently ignored (Discord responds with a 204).
|
||||
* https://github.com/discord/discord-api-docs/issues/6203#issuecomment-1608151265
|
||||
*/
|
||||
export default {
|
||||
async fetch(request: Request, env: Env) {
|
||||
|
@ -24,20 +14,78 @@ interface Env {
|
|||
DISCORD_WEBHOOK_URL: string;
|
||||
}
|
||||
|
||||
const handleRequest = async (request: Request, targetURL: string) => {
|
||||
const handleRequest = async (request: Request, discordWebhookURL: string) => {
|
||||
const requestBody = await request.text();
|
||||
let sender = JSON.parse(requestBody)["sender"]["login"];
|
||||
const requestJSON = JSON.parse(requestBody);
|
||||
const sender = requestJSON["sender"]["login"];
|
||||
if (sender === "cloudflare-pages[bot]" || sender === "CLAassistant") {
|
||||
// Ignore pings from CF bot
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
|
||||
const response = await fetch(targetURL, {
|
||||
// [Note: GitHub specific Discord Webhooks]
|
||||
//
|
||||
// By appending `/github` to the end of the webhook URL, we can get Discord
|
||||
// to automatically parse the payload sent by GitHub.
|
||||
// https://discord.com/developers/docs/resources/webhook#execute-githubcompatible-webhook
|
||||
//
|
||||
// Note that this doesn't work for all events. And sadly, the events it
|
||||
// doesn't work for get silently ignored (Discord responds with a 204).
|
||||
// https://github.com/discord/discord-api-docs/issues/6203#issuecomment-1608151265
|
||||
|
||||
let response = await fetch(`${discordWebhookURL}/github`, {
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: requestBody,
|
||||
});
|
||||
|
||||
if (response.status === 429) {
|
||||
// Sometimes Discord starts returning 429 Rate Limited responses when we
|
||||
// try to invoke the webhook.
|
||||
//
|
||||
// Retry-After: 300
|
||||
// X-Ratelimit-Global: true
|
||||
// X-Ratelimit-Scope: global
|
||||
//
|
||||
// {"message": "You are being rate limited.", "retry_after": 0.3, "global": true}
|
||||
//
|
||||
// This just seems to be a bug on their end, and it goes away on its own
|
||||
// after a while. My best guess is that the IP of the Cloudflare Worker
|
||||
// somehow gets rate limited because of someone else trying to spam from
|
||||
// a worker running on the same IP. But it's a guess. I'm not sure.
|
||||
//
|
||||
// Ref:
|
||||
// https://discord.com/developers/docs/topics/rate-limits#global-rate-limit
|
||||
//
|
||||
// Interestingly, this only happens for the `/github` specific webhook.
|
||||
// The normal webhook still works. So as a workaround, just send a
|
||||
// normal text message to the webhook when we get a 429.
|
||||
|
||||
// The JSON sent by GitHub has a varied schema. This is a stop-gap
|
||||
// arrangement (we shouldn't be getting 429s forever), so just try to
|
||||
// see if we can extract a URL from something we recognize.
|
||||
let activityURL: string | undefined;
|
||||
if (requestJSON["issue"]) {
|
||||
activityURL = requestJSON["issue"]["html_url"];
|
||||
}
|
||||
if (!activityURL && requestJSON["discussion"]) {
|
||||
activityURL = requestJSON["discussion"]["html_url"];
|
||||
}
|
||||
|
||||
// Ignore things like issue label changes.
|
||||
const action = requestJSON["action"];
|
||||
|
||||
if (activityURL && ["created", "opened"].includes(action)) {
|
||||
response = await fetch(discordWebhookURL, {
|
||||
method: request.method,
|
||||
headers: request.headers,
|
||||
body: JSON.stringify({
|
||||
content: `Activity in ${activityURL}`,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const responseBody = await response.text();
|
||||
const newResponse = new Response(responseBody, {
|
||||
status: response.status,
|
||||
|
|
|
@ -4,4 +4,4 @@ compatibility_date = "2024-03-14"
|
|||
|
||||
[vars]
|
||||
# Added as a secret via the Cloudflare dashboard
|
||||
# DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/{webhook.id}/{webhook.token}/github"
|
||||
# DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/{webhook.id}/{webhook.token}"
|
||||
|
|
|
@ -16,7 +16,11 @@ import 'package:photos/services/user_service.dart';
|
|||
import 'package:photos/ui/common/web_page.dart';
|
||||
import 'package:photos/utils/dialog_util.dart';
|
||||
|
||||
const kWebPaymentRedirectUrl = "https://payments.ente.io/frameRedirect";
|
||||
const kWebPaymentRedirectUrl = String.fromEnvironment(
|
||||
"web-payment-redirect",
|
||||
defaultValue: "https://payments.ente.io/frameRedirect",
|
||||
);
|
||||
|
||||
const kWebPaymentBaseEndpoint = String.fromEnvironment(
|
||||
"web-payment",
|
||||
defaultValue: "https://payments.ente.io",
|
||||
|
|
|
@ -331,6 +331,7 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
await _dialog.show();
|
||||
try {
|
||||
final String url = await _billingService.getStripeCustomerPortalUrl();
|
||||
await _dialog.hide();
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
|
@ -342,7 +343,6 @@ class _StripeSubscriptionPageState extends State<StripeSubscriptionPage> {
|
|||
await _dialog.hide();
|
||||
await showGenericErrorDialog(context: context, error: e);
|
||||
}
|
||||
await _dialog.hide();
|
||||
}
|
||||
|
||||
Widget _stripeRenewOrCancelButton() {
|
||||
|
|
|
@ -300,6 +300,11 @@ func (c *StripeController) handleCustomerSubscriptionDeleted(event stripe.Event,
|
|||
return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
|
||||
}
|
||||
|
||||
err = c.BillingRepo.UpdateSubscriptionCancellationStatus(userID, true)
|
||||
if err != nil {
|
||||
return ente.StripeEventLog{}, stacktrace.Propagate(err, "")
|
||||
}
|
||||
|
||||
skipMail := stripeSubscription.Metadata[SkipMailKey]
|
||||
// Send a cancellation notification email for folks who are either on
|
||||
// individual plan or admin of a family plan.
|
||||
|
|
|
@ -16,7 +16,6 @@ import isElectron from "is-electron";
|
|||
import { AppContext } from "pages/_app";
|
||||
import { ClipExtractionStatus, ClipService } from "services/clipService";
|
||||
import { formatNumber } from "utils/number/format";
|
||||
import CacheDirectory from "./Preferences/CacheDirectory";
|
||||
|
||||
export default function AdvancedSettings({ open, onClose, onRootClose }) {
|
||||
const appContext = useContext(AppContext);
|
||||
|
@ -77,22 +76,19 @@ export default function AdvancedSettings({ open, onClose, onRootClose }) {
|
|||
<Box px={"8px"}>
|
||||
<Stack py="20px" spacing="24px">
|
||||
{isElectron() && (
|
||||
<>
|
||||
<CacheDirectory />
|
||||
<Box>
|
||||
<MenuSectionTitle
|
||||
title={t("LABS")}
|
||||
icon={<ScienceIcon />}
|
||||
<Box>
|
||||
<MenuSectionTitle
|
||||
title={t("LABS")}
|
||||
icon={<ScienceIcon />}
|
||||
/>
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
endIcon={<ChevronRight />}
|
||||
onClick={openMlSearchSettings}
|
||||
label={t("ML_SEARCH")}
|
||||
/>
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
endIcon={<ChevronRight />}
|
||||
onClick={openMlSearchSettings}
|
||||
label={t("ML_SEARCH")}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</Box>
|
||||
</>
|
||||
</MenuItemGroup>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<MenuItemGroup>
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import ElectronAPIs from "@ente/shared/electron";
|
||||
import { addLogLine } from "@ente/shared/logging";
|
||||
import { logError } from "@ente/shared/sentry";
|
||||
import Box from "@mui/material/Box";
|
||||
import { DirectoryPath } from "components/Directory";
|
||||
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
||||
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
|
||||
import MenuSectionTitle from "components/Menu/MenuSectionTitle";
|
||||
import { t } from "i18next";
|
||||
import isElectron from "is-electron";
|
||||
import { useEffect, useState } from "react";
|
||||
import DownloadManager from "services/download";
|
||||
|
||||
export default function CacheDirectory() {
|
||||
const [cacheDirectory, setCacheDirectory] = useState(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
if (isElectron()) {
|
||||
const customCacheDirectory =
|
||||
await ElectronAPIs.getCacheDirectory();
|
||||
setCacheDirectory(customCacheDirectory);
|
||||
}
|
||||
};
|
||||
main();
|
||||
}, []);
|
||||
|
||||
const handleCacheDirectoryChange = async () => {
|
||||
try {
|
||||
if (!isElectron()) {
|
||||
return;
|
||||
}
|
||||
const newFolder = await ElectronAPIs.selectDirectory();
|
||||
if (!newFolder) {
|
||||
return;
|
||||
}
|
||||
addLogLine(`Export folder changed to ${newFolder}`);
|
||||
await ElectronAPIs.setCustomCacheDirectory(newFolder);
|
||||
setCacheDirectory(newFolder);
|
||||
await DownloadManager.reloadCaches();
|
||||
} catch (e) {
|
||||
logError(e, "handleCacheDirectoryChange failed");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<MenuSectionTitle title={t("CACHE_DIRECTORY")} />
|
||||
<MenuItemGroup>
|
||||
<EnteMenuItem
|
||||
variant="path"
|
||||
onClick={handleCacheDirectoryChange}
|
||||
labelComponent={
|
||||
<DirectoryPath width={265} path={cacheDirectory} />
|
||||
}
|
||||
/>
|
||||
</MenuItemGroup>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -130,11 +130,6 @@ class DownloadManagerImpl {
|
|||
this.progressUpdater = progressUpdater;
|
||||
}
|
||||
|
||||
async reloadCaches() {
|
||||
this.thumbnailCache = await openThumbnailCache();
|
||||
this.diskFileCache = isElectron() && (await openDiskFileCache());
|
||||
}
|
||||
|
||||
private async getCachedThumbnail(fileID: number) {
|
||||
try {
|
||||
const cacheResp: Response = await this.thumbnailCache?.match(
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
# Dependencies
|
||||
|
||||
## Global
|
||||
## DX
|
||||
|
||||
These are some global dev dependencies in the root `package.json`. These set the
|
||||
baseline for how our code be in all the workspaces in the monorepo.
|
||||
baseline for how our code be in all the workspaces in this (yarn) monorepo.
|
||||
|
||||
* "prettier" - Formatter
|
||||
* "eslint" - Linter
|
||||
* "typescript" - Type checker
|
||||
|
||||
They also need some support packages:
|
||||
They also need some support packages, which come from the leaf `@/build-config`
|
||||
package:
|
||||
|
||||
* "@typescript-eslint/parser" - Tells ESLint how to read TypeScript syntax
|
||||
* "@typescript-eslint/eslint-plugin" - Provides TypeScript rules and presets
|
||||
* "eslint-plugin-react-hooks", "eslint-plugin-react-namespace-import" - Some
|
||||
React specific ESLint rules and configurations that are used by the workspaces
|
||||
that have React code.
|
||||
* "prettier-plugin-organize-imports" - A Prettier plugin to sort imports.
|
||||
* "prettier-plugin-packagejson" - A Prettier plugin to also prettify
|
||||
`package.json`.
|
||||
|
||||
## Utils
|
||||
|
||||
|
|
|
@ -10,8 +10,5 @@
|
|||
"eslint-config-next": "latest",
|
||||
"eslint-config-prettier": "latest",
|
||||
"eslint-plugin-react": "latest"
|
||||
},
|
||||
"standard": {
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ export interface ElectronAPIsType {
|
|||
) => Promise<void>;
|
||||
saveFileToDisk: (path: string, file: any) => Promise<void>;
|
||||
selectDirectory: () => Promise<string>;
|
||||
sendNotification: (content: string) => void;
|
||||
readTextFile: (path: string) => Promise<string>;
|
||||
showUploadFilesDialog: () => Promise<ElectronFile[]>;
|
||||
showUploadDirsDialog: () => Promise<ElectronFile[]>;
|
||||
|
@ -93,7 +92,6 @@ export interface ElectronAPIsType {
|
|||
maxDimension: number,
|
||||
maxSize: number,
|
||||
) => Promise<Uint8Array>;
|
||||
logRendererProcessMemoryUsage: (message: string) => Promise<void>;
|
||||
registerForegroundEventListener: (onForeground: () => void) => void;
|
||||
openDirectory: (dirPath: string) => Promise<void>;
|
||||
moveFile: (oldPath: string, newPath: string) => Promise<void>;
|
||||
|
@ -105,7 +103,4 @@ export interface ElectronAPIsType {
|
|||
imageData: Uint8Array,
|
||||
) => Promise<Float32Array>;
|
||||
computeTextEmbedding: (model: Model, text: string) => Promise<Float32Array>;
|
||||
getPlatform: () => Promise<"mac" | "windows" | "linux">;
|
||||
setCustomCacheDirectory: (directory: string) => Promise<void>;
|
||||
getCacheDirectory: () => Promise<string>;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue