Compare commits
262 Commits
photos-v0.
...
auth-v2.0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdbb9ab3c3 | ||
|
|
f3356147f5 | ||
|
|
628d13ea53 | ||
|
|
4f76cfb912 | ||
|
|
9dc4a17593 | ||
|
|
b0fbd68d27 | ||
|
|
4d09412f0a | ||
|
|
99b248f7fa | ||
|
|
b62fc60eb5 | ||
|
|
9127c48787 | ||
|
|
5edca461f7 | ||
|
|
4aa3d68e36 | ||
|
|
8cb3bf255c | ||
|
|
c729516faf | ||
|
|
56ce5c0b0e | ||
|
|
d441418b5b | ||
|
|
0d0e20f7c4 | ||
|
|
7e5f01da18 | ||
|
|
1dc8f4617e | ||
|
|
b8968d2904 | ||
|
|
46188313ad | ||
|
|
73eacfb30d | ||
|
|
6bf22fa864 | ||
|
|
2b7aa372bd | ||
|
|
3c9f11ee60 | ||
|
|
72dea7eca6 | ||
|
|
7c82b57ca0 | ||
|
|
9e36032019 | ||
|
|
9555c86b5f | ||
|
|
54a973c457 | ||
|
|
d06fede2b5 | ||
|
|
7cec46ef3d | ||
|
|
11c80a6fa9 | ||
|
|
c6a0af7cbc | ||
|
|
542cd31655 | ||
|
|
cdce7d5922 | ||
|
|
7dee92f44e | ||
|
|
a45bf52a4d | ||
|
|
cc5558db5e | ||
|
|
a2dfffd778 | ||
|
|
f37c46935c | ||
|
|
59bda25be2 | ||
|
|
f92a4c2a6e | ||
|
|
92a3650696 | ||
|
|
a1c9ceae6b | ||
|
|
3e3712efb3 | ||
|
|
5339b1aa89 | ||
|
|
0be549c91b | ||
|
|
826cacd6bf | ||
|
|
90a770c619 | ||
|
|
f4f041552f | ||
|
|
730da7648c | ||
|
|
9cface7902 | ||
|
|
a436a6c766 | ||
|
|
ed10e3ec30 | ||
|
|
7d39c0645a | ||
|
|
48f741b792 | ||
|
|
8a115edef8 | ||
|
|
1870c2a468 | ||
|
|
669f428fa3 | ||
|
|
7be4b47e51 | ||
|
|
79250b9efa | ||
|
|
191d19a0fc | ||
|
|
231bc2fc66 | ||
|
|
f65e738507 | ||
|
|
c8089fbb60 | ||
|
|
d10908458e | ||
|
|
15e290a993 | ||
|
|
12fa3be6c5 | ||
|
|
5ae6d7d47b | ||
|
|
47ab361494 | ||
|
|
15d9c7da3b | ||
|
|
e8f21a7247 | ||
|
|
18e47b3d4e | ||
|
|
35736c447d | ||
|
|
c458b429a0 | ||
|
|
adb796e35f | ||
|
|
c5bb479c4f | ||
|
|
feb8deb648 | ||
|
|
4a4a53f994 | ||
|
|
504f23fe4e | ||
|
|
f81fb5b626 | ||
|
|
78f4f9b42d | ||
|
|
84d21984e0 | ||
|
|
da9a704094 | ||
|
|
a96ad6dfa2 | ||
|
|
44666d6772 | ||
|
|
b3842dab04 | ||
|
|
873b158718 | ||
|
|
7913d5ec2e | ||
|
|
4ceaf7cf13 | ||
|
|
c728b3b8be | ||
|
|
510a4a5978 | ||
|
|
0b3165b812 | ||
|
|
99579fbf51 | ||
|
|
75d041dd9c | ||
|
|
d992085dbd | ||
|
|
cb6cfee9a3 | ||
|
|
60321111c2 | ||
|
|
49b5bff042 | ||
|
|
7c0ab6dd8a | ||
|
|
01e6e79819 | ||
|
|
e5b2d737b4 | ||
|
|
5e6f057c4c | ||
|
|
3cb51184b3 | ||
|
|
9025ad3c57 | ||
|
|
d317da6536 | ||
|
|
0071182721 | ||
|
|
5700b101b2 | ||
|
|
02207ca96c | ||
|
|
ddb2952b6a | ||
|
|
c983af0dea | ||
|
|
363e2b116b | ||
|
|
d8b7dd06f6 | ||
|
|
9b73cd2176 | ||
|
|
1205b864d2 | ||
|
|
54a1b3ca3e | ||
|
|
4b074f4475 | ||
|
|
b650372d35 | ||
|
|
04c0fd0617 | ||
|
|
e779ae5189 | ||
|
|
14fe0a46b0 | ||
|
|
568f4540e3 | ||
|
|
6bff42ad9b | ||
|
|
d00211964b | ||
|
|
8695f46b43 | ||
|
|
8020d83ced | ||
|
|
121df66ada | ||
|
|
a98a29800b | ||
|
|
afe94b72ba | ||
|
|
167e5a95ca | ||
|
|
2bd02eac4b | ||
|
|
bfc147b4d1 | ||
|
|
16cb63edfe | ||
|
|
6333792d64 | ||
|
|
69bd822499 | ||
|
|
6a31331ac4 | ||
|
|
e16834e52e | ||
|
|
10b0d9f533 | ||
|
|
791cc61ca7 | ||
|
|
7a674dcf95 | ||
|
|
5c313fb87d | ||
|
|
411984ebdc | ||
|
|
8764e5cf4d | ||
|
|
cf27f3236c | ||
|
|
42a59f2fb5 | ||
|
|
7807d3a413 | ||
|
|
461430a972 | ||
|
|
0b39759ad9 | ||
|
|
fe6215d0fd | ||
|
|
e680970cdf | ||
|
|
63387d8819 | ||
|
|
356ad6f004 | ||
|
|
7be2c66fb6 | ||
|
|
c32badc82c | ||
|
|
dbdad18c96 | ||
|
|
ae51531bd0 | ||
|
|
4ad060c4e4 | ||
|
|
5266e5d1dc | ||
|
|
5904e3dd2c | ||
|
|
74decc32de | ||
|
|
5e8f41cbfe | ||
|
|
360aa2903f | ||
|
|
0d48284b4f | ||
|
|
687efe506a | ||
|
|
e0cfa36f08 | ||
|
|
858ba88c65 | ||
|
|
e7e8ded1ed | ||
|
|
fa7cbaea18 | ||
|
|
9785bbcb26 | ||
|
|
039387a84e | ||
|
|
866b52b002 | ||
|
|
2c098904fb | ||
|
|
a68dce35f6 | ||
|
|
1d0f30ad91 | ||
|
|
7374fe2ecc | ||
|
|
55454f9454 | ||
|
|
1aa39e83d7 | ||
|
|
d84ee7223a | ||
|
|
4e5fcebb95 | ||
|
|
e170b6811d | ||
|
|
3ed2186dcf | ||
|
|
b6177a5bc3 | ||
|
|
27410b2da9 | ||
|
|
ae061d2a44 | ||
|
|
d9d03d8451 | ||
|
|
2f5abb6318 | ||
|
|
e53c923675 | ||
|
|
39228270c1 | ||
|
|
384ec365e8 | ||
|
|
b2da2c7e88 | ||
|
|
c25cc6f8a4 | ||
|
|
b2cf6be5f5 | ||
|
|
fce68ba1be | ||
|
|
29550317f7 | ||
|
|
876c5800f9 | ||
|
|
608cb6c85e | ||
|
|
712b99b8f3 | ||
|
|
935e47fbca | ||
|
|
fcb26d39f1 | ||
|
|
ff6d0d32cf | ||
|
|
52c47234fd | ||
|
|
756050ae8c | ||
|
|
a2d39a46be | ||
|
|
407eca5414 | ||
|
|
87dc7d76ca | ||
|
|
8b643549fe | ||
|
|
d8190926fd | ||
|
|
4255e48abb | ||
|
|
a8a5cc8b59 | ||
|
|
949a42004f | ||
|
|
cb94dd8b42 | ||
|
|
56d500f4e8 | ||
|
|
7a41ba43a5 | ||
|
|
7a729183e2 | ||
|
|
aa5422db6c | ||
|
|
c0fee7bc91 | ||
|
|
1411ca6fad | ||
|
|
92715b658c | ||
|
|
2ddf4c897c | ||
|
|
ffefae89a6 | ||
|
|
9d7a342aa9 | ||
|
|
ee33a3229f | ||
|
|
54c4862e71 | ||
|
|
b97839adae | ||
|
|
37c4295df9 | ||
|
|
089be79688 | ||
|
|
0034d880f9 | ||
|
|
81bdc0fe73 | ||
|
|
76dca4d819 | ||
|
|
d0f1bbfca7 | ||
|
|
8a00f1b85f | ||
|
|
f10f751a2f | ||
|
|
d28daece8a | ||
|
|
24bce96d71 | ||
|
|
ad6dea2ecb | ||
|
|
212dcfb88a | ||
|
|
a689aca4a6 | ||
|
|
e2fd88bff0 | ||
|
|
764b6bf2f3 | ||
|
|
39ec761949 | ||
|
|
01f842c445 | ||
|
|
2fe703df92 | ||
|
|
18c48c7e0a | ||
|
|
ca688d0d46 | ||
|
|
885308471f | ||
|
|
125f7bfece | ||
|
|
011aee20d5 | ||
|
|
85778bcdaa | ||
|
|
43e97d225e | ||
|
|
b3a86874db | ||
|
|
5c1ed5be8f | ||
|
|
14fde54d87 | ||
|
|
26b35cec9e | ||
|
|
b8100b1273 | ||
|
|
a5fcbbf901 | ||
|
|
6213628aee | ||
|
|
a7625cd83d | ||
|
|
cc90dd7ba5 | ||
|
|
b3630f9543 | ||
|
|
9cb289e002 | ||
|
|
b95fc54adb |
3
.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 libffi7
|
||||
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: |
|
||||
|
||||
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
|
||||
|
||||
4
.github/workflows/web-deploy-payments.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
|
||||
@@ -39,5 +39,5 @@ jobs:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
projectName: ente
|
||||
branch: deploy/payments
|
||||
directory: web/apps/payments/out
|
||||
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"
|
||||
4
.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
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
projectName: ente
|
||||
branch: n-payments
|
||||
directory: web/apps/payments/out
|
||||
directory: web/apps/payments/dist
|
||||
wranglerVersion: "3"
|
||||
|
||||
- name: Build photos
|
||||
|
||||
2
.github/workflows/web-preview.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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.0 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
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#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 <tray_manager/tray_manager_plugin.h>
|
||||
@@ -42,9 +41,6 @@ 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);
|
||||
|
||||
@@ -10,7 +10,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
gtk
|
||||
screen_retriever
|
||||
sentry_flutter
|
||||
smart_auth
|
||||
sodium_libs
|
||||
sqlite3_flutter_libs
|
||||
tray_manager
|
||||
|
||||
@@ -25,3 +25,4 @@ startup_notify: false
|
||||
# - libcurl.so.4
|
||||
include:
|
||||
- libffi.so.7
|
||||
- libtiff.so.5
|
||||
|
||||
@@ -9,7 +9,7 @@ url: https://github.com/ente-io/ente
|
||||
|
||||
display_name: Auth
|
||||
|
||||
dependencies:
|
||||
requires:
|
||||
- libsqlite3x
|
||||
- webkit2gtk-4.0
|
||||
- libsodium
|
||||
|
||||
@@ -20,7 +20,6 @@ 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
|
||||
@@ -44,7 +43,6 @@ 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"))
|
||||
|
||||
@@ -1109,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:
|
||||
@@ -1334,14 +1334,6 @@ packages:
|
||||
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:
|
||||
@@ -1551,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.50+250
|
||||
version: 2.0.55+255
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -75,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
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
#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 <tray_manager/tray_manager_plugin.h>
|
||||
@@ -44,8 +43,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("SentryFlutterPlugin"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
SmartAuthPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SmartAuthPlugin"));
|
||||
SodiumLibsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SodiumLibsPluginCApi"));
|
||||
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||
|
||||
@@ -13,7 +13,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
screen_retriever
|
||||
sentry_flutter
|
||||
share_plus
|
||||
smart_auth
|
||||
sodium_libs
|
||||
sqlite3_flutter_libs
|
||||
tray_manager
|
||||
|
||||
@@ -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)
|
||||
- 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.
|
||||
|
||||
## ZIP
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -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,12 @@ 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.
|
||||
mainWindow.on("focus", () =>
|
||||
mainWindow.webContents.send("app-in-foreground"),
|
||||
);
|
||||
};
|
||||
|
||||
const main = () => {
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
@@ -144,6 +159,7 @@ const main = () => {
|
||||
|
||||
initLogging();
|
||||
setupRendererServer();
|
||||
logStartupBanner();
|
||||
handleDockIconHideOnAutoLaunch();
|
||||
increaseDiskCache();
|
||||
enableSharedArrayBufferSupport();
|
||||
@@ -163,7 +179,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 +190,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,14 +10,6 @@
|
||||
|
||||
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,
|
||||
@@ -34,6 +26,17 @@ import {
|
||||
convertToJPEG,
|
||||
generateImageThumbnail,
|
||||
} from "../services/imageProcessor";
|
||||
import {
|
||||
clearElectronStore,
|
||||
getEncryptionKey,
|
||||
setEncryptionKey,
|
||||
} from "../services/store";
|
||||
import {
|
||||
getElectronFilesFromGoogleZip,
|
||||
getPendingUploads,
|
||||
setToUploadCollection,
|
||||
setToUploadFiles,
|
||||
} from "../services/upload";
|
||||
import {
|
||||
addWatchMapping,
|
||||
getWatchMappings,
|
||||
@@ -91,16 +94,16 @@ 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", (_) => {
|
||||
ipcMain.on("clear-electron-store", () => {
|
||||
clearElectronStore();
|
||||
});
|
||||
|
||||
@@ -108,11 +111,11 @@ export const attachIPCHandlers = () => {
|
||||
setEncryptionKey(encryptionKey),
|
||||
);
|
||||
|
||||
ipcMain.handle("getEncryptionKey", (_) => getEncryptionKey());
|
||||
ipcMain.handle("getEncryptionKey", () => getEncryptionKey());
|
||||
|
||||
// - App update
|
||||
|
||||
ipcMain.on("update-and-restart", (_) => updateAndRestart());
|
||||
ipcMain.on("update-and-restart", () => updateAndRestart());
|
||||
|
||||
ipcMain.on("skip-app-update", (_, version) => skipAppUpdate(version));
|
||||
|
||||
@@ -157,13 +160,13 @@ export const attachIPCHandlers = () => {
|
||||
|
||||
// - 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 +180,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 +206,7 @@ export const attachIPCHandlers = () => {
|
||||
|
||||
// - Upload
|
||||
|
||||
ipcMain.handle("getPendingUploads", (_) => getPendingUploads());
|
||||
ipcMain.handle("getPendingUploads", () => getPendingUploads());
|
||||
|
||||
ipcMain.handle(
|
||||
"setToUploadFiles",
|
||||
@@ -252,7 +255,7 @@ export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => {
|
||||
removeWatchMapping(watcher, folderPath),
|
||||
);
|
||||
|
||||
ipcMain.handle("getWatchMappings", (_) => getWatchMappings());
|
||||
ipcMain.handle("getWatchMappings", () => getWatchMappings());
|
||||
|
||||
ipcMain.handle(
|
||||
"updateWatchMappingSyncedFiles",
|
||||
|
||||
@@ -15,7 +15,7 @@ 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;
|
||||
@@ -31,25 +31,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,7 +60,7 @@ 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) => {
|
||||
@@ -98,12 +80,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 +94,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 +104,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,
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
@@ -53,7 +54,7 @@ import type {
|
||||
const appVersion = (): Promise<string> => ipcRenderer.invoke("appVersion");
|
||||
|
||||
const openDirectory = (dirPath: string): Promise<void> =>
|
||||
ipcRenderer.invoke("openDirectory");
|
||||
ipcRenderer.invoke("openDirectory", dirPath);
|
||||
|
||||
const openLogDirectory = (): Promise<void> =>
|
||||
ipcRenderer.invoke("openLogDirectory");
|
||||
@@ -68,9 +69,7 @@ const fsExists = (path: string): Promise<boolean> =>
|
||||
|
||||
const registerForegroundEventListener = (onForeground: () => void) => {
|
||||
ipcRenderer.removeAllListeners("app-in-foreground");
|
||||
ipcRenderer.on("app-in-foreground", () => {
|
||||
onForeground();
|
||||
});
|
||||
ipcRenderer.on("app-in-foreground", onForeground);
|
||||
};
|
||||
|
||||
const clearElectronStore = () => {
|
||||
@@ -228,11 +227,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,7 +307,7 @@ 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,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { compareVersions } from "compare-versions";
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { default as ElectronLog, default as log } from "electron-log";
|
||||
import { default as electronLog } from "electron-log";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { setIsAppQuitting, setIsUpdateAvailable } from "../main";
|
||||
import { logErrorSentry } from "../main/log";
|
||||
import log from "../main/log";
|
||||
import { AppUpdateInfo } from "../types/ipc";
|
||||
import {
|
||||
clearMuteUpdateNotificationVersion,
|
||||
@@ -18,7 +18,7 @@ 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.logger = electronLog;
|
||||
autoUpdater.autoDownload = false;
|
||||
checkForUpdateAndNotify(mainWindow);
|
||||
setInterval(
|
||||
@@ -33,49 +33,36 @@ export function forceCheckForUpdateAndNotify(mainWindow: BrowserWindow) {
|
||||
clearMuteUpdateNotificationVersion();
|
||||
checkForUpdateAndNotify(mainWindow);
|
||||
} catch (e) {
|
||||
logErrorSentry(e, "forceCheckForUpdateAndNotify failed");
|
||||
log.error("forceCheckForUpdateAndNotify failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
log.debug(() => "checkForUpdateAndNotify");
|
||||
const { updateInfo } = await autoUpdater.checkForUpdates();
|
||||
log.debug(() => `Update version ${updateInfo.version}`);
|
||||
if (compareVersions(updateInfo.version, app.getVersion()) <= 0) {
|
||||
log.debug(() => "Skipping update, already at latest version");
|
||||
return;
|
||||
}
|
||||
const skipAppVersion = getSkipAppVersion();
|
||||
if (
|
||||
skipAppVersion &&
|
||||
updateCheckResult.updateInfo.version === skipAppVersion
|
||||
) {
|
||||
log.info(
|
||||
"user chose to skip version ",
|
||||
updateCheckResult.updateInfo.version,
|
||||
);
|
||||
if (skipAppVersion && updateInfo.version === skipAppVersion) {
|
||||
log.info(`User chose to skip version ${updateInfo.version}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
log.debug("attempting auto update");
|
||||
log.debug(() => "Attempting auto update");
|
||||
autoUpdater.downloadUpdate();
|
||||
const muteUpdateNotificationVersion =
|
||||
getMuteUpdateNotificationVersion();
|
||||
if (
|
||||
muteUpdateNotificationVersion &&
|
||||
updateCheckResult.updateInfo.version ===
|
||||
muteUpdateNotificationVersion
|
||||
updateInfo.version === muteUpdateNotificationVersion
|
||||
) {
|
||||
log.info(
|
||||
"user chose to mute update notification for version ",
|
||||
updateCheckResult.updateInfo.version,
|
||||
`User has muted update notifications for version ${updateInfo.version}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -84,28 +71,28 @@ async function checkForUpdateAndNotify(mainWindow: BrowserWindow) {
|
||||
() =>
|
||||
showUpdateDialog(mainWindow, {
|
||||
autoUpdatable: true,
|
||||
version: updateCheckResult.updateInfo.version,
|
||||
version: updateInfo.version,
|
||||
}),
|
||||
FIVE_MIN_IN_MICROSECOND,
|
||||
);
|
||||
});
|
||||
autoUpdater.on("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
logErrorSentry(error, "auto update failed");
|
||||
log.error("Auto update failed", error);
|
||||
showUpdateDialog(mainWindow, {
|
||||
autoUpdatable: false,
|
||||
version: updateCheckResult.updateInfo.version,
|
||||
version: updateInfo.version,
|
||||
});
|
||||
});
|
||||
|
||||
setIsUpdateAvailable(true);
|
||||
} catch (e) {
|
||||
logErrorSentry(e, "checkForUpdateAndNotify failed");
|
||||
log.error("checkForUpdateAndNotify failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAndRestart() {
|
||||
ElectronLog.log("user quit the app");
|
||||
log.info("user quit the app");
|
||||
setIsAppQuitting(true);
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2,11 +2,10 @@ 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 log from "../main/log";
|
||||
import { execAsync, isDev } from "../main/util";
|
||||
import { Model } from "../types/ipc";
|
||||
import { CustomErrors, Model, isModel } from "../types/ipc";
|
||||
import Tokenizer from "../utils/clip-bpe-ts/mod";
|
||||
import { getPlatform } from "../utils/common/platform";
|
||||
import { generateTempFilePath } from "../utils/temp";
|
||||
@@ -78,7 +77,7 @@ async function downloadModel(saveLocation: string, url: string) {
|
||||
|
||||
let imageModelDownloadInProgress: Promise<void> = null;
|
||||
|
||||
export async function getClipImageModelPath(type: "ggml" | "onnx") {
|
||||
const getClipImageModelPath = async (type: "ggml" | "onnx") => {
|
||||
try {
|
||||
const modelSavePath = getModelSavePath(IMAGE_MODEL_NAME[type]);
|
||||
if (imageModelDownloadInProgress) {
|
||||
@@ -86,7 +85,7 @@ export async function getClipImageModelPath(type: "ggml" | "onnx") {
|
||||
await imageModelDownloadInProgress;
|
||||
} else {
|
||||
if (!existsSync(modelSavePath)) {
|
||||
log.info("clip image model not found, downloading");
|
||||
log.info("CLIP image model not found, downloading");
|
||||
imageModelDownloadInProgress = downloadModel(
|
||||
modelSavePath,
|
||||
IMAGE_MODEL_DOWNLOAD_URL[type],
|
||||
@@ -96,7 +95,7 @@ export async function getClipImageModelPath(type: "ggml" | "onnx") {
|
||||
const localFileSize = (await fs.stat(modelSavePath)).size;
|
||||
if (localFileSize !== IMAGE_MODEL_SIZE_IN_BYTES[type]) {
|
||||
log.info(
|
||||
`clip image model size mismatch, downloading again got: ${localFileSize}`,
|
||||
`CLIP image model size mismatch, downloading again got: ${localFileSize}`,
|
||||
);
|
||||
imageModelDownloadInProgress = downloadModel(
|
||||
modelSavePath,
|
||||
@@ -110,21 +109,22 @@ export async function getClipImageModelPath(type: "ggml" | "onnx") {
|
||||
} finally {
|
||||
imageModelDownloadInProgress = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let textModelDownloadInProgress: boolean = false;
|
||||
|
||||
export async function getClipTextModelPath(type: "ggml" | "onnx") {
|
||||
const getClipTextModelPath = async (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");
|
||||
log.info("CLIP text model not found, downloading");
|
||||
textModelDownloadInProgress = true;
|
||||
downloadModel(modelSavePath, TEXT_MODEL_DOWNLOAD_URL[type])
|
||||
.catch(() => {
|
||||
// ignore
|
||||
.catch((e) => {
|
||||
// log but otherwise ignore
|
||||
log.error("CLIP text model download failed", e);
|
||||
})
|
||||
.finally(() => {
|
||||
textModelDownloadInProgress = false;
|
||||
@@ -134,12 +134,13 @@ export async function getClipTextModelPath(type: "ggml" | "onnx") {
|
||||
const localFileSize = (await fs.stat(modelSavePath)).size;
|
||||
if (localFileSize !== TEXT_MODEL_SIZE_IN_BYTES[type]) {
|
||||
log.info(
|
||||
`clip text model size mismatch, downloading again got: ${localFileSize}`,
|
||||
`CLIP text model size mismatch, downloading again got: ${localFileSize}`,
|
||||
);
|
||||
textModelDownloadInProgress = true;
|
||||
downloadModel(modelSavePath, TEXT_MODEL_DOWNLOAD_URL[type])
|
||||
.catch(() => {
|
||||
// ignore
|
||||
.catch((e) => {
|
||||
// log but otherwise ignore
|
||||
log.error("CLIP text model download failed", e);
|
||||
})
|
||||
.finally(() => {
|
||||
textModelDownloadInProgress = false;
|
||||
@@ -149,7 +150,7 @@ export async function getClipTextModelPath(type: "ggml" | "onnx") {
|
||||
}
|
||||
}
|
||||
return modelSavePath;
|
||||
}
|
||||
};
|
||||
|
||||
function getGGMLClipPath() {
|
||||
return isDev
|
||||
@@ -198,6 +199,8 @@ export const computeImageEmbedding = async (
|
||||
model: Model,
|
||||
imageData: Uint8Array,
|
||||
): Promise<Float32Array> => {
|
||||
if (!isModel(model)) throw new Error(`Invalid CLIP model ${model}`);
|
||||
|
||||
let tempInputFilePath = null;
|
||||
try {
|
||||
tempInputFilePath = await generateTempFilePath("");
|
||||
@@ -243,180 +246,69 @@ async function computeImageEmbedding_(
|
||||
inputFilePath: string,
|
||||
): Promise<Float32Array> {
|
||||
if (!existsSync(inputFilePath)) {
|
||||
throw Error(CustomErrors.INVALID_FILE_PATH);
|
||||
throw new Error("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));
|
||||
switch (model) {
|
||||
case "ggml-clip":
|
||||
return await computeGGMLImageEmbedding(inputFilePath);
|
||||
case "onnx-clip":
|
||||
return await computeONNXImageEmbedding(inputFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
export async function computeGGMLImageEmbedding(
|
||||
const computeGGMLImageEmbedding = async (
|
||||
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;
|
||||
}
|
||||
});
|
||||
): Promise<Float32Array> => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
export async function computeONNXImageEmbedding(
|
||||
const computeONNXImageEmbedding = async (
|
||||
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;
|
||||
}
|
||||
}
|
||||
): Promise<Float32Array> => {
|
||||
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);
|
||||
};
|
||||
|
||||
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 rawImageData = jpeg.decode(jpegData, {
|
||||
useTArray: true,
|
||||
formatAsRGBA: false,
|
||||
});
|
||||
|
||||
const nx: number = rawImageData.width;
|
||||
const ny: number = rawImageData.height;
|
||||
@@ -479,21 +371,7 @@ async function getRGBData(inputFilePath: string) {
|
||||
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) => {
|
||||
const normalizeEmbedding = (embedding: Float32Array) => {
|
||||
let normalization = 0;
|
||||
for (let index = 0; index < embedding.length; index++) {
|
||||
normalization += embedding[index] * embedding[index];
|
||||
@@ -504,3 +382,82 @@ export const normalizeEmbedding = (embedding: Float32Array) => {
|
||||
}
|
||||
return embedding;
|
||||
};
|
||||
|
||||
export async function computeTextEmbedding(
|
||||
model: Model,
|
||||
text: string,
|
||||
): Promise<Float32Array> {
|
||||
if (!isModel(model)) throw new Error(`Invalid CLIP model ${model}`);
|
||||
|
||||
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> {
|
||||
switch (model) {
|
||||
case "ggml-clip":
|
||||
return await computeGGMLTextEmbedding(text);
|
||||
case "onnx-clip":
|
||||
return await computeONNXTextEmbedding(text);
|
||||
}
|
||||
}
|
||||
|
||||
export async function computeGGMLTextEmbedding(
|
||||
text: string,
|
||||
): Promise<Float32Array> {
|
||||
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;
|
||||
}
|
||||
|
||||
export async function computeONNXTextEmbedding(
|
||||
text: string,
|
||||
): Promise<Float32Array> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { existsSync } from "fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "path";
|
||||
import { CustomErrors } from "../constants/errors";
|
||||
import { writeStream } from "../main/fs";
|
||||
import { logError, logErrorSentry } from "../main/log";
|
||||
import log from "../main/log";
|
||||
import { execAsync, isDev } from "../main/util";
|
||||
import { ElectronFile } from "../types/ipc";
|
||||
import { CustomErrors, ElectronFile } from "../types/ipc";
|
||||
import { isPlatform } from "../utils/common/platform";
|
||||
import { generateTempFilePath } from "../utils/temp";
|
||||
import { deleteTempFile } from "./ffmpeg";
|
||||
@@ -103,18 +102,21 @@ async function convertToJPEG_(
|
||||
|
||||
return new Uint8Array(await fs.readFile(tempOutputFilePath));
|
||||
} catch (e) {
|
||||
logErrorSentry(e, "failed to convert heic");
|
||||
log.error("Failed to convert HEIC", e);
|
||||
throw e;
|
||||
} finally {
|
||||
try {
|
||||
await fs.rm(tempInputFilePath, { force: true });
|
||||
} catch (e) {
|
||||
logErrorSentry(e, "failed to remove tempInputFile");
|
||||
log.error(`Failed to remove tempInputFile ${tempInputFilePath}`, e);
|
||||
}
|
||||
try {
|
||||
await fs.rm(tempOutputFilePath, { force: true });
|
||||
} catch (e) {
|
||||
logErrorSentry(e, "failed to remove tempOutputFile");
|
||||
log.error(
|
||||
`Failed to remove tempOutputFile ${tempOutputFilePath}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +152,7 @@ function constructConvertCommand(
|
||||
},
|
||||
);
|
||||
} else {
|
||||
throw Error(CustomErrors.INVALID_OS(process.platform));
|
||||
throw new Error(`Unsupported OS ${process.platform}`);
|
||||
}
|
||||
return convertCmd;
|
||||
}
|
||||
@@ -187,7 +189,7 @@ export async function generateImageThumbnail(
|
||||
try {
|
||||
await deleteTempFile(inputFilePath);
|
||||
} catch (e) {
|
||||
logError(e, "failed to deleteTempFile");
|
||||
log.error(`Failed to deleteTempFile ${inputFilePath}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,13 +219,16 @@ async function generateImageThumbnail_(
|
||||
} while (thumbnail.length > maxSize && quality > MIN_QUALITY);
|
||||
return thumbnail;
|
||||
} catch (e) {
|
||||
logErrorSentry(e, "generate image thumbnail failed");
|
||||
log.error("Failed to generate image thumbnail", e);
|
||||
throw e;
|
||||
} finally {
|
||||
try {
|
||||
await fs.rm(tempOutputFilePath, { force: true });
|
||||
} catch (e) {
|
||||
logErrorSentry(e, "failed to remove tempOutputFile");
|
||||
log.error(
|
||||
`Failed to remove tempOutputFile ${tempOutputFilePath}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,7 +288,7 @@ function constructThumbnailGenerationCommand(
|
||||
return cmdPart;
|
||||
});
|
||||
} else {
|
||||
throw Error(CustomErrors.INVALID_OS(process.platform));
|
||||
throw new Error(`Unsupported OS ${process.platform}`);
|
||||
}
|
||||
return thumbnailGenerationCmd;
|
||||
}
|
||||
|
||||
26
desktop/src/services/store.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { safeStorage } from "electron/main";
|
||||
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 = () => {
|
||||
uploadStatusStore.clear();
|
||||
keysStore.clear();
|
||||
safeStorageStore.clear();
|
||||
watchStore.clear();
|
||||
};
|
||||
|
||||
export async function setEncryptionKey(encryptionKey: string) {
|
||||
const encryptedKey: Buffer = await safeStorage.encryptString(encryptionKey);
|
||||
const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64");
|
||||
safeStorageStore.set("encryptionKey", b64EncryptedKey);
|
||||
}
|
||||
|
||||
export async function getEncryptionKey(): Promise<string> {
|
||||
const b64EncryptedKey = safeStorageStore.get("encryptionKey");
|
||||
if (b64EncryptedKey) {
|
||||
const keyBuffer = Buffer.from(b64EncryptedKey, "base64");
|
||||
return await safeStorage.decryptString(keyBuffer);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,39 @@
|
||||
import StreamZip from "node-stream-zip";
|
||||
import path from "path";
|
||||
import { getElectronFile } from "../services/fs";
|
||||
import { uploadStatusStore } from "../stores/upload.store";
|
||||
import { ElectronFile, FILE_PATH_TYPE } from "../types/ipc";
|
||||
import { FILE_PATH_KEYS } from "../types/main";
|
||||
import { getValidPaths, getZipFileStream } from "./fs";
|
||||
|
||||
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 const getSavedFilePaths = (type: FILE_PATH_TYPE) => {
|
||||
const paths =
|
||||
getValidPaths(
|
||||
|
||||
1
desktop/src/types/any-shell-escape.d.ts
vendored
@@ -19,6 +19,7 @@
|
||||
* curl -v -H "Location;" -H "User-Agent: FooBar's so-called ""Browser""" "http://www.daveeddy.com/?name=dave&age=24"
|
||||
Which is suitable for being executed by the shell.
|
||||
*/
|
||||
/* eslint-disable no-unused-vars */
|
||||
declare module "any-shell-escape" {
|
||||
declare const shellescape: (args: readonly string | string[]) => string;
|
||||
export default shellescape;
|
||||
|
||||
@@ -4,6 +4,32 @@
|
||||
* This file is manually kept in sync with the renderer code.
|
||||
* See [Note: types.ts <-> preload.ts <-> ipc.ts]
|
||||
*/
|
||||
|
||||
/**
|
||||
* Errors that have special semantics on the web side.
|
||||
*
|
||||
* [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",
|
||||
UNSUPPORTED_PLATFORM: (platform: string, arch: string) =>
|
||||
`Unsupported platform - ${platform} ${arch}`,
|
||||
MODEL_DOWNLOAD_PENDING:
|
||||
"Model download pending, skipping clip search request",
|
||||
};
|
||||
|
||||
/**
|
||||
* Deprecated - Use File + webUtils.getPathForFile instead
|
||||
*
|
||||
@@ -45,6 +71,7 @@ export interface WatchStoreType {
|
||||
}
|
||||
|
||||
export enum FILE_PATH_TYPE {
|
||||
/* eslint-disable no-unused-vars */
|
||||
FILES = "files",
|
||||
ZIPS = "zips",
|
||||
}
|
||||
@@ -54,7 +81,6 @@ export interface AppUpdateInfo {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export enum Model {
|
||||
GGML_CLIP = "ggml-clip",
|
||||
ONNX_CLIP = "onnx-clip",
|
||||
}
|
||||
export type Model = "ggml-clip" | "onnx-clip";
|
||||
|
||||
export const isModel = (s: unknown) => s == "ggml-clip" || s == "onnx-clip";
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface KeysStoreType {
|
||||
};
|
||||
}
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
export const FILE_PATH_KEYS: {
|
||||
[k in FILE_PATH_TYPE]: keyof UploadStoreType;
|
||||
} = {
|
||||
|
||||
@@ -202,6 +202,10 @@ export const sidebar = [
|
||||
{
|
||||
text: "Troubleshooting",
|
||||
items: [
|
||||
{
|
||||
text: "Uploads",
|
||||
link: "/self-hosting/troubleshooting/uploads",
|
||||
},
|
||||
{
|
||||
text: "Yarn",
|
||||
link: "/self-hosting/troubleshooting/yarn",
|
||||
@@ -219,80 +223,3 @@ export const sidebar = [
|
||||
link: "/about/contribute",
|
||||
},
|
||||
];
|
||||
|
||||
function sidebarOld() {
|
||||
return [
|
||||
{
|
||||
text: "Welcome",
|
||||
items: [
|
||||
{
|
||||
text: "Features",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: "Family Plan",
|
||||
link: "/photos/features/family-plan",
|
||||
},
|
||||
{ text: "Albums", link: "/photos/features/albums" },
|
||||
{ text: "Archive", link: "/photos/features/archive" },
|
||||
{ text: "Hidden", link: "/photos/features/hidden" },
|
||||
{ text: "Map", link: "/photos/features/map" },
|
||||
{
|
||||
text: "Location Tags",
|
||||
link: "/photos/features/location",
|
||||
},
|
||||
{
|
||||
text: "Collect Photos",
|
||||
link: "/photos/features/collect",
|
||||
},
|
||||
{
|
||||
text: "Public links",
|
||||
link: "/photos/features/public-links",
|
||||
},
|
||||
{
|
||||
text: "Quick link",
|
||||
link: "/photos/features/quick-link",
|
||||
},
|
||||
{
|
||||
text: "Watch folder",
|
||||
link: "/photos/features/watch-folders",
|
||||
},
|
||||
{ text: "Trash", link: "/photos/features/trash" },
|
||||
{
|
||||
text: "Uncategorized",
|
||||
link: "/photos/features/uncategorized",
|
||||
},
|
||||
{
|
||||
text: "Referral Plan",
|
||||
link: "/photos/features/referral",
|
||||
},
|
||||
{
|
||||
text: "Live & Motion Photos",
|
||||
link: "/photos/features/live-photos",
|
||||
},
|
||||
{ text: "Cast", link: "/photos/features/cast" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Troubleshoot",
|
||||
collapsed: true,
|
||||
link: "/photos/troubleshooting/files-not-uploading",
|
||||
items: [
|
||||
{
|
||||
text: "Files not uploading",
|
||||
link: "/photos/troubleshooting/files-not-uploading",
|
||||
},
|
||||
{
|
||||
text: "Failed to play video",
|
||||
link: "/photos/troubleshooting/video-not-playing",
|
||||
},
|
||||
{
|
||||
text: "Report bug",
|
||||
link: "/photos/troubleshooting/report-bug",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -12,15 +12,17 @@ in a local drive or NAS of your choice. This way, you can use Ente in your day
|
||||
to day use, but will have an additional guarantee that a copy of your original
|
||||
photos and videos are always available in normal directories and files.
|
||||
|
||||
* You can use [Ente's CLI](https://github.com/ente-io/ente/tree/main/cli#export)
|
||||
to export your data in a cron job to a location of your choice. The exports
|
||||
are incremental, and will also gracefully handle interruptions.
|
||||
- You can use
|
||||
[Ente's CLI](https://github.com/ente-io/ente/tree/main/cli#export) to export
|
||||
your data in a cron job to a location of your choice. The exports are
|
||||
incremental, and will also gracefully handle interruptions.
|
||||
|
||||
* Similarly, you can use Ente's [desktop app](https://ente.io/download/desktop)
|
||||
to export your data to a folder of your choice. The desktop app also supports
|
||||
"continuous" exports, where it will automatically export new items in the
|
||||
background without you needing to run any other cron jobs. See
|
||||
[migration/export](/photos/migration/export/) for more details.
|
||||
- Similarly, you can use Ente's
|
||||
[desktop app](https://ente.io/download/desktop) to export your data to a
|
||||
folder of your choice. The desktop app also supports "continuous" exports,
|
||||
where it will automatically export new items in the background without you
|
||||
needing to run any other cron jobs. See
|
||||
[migration/export](/photos/migration/export/) for more details.
|
||||
|
||||
## Does the exported data from Ente photos preserve the same folder and album structure as in the app?
|
||||
|
||||
|
||||
@@ -77,26 +77,35 @@ It's like cafe 😊. kaf-_ay_. en-_tay_.
|
||||
|
||||
## Does Ente apply compression to uploaded photos?
|
||||
|
||||
Ente does not apply compression to uploaded photos. The file size of your photos in Ente will be similar to the original file sizes you have.
|
||||
Ente does not apply compression to uploaded photos. The file size of your photos
|
||||
in Ente will be similar to the original file sizes you have.
|
||||
|
||||
## Can I add photos from a shared album to albums that I created in Ente?
|
||||
|
||||
Currently, Ente does not support adding photos from a shared album to your personal albums. If you want to include photos from a shared album in your own albums, you will need to ask the owner of the photos to add them to your album.
|
||||
Currently, Ente does not support adding photos from a shared album to your
|
||||
personal albums. If you want to include photos from a shared album in your own
|
||||
albums, you will need to ask the owner of the photos to add them to your album.
|
||||
|
||||
## How do I ensure that the Ente desktop app stays up to date on my system?
|
||||
|
||||
Ente desktop includes an auto-update feature, ensuring that whenever updates are deployed, the app will automatically download and install them. You don't need to manually update the software.
|
||||
Ente desktop includes an auto-update feature, ensuring that whenever updates are
|
||||
deployed, the app will automatically download and install them. You don't need
|
||||
to manually update the software.
|
||||
|
||||
## Can I sync a folder containing multiple subfolders, each representing an album?
|
||||
|
||||
Yes, when you drag and drop the folder onto the desktop app, the app will detect the multiple folders and prompt you to choose whether you want to create a single album or separate albums for each folder.
|
||||
Yes, when you drag and drop the folder onto the desktop app, the app will detect
|
||||
the multiple folders and prompt you to choose whether you want to create a
|
||||
single album or separate albums for each folder.
|
||||
|
||||
## What is the difference between **Magic** and **Content** search results on the desktop?
|
||||
|
||||
**Magic** is where you can search for long queries. Like, "baby in red dress", or "dog playing at the beach".
|
||||
**Magic** is where you can search for long queries. Like, "baby in red dress",
|
||||
or "dog playing at the beach".
|
||||
|
||||
**Content** is where you can search for single-words. Like, "car" or "pizza".
|
||||
|
||||
## How do I identify which files experienced upload issues within the desktop app?
|
||||
|
||||
Check the sections within the upload progress bar for "Failed Uploads," "Ignored Uploads," and "Unsuccessful Uploads."
|
||||
Check the sections within the upload progress bar for "Failed Uploads," "Ignored
|
||||
Uploads," and "Unsuccessful Uploads."
|
||||
|
||||
@@ -159,4 +159,6 @@ We do offer a generous free trial for you to experience the product.
|
||||
|
||||
## Will I need to pay for Ente Auth after my Ente Photos free plan expires?
|
||||
|
||||
No, you will not need to pay for Ente Auth after your Ente Photos free plan expires. Ente Auth is completely free to use, and the expiration of your Ente Photos free plan will not impact your ability to access or use Ente Auth.
|
||||
No, you will not need to pay for Ente Auth after your Ente Photos free plan
|
||||
expires. Ente Auth is completely free to use, and the expiration of your Ente
|
||||
Photos free plan will not impact your ability to access or use Ente Auth.
|
||||
|
||||
@@ -13,7 +13,7 @@ videos you have uploaded to Ente.
|
||||
|
||||

|
||||
|
||||
2. Open the side bar, and select the option to **export data**.
|
||||
2. Open the side bar, and select the option to **Export Data**.
|
||||
|
||||

|
||||
|
||||
@@ -33,7 +33,7 @@ videos you have uploaded to Ente.
|
||||
|
||||
</div>
|
||||
|
||||
5. Wait for the export to get completed.
|
||||
5. Wait for the export to complete.
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -42,7 +42,7 @@ videos you have uploaded to Ente.
|
||||
</div>
|
||||
|
||||
6. In case your download gets interrupted, Ente will resume from where it left
|
||||
off. Simply select **export data** again and click on **Resync**.
|
||||
off. Simply select **Export Data** again and click on **Resync**.
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -50,18 +50,20 @@ videos you have uploaded to Ente.
|
||||
|
||||
</div>
|
||||
|
||||
7. **Sync continuously** : You can utilize Continuous Sync to eliminate manual
|
||||
exports each time new photos are added to Ente. This feature automatically
|
||||
detects new files and runs exports accordingly, It also ensures that exported
|
||||
data reflects the latest album states with new files, moves, and deletions.
|
||||
### Sync continuously
|
||||
|
||||

|
||||
You can switch on the toggle to **Sync continuously** to eliminate manual
|
||||
exports each time new photos are added to Ente. This feature automatically
|
||||
detects new files and runs exports accordingly. It also ensures that exported
|
||||
data reflects the latest album states with new files, moves, and deletions.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
If you run into any issues during your data export, please reach out to
|
||||
[support@ente.io](mailto:support@ente.io) and we will be happy to help you!
|
||||
|
||||
Note that we also provide a [CLI
|
||||
tool](https://github.com/ente-io/ente/tree/main/cli#export) to export your data.
|
||||
Some more details are in this [FAQ entry](/photos/faq/export).
|
||||
Please find more details [here](/photos/faq/export).
|
||||
|
||||
@@ -24,18 +24,32 @@ and subsequently increase the
|
||||
[storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md)
|
||||
using the CLI.
|
||||
|
||||
For the admin actions, you can create `server/museum.yaml`, and whitelist add
|
||||
the admin userID `internal.admins`. See
|
||||
[local.yaml](https://github.com/ente-io/ente/blob/main/server/configurations/local.yaml#L211C1-L232C1)
|
||||
For security purposes, we need to whitelist the user IDs that can perform admin
|
||||
actions on the server. To do this,
|
||||
|
||||
- Create a `museum.yaml` in the directory where you're starting museum from.
|
||||
For example, if you're running using `docker compose up`, then this file
|
||||
should be in the same directory as `compose.yaml` (generally,
|
||||
`server/museum.yaml`).
|
||||
|
||||
> Docker might've created an empty `museum.yaml` _directory_ on your machine
|
||||
> previously. If so, delete that empty directory and create a new file named
|
||||
> `museum.yaml`.
|
||||
|
||||
- In this `museum.yaml` we can add overrides over the default configuration.
|
||||
|
||||
For whitelisting the admin userIDs we need to define an `internal.admins`. See
|
||||
the "internal" section in
|
||||
[local.yaml](https://github.com/ente-io/ente/blob/main/server/configurations/local.yaml)
|
||||
in the server source code for details about how to define this.
|
||||
|
||||
```yaml
|
||||
....
|
||||
internal:
|
||||
admins:
|
||||
# - 1580559962386440
|
||||
Here is an example. Suppose we wanted to whitelist a user with ID
|
||||
`1580559962386440`, we can create the following `museum.yaml`
|
||||
|
||||
....
|
||||
```yaml
|
||||
internal:
|
||||
admins:
|
||||
- 1580559962386440
|
||||
```
|
||||
|
||||
You can use
|
||||
|
||||
@@ -26,8 +26,8 @@ docker compose up --build
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> You can also use a pre-built Docker image from `ghcr.io/ente-io/server` ([More
|
||||
> info](https://github.com/ente-io/ente/blob/main/server/docs/docker.md))
|
||||
> You can also use a pre-built Docker image from `ghcr.io/ente-io/server`
|
||||
> ([More info](https://github.com/ente-io/ente/blob/main/server/docs/docker.md))
|
||||
|
||||
Then in a separate terminal, you can run (e.g) the web client
|
||||
|
||||
|
||||
13
docs/docs/self-hosting/troubleshooting/uploads.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Uploads failing
|
||||
description: Fixing upload errors when trying to self host Ente
|
||||
---
|
||||
|
||||
# Uploads failing
|
||||
|
||||
If uploads to your self-hosted server are failing, make sure that
|
||||
`credentials.yaml` has `yourserverip:3200` for all three minio locations.
|
||||
|
||||
By default it is `localhost:3200`, and it needs to be changed to an IP that is
|
||||
accessible from both where you are running the Ente clients (e.g. the mobile
|
||||
app) and also from within the Docker compose cluster.
|
||||
54
infra/services/listmonk/README.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Listmonk
|
||||
|
||||
We use [Listmonk](https://listmonk.app/) to manage our mailing lists.
|
||||
|
||||
- Museum lets Listmonk know about new users and account deletion (this allows
|
||||
Listmonk to create corresponding accounts).
|
||||
|
||||
- Subsequently, Listmonk handles user subscription / unsubscription etc
|
||||
(Listmonk stores its data in an external Postgres).
|
||||
|
||||
## Installing
|
||||
|
||||
Install [nginx](../nginx/README.md).
|
||||
|
||||
Add Listmonk's configuration.
|
||||
|
||||
```sh
|
||||
sudo mkdir -p /root/listmonk
|
||||
sudo tee /root/listmonk/config.toml
|
||||
```
|
||||
|
||||
Add the service definition and nginx configuration.
|
||||
|
||||
```sh
|
||||
scp services/listmonk/listmonk.* <instance>:
|
||||
|
||||
sudo mv listmonk.service /etc/systemd/system/
|
||||
sudo mv listmonk.nginx.conf /root/nginx/conf.d
|
||||
```
|
||||
|
||||
> The very first time we ran Listmonk, at this point we also needed to get it to
|
||||
> install the tables it needs in the Postgres DB. For this, we used the
|
||||
> `initialize-db.sh` script.
|
||||
>
|
||||
> ```sh
|
||||
> scp services/listmonk/initialize-db.sh <instance>:
|
||||
>
|
||||
> sudo sh initialize-db.sh
|
||||
> rm initialize-db.sh
|
||||
> ```
|
||||
|
||||
Tell systemd to pick up new service definitions, enable the unit (so that it
|
||||
automatically starts on boot), and start it this time around.
|
||||
|
||||
```sh
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now listmonk
|
||||
```
|
||||
|
||||
Tell nginx to pick up the new configuration.
|
||||
|
||||
```sh
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
14
infra/services/listmonk/initialize-db.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This script needs to be manually run the once (and only once) before starting
|
||||
# Listmonk for the first time. It uses the provided credentials to initialize
|
||||
# its database.
|
||||
|
||||
set -o errexit
|
||||
set -o xtrace
|
||||
|
||||
docker pull listmonk/listmonk
|
||||
|
||||
docker run -it --rm --name listmonk \
|
||||
-v /root/listmonk/config.toml:/listmonk/config.toml:ro \
|
||||
listmonk/listmonk ./listmonk --install
|
||||
26
infra/services/listmonk/listmonk.nginx.conf
Normal file
@@ -0,0 +1,26 @@
|
||||
# This file gets loaded in a top level http block by the default nginx.conf
|
||||
# See infra/services/nginx/README.md for more details.
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
ssl_certificate /etc/ssl/certs/cert.pem;
|
||||
ssl_certificate_key /etc/ssl/private/key.pem;
|
||||
|
||||
server_name lists.ente.io;
|
||||
|
||||
location / {
|
||||
proxy_pass http://host.docker.internal:9000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Use HTTP/1.1 when talking to upstream
|
||||
# Also, while not necessary (AFAIK), also allow websockets.
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
19
infra/services/listmonk/listmonk.service
Normal file
@@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Documentation=https://listmonk.app/docs/installation/
|
||||
Requires=docker.service
|
||||
After=docker.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Service]
|
||||
ExecStartPre=docker pull listmonk/listmonk
|
||||
ExecStartPre=-docker stop listmonk
|
||||
ExecStartPre=-docker rm listmonk
|
||||
ExecStartPre=-docker run --rm --name listmonk \
|
||||
-v /root/listmonk/config.toml:/listmonk/config.toml:ro \
|
||||
listmonk/listmonk ./listmonk --upgrade --yes
|
||||
ExecStart=docker run --name listmonk \
|
||||
-p 9000:9000 \
|
||||
-v /root/listmonk/config.toml:/listmonk/config.toml:ro \
|
||||
listmonk/listmonk
|
||||
@@ -62,3 +62,12 @@ We can see this in the default configuration of nginx:
|
||||
This is a [handy tool](https://nginx-playground.wizardzines.com) to check the
|
||||
syntax of the configuration files. Alternatively, you can run `docker exec nginx
|
||||
nginx -t` on the instance to ask nginx to check the configuration.
|
||||
|
||||
## Updating configuration
|
||||
|
||||
Nginx configuration files can be changed without needing to restart anything.
|
||||
|
||||
1. Update the configuration file at `/root/nginx/conf.d/museum.conf`
|
||||
2. Verify that there are no errors in the configuration by using `sudo docker
|
||||
exec nginx nginx -t`.
|
||||
3. Ask nginx to reload the configuration `sudo systemctl reload nginx`.
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
# See infra/services/nginx/README.md for more details.
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
ssl_certificate /etc/ssl/certs/cert.pem;
|
||||
ssl_certificate_key /etc/ssl/private/key.pem;
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
ente ist eine einfache App, um Ihre Fotos und Videos automatisch zu sichern und zu organisieren.
|
||||
|
||||
Wenn Sie auf der Suche nach einer datenschutzfreundlichen Alternative zu Google Fotos sind, sind Sie an der richtigen Stelle. Mit Ente werden Ihre Fotos Ende-zu-Ende-verschlüsselt gespeichert (e2ee). Dies bedeutet, dass nur Sie sie sehen können.
|
||||
|
||||
Ihre Fotos werden verschlüsselt (e2ee) zwischen allen Geräten synchronisiert.
|
||||
|
||||
ente ermöglicht es, deine Alben simpel & schnell mit deinen Geliebten zu teilen. Sie können öffentlich einsehbare Links teilen, sodass andere sogar ohne einen Account oder eine App Ihr Album sehen und darin zusammenarbeiten können, indem sie Fotos hinzufügen.
|
||||
|
||||
Ihre verschlüsselten Daten werden an 3 verschiedenen Orten gespeichert, unter anderem in einem Schutzbunker in Paris. Wir nehmen die Erhaltung der Nachwelt ernst und machen es Ihnen leicht, dafür zu sorgen, dass Ihre Erinnerungen Sie überdauern.
|
||||
|
||||
Wir sind hier, um die sicherste Foto-App aller Zeiten zu entwickeln, begleite uns auf unserem Weg!
|
||||
|
||||
FEATURES
|
||||
- Sicherungen in Originalqualität, weil jeder Pixel zählt
|
||||
- Familien-Abos, damit Sie den Speicherplatz mit Ihrer Familie teilen können
|
||||
- Kollaborative Alben, sodass Sie nach einer Reise Fotos sammeln können
|
||||
- Geteilte Ordner für den Fall, dass Ihr Partner Ihre "Kamera" Klicks genießen soll
|
||||
- Album-Links, die mit einem Passwort geschützt werden können
|
||||
- Möglichkeit, Speicherplatz freizugeben, indem bereits gesicherte Daten auf dem Gerät entfernt werden
|
||||
- Menschlicher Support, denn Sie sind es wert
|
||||
- Beschreibungen, damit Sie Ihre Erinnerungen beschriften und leicht wiederfinden können
|
||||
- Foto-Editor, um Ihren Fotos den Feinschliff zu verpassen
|
||||
- Favorisieren, verstecken und erleben Sie Ihre Erinnerungen, denn sie sind kostbar
|
||||
- Ein-Klick-Import von Google, Apple, Ihrer Festplatte und mehr
|
||||
- Dunkles Theme, weil Ihre Fotos darin gut aussehen
|
||||
- 2FA, 3FA, biometrische Authentifizierung
|
||||
- und noch VIELES mehr!
|
||||
|
||||
BERECHTIGUNGEN
|
||||
Diese können unter folgendem Link überprüft werden: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md
|
||||
|
||||
PREIS
|
||||
Wir bieten keine lebenslang kostenlosen Abonnements an, da es für uns wichtig ist, einen nachhaltigen Service anzubieten. Wir bieten jedoch bezahlbare Abonemments an, welche auch mit der Familie geteilt werden können. Mehr Informationen sind auf ente.io zu finden.
|
||||
|
||||
SUPPORT
|
||||
Wir sind stolz darauf, einen persönlichen Support anzubieten. Falls Sie ein Abonnement besitzen, können Sie sich mit Ihrem Anliegen via E-Mail an team@ente.io wenden und erhalten eine Antwort innerhalb von 24 Stunden.
|
||||
@@ -0,0 +1 @@
|
||||
ente ist eine Ende-zu-Ende-verschlüsselte Fotospeicher-App
|
||||
1
mobile/android/app/src/main/play/listings/de/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
ente - verschlüsselter Fotospeicher
|
||||
@@ -0,0 +1,36 @@
|
||||
Ente is a simple app to backup and share your photos and videos.
|
||||
|
||||
If you've been looking for a privacy-friendly alternative to Google Photos, you've come to the right place. With Ente, they are stored end-to-end encrypted (e2ee). This means that only you can view them.
|
||||
|
||||
We have open-source apps across Android, iOS, web and desktop, and your photos will seamlessly sync between all of them in an end-to-end encrypted (e2ee) manner.
|
||||
|
||||
Ente also makes it simple to share your albums with your loved ones, even if they aren't on Ente. You can share publicly viewable links, where they can view your album and collaborate by adding photos to it, even without an account or app.
|
||||
|
||||
Your encrypted data is replicated to 3 different locations, including a fall-out shelter in Paris. We take posterity seriously and make it easy to ensure that your memories outlive you.
|
||||
|
||||
We are here to make the safest photos app ever, come join our journey!
|
||||
|
||||
FEATURES
|
||||
- Original quality backups, because every pixel is important
|
||||
- Family plans, so you can share storage with your family
|
||||
- Collaborative albums, so you can pool together photos after a trip
|
||||
- Shared folders, in case you want your partner to enjoy your "Camera" clicks
|
||||
- Album links, that can be protected with a password
|
||||
- Ability to free up space, by removing files that have been safely backed up
|
||||
- Human support, because you're worth it
|
||||
- Descriptions, so you can caption your memories and find them easily
|
||||
- Image editor, to add finishing touches
|
||||
- Favorite, hide and relive your memories, for they are precious
|
||||
- One-click import from Google, Apple, your hard drive and more
|
||||
- Dark theme, because your photos look good in it
|
||||
- 2FA, 3FA, biometric auth
|
||||
- and a LOT more!
|
||||
|
||||
PERMISSIONS
|
||||
Ente requests for certain permissions to serve the purpose of a photo storage provider, which can be reviewed here: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md
|
||||
|
||||
PRICING
|
||||
We don't offer forever free plans, because it is important to us that we remain sustainable and withstand the test of time. Instead we offer affordable plans that you can freely share with your family. You can find more information at ente.io.
|
||||
|
||||
SUPPORT
|
||||
We take pride in offering human support. If you are our paid customer, you can reach out to team@ente.io and expect a response from our team within 24 hours.
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 690 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 853 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1 @@
|
||||
Ente Photos is an open source photos app, that provides end-to-end encrypted backups for your photos and videos.
|
||||
@@ -0,0 +1 @@
|
||||
Ente Photos - Open source, end-to-end encrypted alternative to Google Photos
|
||||
@@ -0,0 +1,36 @@
|
||||
ente es una aplicación simple para hacer copias de seguridad y compartir tus fotos y videos.
|
||||
|
||||
Si has estado buscando una alternativa a Google Photos que sea amigable con la privacidad, has llegado al lugar correcto. Con Ente, se almacenan cifradas de extremo a extremo (e2ee). Esto significa que solo tú puedes verlas.
|
||||
|
||||
Tenemos aplicaciones en Android, iOS, web y escritorio, y tus fotos se sincronizarán perfectamente entre todos tus dispositivos encriptadas de extremo a extremo (e2ee).
|
||||
|
||||
ente también hace fácil compartir tus álbumes con tus seres queridos, incluso si no están en ente. Puedes compartir enlaces visibles públicamente, donde pueden ver tu álbum y colaborar añadiendo fotos a él, incluso sin una cuenta o aplicación.
|
||||
|
||||
Sus datos cifrados se replican en 3 ubicaciones diferentes, incluyendo un bunker en París. Nos tomamos la posteridad en serio y facilitamos que sus recuerdos sobrevivan a usted.
|
||||
|
||||
Estamos aquí para hacer la aplicación de fotos más segura jamás creada, ¡únete a nuestro viaje!
|
||||
|
||||
CARACTERÍSTICAS
|
||||
- Copias de seguridad con la calidad original, porque cada pixel es importante
|
||||
- Planes familiares, para que puedas compartir el almacenamiento con tu familia
|
||||
- Álbumes colaborativos, para que puedas juntar fotos después de un viaje
|
||||
- Carpetas compartidas, por si quieres que tu pareja disfrute de tus fotos
|
||||
- Enlaces al álbum, que se pueden proteger con una contraseña
|
||||
- Capacidad para liberar espacio, eliminando archivos de los que ya tienes una copia de seguridad
|
||||
- Apoyo humano, porque tú lo vales
|
||||
- Descripciones, para que puedas encontrar tus recuerdos fácilmente
|
||||
- Editor de imagen, para añadir retoques finales
|
||||
- Marca como favoritos, oculta y revive tus recuerdos, porque son preciosos
|
||||
- Importa en un click desde Google, Apple, tu disco duro y más
|
||||
- Tema oscuro, porque tus fotos quedan bien con él
|
||||
- 2FA, 3FA, autenticación biométrica
|
||||
- ¡Y mucho más!
|
||||
|
||||
PERMISOS
|
||||
ente solicita ciertos permisos para servir al propósito de un proveedor de almacenamiento de fotos, que puede ser revisado aquí: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md
|
||||
|
||||
PRECIOS
|
||||
No ofrecemos planes gratis para siempre, porque es importante para nosotros seguir siendo sostenibles y resistir a la prueba del tiempo. En su lugar, ofrecemos planes asequibles que puedes compartir libremente con tu familia. Puedes encontrar más información en ente.io.
|
||||
|
||||
SOPORTE
|
||||
Estamos orgullosos de ofrecer apoyo humano. Si eres un cliente de pago, puedes contactar con team@ente.io y esperar una respuesta de nuestro equipo en 24 horas.
|
||||
@@ -0,0 +1 @@
|
||||
ente es una aplicación de almacenamiento de fotos cifrado de extremo a extremo
|
||||
1
mobile/android/app/src/main/play/listings/es/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
ente - almacenamiento de fotos encriptado
|
||||
@@ -0,0 +1,36 @@
|
||||
entre est une application simple qui permet de sauvegarder et partager vos photos et vidéos.
|
||||
|
||||
Si vous êtes à la recherche d'une alternative à Google Photos respectueuse de la vie privée, vous êtes au bon endroit. Avec ente, ils sont stockés chiffrés de bout-en-bout (e2ee). Cela signifie que vous-seul pouvez les voir.
|
||||
|
||||
Nous avons des applications sur Android, iOS, Web et Ordinateur, et vos photos seront synchronisées de manière transparente entre tous vos appareils chiffrée de bout en bout (e2ee).
|
||||
|
||||
ente vous permet également de partager vos albums avec vos proches, même s'ils ne sont pas sur ente. Vous pouvez partager des liens visibles publiquement, où ils peuvent voir votre album et collaborer en y ajoutant des photos, même sans compte ou application.
|
||||
|
||||
Vos données chiffrées sont répliqué à 3 endroits différents, dont un abri antiatomique à Paris. Nous prenons la postérité au sérieux et facilitons la conservation de vos souvenirs.
|
||||
|
||||
Nous sommes là pour faire l'application photo la plus sûre de tous les temps, rejoignez-nous !
|
||||
|
||||
CARACTÉRISTIQUES
|
||||
- Sauvegardes de qualité originales, car chaque pixel est important
|
||||
- Abonnement familiaux, pour que vous puissiez partager l'espace de stockage avec votre famille
|
||||
- Albums collaboratifs, pour que vous puissiez regrouper des photos après un voyage
|
||||
- Dossiers partagés, si vous voulez que votre partenaire profite de vos clichés
|
||||
- Liens ves les albums qui peuvent être protégés par un mot de passe
|
||||
- Possibilité de libérer de l'espace en supprimant les fichiers qui ont été sauvegardés en toute sécurité
|
||||
- Support humain, car vous en valez la peine
|
||||
- Descriptions, afin que vous puissiez légender vos souvenirs et les retrouver facilement
|
||||
- Éditeur d'images, pour ajouter des touches de finition
|
||||
- Favoriser, cacher et revivre vos souvenirs, car ils sont précieux
|
||||
- Importation en un clic depuis Google, Apple, votre disque dur et plus encore
|
||||
- Thème sombre, parce que vos photos y sont jolies
|
||||
- 2FA, 3FA, authentification biométrique
|
||||
- et beaucoup de choses encore !
|
||||
|
||||
PERMISSIONS
|
||||
ente sollicite diverses autorisations dans le but de fonctionner en tant que service de stockage de photos, et ces autorisations sont détaillées ici : https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md
|
||||
|
||||
PRIX
|
||||
Nous ne proposons pas d'abonnement gratuits pour toujours, car il est important pour nous de rester durables et de résister à l'épreuve du temps. Au lieu de cela, nous vous proposons des abonnements abordables que vous pouvez partager librement avec votre famille. Vous pouvez trouver plus d'informations sur ente.io.
|
||||
|
||||
ASSISTANCE
|
||||
Nous sommes fiers d'offrir un support humain. Si vous êtes un abonné, vous pouvez contacter team@ente.io et vous recevrez une réponse de notre équipe dans les 24 heures.
|
||||
@@ -0,0 +1 @@
|
||||
ente est une application de stockage de photos chiffrées de bout en bout
|
||||
1
mobile/android/app/src/main/play/listings/fr/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
ente - stockage de photos chiffré
|
||||
@@ -0,0 +1,36 @@
|
||||
האפליקציה Ente היא אפליקציה פשוטה לגיבוי ושיתוף של התמונות והסרטונים שלך.
|
||||
|
||||
אם חיפשת אלטרנטיבה ידידותית לפרטיות לGoogle Photos, הגעת למקום הנכון. עם Ente, התמונות והסרטונים מאוחסנים בצורה מאובטחת באמצעות הצפנה קצה-אל-קצה (e2ee). זה אומר שרק אתה יכול לצפות בהם.
|
||||
|
||||
יש לנו אפלקציות קוד פתוח זמינות לAndroid, iOS, רשת ולמחשב, וכל התמונות שלך ייסתנכרנו באופן חלק בין כולם באופן מאובטח על ידי הצפנה קצה-אל-קצה (e2ee).
|
||||
|
||||
ente גם מקל על שיתוף האלבומים שלך עם קרובך, גם אם הם אינם ב-ente. תוכל לשתף קישורים שניתן לצפות בהם בצורה פומבית, שבאמצעותם יתאפשר להם לצפות באלבום שלך ולשתף פעולה על ידי הוספת תמונות אליו, גם בלי חשבון או האפליקציה.
|
||||
|
||||
הנתונים המוצפנים שלך מאוחסנים ב3 מקומות שונים, כולל מקלט גרעיני בפריז. אנחנו מתייחסים ברצינות לעתידות ומקלים עליך לוודא שזכרונותיך ישרדו אחרייך.
|
||||
|
||||
הגענו לכאן כדי ליצור את היישומון לתמונות המאובטח ביותר אי פעם, הצטרפו אלינו למסע!
|
||||
|
||||
מאפיינים
|
||||
- גיבויים באיכות המקורית, כי כל פיקסל חשוב
|
||||
- תוכניות משפחתיות, כך שתוכלו לשתף אחסון עם המשפחה שלכם
|
||||
- אלבומים משותפים, כך שתוכל לאגד יחד תמונות אחרי טיול
|
||||
- תיקיות משותפות, במקרה ותרצה שהבן זוג שלך יהנה מהקליקים של ה"מצלמה" שלך
|
||||
- קישורי אלבום, המאובטחים בעזרת סיסמא
|
||||
- יכולת לשחרר מקום, על ידי הסרת קבצים שכבר גובו באופן מאובטח
|
||||
- תמיכה אנושית, כי אתה שווה את זה
|
||||
- תיאורים, כך שתוכל לתאר את הזכרונות שלך ולמצוא אותם בקלות
|
||||
- עורך תמונות, להוסיף למראה הסופי
|
||||
- סמן כמועדפים, הסתר ולחזור על זכרונות שלך, כי הם יקרים ללבך
|
||||
- ייבוא בלחיצה אחת מ-Google, Apple, הכונן הקשיח שלך ועוד
|
||||
- ערכת נושא כהה, כי התמונות שלך נראות יפות בה
|
||||
- 2FA, 3FA, אימות ביומטרי
|
||||
- ועוד הרבה יותר!
|
||||
|
||||
הרשאות
|
||||
ente מבקש הרשאות מסוימות כדי לספק שירותי אחסון תמונות, וניתן לסקור אותן כאן: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md
|
||||
|
||||
מחיר
|
||||
אנחנו לא מציעים תוכניות בחינם לתמיד, משום שזה חשוב לנו להיות עמידים ולעמוד במבחן הזמן. במקום זאת אנחנו מציעים תוכניות במחיר סביר כדי שתוכל לשתף באופן חופשי עם המשפחה שלך. ניתן למצוא עוד מידע ב-ente.io.
|
||||
|
||||
תמיכה
|
||||
אנחנו גאים להציע תמיכה אנושית. אם אתה לקום משלם, אתה יכול לפנות אלינו בכתובת team@ente.io ולצפות לתשובה תוך 24 שעות.
|
||||
@@ -0,0 +1 @@
|
||||
ente הוא אפליקציה לאחסון תמונות המשתמשת בהצפנה קצה-אל-קצה
|
||||
1
mobile/android/app/src/main/play/listings/he/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
ente - אחסון תמונות באופן מוצפן
|
||||
@@ -0,0 +1,36 @@
|
||||
ente è una semplice app per il backup e la condivisione di foto e video.
|
||||
|
||||
Se siete alla ricerca di un'alternativa rispettosa della privacy a Google Photos, siete nel posto giusto. Con ente, sono memorizzati con crittografia end-to-end (e2ee). Questo significa che solo tu puoi vederli.
|
||||
|
||||
Abbiamo applicazioni open-source su Android, iOS, web e desktop, e le tue foto saranno sincronizzate tra tutti i dispositivi utilizzando la crittografia end-to-end (e2ee).
|
||||
|
||||
ente rende anche semplice condividere i tuoi album con i tuoi cari, anche se non sono utenti ente. Puoi condividere link visualizzabili pubblicamente, dove possono visualizzare il tuo album e collaborare aggiungendo le foto, anche senza un account o un'app installata.
|
||||
|
||||
I tuoi dati crittografati vengono replicati in 3 luoghi diversi, tra cui un rifugio antiatomico a Parigi. I tuoi ricordi continueranno a vivere anche quando non ci sarai più.
|
||||
|
||||
Siamo qui per creare l'app per la gestione di foto e video più sicura di sempre, unisciti al nostro viaggio!
|
||||
|
||||
CARATTERISTICHE
|
||||
- Backup di qualità originale, perché ogni pixel è importante
|
||||
- Piani famiglia, in modo da poter condividere lo spazio disponibile con la tua famiglia
|
||||
- Album collaborativi, per poter mettere insieme le foto dopo un viaggio
|
||||
- Cartelle condivise, nel caso in cui desideri condividere le tue foto subito con il tuo o la tua partner
|
||||
- Collegamenti di album, che possono essere anche protetti con una password
|
||||
- Possibilità di liberare spazio, rimuovendo i file che sono stati salvati in modo sicuro
|
||||
- Supporto umano, perché ne vale la pena
|
||||
- Descrizioni, in modo da poter descrivere i tuoi ricordi e trovarli facilmente
|
||||
- Editor di immagini, per ritocchi finali
|
||||
- Preferiti, nascondi e rivivi i tuoi ricordi, perché sono preziosi
|
||||
- Importa da Google, Apple o dal tuo hard disk con un semplice clic
|
||||
- Tema scuro, per valorizzare le tue foto
|
||||
- 2FA, 3FA, Autenticazione biometrica
|
||||
- e molto altro ancora!
|
||||
|
||||
PERMESSI
|
||||
ente richiede alcune autorizzazioni per servire lo scopo di un provider di storage fotografico, che può essere esaminato qui: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md
|
||||
|
||||
PREZZO
|
||||
Non offriamo piani gratuiti per sempre, perché per noi è importante rimanere sostenibili e resistere alla prova del tempo. Offriamo invece piani accessibili che si possono condividere liberamente con la propria famiglia. Puoi trovare maggiori informazioni su ente.io.
|
||||
|
||||
SUPPORTO
|
||||
Siamo orgogliosi di offrire supporto umano. Se sei un nostro cliente a pagamento, puoi contattare team@ente.io e aspettarti una risposta dal nostro team entro 24 ore.
|
||||
@@ -0,0 +1 @@
|
||||
ente è un'applicazione di archiviazione foto e video crittografata end-to-end
|
||||
1
mobile/android/app/src/main/play/listings/it/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
ente - archivio fotografico crittografato
|
||||
@@ -0,0 +1,36 @@
|
||||
ente is een eenvoudige app om jouw foto's en video's automatisch te back-uppen en delen.
|
||||
|
||||
Als je op zoek bent naar een privacy-vriendelijk alternatief voor Google Photos, dan ben je hier op de juiste plaats. Bij ente worden ze end-to-end encrypted (e2ee). Dit betekent dat alleen jij ze kunt bekijken.
|
||||
|
||||
We hebben open-source apps op Android, iOS, web en Desktop, en je foto's zullen naadloos synchroniseren tussen al je apparaten op een end-to-end versleutelde (e2ee) manier.
|
||||
|
||||
ente maakt het ook simpeler om album te delen met je dierbaren, zelfs als die ente niet gebruiken. Je kunt openbaar zichtbare links delen, waar anderen jouw album kunnen bekijken en er foto's aan toe kunnen voegen, zelfs zonder account of app.
|
||||
|
||||
Jouw versleutelde gegevens worden drievoudig opgeslagen op meerdere locaties, waaronder een kernbunker in Parijs. Wij nemen opslag voor de lange termijn serieus, en zorgen ervoor dat je herinneringen minstens je hele leven bewaard worden.
|
||||
|
||||
Ons doel is om de veiligste foto app ooit te maken, sluit je bij ons aan!
|
||||
|
||||
FUNCTIES
|
||||
- Backups van originele kwaliteit, omdat elke pixel belangrijk is
|
||||
- Familieplannen, zodat je de opslag kunt delen met je familie
|
||||
- Gezamenlijke albums, zodat je foto's kunt samenvoegen na een reis
|
||||
- Gedeelde mappen, voor het geval je jouw partner wilt laten meegenieten van jouw "Camera" klikjes
|
||||
- Album links, die met een wachtwoord beschermd kunnen worden
|
||||
- Mogelijkheid om ruimte vrij te maken op je apparaat, door bestanden die veilig zijn geback-upt te verwijderen
|
||||
- Menselijke klantenservice, omdat je het waard bent
|
||||
- Beschrijvingen, zodat je je herinneringen kunt bijhouden en ze gemakkelijk kunt vinden
|
||||
- Fotobewerker om de laatste finishing touches toe te voegen
|
||||
- Favorieten, verbergen en herleven van je herinneringen, want ze zijn kostbaar
|
||||
- Met één klik importeren vanuit Google, Apple, je harde schijf en meer
|
||||
- Donker thema, omdat je foto's er goed in uit zien
|
||||
- 2FA, 3FA, biometrische authenticatie
|
||||
- en nog veel meer!
|
||||
|
||||
TOESTEMMINGEN
|
||||
ente heeft bepaalde machtigingen nodig om uw foto's op te slaan, die hier bekeken kunnen worden: https://github.com/ente-io/ente/blob/f-droid/mobile/android/permissions.md
|
||||
|
||||
PRIJZEN
|
||||
We bieden geen oneindig gratis plannen aan, omdat het voor ons belangrijk is dat we duurzaam blijven en de tand des tijds weerstaan. In plaats daarvan bieden we betaalbare plannen aan die je vrij kunt delen met je familie. Je kunt meer informatie vinden op ente.io.
|
||||
|
||||
KLANTENSERVICE
|
||||
Wij zijn trots op het bieden van menselijke klantenservice. Als je een betaalde klant bent, kun je contact opnemen met team@ente.io en binnen 24 uur een antwoord van ons verwachten.
|
||||
@@ -0,0 +1 @@
|
||||
ente is een end-to-end versteutelde app voor foto opslag
|
||||
1
mobile/android/app/src/main/play/listings/nl/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
ente - versleutelde foto opslag
|
||||