Merge branch 'update_deps_and_flutter' of https://github.com/ente-io/auth into update_deps_and_flutter
BIN
.github/assets/github-badge.png
vendored
Before Width: | Height: | Size: 12 KiB |
9
.github/workflows/auth-release.yml
vendored
|
@ -85,7 +85,8 @@ jobs:
|
|||
- name: Install dependencies for desktop build
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev
|
||||
sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev libffi-dev libtiff5
|
||||
sudo updatedb --localpaths='/usr/lib/x86_64-linux-gnu'
|
||||
|
||||
- name: Install appimagetool
|
||||
run: |
|
||||
|
@ -173,6 +174,9 @@ jobs:
|
|||
- name: Zip Windows EXE and DLLs
|
||||
run: tar.exe -a -c -f artifacts/ente-${{ github.ref_name }}-windows.zip ente-${{ github.ref_name }}-windows
|
||||
|
||||
- name: Generate checksums
|
||||
run: sha256sum artifacts/ente-* > artifacts/sha256sum-windows
|
||||
|
||||
- name: Create a draft GitHub release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
|
@ -267,6 +271,9 @@ jobs:
|
|||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
- name: Generate checksums
|
||||
run: shasum -a 256 artifacts/ente-* > artifacts/sha256sum-macos
|
||||
|
||||
- name: Create a draft GitHub release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
|
|
2
.github/workflows/server-publish.yml
vendored
|
@ -32,6 +32,8 @@ jobs:
|
|||
image: server
|
||||
registry: ghcr.io
|
||||
enableBuildKit: true
|
||||
multiPlatform: true
|
||||
platform: linux/amd64,linux/arm64,linux/arm/v7
|
||||
buildArgs: GIT_COMMIT=${{ inputs.commit }}
|
||||
tags: ${{ inputs.commit }}, latest
|
||||
username: ${{ github.actor }}
|
||||
|
|
2
.github/workflows/web-crowdin.yml
vendored
|
@ -5,7 +5,7 @@ on:
|
|||
branches: [main]
|
||||
paths:
|
||||
# Run workflow when web's en-US/translation.json is changed
|
||||
- "web/apps/photos/public/locales/en-US/translation.json"
|
||||
- "web/packages/next/locales/en-US/translation.json"
|
||||
# Or the workflow itself is changed
|
||||
- ".github/workflows/web-crowdin.yml"
|
||||
schedule:
|
||||
|
|
2
.github/workflows/web-deploy-accounts.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
|||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "docs/yarn.lock"
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
|
2
.github/workflows/web-deploy-auth.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
|||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "docs/yarn.lock"
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
|
2
.github/workflows/web-deploy-cast.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
|||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "docs/yarn.lock"
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
|
43
.github/workflows/web-deploy-payments.yml
vendored
Normal file
|
@ -0,0 +1,43 @@
|
|||
name: "Deploy (payments)"
|
||||
|
||||
on:
|
||||
push:
|
||||
# Run workflow on pushes to the deploy/payments
|
||||
branches: [deploy/payments]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Build payments
|
||||
run: yarn build:payments
|
||||
|
||||
- name: Publish payments
|
||||
uses: cloudflare/pages-action@1
|
||||
with:
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
projectName: ente
|
||||
branch: deploy/payments
|
||||
directory: web/apps/payments/dist
|
||||
wranglerVersion: "3"
|
2
.github/workflows/web-deploy-photos.yml
vendored
|
@ -24,7 +24,7 @@ jobs:
|
|||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "docs/yarn.lock"
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
|
48
.github/workflows/web-deploy-staff.yml
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
name: "Deploy (staff)"
|
||||
|
||||
on:
|
||||
# Run on every push to main that changes web/apps/staff/
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "web/apps/staff/**"
|
||||
- ".github/workflows/web-deploy-staff.yml"
|
||||
# Also allow manually running the workflow
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup node and enable yarn caching
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Build staff
|
||||
run: yarn build:staff
|
||||
|
||||
- name: Publish staff
|
||||
uses: cloudflare/pages-action@1
|
||||
with:
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
projectName: ente
|
||||
branch: deploy/staff
|
||||
directory: web/apps/staff/dist
|
||||
wranglerVersion: "3"
|
15
.github/workflows/web-nightly.yml
vendored
|
@ -34,7 +34,7 @@ jobs:
|
|||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "docs/yarn.lock"
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
@ -78,6 +78,19 @@ jobs:
|
|||
directory: web/apps/cast/out
|
||||
wranglerVersion: "3"
|
||||
|
||||
- name: Build payments
|
||||
run: yarn build:payments
|
||||
|
||||
- name: Publish payments
|
||||
uses: cloudflare/pages-action@1
|
||||
with:
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
projectName: ente
|
||||
branch: n-payments
|
||||
directory: web/apps/payments/dist
|
||||
wranglerVersion: "3"
|
||||
|
||||
- name: Build photos
|
||||
run: yarn build:photos
|
||||
env:
|
||||
|
|
3
.github/workflows/web-preview.yml
vendored
|
@ -12,6 +12,7 @@ on:
|
|||
- "accounts"
|
||||
- "auth"
|
||||
- "cast"
|
||||
- "payments"
|
||||
- "photos"
|
||||
|
||||
jobs:
|
||||
|
@ -33,7 +34,7 @@ jobs:
|
|||
with:
|
||||
node-version: 20
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "docs/yarn.lock"
|
||||
cache-dependency-path: "web/yarn.lock"
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
|
|
@ -59,7 +59,10 @@ See [docs/](docs/README.md) for how to edit these documents.
|
|||
|
||||
## Code contributions
|
||||
|
||||
If you'd like to contribute code, it is best to start small.
|
||||
Code is a small aspect of community, and the ways mentioned above are more
|
||||
important in helping us. But if you'd _really_ like to contribute code, it is
|
||||
best to start small. Consider some well-scoped changes, say like adding more
|
||||
[custom icons to auth](auth/docs/adding-icons.md).
|
||||
|
||||
Each of the individual product/platform specific directories in this repository
|
||||
have instructions on setting up a dev environment and making changes. The issues
|
||||
|
|
|
@ -70,7 +70,7 @@ existing users will be grandfathered in.
|
|||
[<img height="42" src=".github/assets/app-store-badge.svg">](https://apps.apple.com/app/id6444121398)
|
||||
[<img height="42" src=".github/assets/play-store-badge.png">](https://play.google.com/store/apps/details?id=io.ente.auth)
|
||||
[<img height="42" src=".github/assets/f-droid-badge.png">](https://f-droid.org/packages/io.ente.auth/)
|
||||
[<img height="42" src=".github/assets/github-badge.png">](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
|
||||
[<img height="42" src=".github/assets/desktop-badge.png">](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
|
||||
[<img height="42" src=".github/assets/web-badge.svg">](https://auth.ente.io)
|
||||
|
||||
</div>
|
||||
|
|
|
@ -31,14 +31,16 @@ You can alternatively install the build from PlayStore or F-Droid.
|
|||
<img height="59" src="../.github/assets/app-store-badge.svg">
|
||||
</a>
|
||||
|
||||
### Desktop
|
||||
|
||||
You can [**download**](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
|
||||
a native desktop app from this repository's GitHub releases. The desktop app
|
||||
works on Windows, Linux and macOS.
|
||||
|
||||
### Web
|
||||
|
||||
You can view your 2FA codes at [auth.ente.io](https://auth.ente.io). For adding
|
||||
or managing your secrets, please use our mobile app.
|
||||
|
||||
### Desktop
|
||||
|
||||
A native desktop app is coming soon!
|
||||
or managing your secrets, please use our mobile or desktop app.
|
||||
|
||||
## 🧑💻 Build from source
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
Ente Auth helps you generate and store 2 step verification (2FA)
|
||||
tokens on your mobile devices.
|
||||
|
||||
|
||||
FEATURES
|
||||
|
||||
- Secure Backups
|
||||
Auth provides end-to-end encrypted cloud backups so that you don't have to worry
|
||||
about losing your tokens. We use the same protocols ente Photos uses to encrypt
|
||||
and preserve your data.
|
||||
|
||||
- Multi Device Synchronization
|
||||
Auth will automatically sync the 2FA tokens you add to your account, across all
|
||||
your devices. Every new device you sign into will have access to these tokens.
|
||||
|
||||
- Web access
|
||||
You can access your 2FA code from any web browser by visiting https://auth.ente.io .
|
||||
|
||||
- Offline Mode
|
||||
Auth generates 2FA tokens offline, so your network connectivity will not get in
|
||||
the way of your workflow.
|
||||
|
||||
- Import and Export Tokens
|
||||
You can add tokens to Auth by one of the following methods:
|
||||
1. Scanning a QR code
|
||||
2. Manually entering (copy-pasting) a 2FA secret
|
||||
3. Bulk importing from a file that contains a list of codes in the following format:
|
||||
|
||||
otpauth://totp/provider.com:you@email.com?secret=YOUR_SECRET
|
||||
|
||||
The codes maybe separated by new lines or commas.
|
||||
|
||||
You can also export the codes you have added to Auth, to an **unencrypted** text
|
||||
file, that adheres to the above format.
|
||||
|
||||
|
||||
SUPPORT
|
||||
|
||||
If you need help, please reach out to support@ente.io, and a human will get in touch with you.
|
||||
If you have feature requests, please create an issue @ https://github.com/ente-io/ente
|
After Width: | Height: | Size: 5 KiB |
After Width: | Height: | Size: 123 KiB |
After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 89 KiB |
After Width: | Height: | Size: 58 KiB |
|
@ -0,0 +1 @@
|
|||
Auth is a FOSS authenticator app that provides end-to-end encrypted backups for your 2FA secrets.
|
1
auth/android/app/src/main/play/listings/en-US/title.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Ente Auth
|
|
@ -36,7 +36,9 @@
|
|||
},
|
||||
{
|
||||
"title": "BorgBase",
|
||||
"altNames": ["borg"],
|
||||
"altNames": [
|
||||
"borg"
|
||||
],
|
||||
"slug": "BorgBase"
|
||||
},
|
||||
{
|
||||
|
@ -46,11 +48,17 @@
|
|||
{
|
||||
"title": "Bybit"
|
||||
},
|
||||
{
|
||||
"title": "CERN"
|
||||
},
|
||||
{
|
||||
"title": "Channel Island Hosting",
|
||||
"slug": "cih",
|
||||
"hex": "D14633"
|
||||
},
|
||||
{
|
||||
"title": "ConfigCat"
|
||||
},
|
||||
{
|
||||
"title": "Cloudflare"
|
||||
},
|
||||
|
@ -64,7 +72,9 @@
|
|||
},
|
||||
{
|
||||
"title": "DCS",
|
||||
"altNames": ["Digital Combat Simulator"],
|
||||
"altNames": [
|
||||
"Digital Combat Simulator"
|
||||
],
|
||||
"slug": "dcs"
|
||||
},
|
||||
{
|
||||
|
@ -112,9 +122,14 @@
|
|||
},
|
||||
{
|
||||
"title": "Gosuslugi",
|
||||
"altNames": ["Госуслуги"],
|
||||
"altNames": [
|
||||
"Госуслуги"
|
||||
],
|
||||
"slug": "Gosuslugi"
|
||||
},
|
||||
{
|
||||
"title": "Habbo"
|
||||
},
|
||||
{
|
||||
"title": "Healthchecks.io",
|
||||
"slug": "healthchecks"
|
||||
|
@ -177,13 +192,24 @@
|
|||
},
|
||||
{
|
||||
"title": "Mastodon",
|
||||
"altNames": ["mstdn", "fediscience", "mathstodon", "fosstodon"],
|
||||
"altNames": [
|
||||
"mstdn",
|
||||
"fediscience",
|
||||
"mathstodon",
|
||||
"fosstodon"
|
||||
],
|
||||
"slug": "mastodon",
|
||||
"hex": "6364FF"
|
||||
},
|
||||
{
|
||||
"title": "Mercado Livre",
|
||||
"slug": "mercado_livre"
|
||||
},
|
||||
{
|
||||
"title": "Murena",
|
||||
"altNames": ["eCloud"],
|
||||
"altNames": [
|
||||
"eCloud"
|
||||
],
|
||||
"slug": "ecloud"
|
||||
},
|
||||
{
|
||||
|
@ -281,6 +307,9 @@
|
|||
"slug": "rust_language_forum",
|
||||
"hex": "000000"
|
||||
},
|
||||
{
|
||||
"title": "Sendgrid"
|
||||
},
|
||||
{
|
||||
"title": "service-bw"
|
||||
},
|
||||
|
@ -371,13 +400,18 @@
|
|||
},
|
||||
{
|
||||
"title": "X",
|
||||
"altNames": ["twitter"],
|
||||
"altNames": [
|
||||
"twitter"
|
||||
],
|
||||
"slug": "x"
|
||||
},
|
||||
{
|
||||
"title": "Yandex",
|
||||
"altNames": ["Ya", "Яндекс"],
|
||||
"altNames": [
|
||||
"Ya",
|
||||
"Яндекс"
|
||||
],
|
||||
"slug": "Yandex"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
47
auth/assets/custom-icons/icons/CERN.svg
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 283.465 283.465" enable-background="new 0 0 283.465 283.465" xml:space="preserve">
|
||||
<rect x="0.752" y="-0.337" fill="#FFFFFF" width="283.465" height="283.465"/>
|
||||
<path fill="#0033A0" d="M210.282,120.915c0.429,24.998-11.023,41.967-14.629,47.856c-3.143,5.13-10.654,15.024-25.11,30.374
|
||||
c-18.235,19.365-75.231,79.947-79.029,83.983h154.558l-35.636-162.227L210.282,120.915z M224.498,203.72l1.277,5.803
|
||||
c-15.302,16.528-38.73,28.912-66,28.912c-5.841,0-11.941-0.565-18.719-2.005c1.375-1.463,2.693-2.867,3.937-4.188
|
||||
c4.352,0.773,9.361,1.284,14.506,1.286C186.023,233.541,209.198,221.605,224.498,203.72z M0.752-0.337v283.465h84.595l83.686-88.992
|
||||
l-0.117-0.107c-13.455,9.491-30.532,14.646-48.943,14.646c-34.121,0-64.191-20.244-77.232-45.437l-0.132,0.088l17.755,59.9h-4.806
|
||||
c0,0-8.809-30.352-16.33-56.812c-5.692-20.026-9.198-34.861-9.154-48.029c0.156-45.284,35.77-86.809,83.128-89.874
|
||||
c1.3-0.086,5.328-0.506,11.328-0.511c36.48-0.022,148.884,0.84,159.687,0.923v-29.26H0.752z M100.261,210.453
|
||||
c6.786,5.88,17.542,13.273,30.838,18.05c-1.113,1.185-2.321,2.469-3.603,3.833c-13.279-5.257-26.271-13.409-36.369-24.392
|
||||
C93.969,208.901,97.099,209.768,100.261,210.453z M145.883,32.427c13.977,3.923,27.086,11.109,37.504,20.825
|
||||
c-3.006-0.803-6.071-1.451-9.188-1.939c-14.647-11.578-33.714-18.482-53.64-18.482c-47.069,0-85.592,38.386-85.592,85.565
|
||||
c0,47.18,38.382,85.564,85.565,85.564c47.179,0,85.565-38.384,85.565-85.564c0-18.252-6.876-36.487-16.318-49.367
|
||||
c2.255,0.678,5.004,1.841,8.162,3.708c6.393,9.69,12.32,25.515,17.398,49.787c5.33,25.462,32.732,147.426,35.694,160.605h33.182
|
||||
V33.152l-138.333-0.85C145.883,32.303,145.883,32.311,145.883,32.427z M50.595,116.383c0-11.441,8.652-19.051,20.412-19.051
|
||||
c4.577,0,9.814,1.409,12.738,2.664c-0.611,1.353-1.113,3.142-1.326,4.258l-0.319,0.106c-2.261-2.503-5.898-4.672-11.279-4.672
|
||||
c-6.83,0-14.689,5.528-14.689,16.557c0,10.737,8.009,16.442,15.169,16.442c6.434,0,9.513-3,12.278-5.337l0.212,0.213l-0.783,4.172
|
||||
c-1.268,0.96-5.664,3.695-12.279,3.695C58.754,135.429,50.595,127.885,50.595,116.383z M78.765,187.656
|
||||
c-6.344-12.899-9.219-25.995-9.6-38.519c1.612,0,3.481,0.067,5.093,0.067c0.556,11.987,3.274,26.892,12.648,43.087
|
||||
C83.521,191.047,80.974,189.405,78.765,187.656z M112.787,134.755c0,0.001,0,0.001,0,0.001c-1.911-0.098-4.565-0.176-7.084-0.221
|
||||
c-1.451-0.024-2.861-0.041-3.973-0.044c-0.158,0-0.319,0-0.47,0c-3.247,0-8.227,0.105-11.475,0.266
|
||||
c0.214-4.631,0.429-9.26,0.429-13.836v-9.15c0-4.578-0.215-9.206-0.429-13.728c3.193,0.16,8.121,0.265,11.313,0.265
|
||||
c3.193,0,9.149-0.141,10.991-0.265c-0.078,0.498-0.127,1.089-0.127,1.815c0,0.725,0.071,1.473,0.127,1.836
|
||||
c-3.505-0.263-9.766-0.692-16.682-0.692c-0.056,2.287-0.162,11.991-0.162,13.324c6.278,0,10.301-0.269,13.441-0.532
|
||||
c-0.105,0.532-0.16,1.486-0.16,2.017c0,0.532,0.054,1.32,0.16,1.853c-3.671-0.374-11.887-0.48-13.441-0.48
|
||||
c-0.095,1.779-0.012,13.294,0.053,14.266c3.889-0.057,13.857-0.359,17.489-0.693c-0.057,0.403-0.125,1.233-0.125,2.042
|
||||
C112.661,133.608,112.708,134.198,112.787,134.755z M144.039,134.58c-0.485,0-2.321,0.017-3.334,0.177
|
||||
c-2.103-3.205-8.839-13.302-13.419-18.015c-0.137,0-2.708,0.003-2.708,0.003v4.23c0,4.575,0.212,9.205,0.426,13.781
|
||||
c-0.906-0.161-2.542-0.177-2.877-0.177c-0.335,0-1.971,0.017-2.878,0.177c0.214-4.577,0.428-9.206,0.428-13.781v-9.152
|
||||
c0-4.577-0.213-9.207-0.428-13.782c2.024,0.16,4.584,0.265,6.606,0.265c2.021,0,4.043-0.265,6.064-0.265
|
||||
c6.013,0,11.523,1.776,11.523,8.472c0,7.084-7.06,9.632-11.104,10.163c2.606,3.246,11.946,14.619,15.032,18.08
|
||||
C146.309,134.596,144.525,134.58,144.039,134.58z M184.319,135.253l-1.742-0.017c-2.13-2.888-24.461-26.18-26.395-28.237
|
||||
c-0.053,1.967-0.055,6.062-0.055,10.043c0,5.287,0.4,13.354,0.634,17.714c-0.54-0.098-1.338-0.195-2.268-0.195
|
||||
c-0.939,0-1.709,0.086-2.331,0.195c0.436-5.625,0.558-14.755,0.558-23.337c0-6.704-0.099-10.38-0.178-13.762l1.744,0.015
|
||||
c2.256,2.45,24.457,25.428,26.392,27.487c0.053-1.967,0.057-5.441,0.057-9.424c0-5.285-0.402-13.354-0.636-17.711
|
||||
c0.542,0.094,1.338,0.192,2.269,0.192c0.942,0,1.71-0.084,2.332-0.192c-0.438,5.624-0.56,14.753-0.56,23.336
|
||||
C184.14,128.063,184.239,131.868,184.319,135.253z M230.093,88.11c9.889,12.128,17.896,31.314,19.065,47.379h0.156l8.24-85.104
|
||||
l4.646-0.003c0,0-5.271,55.013-8.341,82.351c-3.844,34.241-8.729,48.448-18.071,63.403l-1.43-6.508
|
||||
c7.28-12.787,9.357-23.946,10.306-29.823c2.956-18.304-1.273-43.291-14.025-62.789c-14.291-21.849-39.84-37.924-71.2-37.924
|
||||
c-25.762,0-48.143,11.327-63.766,28.993l-3.789-3.003c16.539-18.764,40.576-30.737,67.558-30.737
|
||||
C187.735,54.346,212.924,67.058,230.093,88.11z M138.305,107.241c0-5.29-4.629-6.973-8.248-6.973c-2.448,0-4.043,0.161-5.162,0.268
|
||||
c-0.159,3.885-0.318,7.457-0.318,11.287v2.926c0.529,0.071,3.021,0.059,3.566,0.049
|
||||
C132.537,114.707,138.305,113.312,138.305,107.241z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.9 KiB |
9
auth/assets/custom-icons/icons/configcat.svg
Normal file
After Width: | Height: | Size: 68 KiB |
9
auth/assets/custom-icons/icons/habbo.svg
Normal file
After Width: | Height: | Size: 26 KiB |
9
auth/assets/custom-icons/icons/mercado_livre.svg
Normal file
After Width: | Height: | Size: 57 KiB |
9
auth/assets/custom-icons/icons/sendgrid.svg
Normal file
After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 2.1 KiB |
BIN
auth/assets/icons/auth-icon.ico
Normal file
After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
@ -1 +1 @@
|
|||
ente Authenticator
|
||||
Ente Authenticator
|
|
@ -1,6 +1,6 @@
|
|||
flutter_icons:
|
||||
android: "launcher_icon"
|
||||
image_path: "assets/icon-light.png"
|
||||
adaptive_icon_foreground: "assets/icon-light-adaptive-fg.png"
|
||||
adaptive_icon_background: "#ffffff"
|
||||
|
||||
flutter_icons:
|
||||
android: "launcher_icon"
|
||||
image_path: "assets/generation-icons/icon-light.png"
|
||||
adaptive_icon_foreground: "assets/generation-icons/icon-light-adaptive-fg.png"
|
||||
adaptive_icon_background: "#ffffff"
|
||||
|
||||
|
|
|
@ -67,8 +67,6 @@ PODS:
|
|||
- Toast
|
||||
- local_auth_darwin (0.0.1):
|
||||
- Flutter
|
||||
- local_auth_ios (0.0.1):
|
||||
- Flutter
|
||||
- move_to_background (0.0.1):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner (5.0.11)
|
||||
|
@ -99,8 +97,6 @@ PODS:
|
|||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- smart_auth (0.0.1):
|
||||
- Flutter
|
||||
- sodium_libs (2.2.1):
|
||||
- Flutter
|
||||
- sqflite (0.0.3):
|
||||
|
@ -142,7 +138,6 @@ DEPENDENCIES:
|
|||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
|
||||
- move_to_background (from `.symlinks/plugins/move_to_background/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
|
@ -151,7 +146,6 @@ DEPENDENCIES:
|
|||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- smart_auth (from `.symlinks/plugins/smart_auth/ios`)
|
||||
- sodium_libs (from `.symlinks/plugins/sodium_libs/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
|
||||
|
@ -202,8 +196,6 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
local_auth_darwin:
|
||||
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
||||
local_auth_ios:
|
||||
:path: ".symlinks/plugins/local_auth_ios/ios"
|
||||
move_to_background:
|
||||
:path: ".symlinks/plugins/move_to_background/ios"
|
||||
package_info_plus:
|
||||
|
@ -220,8 +212,6 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
smart_auth:
|
||||
:path: ".symlinks/plugins/smart_auth/ios"
|
||||
sodium_libs:
|
||||
:path: ".symlinks/plugins/sodium_libs/ios"
|
||||
sqflite:
|
||||
|
@ -245,11 +235,10 @@ SPEC CHECKSUMS:
|
|||
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
|
||||
flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
|
||||
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
|
||||
local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
|
||||
local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
|
||||
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
|
@ -264,7 +253,6 @@ SPEC CHECKSUMS:
|
|||
SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe
|
||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
||||
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
|
||||
smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2
|
||||
sodium_libs: 1faae17af662384acbd13e41867a0008cd2e2318
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
sqlite3: 73b7fc691fdc43277614250e04d183740cb15078
|
||||
|
|
|
@ -365,7 +365,7 @@
|
|||
DEVELOPMENT_TEAM = 6Z68YJY9Q2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = auth;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ente Auth";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -439,7 +439,7 @@
|
|||
DEVELOPMENT_TEAM = 6Z68YJY9Q2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = auth;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ente Auth";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -513,7 +513,7 @@
|
|||
DEVELOPMENT_TEAM = 6Z68YJY9Q2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = auth;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ente Auth";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -587,7 +587,7 @@
|
|||
DEVELOPMENT_TEAM = 6Z68YJY9Q2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = auth;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ente Auth";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -661,7 +661,7 @@
|
|||
DEVELOPMENT_TEAM = 6Z68YJY9Q2;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = auth;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ente Auth";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
|
|
@ -18,7 +18,7 @@ import 'package:ente_auth/ui/settings/app_update_dialog.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:system_tray/system_tray.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class App extends StatefulWidget {
|
||||
|
@ -34,7 +34,7 @@ class App extends StatefulWidget {
|
|||
State<App> createState() => _AppState();
|
||||
}
|
||||
|
||||
class _AppState extends State<App> with WindowListener {
|
||||
class _AppState extends State<App> with WindowListener, TrayListener {
|
||||
late StreamSubscription<SignedOutEvent> _signedOutEvent;
|
||||
late StreamSubscription<SignedInEvent> _signedInEvent;
|
||||
Locale? locale;
|
||||
|
@ -46,12 +46,17 @@ class _AppState extends State<App> with WindowListener {
|
|||
|
||||
Future<void> initWindowManager() async {
|
||||
windowManager.addListener(this);
|
||||
await windowManager.setPreventClose(true);
|
||||
}
|
||||
|
||||
Future<void> initTrayManager() async {
|
||||
trayManager.addListener(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
initWindowManager();
|
||||
initTrayManager();
|
||||
|
||||
_signedOutEvent = Bus.instance.on<SignedOutEvent>().listen((event) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
|
@ -85,7 +90,10 @@ class _AppState extends State<App> with WindowListener {
|
|||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
|
||||
windowManager.removeListener(this);
|
||||
trayManager.removeListener(this);
|
||||
|
||||
_signedOutEvent.cancel();
|
||||
_signedInEvent.cancel();
|
||||
}
|
||||
|
@ -145,14 +153,44 @@ class _AppState extends State<App> with WindowListener {
|
|||
};
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowClose() async {
|
||||
final AppWindow appWindow = AppWindow();
|
||||
await appWindow.hide();
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowResize() {
|
||||
WindowListenerService.instance.onWindowResize().ignore();
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayIconMouseDown() {
|
||||
if (Platform.isWindows) {
|
||||
windowManager.show();
|
||||
} else {
|
||||
trayManager.popUpContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayIconRightMouseDown() {
|
||||
if (Platform.isWindows) {
|
||||
trayManager.popUpContextMenu();
|
||||
} else {
|
||||
windowManager.show();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onTrayIconRightMouseUp() {}
|
||||
|
||||
@override
|
||||
void onTrayMenuItemClick(MenuItem menuItem) {
|
||||
switch (menuItem.key) {
|
||||
case 'hide_window':
|
||||
windowManager.hide();
|
||||
break;
|
||||
case 'show_window':
|
||||
windowManager.show();
|
||||
break;
|
||||
case 'exit_app':
|
||||
windowManager.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,14 +78,14 @@
|
|||
"data": "Data",
|
||||
"importCodes": "Import codes",
|
||||
"importTypePlainText": "Plain text",
|
||||
"importTypeEnteEncrypted": "ente Encrypted export",
|
||||
"importTypeEnteEncrypted": "Ente Encrypted export",
|
||||
"passwordForDecryptingExport": "Password to decrypt export",
|
||||
"passwordEmptyError": "Password can not be empty",
|
||||
"importFromApp": "Import codes from {appName}",
|
||||
"importGoogleAuthGuide": "Export your accounts from Google Authenticator to a QR code using the \"Transfer Accounts\" option. Then using another device, scan the QR code.\n\nTip: You can use your laptop's webcam to take a picture of the QR code.",
|
||||
"importSelectJsonFile": "Select JSON file",
|
||||
"importSelectAppExport": "Select {appName} export file",
|
||||
"importEnteEncGuide": "Select the encrypted JSON file exported from ente",
|
||||
"importEnteEncGuide": "Select the encrypted JSON file exported from Ente",
|
||||
"importRaivoGuide": "Use the \"Export OTPs to Zip archive\" option in Raivo's Settings.\n\nExtract the zip file and import the JSON file.",
|
||||
"importBitwardenGuide": "Use the \"Export vault\" option within Bitwarden Tools and import the unencrypted JSON file.",
|
||||
"importAegisGuide": "Use the \"Export the vault\" option in Aegis's Settings.\n\nIf your vault is encrypted, you will need to enter vault password to decrypt the vault.",
|
||||
|
@ -115,22 +115,22 @@
|
|||
"copied": "Copied",
|
||||
"pleaseTryAgain": "Please try again",
|
||||
"existingUser": "Existing User",
|
||||
"newUser": "New to ente",
|
||||
"newUser": "New to Ente",
|
||||
"delete": "Delete",
|
||||
"enterYourPasswordHint": "Enter your password",
|
||||
"forgotPassword": "Forgot password",
|
||||
"oops": "Oops",
|
||||
"suggestFeatures": "Suggest features",
|
||||
"faq": "FAQ",
|
||||
"faq_q_1": "How secure is ente Auth?",
|
||||
"faq_a_1": "All codes you backup via ente is stored end-to-end encrypted. This means only you can access your codes. Our apps are open source and our cryptography has been externally audited.",
|
||||
"faq_q_1": "How secure is Auth?",
|
||||
"faq_a_1": "All codes you backup via Auth is stored end-to-end encrypted. This means only you can access your codes. Our apps are open source and our cryptography has been externally audited.",
|
||||
"faq_q_2": "Can I access my codes on desktop?",
|
||||
"faq_a_2": "You can access your codes on the web @ auth.ente.io.",
|
||||
"faq_q_3": "How can I delete codes?",
|
||||
"faq_a_3": "You can delete a code by swiping left on that item.",
|
||||
"faq_q_4": "How can I support this project?",
|
||||
"faq_a_4": "You can support the development of this project by subscribing to our Photos app @ ente.io.",
|
||||
"faq_q_5": "How can I enable FaceID lock in ente Auth",
|
||||
"faq_q_5": "How can I enable FaceID lock in Auth",
|
||||
"faq_a_5": "You can enable FaceID lock under Settings → Security → Lockscreen.",
|
||||
"somethingWentWrongMessage": "Something went wrong, please try again",
|
||||
"leaveFamily": "Leave family",
|
||||
|
@ -350,7 +350,7 @@
|
|||
"deleteCodeAuthMessage": "Authenticate to delete code",
|
||||
"showQRAuthMessage": "Authenticate to show QR code",
|
||||
"confirmAccountDeleteTitle": "Confirm account deletion",
|
||||
"confirmAccountDeleteMessage": "This account is linked to other ente apps, if you use any.\n\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"confirmAccountDeleteMessage": "This account is linked to other Ente apps, if you use any.\n\nYour uploaded data, across all Ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
|
||||
"androidBiometricHint": "Verify identity",
|
||||
"@androidBiometricHint": {
|
||||
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
|
||||
|
|
|
@ -31,42 +31,34 @@ import 'package:flutter_displaymode/flutter_displaymode.dart';
|
|||
import 'package:logging/logging.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:privacy_screen/privacy_screen.dart';
|
||||
import 'package:system_tray/system_tray.dart';
|
||||
import 'package:tray_manager/tray_manager.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
final _logger = Logger("main");
|
||||
|
||||
Future<void> initSystemTray() async {
|
||||
String path =
|
||||
Platform.isWindows ? 'assets/icon-light.ico' : 'assets/icon-light.png';
|
||||
|
||||
final AppWindow appWindow = AppWindow();
|
||||
final SystemTray systemTray = SystemTray();
|
||||
|
||||
// We first init the systray menu
|
||||
await systemTray.initSystemTray(
|
||||
title: "",
|
||||
iconPath: path,
|
||||
String path = Platform.isWindows
|
||||
? 'assets/icons/auth-icon.ico'
|
||||
: 'assets/icons/auth-icon.png';
|
||||
await trayManager.setIcon(path);
|
||||
Menu menu = Menu(
|
||||
items: [
|
||||
MenuItem(
|
||||
key: 'hide_window',
|
||||
label: 'Hide Window',
|
||||
),
|
||||
MenuItem(
|
||||
key: 'show_window',
|
||||
label: 'Show Window',
|
||||
),
|
||||
MenuItem.separator(),
|
||||
MenuItem(
|
||||
key: 'exit_app',
|
||||
label: 'Exit App',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// create context menu
|
||||
final show = MenuItem(label: 'Show', onClicked: () => appWindow.show());
|
||||
final hide = MenuItem(label: 'Hide', onClicked: () => appWindow.hide());
|
||||
final exit = MenuItem(label: 'Exit', onClicked: () => appWindow.close());
|
||||
|
||||
// set context menu
|
||||
await systemTray.setContextMenu([show, hide, exit]);
|
||||
|
||||
const kSystemTrayEventClick = 'leftMouseDown';
|
||||
const kSystemTrayEventRightClick = 'rightMouseDown';
|
||||
// // handle system tray event
|
||||
systemTray.registerSystemTrayEventHandler((eventName) {
|
||||
if (eventName == kSystemTrayEventClick) {
|
||||
Platform.isWindows ? appWindow.show() : systemTray.popUpContextMenu();
|
||||
} else if (eventName == kSystemTrayEventRightClick) {
|
||||
Platform.isWindows ? systemTray.popUpContextMenu() : appWindow.show();
|
||||
}
|
||||
});
|
||||
await trayManager.setContextMenu(menu);
|
||||
}
|
||||
|
||||
void main() async {
|
||||
|
|
|
@ -17,8 +17,8 @@ import 'package:ente_auth/utils/dialog_util.dart';
|
|||
import 'package:ente_auth/utils/platform_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_context_menu/flutter_context_menu.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:move_to_background/move_to_background.dart';
|
||||
|
@ -86,108 +86,122 @@ class _CodeWidgetState extends State<CodeWidget> {
|
|||
final l10n = context.l10n;
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
|
||||
child: Slidable(
|
||||
key: ValueKey(widget.code.hashCode),
|
||||
endActionPane: ActionPane(
|
||||
extentRatio: 0.60,
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onShowQrPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.qr_code_2_outlined,
|
||||
label: "QR",
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onEditPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.edit_outlined,
|
||||
label: l10n.edit,
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onDeletePressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor: const Color(0xFFFE4A49),
|
||||
icon: Icons.delete,
|
||||
label: l10n.delete,
|
||||
padding: const EdgeInsets.only(left: 0, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return RawGestureDetector(
|
||||
gestures: {
|
||||
PanGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<PanGestureRecognizer>(
|
||||
() => PanGestureRecognizer(
|
||||
debugOwner: this,
|
||||
// This recognizer accepts any button press made with a secondary button.
|
||||
allowedButtonsFilter: (int buttons) =>
|
||||
buttons & kSecondaryButton != 0,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (PlatformUtil.isDesktop()) {
|
||||
return ContextMenuRegion(
|
||||
contextMenu: ContextMenu(
|
||||
entries: <ContextMenuEntry>[
|
||||
MenuItem(
|
||||
label: 'QR',
|
||||
icon: Icons.qr_code_2_outlined,
|
||||
onSelected: () => _onShowQrPressed(null),
|
||||
),
|
||||
(PanGestureRecognizer instance) {
|
||||
instance
|
||||
..dragStartBehavior = DragStartBehavior.down
|
||||
..onEnd = (DragEndDetails details) {
|
||||
Slidable.of(context)?.openEndActionPane();
|
||||
};
|
||||
},
|
||||
),
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
onTap: () {
|
||||
_copyCurrentOTPToClipboard();
|
||||
},
|
||||
onDoubleTap: isMaskingEnabled
|
||||
? () {
|
||||
setState(
|
||||
() {
|
||||
_hideCode = !_hideCode;
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
onLongPress: () {
|
||||
_copyCurrentOTPToClipboard();
|
||||
},
|
||||
child: _getCardContents(l10n),
|
||||
),
|
||||
MenuItem(
|
||||
label: l10n.edit,
|
||||
icon: Icons.edit,
|
||||
onSelected: () => _onEditPressed(null),
|
||||
),
|
||||
),
|
||||
const MenuDivider(),
|
||||
MenuItem(
|
||||
label: l10n.delete,
|
||||
value: "Delete",
|
||||
icon: Icons.delete,
|
||||
onSelected: () => _onDeletePressed(null),
|
||||
),
|
||||
],
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
),
|
||||
child: _clippedCard(l10n),
|
||||
);
|
||||
},
|
||||
}
|
||||
|
||||
return Slidable(
|
||||
key: ValueKey(widget.code.hashCode),
|
||||
endActionPane: ActionPane(
|
||||
extentRatio: 0.60,
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onShowQrPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.qr_code_2_outlined,
|
||||
label: "QR",
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onEditPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.edit_outlined,
|
||||
label: l10n.edit,
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onDeletePressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor: const Color(0xFFFE4A49),
|
||||
icon: Icons.delete,
|
||||
label: l10n.delete,
|
||||
padding: const EdgeInsets.only(left: 0, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) => _clippedCard(l10n),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _clippedCard(AppLocalizations l10n) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
onTap: () {
|
||||
_copyCurrentOTPToClipboard();
|
||||
},
|
||||
onDoubleTap: isMaskingEnabled
|
||||
? () {
|
||||
setState(
|
||||
() {
|
||||
_hideCode = !_hideCode;
|
||||
},
|
||||
);
|
||||
}
|
||||
: null,
|
||||
onLongPress: () {
|
||||
_copyCurrentOTPToClipboard();
|
||||
},
|
||||
child: _getCardContents(l10n),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -93,12 +93,22 @@ class _HomePageState extends State<HomePage> {
|
|||
void _applyFilteringAndRefresh() {
|
||||
if (_searchText.isNotEmpty && _showSearchBox) {
|
||||
final String val = _searchText.toLowerCase();
|
||||
_filteredCodes = _codes
|
||||
.where(
|
||||
(element) => (element.account.toLowerCase().contains(val) ||
|
||||
element.issuer.toLowerCase().contains(val)),
|
||||
)
|
||||
.toList();
|
||||
// Prioritize issuer match above account for better UX while searching
|
||||
// for a specific TOTP for email providers. Searching for "emailProvider" like (gmail, proton) should
|
||||
// show the email provider first instead of other accounts where protonmail
|
||||
// is the account name.
|
||||
final List<Code> issuerMatch = [];
|
||||
final List<Code> accountMatch = [];
|
||||
|
||||
for (final Code code in _codes) {
|
||||
if (code.issuer.toLowerCase().contains(val)) {
|
||||
issuerMatch.add(code);
|
||||
} else if (code.account.toLowerCase().contains(val)) {
|
||||
accountMatch.add(code);
|
||||
}
|
||||
}
|
||||
_filteredCodes = issuerMatch;
|
||||
_filteredCodes.addAll(accountMatch);
|
||||
} else {
|
||||
_filteredCodes = _codes;
|
||||
}
|
||||
|
@ -180,7 +190,7 @@ class _HomePageState extends State<HomePage> {
|
|||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(
|
||||
title: !_showSearchBox
|
||||
? const Text('ente Auth')
|
||||
? const Text('Ente Auth')
|
||||
: TextField(
|
||||
autofocus: _searchText.isEmpty,
|
||||
controller: _textController,
|
||||
|
|
|
@ -24,7 +24,7 @@ Future<void> showEncryptedImportInstruction(BuildContext context) async {
|
|||
final l10n = context.l10n;
|
||||
final result = await showDialogWidget(
|
||||
context: context,
|
||||
title: l10n.importFromApp("ente Auth"),
|
||||
title: l10n.importFromApp("Ente Auth"),
|
||||
body: l10n.importEnteEncGuide,
|
||||
buttons: [
|
||||
ButtonWidget(
|
||||
|
|
|
@ -4,8 +4,7 @@ import 'package:ente_auth/services/user_service.dart';
|
|||
import 'package:ente_auth/ui/lifecycle_event_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:pinput/pinput.dart';
|
||||
import 'package:pinput/pin_put/pin_put.dart';
|
||||
|
||||
class TwoFactorAuthenticationPage extends StatefulWidget {
|
||||
final String sessionID;
|
||||
|
@ -20,6 +19,10 @@ class TwoFactorAuthenticationPage extends StatefulWidget {
|
|||
class _TwoFactorAuthenticationPageState
|
||||
extends State<TwoFactorAuthenticationPage> {
|
||||
final _pinController = TextEditingController();
|
||||
final _pinPutDecoration = BoxDecoration(
|
||||
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
);
|
||||
String _code = "";
|
||||
late LifecycleEventHandler _lifecycleEventHandler;
|
||||
|
||||
|
@ -60,16 +63,6 @@ class _TwoFactorAuthenticationPageState
|
|||
|
||||
Widget _getBody() {
|
||||
final l10n = context.l10n;
|
||||
final pinPutDecoration = BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.inputDecorationTheme
|
||||
.focusedBorder!
|
||||
.borderSide
|
||||
.color,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
@ -86,31 +79,32 @@ class _TwoFactorAuthenticationPageState
|
|||
const Padding(padding: EdgeInsets.all(32)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(40, 0, 40, 0),
|
||||
child: Pinput(
|
||||
onSubmitted: (String code) {
|
||||
child: PinPut(
|
||||
fieldsCount: 6,
|
||||
onSubmit: (String code) {
|
||||
_verifyTwoFactorCode(code);
|
||||
},
|
||||
length: 6,
|
||||
defaultPinTheme: const PinTheme(),
|
||||
submittedPinTheme: PinTheme(
|
||||
decoration: pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
),
|
||||
focusedPinTheme: PinTheme(
|
||||
decoration: pinPutDecoration,
|
||||
),
|
||||
followingPinTheme: PinTheme(
|
||||
decoration: pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
),
|
||||
onChanged: (String pin) {
|
||||
setState(() {
|
||||
_code = pin;
|
||||
});
|
||||
},
|
||||
controller: _pinController,
|
||||
submittedFieldDecoration: _pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
selectedFieldDecoration: _pinPutDecoration,
|
||||
followingFieldDecoration: _pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
border: Border.all(
|
||||
color: const Color.fromRGBO(45, 194, 98, 0.5),
|
||||
),
|
||||
),
|
||||
inputDecoration: const InputDecoration(
|
||||
focusedBorder: InputBorder.none,
|
||||
border: InputBorder.none,
|
||||
counterText: '',
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter_local_authentication/flutter_local_authentication.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:local_auth_android/local_auth_android.dart';
|
||||
import 'package:local_auth_ios/types/auth_messages_ios.dart';
|
||||
import 'package:local_auth_darwin/types/auth_messages_ios.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
Future<bool> requestAuthentication(BuildContext context, String reason) async {
|
||||
|
|
|
@ -13,10 +13,9 @@
|
|||
#include <gtk/gtk_plugin.h>
|
||||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <sentry_flutter/sentry_flutter_plugin.h>
|
||||
#include <smart_auth/smart_auth_plugin.h>
|
||||
#include <sodium_libs/sodium_libs_plugin.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
#include <system_tray/system_tray_plugin.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
|
@ -42,18 +41,15 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||
g_autoptr(FlPluginRegistrar) sentry_flutter_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin");
|
||||
sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar);
|
||||
g_autoptr(FlPluginRegistrar) smart_auth_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "SmartAuthPlugin");
|
||||
smart_auth_plugin_register_with_registrar(smart_auth_registrar);
|
||||
g_autoptr(FlPluginRegistrar) sodium_libs_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "SodiumLibsPlugin");
|
||||
sodium_libs_plugin_register_with_registrar(sodium_libs_registrar);
|
||||
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
|
||||
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
|
||||
g_autoptr(FlPluginRegistrar) system_tray_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "SystemTrayPlugin");
|
||||
system_tray_plugin_register_with_registrar(system_tray_registrar);
|
||||
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
|
||||
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
|
|
@ -10,10 +10,9 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
gtk
|
||||
screen_retriever
|
||||
sentry_flutter
|
||||
smart_auth
|
||||
sodium_libs
|
||||
sqlite3_flutter_libs
|
||||
system_tray
|
||||
tray_manager
|
||||
url_launcher_linux
|
||||
window_manager
|
||||
)
|
||||
|
|
|
@ -53,13 +53,13 @@ static void my_application_activate(GApplication *application)
|
|||
{
|
||||
GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "ente Auth");
|
||||
gtk_header_bar_set_title(header_bar, "Ente Auth");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
}
|
||||
else
|
||||
{
|
||||
gtk_window_set_title(window, "ente Auth");
|
||||
gtk_window_set_title(window, "Ente Auth");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
display_name: Auth
|
||||
license: GPLv3
|
||||
|
||||
icon: assets/icon/auth-icon.png
|
||||
|
||||
keywords:
|
||||
- Authentication
|
||||
- 2FA
|
||||
|
||||
generic_name: ente Auth
|
||||
|
||||
categories:
|
||||
- Utility
|
||||
|
||||
startup_notify: false
|
||||
|
||||
# You can specify the shared libraries that you want to bundle with your app
|
||||
#
|
||||
# flutter_distributor automatically detects the shared libraries that your app
|
||||
# depends on, but you can also specify them manually here.
|
||||
#
|
||||
# The following example shows how to bundle the libcurl library with your app.
|
||||
#
|
||||
# include:
|
||||
# - libcurl.so.4
|
||||
include: []
|
||||
display_name: Auth
|
||||
license: GPLv3
|
||||
|
||||
icon: assets/icons/auth-icon.png
|
||||
|
||||
keywords:
|
||||
- Authentication
|
||||
- 2FA
|
||||
|
||||
generic_name: Ente Auth
|
||||
|
||||
categories:
|
||||
- Utility
|
||||
|
||||
startup_notify: false
|
||||
|
||||
# You can specify the shared libraries that you want to bundle with your app
|
||||
#
|
||||
# flutter_distributor automatically detects the shared libraries that your app
|
||||
# depends on, but you can also specify them manually here.
|
||||
#
|
||||
# The following example shows how to bundle the libcurl library with your app.
|
||||
#
|
||||
# include:
|
||||
# - libcurl.so.4
|
||||
include:
|
||||
- libffi.so.7
|
||||
- libtiff.so.5
|
||||
|
|
|
@ -1,34 +1,34 @@
|
|||
display_name: Auth
|
||||
package_name: auth
|
||||
maintainer:
|
||||
name: Ente.io Developers
|
||||
email: human@ente.io
|
||||
priority: optional
|
||||
section: x11
|
||||
essential: false
|
||||
license: GPLv3
|
||||
icon: assets/icon/auth-icon.png
|
||||
installed_size: 36000
|
||||
|
||||
dependencies:
|
||||
- libwebkit2gtk-4.0-37
|
||||
- libsqlite3-0
|
||||
- libsodium23
|
||||
- libsecret-1-0
|
||||
- libappindicator3-1 | libayatana-appindicator3-1
|
||||
- gir1.2-appindicator3-0.1 | gir1.2-ayatanaappindicator3-0.1
|
||||
- libayatana-ido3-0.4-0
|
||||
|
||||
keywords:
|
||||
- Authentication
|
||||
- 2FA
|
||||
|
||||
generic_name: Ente Authentication
|
||||
|
||||
categories:
|
||||
- Utility
|
||||
|
||||
startup_notify: false
|
||||
|
||||
supported_mime_type:
|
||||
- x-scheme-handler/ente
|
||||
display_name: Auth
|
||||
package_name: auth
|
||||
maintainer:
|
||||
name: Ente.io Developers
|
||||
email: human@ente.io
|
||||
priority: optional
|
||||
section: x11
|
||||
essential: false
|
||||
license: GPLv3
|
||||
icon: assets/icons/auth-icon.png
|
||||
installed_size: 36000
|
||||
|
||||
dependencies:
|
||||
- libwebkit2gtk-4.0-37
|
||||
- libsqlite3-0
|
||||
- libsodium23
|
||||
- libsecret-1-0
|
||||
- libappindicator3-1 | libayatana-appindicator3-1
|
||||
- gir1.2-appindicator3-0.1 | gir1.2-ayatanaappindicator3-0.1
|
||||
- libayatana-ido3-0.4-0
|
||||
|
||||
keywords:
|
||||
- Authentication
|
||||
- 2FA
|
||||
|
||||
generic_name: Ente Authentication
|
||||
|
||||
categories:
|
||||
- Utility
|
||||
|
||||
startup_notify: false
|
||||
|
||||
supported_mime_type:
|
||||
- x-scheme-handler/ente
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
icon: assets/icon/auth-icon.png
|
||||
summary: 2FA app with free end-to-end encrypted backup and sync
|
||||
group: Application/Utility
|
||||
vendor: Ente.io
|
||||
packager: Ente.io Developers
|
||||
packagerEmail: human@ente.io
|
||||
license: GPLv3
|
||||
url: https://github.com/ente-io/ente
|
||||
|
||||
display_name: Auth
|
||||
|
||||
dependencies:
|
||||
- libsqlite3x
|
||||
- webkit2gtk-4.0
|
||||
- libsodium
|
||||
- libsecret
|
||||
- libappindicator
|
||||
|
||||
keywords:
|
||||
- Authentication
|
||||
- 2FA
|
||||
|
||||
generic_name: Ente Authentication
|
||||
|
||||
categories:
|
||||
- Utility
|
||||
|
||||
startup_notify: false
|
||||
|
||||
supported_mime_type:
|
||||
- x-scheme-handler/ente
|
||||
icon: assets/icons/auth-icon.png
|
||||
summary: 2FA app with free end-to-end encrypted backup and sync
|
||||
group: Application/Utility
|
||||
vendor: Ente.io
|
||||
packager: Ente.io Developers
|
||||
packagerEmail: human@ente.io
|
||||
license: GPLv3
|
||||
url: https://github.com/ente-io/ente
|
||||
|
||||
display_name: Auth
|
||||
|
||||
requires:
|
||||
- libsqlite3x
|
||||
- webkit2gtk-4.0
|
||||
- libsodium
|
||||
- libsecret
|
||||
- libappindicator
|
||||
|
||||
keywords:
|
||||
- Authentication
|
||||
- 2FA
|
||||
|
||||
generic_name: Ente Authentication
|
||||
|
||||
categories:
|
||||
- Utility
|
||||
|
||||
startup_notify: false
|
||||
|
||||
supported_mime_type:
|
||||
- x-scheme-handler/ente
|
||||
|
|
|
@ -20,11 +20,10 @@ import screen_retriever
|
|||
import sentry_flutter
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import smart_auth
|
||||
import sodium_libs
|
||||
import sqflite
|
||||
import sqlite3_flutter_libs
|
||||
import system_tray
|
||||
import tray_manager
|
||||
import url_launcher_macos
|
||||
import window_manager
|
||||
|
||||
|
@ -44,11 +43,10 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SmartAuthPlugin.register(with: registry.registrar(forPlugin: "SmartAuthPlugin"))
|
||||
SodiumLibsPlugin.register(with: registry.registrar(forPlugin: "SodiumLibsPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
|
||||
SystemTrayPlugin.register(with: registry.registrar(forPlugin: "SystemTrayPlugin"))
|
||||
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ PODS:
|
|||
- sqlite3/fts5
|
||||
- sqlite3/perf-threadsafe
|
||||
- sqlite3/rtree
|
||||
- system_tray (0.0.1):
|
||||
- tray_manager (0.0.1):
|
||||
- FlutterMacOS
|
||||
- url_launcher_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
|
@ -91,7 +91,7 @@ DEPENDENCIES:
|
|||
- sodium_libs (from `Flutter/ephemeral/.symlinks/plugins/sodium_libs/macos`)
|
||||
- sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`)
|
||||
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`)
|
||||
- system_tray (from `Flutter/ephemeral/.symlinks/plugins/system_tray/macos`)
|
||||
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
|
||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
||||
- window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`)
|
||||
|
||||
|
@ -144,8 +144,8 @@ EXTERNAL SOURCES:
|
|||
:path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin
|
||||
sqlite3_flutter_libs:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos
|
||||
system_tray:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/system_tray/macos
|
||||
tray_manager:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos
|
||||
url_launcher_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||
window_manager:
|
||||
|
@ -177,7 +177,7 @@ SPEC CHECKSUMS:
|
|||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
sqlite3: 73b7fc691fdc43277614250e04d183740cb15078
|
||||
sqlite3_flutter_libs: 06a05802529659a272beac4ee1350bfec294f386
|
||||
system_tray: e53c972838c69589ff2e77d6d3abfd71332f9e5d
|
||||
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
||||
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
/* Begin PBXFileReference section */
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
33CC10ED2044A3C60003C045 /* ente Auth.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ente Auth.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10ED2044A3C60003C045 /* Ente Auth.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ente Auth.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
|
@ -122,7 +122,7 @@
|
|||
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10ED2044A3C60003C045 /* ente Auth.app */,
|
||||
33CC10ED2044A3C60003C045 /* Ente Auth.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
@ -192,7 +192,7 @@
|
|||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* ente Auth.app */;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* Ente Auth.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "ente Auth.app"
|
||||
BuildableName = "Ente Auth.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
@ -31,7 +31,7 @@
|
|||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "ente Auth.app"
|
||||
BuildableName = "Ente Auth.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
@ -54,7 +54,7 @@
|
|||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "ente Auth.app"
|
||||
BuildableName = "Ente Auth.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
@ -71,7 +71,7 @@
|
|||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "ente Auth.app"
|
||||
BuildableName = "Ente Auth.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
// 'flutter create' template.
|
||||
|
||||
// The application's name. By default this is also the title of the Flutter window.
|
||||
PRODUCT_NAME = ente Auth
|
||||
PRODUCT_NAME = Ente Auth
|
||||
|
||||
// The application's bundle identifier
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.ente.auth
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
title: Auth
|
||||
icon: ../../../assets/icon/auth-icon.png
|
||||
contents:
|
||||
- x: 448
|
||||
y: 344
|
||||
type: link
|
||||
path: "/Applications"
|
||||
- x: 192
|
||||
y: 344
|
||||
type: file
|
||||
path: ente Auth.app
|
||||
title: Auth
|
||||
icon: ../../../assets/icons/auth-icon.png
|
||||
contents:
|
||||
- x: 448
|
||||
y: 344
|
||||
type: link
|
||||
path: "/Applications"
|
||||
- x: 192
|
||||
y: 344
|
||||
type: file
|
||||
path: Ente Auth.app
|
||||
|
|
|
@ -85,10 +85,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: bloc
|
||||
sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e
|
||||
sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.3"
|
||||
version: "8.1.4"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -133,10 +133,10 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21"
|
||||
sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.8"
|
||||
version: "2.4.9"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -294,7 +294,7 @@ packages:
|
|||
description:
|
||||
path: "packages/desktop_webview_window"
|
||||
ref: HEAD
|
||||
resolved-ref: "649302f53451dde9ded4dc1fadfbead2b001fe64"
|
||||
resolved-ref: "8cbbf9cd6efcfee5e0f420a36f7f8e7e64b667a1"
|
||||
url: "https://github.com/MixinNetwork/flutter-plugins"
|
||||
source: git
|
||||
version: "0.2.4"
|
||||
|
@ -318,10 +318,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8"
|
||||
sha256: "0978e9a3e45305a80a7210dbeaf79d6ee8bee33f70c8e542dc654c952070217f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.1"
|
||||
version: "5.4.2+1"
|
||||
dotted_border:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -347,6 +347,14 @@ packages:
|
|||
url: "https://github.com/ente-io/ente_crypto_dart.git"
|
||||
source: git
|
||||
version: "1.0.0"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: equatable
|
||||
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
event_bus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -399,10 +407,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: caa6bc229eab3e32eb2f37b53a5f9d22a6981474afd210c512a7546c1e1a04f6
|
||||
sha256: "1bbf65dd997458a08b531042ec3794112a6c39c07c37ff22113d2e7e4f81d4e4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.0"
|
||||
version: "6.2.1"
|
||||
file_saver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -436,10 +444,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_bloc
|
||||
sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1"
|
||||
sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.4"
|
||||
version: "8.1.5"
|
||||
flutter_context_menu:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_context_menu
|
||||
sha256: "9f220a8fa0290c68e38000d6d62a0dc4555d490c15a5bd856a6e6d255d81b8dc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
flutter_displaymode:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -562,10 +578,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_native_splash
|
||||
sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e"
|
||||
sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.10"
|
||||
version: "2.4.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -685,10 +701,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612"
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "4.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -858,21 +874,13 @@ packages:
|
|||
source: hosted
|
||||
version: "1.0.37"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_auth_darwin
|
||||
sha256: "33381a15b0de2279523eca694089393bb146baebdce72a404555d03174ebc1e9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
local_auth_ios:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_auth_ios
|
||||
sha256: "6dde47dc852bc0c8343cb58e66a46efb16b62eddf389ce103d4dacb0c6c40c71"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.7"
|
||||
local_auth_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -913,6 +921,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.0"
|
||||
menu_base:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: menu_base
|
||||
sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1093,10 +1109,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: pinput
|
||||
sha256: a92b55ecf9c25d1b9e100af45905385d5bc34fc9b6b04177a9e82cb88fe4d805
|
||||
sha256: "27eb69042f75755bdb6544f6e79a50a6ed09d6e97e2d75c8421744df1e392949"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
version: "1.2.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1229,10 +1245,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956
|
||||
sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
version: "3.4.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1305,19 +1321,19 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
shortid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shortid
|
||||
sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
smart_auth:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: smart_auth
|
||||
sha256: a25229b38c02f733d0a4e98d941b42bed91a976cb589e934895e60ccfa674cf6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
sodium:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1330,10 +1346,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: sodium_libs
|
||||
sha256: "05b9e2de0f850a5315f596379f0c617aa1f938ed5e099126f8919c55499fed46"
|
||||
sha256: f7f6719b7ab3e8512ce7a5ecd7bc8d865482431cdd5a07a46b55b13c152b54e1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.1+1"
|
||||
source_gen:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1371,7 +1387,7 @@ packages:
|
|||
description:
|
||||
path: sqflite
|
||||
ref: HEAD
|
||||
resolved-ref: "07fb76f37e17a396dd7dcc8fb83b81b3f5b62486"
|
||||
resolved-ref: "075b3e2f81e691a19a500e7ff6db2953de7f83a9"
|
||||
url: "https://github.com/tekartik/sqflite"
|
||||
source: git
|
||||
version: "2.3.2"
|
||||
|
@ -1379,18 +1395,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: sqflite_common
|
||||
sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
|
||||
sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.3"
|
||||
version: "2.5.4"
|
||||
sqflite_common_ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sqflite_common_ffi
|
||||
sha256: "754927d82de369a6b9e760fb60640aa81da650f35ffd468d5a992814d6022908"
|
||||
sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2+1"
|
||||
version: "2.3.3"
|
||||
sqlite3:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1463,14 +1479,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0+1"
|
||||
system_tray:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: system_tray
|
||||
sha256: "1bcc11bc230033be20d7443c29f65f68d67169715a838a1122f20fbff5dd2d19"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.1"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1503,6 +1511,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
tray_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tray_manager
|
||||
sha256: "4ab709d70a4374af172f8c39e018db33a4271265549c6fc9d269a65e5f4b0225"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
tuple:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1527,14 +1543,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.2"
|
||||
universal_platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: universal_platform
|
||||
sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0+1"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 2.0.46+246
|
||||
version: 2.0.55+255
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
|
@ -41,6 +41,7 @@ dependencies:
|
|||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.0.1
|
||||
flutter_context_menu: ^0.1.3
|
||||
flutter_displaymode: ^0.6.0
|
||||
flutter_email_sender: ^6.0.2
|
||||
flutter_inappwebview: ^6.0.0
|
||||
|
@ -63,9 +64,9 @@ dependencies:
|
|||
http: ^1.1.0
|
||||
intl: ^0.18.0
|
||||
json_annotation: ^4.5.0
|
||||
local_auth: ^2.1.7
|
||||
local_auth_android: ^1.0.31
|
||||
local_auth_ios: ^1.1.3
|
||||
local_auth: ^2.2.0
|
||||
local_auth_android: ^1.0.37
|
||||
local_auth_darwin: ^1.2.2
|
||||
logging: ^1.0.1
|
||||
modal_bottom_sheet: ^3.0.0-pre
|
||||
move_to_background: ^1.0.2
|
||||
|
@ -74,7 +75,7 @@ dependencies:
|
|||
password_strength: ^0.2.0
|
||||
path: ^1.8.3
|
||||
path_provider: ^2.0.11
|
||||
pinput: ^3.0.1
|
||||
pinput: ^1.2.2
|
||||
pointycastle: ^3.7.3
|
||||
privacy_screen: ^0.0.6
|
||||
protobuf: ^3.0.0
|
||||
|
@ -93,7 +94,7 @@ dependencies:
|
|||
sqlite3_flutter_libs: ^0.5.19+1
|
||||
step_progress_indicator: ^1.0.2
|
||||
styled_text: ^8.1.0
|
||||
system_tray: ^0.1.1
|
||||
tray_manager: ^0.2.1
|
||||
tuple: ^2.0.0
|
||||
url_launcher: ^6.1.5
|
||||
uuid: ^4.2.2
|
||||
|
@ -122,6 +123,7 @@ flutter:
|
|||
# https://docs:flutter:dev/development/ui/assets-and-images:
|
||||
assets:
|
||||
- assets/
|
||||
- assets/icons/
|
||||
- assets/simple-icons/icons/
|
||||
- assets/simple-icons/_data/
|
||||
- assets/custom-icons/icons/
|
||||
|
@ -141,10 +143,10 @@ flutter:
|
|||
|
||||
flutter_icons:
|
||||
android: "launcher_icon"
|
||||
adaptive_icon_foreground: "assets/icon-light-adaptive-fg.png"
|
||||
adaptive_icon_foreground: "assets/generation-icons/icon-light-adaptive-fg.png"
|
||||
adaptive_icon_background: "#ffffff"
|
||||
ios: true
|
||||
image_path: "assets/icon-light.png"
|
||||
image_path: "assets/generation-icons/icon-light.png"
|
||||
remove_alpha_ios: true
|
||||
|
||||
flutter_native_splash:
|
||||
|
|
|
@ -16,10 +16,9 @@
|
|||
#include <screen_retriever/screen_retriever_plugin.h>
|
||||
#include <sentry_flutter/sentry_flutter_plugin.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
#include <smart_auth/smart_auth_plugin.h>
|
||||
#include <sodium_libs/sodium_libs_plugin_c_api.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
#include <system_tray/system_tray_plugin.h>
|
||||
#include <tray_manager/tray_manager_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
#include <window_manager/window_manager_plugin.h>
|
||||
|
||||
|
@ -44,14 +43,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||
registry->GetRegistrarForPlugin("SentryFlutterPlugin"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
SmartAuthPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SmartAuthPlugin"));
|
||||
SodiumLibsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SodiumLibsPluginCApi"));
|
||||
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
||||
SystemTrayPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SystemTrayPlugin"));
|
||||
TrayManagerPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
WindowManagerPluginRegisterWithRegistrar(
|
||||
|
|
|
@ -13,10 +13,9 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
screen_retriever
|
||||
sentry_flutter
|
||||
share_plus
|
||||
smart_auth
|
||||
sodium_libs
|
||||
sqlite3_flutter_libs
|
||||
system_tray
|
||||
tray_manager
|
||||
url_launcher_windows
|
||||
window_manager
|
||||
)
|
||||
|
|
64
auth/windows/packaging/exe/inno_setup.iss
Normal file
|
@ -0,0 +1,64 @@
|
|||
[Setup]
|
||||
AppId={{APP_ID}}
|
||||
AppVersion={{APP_VERSION}}
|
||||
AppName={{DISPLAY_NAME}}
|
||||
AppPublisher={{PUBLISHER_NAME}}
|
||||
AppPublisherURL={{PUBLISHER_URL}}
|
||||
AppSupportURL={{PUBLISHER_URL}}
|
||||
AppUpdatesURL={{PUBLISHER_URL}}
|
||||
DefaultDirName={{INSTALL_DIR_NAME}}
|
||||
DisableProgramGroupPage=yes
|
||||
OutputDir=.
|
||||
OutputBaseFilename={{OUTPUT_BASE_FILENAME}}
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
SetupIconFile={{SETUP_ICON_FILE}}
|
||||
WizardStyle=modern
|
||||
;PrivilegesRequired={{PRIVILEGES_REQUIRED}}
|
||||
PrivilegesRequiredOverridesAllowed=dialog
|
||||
ArchitecturesAllowed=x64
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
UninstallDisplayIcon={app}\auth.exe
|
||||
|
||||
[Languages]
|
||||
{% for locale in LOCALES %}
|
||||
{% if locale == 'en' %}Name: "english"; MessagesFile: "compiler:Default.isl"{% endif %}
|
||||
{% if locale == 'hy' %}Name: "armenian"; MessagesFile: "compiler:Languages\\Armenian.isl"{% endif %}
|
||||
{% if locale == 'bg' %}Name: "bulgarian"; MessagesFile: "compiler:Languages\\Bulgarian.isl"{% endif %}
|
||||
{% if locale == 'ca' %}Name: "catalan"; MessagesFile: "compiler:Languages\\Catalan.isl"{% endif %}
|
||||
{% if locale == 'zh' %}Name: "chinesesimplified"; MessagesFile: "compiler:Languages\\ChineseSimplified.isl"{% endif %}
|
||||
{% if locale == 'co' %}Name: "corsican"; MessagesFile: "compiler:Languages\\Corsican.isl"{% endif %}
|
||||
{% if locale == 'cs' %}Name: "czech"; MessagesFile: "compiler:Languages\\Czech.isl"{% endif %}
|
||||
{% if locale == 'da' %}Name: "danish"; MessagesFile: "compiler:Languages\\Danish.isl"{% endif %}
|
||||
{% if locale == 'nl' %}Name: "dutch"; MessagesFile: "compiler:Languages\\Dutch.isl"{% endif %}
|
||||
{% if locale == 'fi' %}Name: "finnish"; MessagesFile: "compiler:Languages\\Finnish.isl"{% endif %}
|
||||
{% if locale == 'fr' %}Name: "french"; MessagesFile: "compiler:Languages\\French.isl"{% endif %}
|
||||
{% if locale == 'de' %}Name: "german"; MessagesFile: "compiler:Languages\\German.isl"{% endif %}
|
||||
{% if locale == 'he' %}Name: "hebrew"; MessagesFile: "compiler:Languages\\Hebrew.isl"{% endif %}
|
||||
{% if locale == 'is' %}Name: "icelandic"; MessagesFile: "compiler:Languages\\Icelandic.isl"{% endif %}
|
||||
{% if locale == 'it' %}Name: "italian"; MessagesFile: "compiler:Languages\\Italian.isl"{% endif %}
|
||||
{% if locale == 'ja' %}Name: "japanese"; MessagesFile: "compiler:Languages\\Japanese.isl"{% endif %}
|
||||
{% if locale == 'no' %}Name: "norwegian"; MessagesFile: "compiler:Languages\\Norwegian.isl"{% endif %}
|
||||
{% if locale == 'pl' %}Name: "polish"; MessagesFile: "compiler:Languages\\Polish.isl"{% endif %}
|
||||
{% if locale == 'pt' %}Name: "portuguese"; MessagesFile: "compiler:Languages\\Portuguese.isl"{% endif %}
|
||||
{% if locale == 'ru' %}Name: "russian"; MessagesFile: "compiler:Languages\\Russian.isl"{% endif %}
|
||||
{% if locale == 'sk' %}Name: "slovak"; MessagesFile: "compiler:Languages\\Slovak.isl"{% endif %}
|
||||
{% if locale == 'sl' %}Name: "slovenian"; MessagesFile: "compiler:Languages\\Slovenian.isl"{% endif %}
|
||||
{% if locale == 'es' %}Name: "spanish"; MessagesFile: "compiler:Languages\\Spanish.isl"{% endif %}
|
||||
{% if locale == 'tr' %}Name: "turkish"; MessagesFile: "compiler:Languages\\Turkish.isl"{% endif %}
|
||||
{% if locale == 'uk' %}Name: "ukrainian"; MessagesFile: "compiler:Languages\\Ukrainian.isl"{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if CREATE_DESKTOP_ICON != true %}unchecked{% else %}checkedonce{% endif %}
|
||||
Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if LAUNCH_AT_STARTUP != true %}unchecked{% else %}checkedonce{% endif %}
|
||||
[Files]
|
||||
Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"
|
||||
Name: "{autodesktop}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; Tasks: desktopicon
|
||||
Name: "{userstartup}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; WorkingDir: "{app}"; Tasks: launchAtStartup
|
||||
[Run]
|
||||
Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: {% if PRIVILEGES_REQUIRED == 'admin' %}runascurrentuser{% endif %} nowait postinstall skipifsilent
|
|
@ -1,8 +1,10 @@
|
|||
app_id: 9E5F0C93-96A3-4DA9-AE52-1AA6339851FC
|
||||
publisher: ente.io
|
||||
publisher_url: https://github.com/ente-io/ente
|
||||
display_name: ente Auth
|
||||
create_desktop_icon: true
|
||||
install_dir_name: enteauth
|
||||
locales:
|
||||
- en
|
||||
app_id: 9E5F0C93-96A3-4DA9-AE52-1AA6339851FC
|
||||
publisher: ente.io
|
||||
publisher_url: https://github.com/ente-io/ente
|
||||
display_name: Ente Auth
|
||||
create_desktop_icon: false
|
||||
install_dir_name: "{autopf}\\Ente Auth"
|
||||
setup_icon_file: ../../assets/icons/auth-icon.ico
|
||||
script_template: inno_setup.iss
|
||||
locales:
|
||||
- en
|
||||
|
|
|
@ -90,12 +90,12 @@ BEGIN
|
|||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Ente Technologies, Inc." "\0"
|
||||
VALUE "FileDescription", "ente Auth" "\0"
|
||||
VALUE "FileDescription", "Ente Auth" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "ente Auth" "\0"
|
||||
VALUE "InternalName", "Ente Auth" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2024 Ente Technologies, Inc.. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "auth.exe" "\0"
|
||||
VALUE "ProductName", "ente Auth" "\0"
|
||||
VALUE "ProductName", "Ente Auth" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
END
|
||||
END
|
||||
|
|
|
@ -49,7 +49,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
|||
_In_ wchar_t *command_line, _In_ int show_command)
|
||||
{
|
||||
// [app_links]
|
||||
if (SendAppLinkToInstance(L"ente Auth"))
|
||||
if (SendAppLinkToInstance(L"Ente Auth"))
|
||||
{
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
|||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.Create(L"ente Auth", origin, size))
|
||||
if (!window.Create(L"Ente Auth", origin, size))
|
||||
{
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,10 @@ type File struct {
|
|||
Info *FileInfo `json:"info,omitempty"`
|
||||
}
|
||||
|
||||
func (f File) IsRemovedFromAlbum() bool {
|
||||
return f.IsDeleted || f.File.EncryptedData == "-"
|
||||
}
|
||||
|
||||
// FileInfo has information about storage used by the file & it's metadata(future)
|
||||
type FileInfo struct {
|
||||
FileSize int64 `json:"fileSize,omitempty"`
|
||||
|
|
|
@ -98,7 +98,8 @@ func DecryptChaChaBase64(data string, key []byte, nonce string) (string, []byte,
|
|||
// Decode data from base64
|
||||
dataBytes, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("invalid data: %v", err)
|
||||
// safe to log the encrypted data
|
||||
return "", nil, fmt.Errorf("invalid base64 data %s: %v", data, err)
|
||||
}
|
||||
// Decode nonce from base64
|
||||
nonceBytes, err := base64.StdEncoding.DecodeString(nonce)
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
var AppVersion = "0.1.12"
|
||||
var AppVersion = "0.1.13"
|
||||
|
||||
func main() {
|
||||
cliDBPath, err := GetCLIConfigPath()
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ente-io/cli/internal/api"
|
||||
eCrypto "github.com/ente-io/cli/internal/crypto"
|
||||
"github.com/ente-io/cli/pkg/model"
|
||||
|
@ -41,7 +42,7 @@ func MapCollectionToAlbum(ctx context.Context, collection api.Collection, holder
|
|||
if collection.MagicMetadata != nil {
|
||||
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.MagicMetadata.Data, collectionKey, collection.MagicMetadata.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to decrypt magic metadata for collection %d: %w", collection.ID, err)
|
||||
}
|
||||
err = json.Unmarshal(encodedJsonBytes, &album.PrivateMeta)
|
||||
if err != nil {
|
||||
|
@ -51,28 +52,28 @@ func MapCollectionToAlbum(ctx context.Context, collection api.Collection, holder
|
|||
if collection.PublicMagicMetadata != nil {
|
||||
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.PublicMagicMetadata.Data, collectionKey, collection.PublicMagicMetadata.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to decrypt public magic metadata for collection %d: %w", collection.ID, err)
|
||||
}
|
||||
err = json.Unmarshal(encodedJsonBytes, &album.PublicMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to unmarshal public magic metadata for collection %d: %w", collection.ID, err)
|
||||
}
|
||||
}
|
||||
if album.IsShared && collection.SharedMagicMetadata != nil {
|
||||
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.SharedMagicMetadata.Data, collectionKey, collection.SharedMagicMetadata.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to decrypt shared magic metadata for collection %d: %w", collection.ID, err)
|
||||
}
|
||||
err = json.Unmarshal(encodedJsonBytes, &album.SharedMeta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to unmarshal shared magic metadata for collection %d: %w", collection.ID, err)
|
||||
}
|
||||
}
|
||||
return &album, nil
|
||||
}
|
||||
|
||||
func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file api.File, holder *secrets.KeyHolder) (*model.RemoteFile, error) {
|
||||
if file.IsDeleted {
|
||||
if file.IsRemovedFromAlbum() {
|
||||
return nil, errors.New("file is deleted")
|
||||
}
|
||||
albumKey := album.AlbumKey.MustDecrypt(holder.DeviceKey)
|
||||
|
@ -99,7 +100,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap
|
|||
if file.Metadata.DecryptionHeader != "" {
|
||||
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.Metadata.EncryptedData, fileKey, file.Metadata.DecryptionHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to decrypt metadata for file %d: %w", file.ID, err)
|
||||
}
|
||||
err = json.Unmarshal(encodedJsonBytes, &photoFile.Metadata)
|
||||
if err != nil {
|
||||
|
@ -109,7 +110,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap
|
|||
if file.MagicMetadata != nil {
|
||||
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.MagicMetadata.Data, fileKey, file.MagicMetadata.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to decrypt magic metadata for file %d: %w", file.ID, err)
|
||||
}
|
||||
err = json.Unmarshal(encodedJsonBytes, &photoFile.PrivateMetadata)
|
||||
if err != nil {
|
||||
|
@ -119,7 +120,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap
|
|||
if file.PubicMagicMetadata != nil {
|
||||
_, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.PubicMagicMetadata.Data, fileKey, file.PubicMagicMetadata.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to decrypt public magic metadata for file %d: %w", file.ID, err)
|
||||
}
|
||||
err = json.Unmarshal(encodedJsonBytes, &photoFile.PublicMetadata)
|
||||
if err != nil {
|
||||
|
|
|
@ -87,16 +87,16 @@ func (c *ClICtrl) fetchRemoteFiles(ctx context.Context) error {
|
|||
if file.UpdationTime > maxUpdated {
|
||||
maxUpdated = file.UpdationTime
|
||||
}
|
||||
if isFirstSync && file.IsDeleted {
|
||||
if isFirstSync && file.IsRemovedFromAlbum() {
|
||||
// on first sync, no need to sync delete markers
|
||||
continue
|
||||
}
|
||||
albumEntry := model.AlbumFileEntry{AlbumID: album.ID, FileID: file.ID, IsDeleted: file.IsDeleted, SyncedLocally: false}
|
||||
albumEntry := model.AlbumFileEntry{AlbumID: album.ID, FileID: file.ID, IsDeleted: file.IsRemovedFromAlbum(), SyncedLocally: false}
|
||||
putErr := c.UpsertAlbumEntry(ctx, &albumEntry)
|
||||
if putErr != nil {
|
||||
return putErr
|
||||
}
|
||||
if file.IsDeleted {
|
||||
if file.IsRemovedFromAlbum() {
|
||||
continue
|
||||
}
|
||||
photoFile, err := mapper.MapApiFileToPhotoFile(ctx, album, file, c.KeyHolder)
|
||||
|
|
|
@ -7,11 +7,6 @@ module.exports = {
|
|||
// "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: {
|
||||
|
|
|
@ -61,15 +61,15 @@ Electron process. This allows us to directly use the output produced by
|
|||
|
||||
### Others
|
||||
|
||||
* [any-shell-escape](https://github.com/boazy/any-shell-escape) is for
|
||||
escaping shell commands before we execute them (e.g. say when invoking the
|
||||
embedded ffmpeg CLI).
|
||||
- [any-shell-escape](https://github.com/boazy/any-shell-escape) is for
|
||||
escaping shell commands before we execute them (e.g. say when invoking the
|
||||
embedded ffmpeg CLI).
|
||||
|
||||
* [auto-launch](https://github.com/Teamwork/node-auto-launch) is for
|
||||
automatically starting our app on login, if the user so wishes.
|
||||
- [auto-launch](https://github.com/Teamwork/node-auto-launch) is for
|
||||
automatically starting our app on login, if the user so wishes.
|
||||
|
||||
* [electron-store](https://github.com/sindresorhus/electron-store) is used for
|
||||
persisting user preferences and other arbitrary data.
|
||||
- [electron-store](https://github.com/sindresorhus/electron-store) is used for
|
||||
persisting user preferences and other arbitrary data.
|
||||
|
||||
## Dev
|
||||
|
||||
|
@ -79,12 +79,12 @@ 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`.
|
||||
- [concurrently](https://github.com/open-cli-tools/concurrently) for spawning
|
||||
parallel tasks when we do `yarn dev`.
|
||||
|
||||
* [shx](https://github.com/shelljs/shx) for providing a portable way to use Unix
|
||||
commands in our `package.json` scripts. This allows us to use the same
|
||||
commands (like `ln`) across different platforms like Linux and Windows.
|
||||
- [shx](https://github.com/shelljs/shx) for providing a portable way to use
|
||||
Unix commands in our `package.json` scripts. This allows us to use the same
|
||||
commands (like `ln`) across different platforms like Linux and Windows.
|
||||
|
||||
## Functionality
|
||||
|
||||
|
@ -111,11 +111,11 @@ watcher for the watch folders functionality.
|
|||
|
||||
### AI/ML
|
||||
|
||||
* [onnxruntime-node](https://github.com/Microsoft/onnxruntime)
|
||||
* html-entities is used by the bundled clip-bpe-ts.
|
||||
* GGML binaries are bundled
|
||||
* We also use [jpeg-js](https://github.com/jpeg-js/jpeg-js#readme) for
|
||||
conversion of all images to JPEG before processing.
|
||||
- [onnxruntime-node](https://github.com/Microsoft/onnxruntime) is used for
|
||||
natural language searches based on CLIP.
|
||||
- html-entities is used by the bundled clip-bpe-ts tokenizer.
|
||||
- [jpeg-js](https://github.com/jpeg-js/jpeg-js#readme) is used for decoding
|
||||
JPEG data into raw RGB bytes before passing it to ONNX.
|
||||
|
||||
## ZIP
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ are built against `electron`'s packaged `node` version. We use
|
|||
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
|
||||
### lint, 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
|
||||
|
|
|
@ -19,7 +19,6 @@ mac:
|
|||
arch: [universal]
|
||||
category: public.app-category.photography
|
||||
hardenedRuntime: true
|
||||
x64ArchFiles: Contents/Resources/ggmlclip-mac
|
||||
afterSign: electron-builder-notarize
|
||||
extraFiles:
|
||||
- from: build
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import { logError } from "../main/log";
|
||||
import { keysStore } from "../stores/keys.store";
|
||||
import { safeStorageStore } from "../stores/safeStorage.store";
|
||||
import { uploadStatusStore } from "../stores/upload.store";
|
||||
import { watchStore } from "../stores/watch.store";
|
||||
|
||||
export const clearElectronStore = () => {
|
||||
try {
|
||||
uploadStatusStore.clear();
|
||||
keysStore.clear();
|
||||
safeStorageStore.clear();
|
||||
watchStore.clear();
|
||||
} catch (e) {
|
||||
logError(e, "error while clearing electron store");
|
||||
throw e;
|
||||
}
|
||||
};
|
|
@ -1,28 +0,0 @@
|
|||
import { safeStorage } from "electron/main";
|
||||
import { logError } from "../main/log";
|
||||
import { safeStorageStore } from "../stores/safeStorage.store";
|
||||
|
||||
export async function setEncryptionKey(encryptionKey: string) {
|
||||
try {
|
||||
const encryptedKey: Buffer =
|
||||
await safeStorage.encryptString(encryptionKey);
|
||||
const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64");
|
||||
safeStorageStore.set("encryptionKey", b64EncryptedKey);
|
||||
} catch (e) {
|
||||
logError(e, "setEncryptionKey failed");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEncryptionKey(): Promise<string> {
|
||||
try {
|
||||
const b64EncryptedKey = safeStorageStore.get("encryptionKey");
|
||||
if (b64EncryptedKey) {
|
||||
const keyBuffer = Buffer.from(b64EncryptedKey, "base64");
|
||||
return await safeStorage.decryptString(keyBuffer);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, "getEncryptionKey failed");
|
||||
throw e;
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
import { getElectronFile } from "../services/fs";
|
||||
import {
|
||||
getElectronFilesFromGoogleZip,
|
||||
getSavedFilePaths,
|
||||
} from "../services/upload";
|
||||
import { uploadStatusStore } from "../stores/upload.store";
|
||||
import { ElectronFile, FILE_PATH_TYPE } from "../types/ipc";
|
||||
|
||||
export const getPendingUploads = async () => {
|
||||
const filePaths = getSavedFilePaths(FILE_PATH_TYPE.FILES);
|
||||
const zipPaths = getSavedFilePaths(FILE_PATH_TYPE.ZIPS);
|
||||
const collectionName = uploadStatusStore.get("collectionName");
|
||||
|
||||
let files: ElectronFile[] = [];
|
||||
let type: FILE_PATH_TYPE;
|
||||
if (zipPaths.length) {
|
||||
type = FILE_PATH_TYPE.ZIPS;
|
||||
for (const zipPath of zipPaths) {
|
||||
files = [
|
||||
...files,
|
||||
...(await getElectronFilesFromGoogleZip(zipPath)),
|
||||
];
|
||||
}
|
||||
const pendingFilePaths = new Set(filePaths);
|
||||
files = files.filter((file) => pendingFilePaths.has(file.path));
|
||||
} else if (filePaths.length) {
|
||||
type = FILE_PATH_TYPE.FILES;
|
||||
files = await Promise.all(filePaths.map(getElectronFile));
|
||||
}
|
||||
return {
|
||||
files,
|
||||
collectionName,
|
||||
type,
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
getElectronFilesFromGoogleZip,
|
||||
setToUploadCollection,
|
||||
setToUploadFiles,
|
||||
} from "../services/upload";
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* [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
|
||||
*/
|
||||
export const CustomErrors = {
|
||||
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}`,
|
||||
};
|
|
@ -12,6 +12,7 @@ import { app, BrowserWindow, Menu } from "electron/main";
|
|||
import serveNextAt from "next-electron-server";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
addAllowOriginHeader,
|
||||
|
@ -19,7 +20,6 @@ import {
|
|||
handleDockIconHideOnAutoLaunch,
|
||||
handleDownloads,
|
||||
handleExternalLinks,
|
||||
logStartupBanner,
|
||||
setupMacWindowOnDockIconClick,
|
||||
setupTrayItem,
|
||||
} from "./main/init";
|
||||
|
@ -27,7 +27,7 @@ import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc";
|
|||
import log, { initLogging } from "./main/log";
|
||||
import { createApplicationMenu } from "./main/menu";
|
||||
import { isDev } from "./main/util";
|
||||
import { setupAutoUpdater } from "./services/appUpdater";
|
||||
import { setupAutoUpdater } from "./services/app-update";
|
||||
import { initWatcher } from "./services/chokidar";
|
||||
|
||||
let appIsQuitting = false;
|
||||
|
@ -72,6 +72,21 @@ const setupRendererServer = () => {
|
|||
serveNextAt(rendererURL);
|
||||
};
|
||||
|
||||
/**
|
||||
* Log a standard startup banner.
|
||||
*
|
||||
* This helps us identify app starts and other environment details in the logs.
|
||||
*/
|
||||
const logStartupBanner = () => {
|
||||
const version = isDev ? "dev" : app.getVersion();
|
||||
log.info(`Starting ente-photos-desktop ${version}`);
|
||||
|
||||
const platform = process.platform;
|
||||
const osRelease = os.release();
|
||||
const systemVersion = process.getSystemVersion();
|
||||
log.info("Running on", { platform, osRelease, systemVersion });
|
||||
};
|
||||
|
||||
function enableSharedArrayBufferSupport() {
|
||||
app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer");
|
||||
}
|
||||
|
@ -126,12 +141,13 @@ const deleteLegacyDiskCacheDirIfExists = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
function setupAppEventEmitter(mainWindow: BrowserWindow) {
|
||||
// fire event when mainWindow is in foreground
|
||||
mainWindow.on("focus", () => {
|
||||
mainWindow.webContents.send("app-in-foreground");
|
||||
});
|
||||
}
|
||||
const attachEventHandlers = (mainWindow: BrowserWindow) => {
|
||||
// Let ipcRenderer know when mainWindow is in the foreground so that it can
|
||||
// in turn inform the renderer process.
|
||||
mainWindow.on("focus", () =>
|
||||
mainWindow.webContents.send("mainWindowFocus"),
|
||||
);
|
||||
};
|
||||
|
||||
const main = () => {
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
@ -144,6 +160,7 @@ const main = () => {
|
|||
|
||||
initLogging();
|
||||
setupRendererServer();
|
||||
logStartupBanner();
|
||||
handleDockIconHideOnAutoLaunch();
|
||||
increaseDiskCache();
|
||||
enableSharedArrayBufferSupport();
|
||||
|
@ -163,7 +180,6 @@ const main = () => {
|
|||
//
|
||||
// Note that some Electron APIs can only be used after this event occurs.
|
||||
app.on("ready", async () => {
|
||||
logStartupBanner();
|
||||
mainWindow = await createWindow();
|
||||
const watcher = initWatcher(mainWindow);
|
||||
setupTrayItem(mainWindow);
|
||||
|
@ -175,13 +191,13 @@ const main = () => {
|
|||
handleDownloads(mainWindow);
|
||||
handleExternalLinks(mainWindow);
|
||||
addAllowOriginHeader(mainWindow);
|
||||
setupAppEventEmitter(mainWindow);
|
||||
attachEventHandlers(mainWindow);
|
||||
|
||||
try {
|
||||
deleteLegacyDiskCacheDirIfExists();
|
||||
} catch (e) {
|
||||
// Log but otherwise ignore errors during non-critical startup
|
||||
// actions
|
||||
// actions.
|
||||
log.error("Ignoring startup error", e);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { app, BrowserWindow, nativeImage, Tray } from "electron";
|
||||
import { existsSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { isAppQuitting, rendererURL } from "../main";
|
||||
import autoLauncher from "../services/autoLauncher";
|
||||
|
@ -77,8 +76,6 @@ export const createWindow = async () => {
|
|||
return mainWindow;
|
||||
};
|
||||
|
||||
export async function handleUpdates(mainWindow: BrowserWindow) {}
|
||||
|
||||
export const setupTrayItem = (mainWindow: BrowserWindow) => {
|
||||
const iconName = isPlatform("mac")
|
||||
? "taskbar-icon-Template.png"
|
||||
|
@ -149,16 +146,6 @@ export async function handleDockIconHideOnAutoLaunch() {
|
|||
}
|
||||
}
|
||||
|
||||
export function logStartupBanner() {
|
||||
const version = isDev ? "dev" : app.getVersion();
|
||||
log.info(`Hello from ente-photos-desktop ${version}`);
|
||||
|
||||
const platform = process.platform;
|
||||
const osRelease = os.release();
|
||||
const systemVersion = process.getSystemVersion();
|
||||
log.info("Running on", { platform, osRelease, systemVersion });
|
||||
}
|
||||
|
||||
function lowerCaseHeaders(responseHeaders: Record<string, string[]>) {
|
||||
const headers: Record<string, string[]> = {};
|
||||
for (const key of Object.keys(responseHeaders)) {
|
||||
|
|
|
@ -10,30 +10,30 @@
|
|||
|
||||
import type { FSWatcher } from "chokidar";
|
||||
import { ipcMain } from "electron/main";
|
||||
import { clearElectronStore } from "../api/electronStore";
|
||||
import { getEncryptionKey, setEncryptionKey } from "../api/safeStorage";
|
||||
import {
|
||||
getElectronFilesFromGoogleZip,
|
||||
getPendingUploads,
|
||||
setToUploadCollection,
|
||||
setToUploadFiles,
|
||||
} from "../api/upload";
|
||||
import {
|
||||
appVersion,
|
||||
muteUpdateNotification,
|
||||
skipAppUpdate,
|
||||
updateAndRestart,
|
||||
} from "../services/appUpdater";
|
||||
import {
|
||||
computeImageEmbedding,
|
||||
computeTextEmbedding,
|
||||
} from "../services/clipService";
|
||||
updateOnNextRestart,
|
||||
} from "../services/app-update";
|
||||
import { clipImageEmbedding, clipTextEmbedding } from "../services/clip";
|
||||
import { runFFmpegCmd } from "../services/ffmpeg";
|
||||
import { getDirFiles } from "../services/fs";
|
||||
import {
|
||||
convertToJPEG,
|
||||
generateImageThumbnail,
|
||||
} from "../services/imageProcessor";
|
||||
import {
|
||||
clearStores,
|
||||
encryptionKey,
|
||||
saveEncryptionKey,
|
||||
} from "../services/store";
|
||||
import {
|
||||
getElectronFilesFromGoogleZip,
|
||||
getPendingUploads,
|
||||
setToUploadCollection,
|
||||
setToUploadFiles,
|
||||
} from "../services/upload";
|
||||
import {
|
||||
addWatchMapping,
|
||||
getWatchMappings,
|
||||
|
@ -41,12 +41,7 @@ import {
|
|||
updateWatchMappingIgnoredFiles,
|
||||
updateWatchMappingSyncedFiles,
|
||||
} from "../services/watch";
|
||||
import type {
|
||||
ElectronFile,
|
||||
FILE_PATH_TYPE,
|
||||
Model,
|
||||
WatchMapping,
|
||||
} from "../types/ipc";
|
||||
import type { ElectronFile, FILE_PATH_TYPE, WatchMapping } from "../types/ipc";
|
||||
import {
|
||||
selectDirectory,
|
||||
showUploadDirsDialog,
|
||||
|
@ -91,35 +86,33 @@ export const attachIPCHandlers = () => {
|
|||
|
||||
// - General
|
||||
|
||||
ipcMain.handle("appVersion", (_) => appVersion());
|
||||
ipcMain.handle("appVersion", () => appVersion());
|
||||
|
||||
ipcMain.handle("openDirectory", (_, dirPath) => openDirectory(dirPath));
|
||||
|
||||
ipcMain.handle("openLogDirectory", (_) => openLogDirectory());
|
||||
ipcMain.handle("openLogDirectory", () => openLogDirectory());
|
||||
|
||||
// See [Note: Catching exception during .send/.on]
|
||||
ipcMain.on("logToDisk", (_, message) => logToDisk(message));
|
||||
|
||||
ipcMain.on("clear-electron-store", (_) => {
|
||||
clearElectronStore();
|
||||
});
|
||||
ipcMain.on("clearStores", () => clearStores());
|
||||
|
||||
ipcMain.handle("setEncryptionKey", (_, encryptionKey) =>
|
||||
setEncryptionKey(encryptionKey),
|
||||
ipcMain.handle("saveEncryptionKey", (_, encryptionKey) =>
|
||||
saveEncryptionKey(encryptionKey),
|
||||
);
|
||||
|
||||
ipcMain.handle("getEncryptionKey", (_) => getEncryptionKey());
|
||||
ipcMain.handle("encryptionKey", () => encryptionKey());
|
||||
|
||||
// - App update
|
||||
|
||||
ipcMain.on("update-and-restart", (_) => updateAndRestart());
|
||||
ipcMain.on("updateAndRestart", () => updateAndRestart());
|
||||
|
||||
ipcMain.on("skip-app-update", (_, version) => skipAppUpdate(version));
|
||||
|
||||
ipcMain.on("mute-update-notification", (_, version) =>
|
||||
muteUpdateNotification(version),
|
||||
ipcMain.on("updateOnNextRestart", (_, version) =>
|
||||
updateOnNextRestart(version),
|
||||
);
|
||||
|
||||
ipcMain.on("skipAppUpdate", (_, version) => skipAppUpdate(version));
|
||||
|
||||
// - Conversion
|
||||
|
||||
ipcMain.handle("convertToJPEG", (_, fileData, filename) =>
|
||||
|
@ -145,25 +138,23 @@ export const attachIPCHandlers = () => {
|
|||
|
||||
// - ML
|
||||
|
||||
ipcMain.handle(
|
||||
"computeImageEmbedding",
|
||||
(_, model: Model, imageData: Uint8Array) =>
|
||||
computeImageEmbedding(model, imageData),
|
||||
ipcMain.handle("clipImageEmbedding", (_, jpegImageData: Uint8Array) =>
|
||||
clipImageEmbedding(jpegImageData),
|
||||
);
|
||||
|
||||
ipcMain.handle("computeTextEmbedding", (_, model: Model, text: string) =>
|
||||
computeTextEmbedding(model, text),
|
||||
ipcMain.handle("clipTextEmbedding", (_, text: string) =>
|
||||
clipTextEmbedding(text),
|
||||
);
|
||||
|
||||
// - File selection
|
||||
|
||||
ipcMain.handle("selectDirectory", (_) => selectDirectory());
|
||||
ipcMain.handle("selectDirectory", () => selectDirectory());
|
||||
|
||||
ipcMain.handle("showUploadFilesDialog", (_) => showUploadFilesDialog());
|
||||
ipcMain.handle("showUploadFilesDialog", () => showUploadFilesDialog());
|
||||
|
||||
ipcMain.handle("showUploadDirsDialog", (_) => showUploadDirsDialog());
|
||||
ipcMain.handle("showUploadDirsDialog", () => showUploadDirsDialog());
|
||||
|
||||
ipcMain.handle("showUploadZipDialog", (_) => showUploadZipDialog());
|
||||
ipcMain.handle("showUploadZipDialog", () => showUploadZipDialog());
|
||||
|
||||
// - FS
|
||||
|
||||
|
@ -177,12 +168,12 @@ export const attachIPCHandlers = () => {
|
|||
|
||||
ipcMain.handle(
|
||||
"saveStreamToDisk",
|
||||
(_, path: string, fileStream: ReadableStream<any>) =>
|
||||
(_, path: string, fileStream: ReadableStream) =>
|
||||
saveStreamToDisk(path, fileStream),
|
||||
);
|
||||
|
||||
ipcMain.handle("saveFileToDisk", (_, path: string, file: any) =>
|
||||
saveFileToDisk(path, file),
|
||||
ipcMain.handle("saveFileToDisk", (_, path: string, contents: string) =>
|
||||
saveFileToDisk(path, contents),
|
||||
);
|
||||
|
||||
ipcMain.handle("readTextFile", (_, path: string) => readTextFile(path));
|
||||
|
@ -203,7 +194,7 @@ export const attachIPCHandlers = () => {
|
|||
|
||||
// - Upload
|
||||
|
||||
ipcMain.handle("getPendingUploads", (_) => getPendingUploads());
|
||||
ipcMain.handle("getPendingUploads", () => getPendingUploads());
|
||||
|
||||
ipcMain.handle(
|
||||
"setToUploadFiles",
|
||||
|
@ -252,7 +243,7 @@ export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => {
|
|||
removeWatchMapping(watcher, folderPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("getWatchMappings", (_) => getWatchMappings());
|
||||
ipcMain.handle("getWatchMappings", () => getWatchMappings());
|
||||
|
||||
ipcMain.handle(
|
||||
"updateWatchMappingSyncedFiles",
|
||||
|
|
|
@ -15,10 +15,20 @@ import { isDev } from "./util";
|
|||
*/
|
||||
export const initLogging = () => {
|
||||
log.transports.file.fileName = "ente.log";
|
||||
log.transports.file.maxSize = 50 * 1024 * 1024; // 50MB;
|
||||
log.transports.file.maxSize = 50 * 1024 * 1024; // 50 MB
|
||||
log.transports.file.format = "[{y}-{m}-{d}T{h}:{i}:{s}{z}] {text}";
|
||||
|
||||
log.transports.console.level = false;
|
||||
|
||||
// Log unhandled errors and promise rejections.
|
||||
log.errorHandler.startCatching({
|
||||
onError: ({ error, errorName }) => {
|
||||
logError(errorName, error);
|
||||
// Prevent the default electron-log actions (e.g. showing a dialog)
|
||||
// from getting triggered.
|
||||
return false;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -31,25 +41,7 @@ export const logToDisk = (message: string) => {
|
|||
log.info(`[rndr] ${message}`);
|
||||
};
|
||||
|
||||
export const logError = logErrorSentry;
|
||||
|
||||
/** Deprecated, but no alternative yet */
|
||||
export function logErrorSentry(
|
||||
error: any,
|
||||
msg: string,
|
||||
info?: Record<string, unknown>,
|
||||
) {
|
||||
logToDisk(
|
||||
`error: ${error?.name} ${error?.message} ${
|
||||
error?.stack
|
||||
} msg: ${msg} info: ${JSON.stringify(info)}`,
|
||||
);
|
||||
if (isDev) {
|
||||
console.log(error, { msg, info });
|
||||
}
|
||||
}
|
||||
|
||||
const logError1 = (message: string, e?: unknown) => {
|
||||
const logError = (message: string, e?: unknown) => {
|
||||
if (!e) {
|
||||
logError_(message);
|
||||
return;
|
||||
|
@ -78,11 +70,14 @@ const logInfo = (...params: any[]) => {
|
|||
.map((p) => (typeof p == "string" ? p : util.inspect(p)))
|
||||
.join(" ");
|
||||
log.info(`[main] ${message}`);
|
||||
if (isDev) console.log(message);
|
||||
if (isDev) console.log(`[info] ${message}`);
|
||||
};
|
||||
|
||||
const logDebug = (param: () => any) => {
|
||||
if (isDev) console.log(`[debug] ${util.inspect(param())}`);
|
||||
if (isDev) {
|
||||
const p = param();
|
||||
console.log(`[debug] ${typeof p == "string" ? p : util.inspect(p)}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -98,12 +93,13 @@ export default {
|
|||
* Log an error message with an optional associated error object.
|
||||
*
|
||||
* {@link e} is generally expected to be an `instanceof Error` but it can be
|
||||
* any arbitrary object that we obtain, say, when in a try-catch handler.
|
||||
* any arbitrary object that we obtain, say, when in a try-catch handler (in
|
||||
* JavaScript any arbitrary value can be thrown).
|
||||
*
|
||||
* The log is written to disk. In development builds, the log is also
|
||||
* printed to the (Node.js process') console.
|
||||
* printed to the main (Node.js) process console.
|
||||
*/
|
||||
error: logError1,
|
||||
error: logError,
|
||||
/**
|
||||
* Log a message.
|
||||
*
|
||||
|
@ -111,7 +107,7 @@ export default {
|
|||
* arbitrary number of arbitrary parameters that it then serializes.
|
||||
*
|
||||
* The log is written to disk. In development builds, the log is also
|
||||
* printed to the (Node.js process') console.
|
||||
* printed to the main (Node.js) process console.
|
||||
*/
|
||||
info: logInfo,
|
||||
/**
|
||||
|
@ -121,11 +117,11 @@ export default {
|
|||
* function to call to get the log message instead of directly taking the
|
||||
* message. The provided function will only be called in development builds.
|
||||
*
|
||||
* The function can return an arbitrary value which is serialied before
|
||||
* The function can return an arbitrary value which is serialized before
|
||||
* being logged.
|
||||
*
|
||||
* This log is not written to disk. It is printed to the (Node.js process')
|
||||
* console only on development builds.
|
||||
* This log is NOT written to disk. And it is printed to the main (Node.js)
|
||||
* process console, but only on development builds.
|
||||
*/
|
||||
debug: logDebug,
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
shell,
|
||||
} from "electron";
|
||||
import { setIsAppQuitting } from "../main";
|
||||
import { forceCheckForUpdateAndNotify } from "../services/appUpdater";
|
||||
import { forceCheckForAppUpdates } from "../services/app-update";
|
||||
import autoLauncher from "../services/autoLauncher";
|
||||
import {
|
||||
getHideDockIconPreference,
|
||||
|
@ -26,8 +26,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => {
|
|||
const macOSOnly = (options: MenuItemConstructorOptions[]) =>
|
||||
process.platform == "darwin" ? options : [];
|
||||
|
||||
const handleCheckForUpdates = () =>
|
||||
forceCheckForUpdateAndNotify(mainWindow);
|
||||
const handleCheckForUpdates = () => forceCheckForAppUpdates(mainWindow);
|
||||
|
||||
const handleViewChangelog = () =>
|
||||
shell.openExternal(
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
/**
|
||||
* @file The preload script
|
||||
*
|
||||
|
@ -31,9 +32,9 @@
|
|||
* and when changing one of them, remember to see if the other two also need
|
||||
* changing:
|
||||
*
|
||||
* - [renderer] web/packages/shared/electron/types.ts contains docs
|
||||
* - [preload] desktop/src/preload.ts ↕︎
|
||||
* - [main] desktop/src/main/ipc.ts contains impl
|
||||
* - [renderer] web/packages/next/types/electron.ts contains docs
|
||||
* - [preload] desktop/src/preload.ts ↕︎
|
||||
* - [main] desktop/src/main/ipc.ts contains impl
|
||||
*/
|
||||
|
||||
import { contextBridge, ipcRenderer } from "electron/renderer";
|
||||
|
@ -44,7 +45,6 @@ import type {
|
|||
AppUpdateInfo,
|
||||
ElectronFile,
|
||||
FILE_PATH_TYPE,
|
||||
Model,
|
||||
WatchMapping,
|
||||
} from "./types/ipc";
|
||||
|
||||
|
@ -52,60 +52,55 @@ import type {
|
|||
|
||||
const appVersion = (): Promise<string> => ipcRenderer.invoke("appVersion");
|
||||
|
||||
const logToDisk = (message: string): void =>
|
||||
ipcRenderer.send("logToDisk", message);
|
||||
|
||||
const openDirectory = (dirPath: string): Promise<void> =>
|
||||
ipcRenderer.invoke("openDirectory");
|
||||
ipcRenderer.invoke("openDirectory", dirPath);
|
||||
|
||||
const openLogDirectory = (): Promise<void> =>
|
||||
ipcRenderer.invoke("openLogDirectory");
|
||||
|
||||
const logToDisk = (message: string): void =>
|
||||
ipcRenderer.send("logToDisk", message);
|
||||
const clearStores = () => ipcRenderer.send("clearStores");
|
||||
|
||||
const encryptionKey = (): Promise<string | undefined> =>
|
||||
ipcRenderer.invoke("encryptionKey");
|
||||
|
||||
const saveEncryptionKey = (encryptionKey: string): Promise<void> =>
|
||||
ipcRenderer.invoke("saveEncryptionKey", encryptionKey);
|
||||
|
||||
const onMainWindowFocus = (cb?: () => void) => {
|
||||
ipcRenderer.removeAllListeners("mainWindowFocus");
|
||||
if (cb) ipcRenderer.on("mainWindowFocus", cb);
|
||||
};
|
||||
|
||||
// - App update
|
||||
|
||||
const onAppUpdateAvailable = (
|
||||
cb?: ((updateInfo: AppUpdateInfo) => void) | undefined,
|
||||
) => {
|
||||
ipcRenderer.removeAllListeners("appUpdateAvailable");
|
||||
if (cb) {
|
||||
ipcRenderer.on("appUpdateAvailable", (_, updateInfo: AppUpdateInfo) =>
|
||||
cb(updateInfo),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const updateAndRestart = () => ipcRenderer.send("updateAndRestart");
|
||||
|
||||
const updateOnNextRestart = (version: string) =>
|
||||
ipcRenderer.send("updateOnNextRestart", version);
|
||||
|
||||
const skipAppUpdate = (version: string) => {
|
||||
ipcRenderer.send("skipAppUpdate", version);
|
||||
};
|
||||
|
||||
const fsExists = (path: string): Promise<boolean> =>
|
||||
ipcRenderer.invoke("fsExists", path);
|
||||
|
||||
// - AUDIT below this
|
||||
|
||||
const registerForegroundEventListener = (onForeground: () => void) => {
|
||||
ipcRenderer.removeAllListeners("app-in-foreground");
|
||||
ipcRenderer.on("app-in-foreground", () => {
|
||||
onForeground();
|
||||
});
|
||||
};
|
||||
|
||||
const clearElectronStore = () => {
|
||||
ipcRenderer.send("clear-electron-store");
|
||||
};
|
||||
|
||||
const setEncryptionKey = (encryptionKey: string): Promise<void> =>
|
||||
ipcRenderer.invoke("setEncryptionKey", encryptionKey);
|
||||
|
||||
const getEncryptionKey = (): Promise<string> =>
|
||||
ipcRenderer.invoke("getEncryptionKey");
|
||||
|
||||
// - App update
|
||||
|
||||
const registerUpdateEventListener = (
|
||||
showUpdateDialog: (updateInfo: AppUpdateInfo) => void,
|
||||
) => {
|
||||
ipcRenderer.removeAllListeners("show-update-dialog");
|
||||
ipcRenderer.on("show-update-dialog", (_, updateInfo: AppUpdateInfo) => {
|
||||
showUpdateDialog(updateInfo);
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// - Conversion
|
||||
|
||||
const convertToJPEG = (
|
||||
|
@ -142,17 +137,11 @@ const runFFmpegCmd = (
|
|||
|
||||
// - ML
|
||||
|
||||
const computeImageEmbedding = (
|
||||
model: Model,
|
||||
imageData: Uint8Array,
|
||||
): Promise<Float32Array> =>
|
||||
ipcRenderer.invoke("computeImageEmbedding", model, imageData);
|
||||
const clipImageEmbedding = (jpegImageData: Uint8Array): Promise<Float32Array> =>
|
||||
ipcRenderer.invoke("clipImageEmbedding", jpegImageData);
|
||||
|
||||
const computeTextEmbedding = (
|
||||
model: Model,
|
||||
text: string,
|
||||
): Promise<Float32Array> =>
|
||||
ipcRenderer.invoke("computeTextEmbedding", model, text);
|
||||
const clipTextEmbedding = (text: string): Promise<Float32Array> =>
|
||||
ipcRenderer.invoke("clipTextEmbedding", text);
|
||||
|
||||
// - File selection
|
||||
|
||||
|
@ -228,11 +217,11 @@ const checkExistsAndCreateDir = (dirPath: string): Promise<void> =>
|
|||
|
||||
const saveStreamToDisk = (
|
||||
path: string,
|
||||
fileStream: ReadableStream<any>,
|
||||
fileStream: ReadableStream,
|
||||
): Promise<void> => ipcRenderer.invoke("saveStreamToDisk", path, fileStream);
|
||||
|
||||
const saveFileToDisk = (path: string, file: any): Promise<void> =>
|
||||
ipcRenderer.invoke("saveFileToDisk", path, file);
|
||||
const saveFileToDisk = (path: string, contents: string): Promise<void> =>
|
||||
ipcRenderer.invoke("saveFileToDisk", path, contents);
|
||||
|
||||
const readTextFile = (path: string): Promise<string> =>
|
||||
ipcRenderer.invoke("readTextFile", path);
|
||||
|
@ -308,24 +297,22 @@ const getDirFiles = (dirPath: string): Promise<ElectronFile[]> =>
|
|||
//
|
||||
// The copy itself is relatively fast, but the problem with transfering large
|
||||
// amounts of data is potentially running out of memory during the copy.
|
||||
contextBridge.exposeInMainWorld("ElectronAPIs", {
|
||||
contextBridge.exposeInMainWorld("electron", {
|
||||
// - General
|
||||
appVersion,
|
||||
openDirectory,
|
||||
registerForegroundEventListener,
|
||||
clearElectronStore,
|
||||
getEncryptionKey,
|
||||
setEncryptionKey,
|
||||
|
||||
// - Logging
|
||||
openLogDirectory,
|
||||
logToDisk,
|
||||
openDirectory,
|
||||
openLogDirectory,
|
||||
clearStores,
|
||||
encryptionKey,
|
||||
saveEncryptionKey,
|
||||
onMainWindowFocus,
|
||||
|
||||
// - App update
|
||||
onAppUpdateAvailable,
|
||||
updateAndRestart,
|
||||
updateOnNextRestart,
|
||||
skipAppUpdate,
|
||||
muteUpdateNotification,
|
||||
registerUpdateEventListener,
|
||||
|
||||
// - Conversion
|
||||
convertToJPEG,
|
||||
|
@ -333,8 +320,8 @@ contextBridge.exposeInMainWorld("ElectronAPIs", {
|
|||
runFFmpegCmd,
|
||||
|
||||
// - ML
|
||||
computeImageEmbedding,
|
||||
computeTextEmbedding,
|
||||
clipImageEmbedding,
|
||||
clipTextEmbedding,
|
||||
|
||||
// - File selection
|
||||
selectDirectory,
|
||||
|
|
98
desktop/src/services/app-update.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
import { compareVersions } from "compare-versions";
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { default as electronLog } from "electron-log";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { setIsAppQuitting, setIsUpdateAvailable } from "../main";
|
||||
import log from "../main/log";
|
||||
import { userPreferencesStore } from "../stores/user-preferences";
|
||||
import { AppUpdateInfo } from "../types/ipc";
|
||||
|
||||
export const setupAutoUpdater = (mainWindow: BrowserWindow) => {
|
||||
autoUpdater.logger = electronLog;
|
||||
autoUpdater.autoDownload = false;
|
||||
|
||||
const oneDay = 1 * 24 * 60 * 60 * 1000;
|
||||
setInterval(() => checkForUpdatesAndNotify(mainWindow), oneDay);
|
||||
checkForUpdatesAndNotify(mainWindow);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check for app update check ignoring any previously saved skips / mutes.
|
||||
*/
|
||||
export const forceCheckForAppUpdates = (mainWindow: BrowserWindow) => {
|
||||
userPreferencesStore.delete("skipAppVersion");
|
||||
userPreferencesStore.delete("muteUpdateNotificationVersion");
|
||||
checkForUpdatesAndNotify(mainWindow);
|
||||
};
|
||||
|
||||
const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => {
|
||||
try {
|
||||
const { updateInfo } = await autoUpdater.checkForUpdates();
|
||||
const { version } = updateInfo;
|
||||
|
||||
log.debug(() => `Checking for updates found version ${version}`);
|
||||
|
||||
if (compareVersions(version, app.getVersion()) <= 0) {
|
||||
log.debug(() => "Skipping update, already at latest version");
|
||||
return;
|
||||
}
|
||||
|
||||
if (version === userPreferencesStore.get("skipAppVersion")) {
|
||||
log.info(`User chose to skip version ${version}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const mutedVersion = userPreferencesStore.get(
|
||||
"muteUpdateNotificationVersion",
|
||||
);
|
||||
if (version === mutedVersion) {
|
||||
log.info(
|
||||
`User has muted update notifications for version ${version}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const showUpdateDialog = (updateInfo: AppUpdateInfo) =>
|
||||
mainWindow.webContents.send("appUpdateAvailable", updateInfo);
|
||||
|
||||
log.debug(() => "Attempting auto update");
|
||||
autoUpdater.downloadUpdate();
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
const fiveMinutes = 5 * 60 * 1000;
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
timeout = setTimeout(
|
||||
() => showUpdateDialog({ autoUpdatable: true, version }),
|
||||
fiveMinutes,
|
||||
);
|
||||
});
|
||||
autoUpdater.on("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
log.error("Auto update failed", error);
|
||||
showUpdateDialog({ autoUpdatable: false, version });
|
||||
});
|
||||
|
||||
setIsUpdateAvailable(true);
|
||||
} catch (e) {
|
||||
log.error("checkForUpdateAndNotify failed", e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the version of the desktop app
|
||||
*
|
||||
* The return value is of the form `v1.2.3`.
|
||||
*/
|
||||
export const appVersion = () => `v${app.getVersion()}`;
|
||||
|
||||
export const updateAndRestart = () => {
|
||||
log.info("Restarting the app to apply update");
|
||||
setIsAppQuitting(true);
|
||||
autoUpdater.quitAndInstall();
|
||||
};
|
||||
|
||||
export const updateOnNextRestart = (version: string) =>
|
||||
userPreferencesStore.set("muteUpdateNotificationVersion", version);
|
||||
|
||||
export const skipAppUpdate = (version: string) =>
|
||||
userPreferencesStore.set("skipAppVersion", version);
|
|
@ -1,133 +0,0 @@
|
|||
import { compareVersions } from "compare-versions";
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { default as ElectronLog, default as log } from "electron-log";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { setIsAppQuitting, setIsUpdateAvailable } from "../main";
|
||||
import { logErrorSentry } from "../main/log";
|
||||
import { AppUpdateInfo } from "../types/ipc";
|
||||
import {
|
||||
clearMuteUpdateNotificationVersion,
|
||||
clearSkipAppVersion,
|
||||
getMuteUpdateNotificationVersion,
|
||||
getSkipAppVersion,
|
||||
setMuteUpdateNotificationVersion,
|
||||
setSkipAppVersion,
|
||||
} from "./userPreference";
|
||||
|
||||
const FIVE_MIN_IN_MICROSECOND = 5 * 60 * 1000;
|
||||
const ONE_DAY_IN_MICROSECOND = 1 * 24 * 60 * 60 * 1000;
|
||||
|
||||
export function setupAutoUpdater(mainWindow: BrowserWindow) {
|
||||
autoUpdater.logger = log;
|
||||
autoUpdater.autoDownload = false;
|
||||
checkForUpdateAndNotify(mainWindow);
|
||||
setInterval(
|
||||
() => checkForUpdateAndNotify(mainWindow),
|
||||
ONE_DAY_IN_MICROSECOND,
|
||||
);
|
||||
}
|
||||
|
||||
export function forceCheckForUpdateAndNotify(mainWindow: BrowserWindow) {
|
||||
try {
|
||||
clearSkipAppVersion();
|
||||
clearMuteUpdateNotificationVersion();
|
||||
checkForUpdateAndNotify(mainWindow);
|
||||
} catch (e) {
|
||||
logErrorSentry(e, "forceCheckForUpdateAndNotify failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForUpdateAndNotify(mainWindow: BrowserWindow) {
|
||||
try {
|
||||
log.debug("checkForUpdateAndNotify called");
|
||||
const updateCheckResult = await autoUpdater.checkForUpdates();
|
||||
log.debug("update version", updateCheckResult.updateInfo.version);
|
||||
if (
|
||||
compareVersions(
|
||||
updateCheckResult.updateInfo.version,
|
||||
app.getVersion(),
|
||||
) <= 0
|
||||
) {
|
||||
log.debug("already at latest version");
|
||||
return;
|
||||
}
|
||||
const skipAppVersion = getSkipAppVersion();
|
||||
if (
|
||||
skipAppVersion &&
|
||||
updateCheckResult.updateInfo.version === skipAppVersion
|
||||
) {
|
||||
log.info(
|
||||
"user chose to skip version ",
|
||||
updateCheckResult.updateInfo.version,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
log.debug("attempting auto update");
|
||||
autoUpdater.downloadUpdate();
|
||||
const muteUpdateNotificationVersion =
|
||||
getMuteUpdateNotificationVersion();
|
||||
if (
|
||||
muteUpdateNotificationVersion &&
|
||||
updateCheckResult.updateInfo.version ===
|
||||
muteUpdateNotificationVersion
|
||||
) {
|
||||
log.info(
|
||||
"user chose to mute update notification for version ",
|
||||
updateCheckResult.updateInfo.version,
|
||||
);
|
||||
return;
|
||||
}
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
timeout = setTimeout(
|
||||
() =>
|
||||
showUpdateDialog(mainWindow, {
|
||||
autoUpdatable: true,
|
||||
version: updateCheckResult.updateInfo.version,
|
||||
}),
|
||||
FIVE_MIN_IN_MICROSECOND,
|
||||
);
|
||||
});
|
||||
autoUpdater.on("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
logErrorSentry(error, "auto update failed");
|
||||
showUpdateDialog(mainWindow, {
|
||||
autoUpdatable: false,
|
||||
version: updateCheckResult.updateInfo.version,
|
||||
});
|
||||
});
|
||||
|
||||
setIsUpdateAvailable(true);
|
||||
} catch (e) {
|
||||
logErrorSentry(e, "checkForUpdateAndNotify failed");
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAndRestart() {
|
||||
ElectronLog.log("user quit the app");
|
||||
setIsAppQuitting(true);
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the version of the desktop app
|
||||
*
|
||||
* The return value is of the form `v1.2.3`.
|
||||
*/
|
||||
export const appVersion = () => `v${app.getVersion()}`;
|
||||
|
||||
export function skipAppUpdate(version: string) {
|
||||
setSkipAppVersion(version);
|
||||
}
|
||||
|
||||
export function muteUpdateNotification(version: string) {
|
||||
setMuteUpdateNotificationVersion(version);
|
||||
}
|
||||
|
||||
function showUpdateDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
updateInfo: AppUpdateInfo,
|
||||
) {
|
||||
mainWindow.webContents.send("show-update-dialog", updateInfo);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import chokidar from "chokidar";
|
||||
import { BrowserWindow } from "electron";
|
||||
import path from "path";
|
||||
import { logError } from "../main/log";
|
||||
import log from "../main/log";
|
||||
import { getWatchMappings } from "../services/watch";
|
||||
import { getElectronFile } from "./fs";
|
||||
|
||||
|
@ -38,7 +38,7 @@ export function initWatcher(mainWindow: BrowserWindow) {
|
|||
);
|
||||
})
|
||||
.on("error", (error) => {
|
||||
logError(error, "error while watching files");
|
||||
log.error("Error while watching files", error);
|
||||
});
|
||||
|
||||
return watcher;
|
||||
|
|
288
desktop/src/services/clip.ts
Normal file
|
@ -0,0 +1,288 @@
|
|||
/**
|
||||
* @file Compute CLIP embeddings
|
||||
*
|
||||
* @see `web/apps/photos/src/services/clip-service.ts` for more details. This
|
||||
* file implements the Node.js implementation of the actual embedding
|
||||
* computation. By doing it in the Node.js layer, we can use the binary ONNX
|
||||
* runtimes which are 10-20x faster than the WASM based web ones.
|
||||
*
|
||||
* The embeddings are computed using ONNX runtime. The model itself is not
|
||||
* shipped with the app but is downloaded on demand.
|
||||
*/
|
||||
import { app, net } from "electron/main";
|
||||
import { existsSync } from "fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { writeStream } from "../main/fs";
|
||||
import log from "../main/log";
|
||||
import { CustomErrors } from "../types/ipc";
|
||||
import Tokenizer from "../utils/clip-bpe-ts/mod";
|
||||
import { generateTempFilePath } from "../utils/temp";
|
||||
import { deleteTempFile } from "./ffmpeg";
|
||||
const jpeg = require("jpeg-js");
|
||||
const ort = require("onnxruntime-node");
|
||||
|
||||
const textModelName = "clip-text-vit-32-uint8.onnx";
|
||||
const textModelByteSize = 64173509; // 61.2 MB
|
||||
|
||||
const imageModelName = "clip-image-vit-32-float32.onnx";
|
||||
const imageModelByteSize = 351468764; // 335.2 MB
|
||||
|
||||
/** Return the path where the given {@link modelName} is meant to be saved */
|
||||
const modelSavePath = (modelName: string) =>
|
||||
path.join(app.getPath("userData"), "models", modelName);
|
||||
|
||||
const downloadModel = async (saveLocation: string, name: string) => {
|
||||
// `mkdir -p` the directory where we want to save the model.
|
||||
const saveDir = path.dirname(saveLocation);
|
||||
await fs.mkdir(saveDir, { recursive: true });
|
||||
// Download
|
||||
log.info(`Downloading CLIP model from ${name}`);
|
||||
const url = `https://models.ente.io/${name}`;
|
||||
const res = await net.fetch(url);
|
||||
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
||||
// Save
|
||||
await writeStream(saveLocation, res.body);
|
||||
log.info(`Downloaded CLIP model ${name}`);
|
||||
};
|
||||
|
||||
let activeImageModelDownload: Promise<void> | undefined;
|
||||
|
||||
const imageModelPathDownloadingIfNeeded = async () => {
|
||||
try {
|
||||
const modelPath = modelSavePath(imageModelName);
|
||||
if (activeImageModelDownload) {
|
||||
log.info("Waiting for CLIP image model download to finish");
|
||||
await activeImageModelDownload;
|
||||
} else {
|
||||
if (!existsSync(modelPath)) {
|
||||
log.info("CLIP image model not found, downloading");
|
||||
activeImageModelDownload = downloadModel(
|
||||
modelPath,
|
||||
imageModelName,
|
||||
);
|
||||
await activeImageModelDownload;
|
||||
} else {
|
||||
const localFileSize = (await fs.stat(modelPath)).size;
|
||||
if (localFileSize !== imageModelByteSize) {
|
||||
log.error(
|
||||
`CLIP image model size ${localFileSize} does not match the expected size, downloading again`,
|
||||
);
|
||||
activeImageModelDownload = downloadModel(
|
||||
modelPath,
|
||||
imageModelName,
|
||||
);
|
||||
await activeImageModelDownload;
|
||||
}
|
||||
}
|
||||
}
|
||||
return modelPath;
|
||||
} finally {
|
||||
activeImageModelDownload = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
let textModelDownloadInProgress = false;
|
||||
|
||||
const textModelPathDownloadingIfNeeded = async () => {
|
||||
if (textModelDownloadInProgress)
|
||||
throw Error(CustomErrors.MODEL_DOWNLOAD_PENDING);
|
||||
|
||||
const modelPath = modelSavePath(textModelName);
|
||||
if (!existsSync(modelPath)) {
|
||||
log.info("CLIP text model not found, downloading");
|
||||
textModelDownloadInProgress = true;
|
||||
downloadModel(modelPath, textModelName)
|
||||
.catch((e) => {
|
||||
// log but otherwise ignore
|
||||
log.error("CLIP text model download failed", e);
|
||||
})
|
||||
.finally(() => {
|
||||
textModelDownloadInProgress = false;
|
||||
});
|
||||
throw Error(CustomErrors.MODEL_DOWNLOAD_PENDING);
|
||||
} else {
|
||||
const localFileSize = (await fs.stat(modelPath)).size;
|
||||
if (localFileSize !== textModelByteSize) {
|
||||
log.error(
|
||||
`CLIP text model size ${localFileSize} does not match the expected size, downloading again`,
|
||||
);
|
||||
textModelDownloadInProgress = true;
|
||||
downloadModel(modelPath, textModelName)
|
||||
.catch((e) => {
|
||||
// log but otherwise ignore
|
||||
log.error("CLIP text model download failed", e);
|
||||
})
|
||||
.finally(() => {
|
||||
textModelDownloadInProgress = false;
|
||||
});
|
||||
throw Error(CustomErrors.MODEL_DOWNLOAD_PENDING);
|
||||
}
|
||||
}
|
||||
|
||||
return modelPath;
|
||||
};
|
||||
|
||||
const createInferenceSession = async (modelPath: string) => {
|
||||
return await ort.InferenceSession.create(modelPath, {
|
||||
intraOpNumThreads: 1,
|
||||
enableCpuMemArena: false,
|
||||
});
|
||||
};
|
||||
|
||||
let imageSessionPromise: Promise<any> | undefined;
|
||||
|
||||
const onnxImageSession = async () => {
|
||||
if (!imageSessionPromise) {
|
||||
imageSessionPromise = (async () => {
|
||||
const modelPath = await imageModelPathDownloadingIfNeeded();
|
||||
return createInferenceSession(modelPath);
|
||||
})();
|
||||
}
|
||||
return imageSessionPromise;
|
||||
};
|
||||
|
||||
let _textSession: any = null;
|
||||
|
||||
const onnxTextSession = async () => {
|
||||
if (!_textSession) {
|
||||
const modelPath = await textModelPathDownloadingIfNeeded();
|
||||
_textSession = await createInferenceSession(modelPath);
|
||||
}
|
||||
return _textSession;
|
||||
};
|
||||
|
||||
export const clipImageEmbedding = async (jpegImageData: Uint8Array) => {
|
||||
const tempFilePath = await generateTempFilePath("");
|
||||
const imageStream = new Response(jpegImageData.buffer).body;
|
||||
await writeStream(tempFilePath, imageStream);
|
||||
try {
|
||||
return await clipImageEmbedding_(tempFilePath);
|
||||
} finally {
|
||||
await deleteTempFile(tempFilePath);
|
||||
}
|
||||
};
|
||||
|
||||
const clipImageEmbedding_ = async (jpegFilePath: string) => {
|
||||
const imageSession = await onnxImageSession();
|
||||
const t1 = Date.now();
|
||||
const rgbData = await getRGBData(jpegFilePath);
|
||||
const feeds = {
|
||||
input: new ort.Tensor("float32", rgbData, [1, 3, 224, 224]),
|
||||
};
|
||||
const t2 = Date.now();
|
||||
const results = await imageSession.run(feeds);
|
||||
log.debug(
|
||||
() =>
|
||||
`CLIP image embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
|
||||
);
|
||||
const imageEmbedding = results["output"].data; // Float32Array
|
||||
return normalizeEmbedding(imageEmbedding);
|
||||
};
|
||||
|
||||
const getRGBData = async (jpegFilePath: string) => {
|
||||
const jpegData = await fs.readFile(jpegFilePath);
|
||||
const rawImageData = jpeg.decode(jpegData, {
|
||||
useTArray: true,
|
||||
formatAsRGBA: false,
|
||||
});
|
||||
|
||||
const nx: number = rawImageData.width;
|
||||
const ny: number = rawImageData.height;
|
||||
const inputImage: Uint8Array = rawImageData.data;
|
||||
|
||||
const nx2: number = 224;
|
||||
const ny2: number = 224;
|
||||
const totalSize: number = 3 * nx2 * ny2;
|
||||
|
||||
const result: number[] = Array(totalSize).fill(0);
|
||||
const scale: number = Math.max(nx, ny) / 224;
|
||||
|
||||
const nx3: number = Math.round(nx / scale);
|
||||
const ny3: number = Math.round(ny / scale);
|
||||
|
||||
const mean: number[] = [0.48145466, 0.4578275, 0.40821073];
|
||||
const std: number[] = [0.26862954, 0.26130258, 0.27577711];
|
||||
|
||||
for (let y = 0; y < ny3; y++) {
|
||||
for (let x = 0; x < nx3; x++) {
|
||||
for (let c = 0; c < 3; c++) {
|
||||
// Linear interpolation
|
||||
const sx: number = (x + 0.5) * scale - 0.5;
|
||||
const sy: number = (y + 0.5) * scale - 0.5;
|
||||
|
||||
const x0: number = Math.max(0, Math.floor(sx));
|
||||
const y0: number = Math.max(0, Math.floor(sy));
|
||||
|
||||
const x1: number = Math.min(x0 + 1, nx - 1);
|
||||
const y1: number = Math.min(y0 + 1, ny - 1);
|
||||
|
||||
const dx: number = sx - x0;
|
||||
const dy: number = sy - y0;
|
||||
|
||||
const j00: number = 3 * (y0 * nx + x0) + c;
|
||||
const j01: number = 3 * (y0 * nx + x1) + c;
|
||||
const j10: number = 3 * (y1 * nx + x0) + c;
|
||||
const j11: number = 3 * (y1 * nx + x1) + c;
|
||||
|
||||
const v00: number = inputImage[j00];
|
||||
const v01: number = inputImage[j01];
|
||||
const v10: number = inputImage[j10];
|
||||
const v11: number = inputImage[j11];
|
||||
|
||||
const v0: number = v00 * (1 - dx) + v01 * dx;
|
||||
const v1: number = v10 * (1 - dx) + v11 * dx;
|
||||
|
||||
const v: number = v0 * (1 - dy) + v1 * dy;
|
||||
|
||||
const v2: number = Math.min(Math.max(Math.round(v), 0), 255);
|
||||
|
||||
// createTensorWithDataList is dumb compared to reshape and
|
||||
// hence has to be given with one channel after another
|
||||
const i: number = y * nx3 + x + (c % 3) * 224 * 224;
|
||||
|
||||
result[i] = (v2 / 255 - mean[c]) / std[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const normalizeEmbedding = (embedding: Float32Array) => {
|
||||
let normalization = 0;
|
||||
for (let index = 0; index < embedding.length; index++) {
|
||||
normalization += embedding[index] * embedding[index];
|
||||
}
|
||||
const sqrtNormalization = Math.sqrt(normalization);
|
||||
for (let index = 0; index < embedding.length; index++) {
|
||||
embedding[index] = embedding[index] / sqrtNormalization;
|
||||
}
|
||||
return embedding;
|
||||
};
|
||||
|
||||
let _tokenizer: Tokenizer = null;
|
||||
const getTokenizer = () => {
|
||||
if (!_tokenizer) {
|
||||
_tokenizer = new Tokenizer();
|
||||
}
|
||||
return _tokenizer;
|
||||
};
|
||||
|
||||
export const clipTextEmbedding = async (text: string) => {
|
||||
const imageSession = await onnxTextSession();
|
||||
const t1 = Date.now();
|
||||
const tokenizer = getTokenizer();
|
||||
const tokenizedText = Int32Array.from(tokenizer.encodeForCLIP(text));
|
||||
const feeds = {
|
||||
input: new ort.Tensor("int32", tokenizedText, [1, 77]),
|
||||
};
|
||||
const t2 = Date.now();
|
||||
const results = await imageSession.run(feeds);
|
||||
log.debug(
|
||||
() =>
|
||||
`CLIP text embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`,
|
||||
);
|
||||
const textEmbedding = results["output"].data;
|
||||
return normalizeEmbedding(textEmbedding);
|
||||
};
|
|
@ -1,506 +0,0 @@
|
|||
import { app, net } from "electron/main";
|
||||
import { existsSync } from "fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { CustomErrors } from "../constants/errors";
|
||||
import { writeStream } from "../main/fs";
|
||||
import log, { logErrorSentry } from "../main/log";
|
||||
import { execAsync, isDev } from "../main/util";
|
||||
import { Model } from "../types/ipc";
|
||||
import Tokenizer from "../utils/clip-bpe-ts/mod";
|
||||
import { getPlatform } from "../utils/common/platform";
|
||||
import { generateTempFilePath } from "../utils/temp";
|
||||
import { deleteTempFile } from "./ffmpeg";
|
||||
const jpeg = require("jpeg-js");
|
||||
|
||||
const CLIP_MODEL_PATH_PLACEHOLDER = "CLIP_MODEL";
|
||||
const GGMLCLIP_PATH_PLACEHOLDER = "GGML_PATH";
|
||||
const INPUT_PATH_PLACEHOLDER = "INPUT";
|
||||
|
||||
const IMAGE_EMBEDDING_EXTRACT_CMD: string[] = [
|
||||
GGMLCLIP_PATH_PLACEHOLDER,
|
||||
"-mv",
|
||||
CLIP_MODEL_PATH_PLACEHOLDER,
|
||||
"--image",
|
||||
INPUT_PATH_PLACEHOLDER,
|
||||
];
|
||||
|
||||
const TEXT_EMBEDDING_EXTRACT_CMD: string[] = [
|
||||
GGMLCLIP_PATH_PLACEHOLDER,
|
||||
"-mt",
|
||||
CLIP_MODEL_PATH_PLACEHOLDER,
|
||||
"--text",
|
||||
INPUT_PATH_PLACEHOLDER,
|
||||
];
|
||||
const ort = require("onnxruntime-node");
|
||||
|
||||
const TEXT_MODEL_DOWNLOAD_URL = {
|
||||
ggml: "https://models.ente.io/clip-vit-base-patch32_ggml-text-model-f16.gguf",
|
||||
onnx: "https://models.ente.io/clip-text-vit-32-uint8.onnx",
|
||||
};
|
||||
const IMAGE_MODEL_DOWNLOAD_URL = {
|
||||
ggml: "https://models.ente.io/clip-vit-base-patch32_ggml-vision-model-f16.gguf",
|
||||
onnx: "https://models.ente.io/clip-image-vit-32-float32.onnx",
|
||||
};
|
||||
|
||||
const TEXT_MODEL_NAME = {
|
||||
ggml: "clip-vit-base-patch32_ggml-text-model-f16.gguf",
|
||||
onnx: "clip-text-vit-32-uint8.onnx",
|
||||
};
|
||||
const IMAGE_MODEL_NAME = {
|
||||
ggml: "clip-vit-base-patch32_ggml-vision-model-f16.gguf",
|
||||
onnx: "clip-image-vit-32-float32.onnx",
|
||||
};
|
||||
|
||||
const IMAGE_MODEL_SIZE_IN_BYTES = {
|
||||
ggml: 175957504, // 167.8 MB
|
||||
onnx: 351468764, // 335.2 MB
|
||||
};
|
||||
const TEXT_MODEL_SIZE_IN_BYTES = {
|
||||
ggml: 127853440, // 121.9 MB,
|
||||
onnx: 64173509, // 61.2 MB
|
||||
};
|
||||
|
||||
/** Return the path where the given {@link modelName} is meant to be saved */
|
||||
const getModelSavePath = (modelName: string) =>
|
||||
path.join(app.getPath("userData"), "models", modelName);
|
||||
|
||||
async function downloadModel(saveLocation: string, url: string) {
|
||||
// confirm that the save location exists
|
||||
const saveDir = path.dirname(saveLocation);
|
||||
await fs.mkdir(saveDir, { recursive: true });
|
||||
log.info("downloading clip model");
|
||||
const res = await net.fetch(url);
|
||||
if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
||||
await writeStream(saveLocation, res.body);
|
||||
log.info("clip model downloaded");
|
||||
}
|
||||
|
||||
let imageModelDownloadInProgress: Promise<void> = null;
|
||||
|
||||
export async function getClipImageModelPath(type: "ggml" | "onnx") {
|
||||
try {
|
||||
const modelSavePath = getModelSavePath(IMAGE_MODEL_NAME[type]);
|
||||
if (imageModelDownloadInProgress) {
|
||||
log.info("waiting for image model download to finish");
|
||||
await imageModelDownloadInProgress;
|
||||
} else {
|
||||
if (!existsSync(modelSavePath)) {
|
||||
log.info("clip image model not found, downloading");
|
||||
imageModelDownloadInProgress = downloadModel(
|
||||
modelSavePath,
|
||||
IMAGE_MODEL_DOWNLOAD_URL[type],
|
||||
);
|
||||
await imageModelDownloadInProgress;
|
||||
} else {
|
||||
const localFileSize = (await fs.stat(modelSavePath)).size;
|
||||
if (localFileSize !== IMAGE_MODEL_SIZE_IN_BYTES[type]) {
|
||||
log.info(
|
||||
`clip image model size mismatch, downloading again got: ${localFileSize}`,
|
||||
);
|
||||
imageModelDownloadInProgress = downloadModel(
|
||||
modelSavePath,
|
||||
IMAGE_MODEL_DOWNLOAD_URL[type],
|
||||
);
|
||||
await imageModelDownloadInProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
return modelSavePath;
|
||||
} finally {
|
||||
imageModelDownloadInProgress = null;
|
||||
}
|
||||
}
|
||||
|
||||
let textModelDownloadInProgress: boolean = false;
|
||||
|
||||
export async function getClipTextModelPath(type: "ggml" | "onnx") {
|
||||
const modelSavePath = getModelSavePath(TEXT_MODEL_NAME[type]);
|
||||
if (textModelDownloadInProgress) {
|
||||
throw Error(CustomErrors.MODEL_DOWNLOAD_PENDING);
|
||||
} else {
|
||||
if (!existsSync(modelSavePath)) {
|
||||
log.info("clip text model not found, downloading");
|
||||
textModelDownloadInProgress = true;
|
||||
downloadModel(modelSavePath, TEXT_MODEL_DOWNLOAD_URL[type])
|
||||
.catch(() => {
|
||||
// ignore
|
||||
})
|
||||
.finally(() => {
|
||||
textModelDownloadInProgress = false;
|
||||
});
|
||||
throw Error(CustomErrors.MODEL_DOWNLOAD_PENDING);
|
||||
} else {
|
||||
const localFileSize = (await fs.stat(modelSavePath)).size;
|
||||
if (localFileSize !== TEXT_MODEL_SIZE_IN_BYTES[type]) {
|
||||
log.info(
|
||||
`clip text model size mismatch, downloading again got: ${localFileSize}`,
|
||||
);
|
||||
textModelDownloadInProgress = true;
|
||||
downloadModel(modelSavePath, TEXT_MODEL_DOWNLOAD_URL[type])
|
||||
.catch(() => {
|
||||
// ignore
|
||||
})
|
||||
.finally(() => {
|
||||
textModelDownloadInProgress = false;
|
||||
});
|
||||
throw Error(CustomErrors.MODEL_DOWNLOAD_PENDING);
|
||||
}
|
||||
}
|
||||
}
|
||||
return modelSavePath;
|
||||
}
|
||||
|
||||
function getGGMLClipPath() {
|
||||
return isDev
|
||||
? path.join("./build", `ggmlclip-${getPlatform()}`)
|
||||
: path.join(process.resourcesPath, `ggmlclip-${getPlatform()}`);
|
||||
}
|
||||
|
||||
async function createOnnxSession(modelPath: string) {
|
||||
return await ort.InferenceSession.create(modelPath, {
|
||||
intraOpNumThreads: 1,
|
||||
enableCpuMemArena: false,
|
||||
});
|
||||
}
|
||||
|
||||
let onnxImageSessionPromise: Promise<any> = null;
|
||||
|
||||
async function getOnnxImageSession() {
|
||||
if (!onnxImageSessionPromise) {
|
||||
onnxImageSessionPromise = (async () => {
|
||||
const clipModelPath = await getClipImageModelPath("onnx");
|
||||
return createOnnxSession(clipModelPath);
|
||||
})();
|
||||
}
|
||||
return onnxImageSessionPromise;
|
||||
}
|
||||
|
||||
let onnxTextSession: any = null;
|
||||
|
||||
async function getOnnxTextSession() {
|
||||
if (!onnxTextSession) {
|
||||
const clipModelPath = await getClipTextModelPath("onnx");
|
||||
onnxTextSession = await createOnnxSession(clipModelPath);
|
||||
}
|
||||
return onnxTextSession;
|
||||
}
|
||||
|
||||
let tokenizer: Tokenizer = null;
|
||||
function getTokenizer() {
|
||||
if (!tokenizer) {
|
||||
tokenizer = new Tokenizer();
|
||||
}
|
||||
return tokenizer;
|
||||
}
|
||||
|
||||
export const computeImageEmbedding = async (
|
||||
model: Model,
|
||||
imageData: Uint8Array,
|
||||
): Promise<Float32Array> => {
|
||||
let tempInputFilePath = null;
|
||||
try {
|
||||
tempInputFilePath = await generateTempFilePath("");
|
||||
const imageStream = new Response(imageData.buffer).body;
|
||||
await writeStream(tempInputFilePath, imageStream);
|
||||
const embedding = await computeImageEmbedding_(
|
||||
model,
|
||||
tempInputFilePath,
|
||||
);
|
||||
return embedding;
|
||||
} catch (err) {
|
||||
if (isExecError(err)) {
|
||||
const parsedExecError = parseExecError(err);
|
||||
throw Error(parsedExecError);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
if (tempInputFilePath) {
|
||||
await deleteTempFile(tempInputFilePath);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 CustomErrors.UNSUPPORTED_PLATFORM(
|
||||
process.platform,
|
||||
process.arch,
|
||||
);
|
||||
} else {
|
||||
return errMessage;
|
||||
}
|
||||
};
|
||||
|
||||
async function computeImageEmbedding_(
|
||||
model: Model,
|
||||
inputFilePath: string,
|
||||
): Promise<Float32Array> {
|
||||
if (!existsSync(inputFilePath)) {
|
||||
throw Error(CustomErrors.INVALID_FILE_PATH);
|
||||
}
|
||||
if (model === Model.GGML_CLIP) {
|
||||
return await computeGGMLImageEmbedding(inputFilePath);
|
||||
} else if (model === Model.ONNX_CLIP) {
|
||||
return await computeONNXImageEmbedding(inputFilePath);
|
||||
} else {
|
||||
throw Error(CustomErrors.INVALID_CLIP_MODEL(model));
|
||||
}
|
||||
}
|
||||
|
||||
export async function computeGGMLImageEmbedding(
|
||||
inputFilePath: string,
|
||||
): Promise<Float32Array> {
|
||||
try {
|
||||
const clipModelPath = await getClipImageModelPath("ggml");
|
||||
const ggmlclipPath = getGGMLClipPath();
|
||||
const cmd = IMAGE_EMBEDDING_EXTRACT_CMD.map((cmdPart) => {
|
||||
if (cmdPart === GGMLCLIP_PATH_PLACEHOLDER) {
|
||||
return ggmlclipPath;
|
||||
} else if (cmdPart === CLIP_MODEL_PATH_PLACEHOLDER) {
|
||||
return clipModelPath;
|
||||
} else if (cmdPart === INPUT_PATH_PLACEHOLDER) {
|
||||
return inputFilePath;
|
||||
} else {
|
||||
return cmdPart;
|
||||
}
|
||||
});
|
||||
|
||||
const { stdout } = await execAsync(cmd);
|
||||
// parse stdout and return embedding
|
||||
// get the last line of stdout
|
||||
const lines = stdout.split("\n");
|
||||
const lastLine = lines[lines.length - 1];
|
||||
const embedding = JSON.parse(lastLine);
|
||||
const embeddingArray = new Float32Array(embedding);
|
||||
return embeddingArray;
|
||||
} catch (err) {
|
||||
log.error("Failed to compute GGML image embedding", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function computeONNXImageEmbedding(
|
||||
inputFilePath: string,
|
||||
): Promise<Float32Array> {
|
||||
try {
|
||||
const imageSession = await getOnnxImageSession();
|
||||
const t1 = Date.now();
|
||||
const rgbData = await getRGBData(inputFilePath);
|
||||
const feeds = {
|
||||
input: new ort.Tensor("float32", rgbData, [1, 3, 224, 224]),
|
||||
};
|
||||
const t2 = Date.now();
|
||||
const results = await imageSession.run(feeds);
|
||||
log.info(
|
||||
`onnx image embedding time: ${Date.now() - t1} ms (prep:${
|
||||
t2 - t1
|
||||
} ms, extraction: ${Date.now() - t2} ms)`,
|
||||
);
|
||||
const imageEmbedding = results["output"].data; // Float32Array
|
||||
return normalizeEmbedding(imageEmbedding);
|
||||
} catch (err) {
|
||||
log.error("Failed to compute ONNX image embedding", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function computeTextEmbedding(
|
||||
model: Model,
|
||||
text: string,
|
||||
): Promise<Float32Array> {
|
||||
try {
|
||||
const embedding = computeTextEmbedding_(model, text);
|
||||
return embedding;
|
||||
} catch (err) {
|
||||
if (isExecError(err)) {
|
||||
const parsedExecError = parseExecError(err);
|
||||
throw Error(parsedExecError);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function computeTextEmbedding_(
|
||||
model: Model,
|
||||
text: string,
|
||||
): Promise<Float32Array> {
|
||||
if (model === Model.GGML_CLIP) {
|
||||
return await computeGGMLTextEmbedding(text);
|
||||
} else {
|
||||
return await computeONNXTextEmbedding(text);
|
||||
}
|
||||
}
|
||||
|
||||
export async function computeGGMLTextEmbedding(
|
||||
text: string,
|
||||
): Promise<Float32Array> {
|
||||
try {
|
||||
const clipModelPath = await getClipTextModelPath("ggml");
|
||||
const ggmlclipPath = getGGMLClipPath();
|
||||
const cmd = TEXT_EMBEDDING_EXTRACT_CMD.map((cmdPart) => {
|
||||
if (cmdPart === GGMLCLIP_PATH_PLACEHOLDER) {
|
||||
return ggmlclipPath;
|
||||
} else if (cmdPart === CLIP_MODEL_PATH_PLACEHOLDER) {
|
||||
return clipModelPath;
|
||||
} else if (cmdPart === INPUT_PATH_PLACEHOLDER) {
|
||||
return text;
|
||||
} else {
|
||||
return cmdPart;
|
||||
}
|
||||
});
|
||||
|
||||
const { stdout } = await execAsync(cmd);
|
||||
// parse stdout and return embedding
|
||||
// get the last line of stdout
|
||||
const lines = stdout.split("\n");
|
||||
const lastLine = lines[lines.length - 1];
|
||||
const embedding = JSON.parse(lastLine);
|
||||
const embeddingArray = new Float32Array(embedding);
|
||||
return embeddingArray;
|
||||
} catch (err) {
|
||||
if (err.message === CustomErrors.MODEL_DOWNLOAD_PENDING) {
|
||||
log.info(CustomErrors.MODEL_DOWNLOAD_PENDING);
|
||||
} else {
|
||||
log.error("Failed to compute GGML text embedding", err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function computeONNXTextEmbedding(
|
||||
text: string,
|
||||
): Promise<Float32Array> {
|
||||
try {
|
||||
const imageSession = await getOnnxTextSession();
|
||||
const t1 = Date.now();
|
||||
const tokenizer = getTokenizer();
|
||||
const tokenizedText = Int32Array.from(tokenizer.encodeForCLIP(text));
|
||||
const feeds = {
|
||||
input: new ort.Tensor("int32", tokenizedText, [1, 77]),
|
||||
};
|
||||
const t2 = Date.now();
|
||||
const results = await imageSession.run(feeds);
|
||||
log.info(
|
||||
`onnx text embedding time: ${Date.now() - t1} ms (prep:${
|
||||
t2 - t1
|
||||
} ms, extraction: ${Date.now() - t2} ms)`,
|
||||
);
|
||||
const textEmbedding = results["output"].data; // Float32Array
|
||||
return normalizeEmbedding(textEmbedding);
|
||||
} catch (err) {
|
||||
if (err.message === CustomErrors.MODEL_DOWNLOAD_PENDING) {
|
||||
log.info(CustomErrors.MODEL_DOWNLOAD_PENDING);
|
||||
} else {
|
||||
logErrorSentry(err, "Error in computeONNXTextEmbedding");
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function getRGBData(inputFilePath: string) {
|
||||
const jpegData = await fs.readFile(inputFilePath);
|
||||
let rawImageData;
|
||||
try {
|
||||
rawImageData = jpeg.decode(jpegData, {
|
||||
useTArray: true,
|
||||
formatAsRGBA: false,
|
||||
});
|
||||
} catch (err) {
|
||||
logErrorSentry(err, "JPEG decode error");
|
||||
throw err;
|
||||
}
|
||||
|
||||
const nx: number = rawImageData.width;
|
||||
const ny: number = rawImageData.height;
|
||||
const inputImage: Uint8Array = rawImageData.data;
|
||||
|
||||
const nx2: number = 224;
|
||||
const ny2: number = 224;
|
||||
const totalSize: number = 3 * nx2 * ny2;
|
||||
|
||||
const result: number[] = Array(totalSize).fill(0);
|
||||
const scale: number = Math.max(nx, ny) / 224;
|
||||
|
||||
const nx3: number = Math.round(nx / scale);
|
||||
const ny3: number = Math.round(ny / scale);
|
||||
|
||||
const mean: number[] = [0.48145466, 0.4578275, 0.40821073];
|
||||
const std: number[] = [0.26862954, 0.26130258, 0.27577711];
|
||||
|
||||
for (let y = 0; y < ny3; y++) {
|
||||
for (let x = 0; x < nx3; x++) {
|
||||
for (let c = 0; c < 3; c++) {
|
||||
// linear interpolation
|
||||
const sx: number = (x + 0.5) * scale - 0.5;
|
||||
const sy: number = (y + 0.5) * scale - 0.5;
|
||||
|
||||
const x0: number = Math.max(0, Math.floor(sx));
|
||||
const y0: number = Math.max(0, Math.floor(sy));
|
||||
|
||||
const x1: number = Math.min(x0 + 1, nx - 1);
|
||||
const y1: number = Math.min(y0 + 1, ny - 1);
|
||||
|
||||
const dx: number = sx - x0;
|
||||
const dy: number = sy - y0;
|
||||
|
||||
const j00: number = 3 * (y0 * nx + x0) + c;
|
||||
const j01: number = 3 * (y0 * nx + x1) + c;
|
||||
const j10: number = 3 * (y1 * nx + x0) + c;
|
||||
const j11: number = 3 * (y1 * nx + x1) + c;
|
||||
|
||||
const v00: number = inputImage[j00];
|
||||
const v01: number = inputImage[j01];
|
||||
const v10: number = inputImage[j10];
|
||||
const v11: number = inputImage[j11];
|
||||
|
||||
const v0: number = v00 * (1 - dx) + v01 * dx;
|
||||
const v1: number = v10 * (1 - dx) + v11 * dx;
|
||||
|
||||
const v: number = v0 * (1 - dy) + v1 * dy;
|
||||
|
||||
const v2: number = Math.min(Math.max(Math.round(v), 0), 255);
|
||||
|
||||
// createTensorWithDataList is dump compared to reshape and hence has to be given with one channel after another
|
||||
const i: number = y * nx3 + x + (c % 3) * 224 * 224;
|
||||
|
||||
result[i] = (v2 / 255 - mean[c]) / std[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const computeClipMatchScore = async (
|
||||
imageEmbedding: Float32Array,
|
||||
textEmbedding: Float32Array,
|
||||
) => {
|
||||
if (imageEmbedding.length !== textEmbedding.length) {
|
||||
throw Error("imageEmbedding and textEmbedding length mismatch");
|
||||
}
|
||||
let score = 0;
|
||||
for (let index = 0; index < imageEmbedding.length; index++) {
|
||||
score += imageEmbedding[index] * textEmbedding[index];
|
||||
}
|
||||
return score;
|
||||
};
|
||||
|
||||
export const normalizeEmbedding = (embedding: Float32Array) => {
|
||||
let normalization = 0;
|
||||
for (let index = 0; index < embedding.length; index++) {
|
||||
normalization += embedding[index] * embedding[index];
|
||||
}
|
||||
const sqrtNormalization = Math.sqrt(normalization);
|
||||
for (let index = 0; index < embedding.length; index++) {
|
||||
embedding[index] = embedding[index] / sqrtNormalization;
|
||||
}
|
||||
return embedding;
|
||||
};
|
|
@ -1,7 +1,6 @@
|
|||
import pathToFfmpeg from "ffmpeg-static";
|
||||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { CustomErrors } from "../constants/errors";
|
||||
import { writeStream } from "../main/fs";
|
||||
import log from "../main/log";
|
||||
import { execAsync } from "../main/util";
|
||||
|
@ -146,7 +145,7 @@ const promiseWithTimeout = async <T>(
|
|||
} = { current: null };
|
||||
const rejectOnTimeout = new Promise<null>((_, reject) => {
|
||||
timeoutRef.current = setTimeout(
|
||||
() => reject(Error(CustomErrors.WAIT_TIME_EXCEEDED)),
|
||||
() => reject(new Error("Operation timed out")),
|
||||
timeout,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import StreamZip from "node-stream-zip";
|
|||
import { existsSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { logError } from "../main/log";
|
||||
import log from "../main/log";
|
||||
import { ElectronFile } from "../types/ipc";
|
||||
|
||||
const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024;
|
||||
|
@ -115,7 +115,9 @@ export const getZipFileStream = async (
|
|||
const inProgress = {
|
||||
current: false,
|
||||
};
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let resolveObj: (value?: any) => void = null;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let rejectObj: (reason?: any) => void = null;
|
||||
stream.on("readable", () => {
|
||||
try {
|
||||
|
@ -179,7 +181,7 @@ export const getZipFileStream = async (
|
|||
controller.close();
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, "readableStream pull failed");
|
||||
log.error("Failed to pull from readableStream", e);
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
|
|