Compare commits
484 Commits
auth-v4.3.
...
Edit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
109ac573c9 | ||
|
|
23559252e6 | ||
|
|
31b31b1a52 | ||
|
|
8333e2ad7b | ||
|
|
cb5c9f3170 | ||
|
|
7b2e6cb1bd | ||
|
|
d18d939489 | ||
|
|
b3376f27aa | ||
|
|
f238b55df3 | ||
|
|
d15a034869 | ||
|
|
7b3ae417e8 | ||
|
|
e322958b25 | ||
|
|
0d660f239f | ||
|
|
c4a50fc9fb | ||
|
|
8856ad1520 | ||
|
|
e8158ef45a | ||
|
|
4fa0bf76e8 | ||
|
|
92a9b34836 | ||
|
|
10d7162d6e | ||
|
|
2a1b8ae18e | ||
|
|
5abf2cb35e | ||
|
|
367170be95 | ||
|
|
4d7cfee60f | ||
|
|
29152d1f85 | ||
|
|
6b4ffa4822 | ||
|
|
2883f4bed6 | ||
|
|
c96275cdd1 | ||
|
|
9db8324ffd | ||
|
|
0c664b94b9 | ||
|
|
c087e419d5 | ||
|
|
5ba5cae5ef | ||
|
|
4ea211d923 | ||
|
|
8d8202adab | ||
|
|
267f93e41e | ||
|
|
260ec952b4 | ||
|
|
5e311c2813 | ||
|
|
1d3268916f | ||
|
|
73192cd0fd | ||
|
|
9c886b3fa3 | ||
|
|
017832f11e | ||
|
|
67e76bc42f | ||
|
|
9a6579c55c | ||
|
|
17c0cdef14 | ||
|
|
dd0cc71f36 | ||
|
|
21fd6ab463 | ||
|
|
6e2142c605 | ||
|
|
16338682ed | ||
|
|
a7e8d3dfa6 | ||
|
|
6e9014b915 | ||
|
|
b5e7a3f83f | ||
|
|
d8d76f452d | ||
|
|
c2e475c666 | ||
|
|
9a4bc898f0 | ||
|
|
ca92aa8c62 | ||
|
|
56c6d7ed3c | ||
|
|
6ee4bce676 | ||
|
|
ff3f01af56 | ||
|
|
b5ba81e22b | ||
|
|
d5aab7c6df | ||
|
|
2749457611 | ||
|
|
883b14e96a | ||
|
|
59d7e0acac | ||
|
|
68ac3503ed | ||
|
|
58649db181 | ||
|
|
92ca4eeb15 | ||
|
|
d3e06e6cc9 | ||
|
|
3cef3e9bdc | ||
|
|
d318952feb | ||
|
|
6d8051dfa0 | ||
|
|
6acb9cf23f | ||
|
|
87e5457eb0 | ||
|
|
5ee23118ff | ||
|
|
d198f0c273 | ||
|
|
a88249de09 | ||
|
|
89ded523f8 | ||
|
|
f132a1359f | ||
|
|
48d9d03b32 | ||
|
|
11aba9df96 | ||
|
|
2c0fb5e584 | ||
|
|
69c6adcd06 | ||
|
|
e6c72baef7 | ||
|
|
83853e579f | ||
|
|
02652d3cfa | ||
|
|
fe60dbbb08 | ||
|
|
a1842be6e1 | ||
|
|
5f644ae96d | ||
|
|
3f5043a104 | ||
|
|
df55492984 | ||
|
|
b73171a329 | ||
|
|
b0b02e2ffe | ||
|
|
7b79a42cc9 | ||
|
|
54d1363b58 | ||
|
|
8a9afc40a8 | ||
|
|
958f569969 | ||
|
|
a64214ae15 | ||
|
|
69e8ba6743 | ||
|
|
0b73c92ee6 | ||
|
|
196e601929 | ||
|
|
6252b2c267 | ||
|
|
9f462f90ab | ||
|
|
0e19f5d8b3 | ||
|
|
3ff77ee9c0 | ||
|
|
65c2eda941 | ||
|
|
f6a2deb763 | ||
|
|
08ee4c1351 | ||
|
|
c713e1c22b | ||
|
|
c76a7c75ea | ||
|
|
a56a086dc4 | ||
|
|
c1903c7016 | ||
|
|
4dfadc535f | ||
|
|
8e01a5038e | ||
|
|
05a42efb1b | ||
|
|
3d924ab514 | ||
|
|
ae34a4c41a | ||
|
|
6bc9230dc8 | ||
|
|
93186421b1 | ||
|
|
8dce58713f | ||
|
|
7b391ba08f | ||
|
|
199df72cf6 | ||
|
|
59e998f5be | ||
|
|
bf3373697f | ||
|
|
509955f8c1 | ||
|
|
62279ce72f | ||
|
|
0c80c88548 | ||
|
|
ce3b980e27 | ||
|
|
7b25e65da4 | ||
|
|
3510c01e6e | ||
|
|
d20a8495d8 | ||
|
|
b8cf6012bd | ||
|
|
70dc4db1c5 | ||
|
|
1fb30ceafd | ||
|
|
38ec62a23b | ||
|
|
0a3abb20a1 | ||
|
|
9f9288a5c0 | ||
|
|
d047e05bc8 | ||
|
|
e939b06339 | ||
|
|
2eaeb759c5 | ||
|
|
2f2346286d | ||
|
|
8ed1d34301 | ||
|
|
e38152051c | ||
|
|
100c1d3803 | ||
|
|
7cc3ab1004 | ||
|
|
0c86c53a96 | ||
|
|
130e751072 | ||
|
|
408cc05f7c | ||
|
|
9f70aab9b5 | ||
|
|
39f63b6339 | ||
|
|
81e3c41749 | ||
|
|
831563317e | ||
|
|
a3c43cb54e | ||
|
|
83373c4424 | ||
|
|
ad47dda614 | ||
|
|
4466136776 | ||
|
|
bc874a2292 | ||
|
|
e52816feb1 | ||
|
|
3a34fa4257 | ||
|
|
216a3e3e10 | ||
|
|
c5f02a0116 | ||
|
|
7975de0a9a | ||
|
|
bba262e164 | ||
|
|
03a16119b9 | ||
|
|
2e657d88f4 | ||
|
|
ede5e0be90 | ||
|
|
e6981a8c47 | ||
|
|
0883ed39e3 | ||
|
|
223961bf78 | ||
|
|
f50b3743f5 | ||
|
|
10a7c1172b | ||
|
|
da60436e91 | ||
|
|
9405d549c7 | ||
|
|
47ee46b440 | ||
|
|
a9d9173364 | ||
|
|
088ebdb7b5 | ||
|
|
7a85fb2e72 | ||
|
|
c63ae6fc1f | ||
|
|
8bf9607bb8 | ||
|
|
dabae19cf2 | ||
|
|
da930976ef | ||
|
|
0c57ae3b58 | ||
|
|
543f4c43b3 | ||
|
|
5a8f8b8449 | ||
|
|
64363b70e3 | ||
|
|
c84b6f6824 | ||
|
|
fb6751a439 | ||
|
|
802dd21200 | ||
|
|
782008e5d3 | ||
|
|
94de25cb26 | ||
|
|
b1efd289d3 | ||
|
|
1e1b3e9d74 | ||
|
|
ba0bf3dd5b | ||
|
|
a9a2e89e49 | ||
|
|
cc1240b43c | ||
|
|
06830c3881 | ||
|
|
918a7bad68 | ||
|
|
356f98bf52 | ||
|
|
2d3734bf14 | ||
|
|
73a8d4dcda | ||
|
|
f9e25ed14d | ||
|
|
acede69f5b | ||
|
|
0c46aa338e | ||
|
|
de42700914 | ||
|
|
8a2d4a4eee | ||
|
|
5d0ae9edb6 | ||
|
|
dda7b2a28e | ||
|
|
7735d938a5 | ||
|
|
adfe701016 | ||
|
|
76c7d22754 | ||
|
|
54aab6738e | ||
|
|
825dd79795 | ||
|
|
ef5dc18442 | ||
|
|
4521943fb1 | ||
|
|
dc82c24674 | ||
|
|
6c6d524b15 | ||
|
|
5341049bdf | ||
|
|
3f58bbf9bc | ||
|
|
880cba335f | ||
|
|
cb321f49bd | ||
|
|
15b02c59cc | ||
|
|
727a47cf34 | ||
|
|
718dbae521 | ||
|
|
2ce4e8e955 | ||
|
|
df858338bc | ||
|
|
43931b852f | ||
|
|
0db4332a02 | ||
|
|
c3d121e4ac | ||
|
|
b8476769d6 | ||
|
|
aeb3142d23 | ||
|
|
8bb5b9406d | ||
|
|
da1e7788f9 | ||
|
|
7098e93ae8 | ||
|
|
1a71513723 | ||
|
|
331675091a | ||
|
|
106338508d | ||
|
|
500a9481cb | ||
|
|
e4771320b1 | ||
|
|
39e0f34b2d | ||
|
|
9ce9fa2dbf | ||
|
|
6b8800f151 | ||
|
|
d95864be1c | ||
|
|
b01f6d9482 | ||
|
|
5bf3f01de6 | ||
|
|
4fcd938575 | ||
|
|
500cb9d0f2 | ||
|
|
34233875bd | ||
|
|
8871902594 | ||
|
|
912d52ea6b | ||
|
|
27f635dfaa | ||
|
|
7ff6785860 | ||
|
|
d6665b1dbf | ||
|
|
1cbc783bc6 | ||
|
|
e6b446c95f | ||
|
|
480e8682f9 | ||
|
|
bb997039c8 | ||
|
|
a2debd6746 | ||
|
|
f454221634 | ||
|
|
6614e4468d | ||
|
|
8c0cbc7343 | ||
|
|
22f05f73a9 | ||
|
|
d53d5090e0 | ||
|
|
64afcc0c70 | ||
|
|
d904aab804 | ||
|
|
1d8aaa49e7 | ||
|
|
39509813c6 | ||
|
|
f362943ab6 | ||
|
|
976eee005c | ||
|
|
9b15ab2f2f | ||
|
|
31f6671626 | ||
|
|
c32e4be8be | ||
|
|
6ae9003585 | ||
|
|
851aed6a78 | ||
|
|
7732f9eee9 | ||
|
|
06099f00c6 | ||
|
|
8e0b0da68f | ||
|
|
55dbc3a8db | ||
|
|
f6744d4b47 | ||
|
|
fbf626b578 | ||
|
|
9508695bba | ||
|
|
645014460b | ||
|
|
e32af8e0e5 | ||
|
|
6e2f645905 | ||
|
|
5e091af787 | ||
|
|
f9dbbb8cc9 | ||
|
|
1fc72383a3 | ||
|
|
c040ae9dcc | ||
|
|
f70148d652 | ||
|
|
60f94362d2 | ||
|
|
a9bf825dde | ||
|
|
004525ddeb | ||
|
|
2ff03d7303 | ||
|
|
fcaf46fcd1 | ||
|
|
d8c50ce3fa | ||
|
|
15ed5e9d7b | ||
|
|
ef6e4ebbcd | ||
|
|
60b3e0977e | ||
|
|
f183c56c20 | ||
|
|
01e9d79a22 | ||
|
|
ff22c69ca6 | ||
|
|
016b031bf1 | ||
|
|
c7a2001405 | ||
|
|
3871a538ab | ||
|
|
b52ac3ff5d | ||
|
|
be33ee5a1c | ||
|
|
8df7c1b9a4 | ||
|
|
e8997c16a6 | ||
|
|
141d761ecb | ||
|
|
fe5feb0394 | ||
|
|
7ec0c6dbdb | ||
|
|
be84e1856d | ||
|
|
9808ea5d8e | ||
|
|
2577b9c93a | ||
|
|
0981ba5989 | ||
|
|
c2959d06b0 | ||
|
|
eed42c9df5 | ||
|
|
ec30ace822 | ||
|
|
7fa9e2a627 | ||
|
|
ac0c96ae29 | ||
|
|
9900c346b5 | ||
|
|
2108461450 | ||
|
|
270dd02e20 | ||
|
|
e6deea1533 | ||
|
|
d303a40cc7 | ||
|
|
08d435b920 | ||
|
|
efa4c46f6e | ||
|
|
3cd5127488 | ||
|
|
e77a8cdf9b | ||
|
|
77e4506d2a | ||
|
|
c170384607 | ||
|
|
ce7a564cbd | ||
|
|
0d6f71c193 | ||
|
|
ab04bd66a5 | ||
|
|
9f3c4c8542 | ||
|
|
879f16a2dd | ||
|
|
136f8d17cc | ||
|
|
4539acd239 | ||
|
|
4d37e415e7 | ||
|
|
361283f072 | ||
|
|
3b4f9ecc22 | ||
|
|
d1289bb467 | ||
|
|
b81098f88d | ||
|
|
432883685d | ||
|
|
55094b7f2a | ||
|
|
5c9d6610c1 | ||
|
|
da1ac0696b | ||
|
|
c61667290b | ||
|
|
61e306e1b3 | ||
|
|
da565172fc | ||
|
|
c686c75141 | ||
|
|
d8617cb782 | ||
|
|
7a12f6edde | ||
|
|
f0c489587f | ||
|
|
b9a81c3693 | ||
|
|
f143add013 | ||
|
|
7d71a0c9a4 | ||
|
|
fb5bd0bdec | ||
|
|
adbaba8a44 | ||
|
|
01d0915004 | ||
|
|
332e759e6a | ||
|
|
a1557e8d27 | ||
|
|
8d667333e3 | ||
|
|
2843cc36d9 | ||
|
|
1019047eb2 | ||
|
|
42a085221c | ||
|
|
e08b228d05 | ||
|
|
3eee5a5fdc | ||
|
|
97c03a4985 | ||
|
|
f3974cdb8a | ||
|
|
dc402b7bca | ||
|
|
5082343708 | ||
|
|
4e34ecd580 | ||
|
|
fb897d237d | ||
|
|
b6a1a77bf7 | ||
|
|
168ef20e0f | ||
|
|
d880255fc8 | ||
|
|
1b1c33977d | ||
|
|
07f89bb1d6 | ||
|
|
47b0d51f22 | ||
|
|
5e489843fa | ||
|
|
5dea3fd8b0 | ||
|
|
8dd9dc16ad | ||
|
|
d31db6d678 | ||
|
|
a928e87747 | ||
|
|
064092a3e6 | ||
|
|
a76561ebe9 | ||
|
|
6321f50e6c | ||
|
|
93dd0c4943 | ||
|
|
83fdda46a3 | ||
|
|
23943aae89 | ||
|
|
f01d0ff274 | ||
|
|
d158db9499 | ||
|
|
9186b272b6 | ||
|
|
60f1172033 | ||
|
|
5843aee3d6 | ||
|
|
f6b186a167 | ||
|
|
aa9096134d | ||
|
|
1370f0523c | ||
|
|
c1051b8a10 | ||
|
|
b4d532bb41 | ||
|
|
4327fbb9e5 | ||
|
|
636d2a8069 | ||
|
|
21e0edcb85 | ||
|
|
8b11989e0f | ||
|
|
5bc6505cb8 | ||
|
|
a0184013f8 | ||
|
|
d0b5f84854 | ||
|
|
4bb2aea5d2 | ||
|
|
298faf8e0a | ||
|
|
e816504576 | ||
|
|
1506009a55 | ||
|
|
8930a0ddbc | ||
|
|
8611d5644d | ||
|
|
5df815da58 | ||
|
|
59e2906bdc | ||
|
|
79e8fffc7a | ||
|
|
bbd81a6385 | ||
|
|
1ba31e9442 | ||
|
|
7cf8ccdc7e | ||
|
|
ae6e2b1349 | ||
|
|
a65493192f | ||
|
|
cf538a713b | ||
|
|
3440bbd772 | ||
|
|
657a57f46a | ||
|
|
f6db2daaee | ||
|
|
2d8ffae74b | ||
|
|
1efaefbf9c | ||
|
|
29f5693078 | ||
|
|
94bd9f4dd6 | ||
|
|
ce9c08c607 | ||
|
|
a35d16e20d | ||
|
|
77a6508a0b | ||
|
|
347140c14c | ||
|
|
97bc768092 | ||
|
|
cdb81c621d | ||
|
|
bd7fec03d3 | ||
|
|
0c904d37c8 | ||
|
|
dc9f665029 | ||
|
|
4b0536a5b2 | ||
|
|
c2efd198a6 | ||
|
|
a2a74e2166 | ||
|
|
b0f8258a90 | ||
|
|
c75937759f | ||
|
|
466f31bbb9 | ||
|
|
09f6922ccf | ||
|
|
eacc364498 | ||
|
|
3c3ce516f5 | ||
|
|
7fe070b5ae | ||
|
|
b1fb5d548b | ||
|
|
13bcfe61ed | ||
|
|
4d3926c150 | ||
|
|
7d92b5923b | ||
|
|
864f0317fa | ||
|
|
a928676280 | ||
|
|
2073134e7a | ||
|
|
5a411d1d4d | ||
|
|
5f1d767b9c | ||
|
|
1ecff890f0 | ||
|
|
38aae47445 | ||
|
|
e4cd1434df | ||
|
|
f907beab62 | ||
|
|
f2e336c35a | ||
|
|
a8b2423d77 | ||
|
|
f8f2e6f7c7 | ||
|
|
e103d7490e | ||
|
|
f068d6ef24 | ||
|
|
3ec3f9f2e1 | ||
|
|
25c472e584 | ||
|
|
6c412e5803 | ||
|
|
8113a9aa97 | ||
|
|
51235bf81b | ||
|
|
4bd31aeea8 | ||
|
|
f2736c43c1 | ||
|
|
c6b4cba8b4 | ||
|
|
33f29cdb41 | ||
|
|
886cb06590 | ||
|
|
cd2094f75e | ||
|
|
2e3ac8b485 | ||
|
|
47f0c88ed8 | ||
|
|
162ce32b8e | ||
|
|
a1dbdfd6ba | ||
|
|
74072b952d | ||
|
|
f27ad4786a | ||
|
|
cf0ef0f9f4 | ||
|
|
00c6de0e53 | ||
|
|
4c7d92530f | ||
|
|
cafbdc70e8 |
14
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -26,6 +26,20 @@ body:
|
||||
label: Version
|
||||
description: The version can be seen at the bottom of settings.
|
||||
placeholder: e.g. v1.2.3
|
||||
- type: input
|
||||
attributes:
|
||||
label: Last working version
|
||||
description: >
|
||||
The version where the feature was last known to be working. It is
|
||||
fine if you don't remember the exact version (mention roughly
|
||||
then), but if there just isn't a last known working version, then
|
||||
it is likely that what is being reported is not an issue but a
|
||||
feature request. The difference between the two categories is not
|
||||
just semantic - feature requests use GitHub discussions and so can
|
||||
be [upvoted by the
|
||||
community](https://github.com/ente-io/ente/discussions/categories/feature-requests)
|
||||
(issues can't be).
|
||||
placeholder: e.g. v1.2.3
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: What product are you using?
|
||||
|
||||
4
.github/workflows/auth-release.yml
vendored
@@ -36,7 +36,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
build-linux-latest:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
- name: Install dependencies for desktop build
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y libsecret-1-dev libsodium-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate libayatana-appindicator3-dev libffi-dev libtiff6 xz-utils libarchive-tools libcurl4-openssl-dev
|
||||
sudo apt-get install -y libsecret-1-dev libsodium-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate libayatana-appindicator3-dev libffi-dev libtiff5 xz-utils libarchive-tools libcurl4-openssl-dev
|
||||
sudo updatedb --localpaths='/usr/lib/x86_64-linux-gnu'
|
||||
|
||||
- name: Install appimagetool
|
||||
|
||||
3
auth/.fvmrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"flutter": "3.24.3"
|
||||
}
|
||||
5
auth/.gitignore
vendored
@@ -41,4 +41,7 @@ lib/generated_plugin_registrant.dart
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
|
||||
android/key.properties
|
||||
dist/
|
||||
dist/
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
2
auth/android/.gitignore
vendored
@@ -5,6 +5,8 @@ gradle-wrapper.jar
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
/app/.cxx/
|
||||
/.kotlin/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
|
||||
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 8.7 KiB |
@@ -1,6 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_foreground"
|
||||
android:inset="0%" />
|
||||
</foreground>
|
||||
<monochrome>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_monochrome"
|
||||
android:inset="0%" />
|
||||
</monochrome>
|
||||
</adaptive-icon>
|
||||
|
||||
@@ -132,6 +132,10 @@
|
||||
"Binance US"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Bitkub",
|
||||
"slug": "bitkub"
|
||||
},
|
||||
{
|
||||
"title": "Bitfinex"
|
||||
},
|
||||
@@ -183,6 +187,9 @@
|
||||
"title": "Bluesky",
|
||||
"slug": "blue_sky"
|
||||
},
|
||||
{
|
||||
"title": "bonify"
|
||||
},
|
||||
{
|
||||
"title": "Booking",
|
||||
"altNames": [
|
||||
@@ -208,6 +215,13 @@
|
||||
{
|
||||
"title": "Bugzilla"
|
||||
},
|
||||
{
|
||||
"title": "Bundesagentur für Arbeit",
|
||||
"slug": "bundesagentur_fur_arbeit",
|
||||
"altNames": [
|
||||
"Agentur für Arbeit"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "ButterflyMX",
|
||||
"slug": "butterflymx"
|
||||
@@ -385,6 +399,13 @@
|
||||
],
|
||||
"hex": "858585"
|
||||
},
|
||||
{
|
||||
"title": "Fanatical",
|
||||
"slug": "fanatical",
|
||||
"altNames": [
|
||||
"FANATICAL"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Fastmail"
|
||||
},
|
||||
@@ -414,6 +435,9 @@
|
||||
"title": "Firefox",
|
||||
"slug": "mozilla"
|
||||
},
|
||||
{
|
||||
"title": "fortrabbit"
|
||||
},
|
||||
{
|
||||
"title": "ForUsAll"
|
||||
},
|
||||
@@ -508,12 +532,19 @@
|
||||
"slug": "id_me"
|
||||
},
|
||||
{
|
||||
"title": "Infomaniak"
|
||||
"title": "ImmoScout24",
|
||||
"slug": "immo_scout_24",
|
||||
"altNames": [
|
||||
"ImmobilienScout24"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Impact.com",
|
||||
"slug": "impact"
|
||||
},
|
||||
{
|
||||
"title": "Infomaniak"
|
||||
},
|
||||
{
|
||||
"title": "ING"
|
||||
},
|
||||
@@ -615,8 +646,7 @@
|
||||
},
|
||||
{
|
||||
"title": "LinkedIn",
|
||||
"slug": "linkedin",
|
||||
"hex": "2596be"
|
||||
"slug": "linkedin"
|
||||
},
|
||||
{
|
||||
"title": "Linux.Do",
|
||||
|
||||
@@ -1 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 150 150"><defs><style>.e{fill:#2a54ff;}.f{fill:url(#d);}.g{fill:none;}</style><linearGradient id="d" x1="17.68" y1="116.45" x2="132.14" y2="32.11" gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2a54ff"/><stop offset=".52" stop-color="#2143cb"/><stop offset="1" stop-color="#2a54ff"/></linearGradient></defs><g id="b"><path id="c" class="g" d="M0,0H150V150H0V0Z"/></g><path class="f" d="M140.2,22.33c-25.18-.09-49.79,10.83-66.63,29.47-6.06,6.27-10.1,13.95-14.96,21.06-11.64,15.93-29.81,25.14-49.5,25.13h0v28.65h0c25.17,.1,49.78-10.86,66.63-29.5,6.03-6.27,10.13-13.94,14.96-21.06,11.64-15.91,29.81-25.12,49.5-25.11V22.33h0Z"/><path class="e" d="M140.2,97.99c-19.68,0-37.86-9.2-49.5-25.11-4.81-7.12-8.92-14.78-14.94-21.06C58.95,33.18,34.3,22.24,9.13,22.35h0v28.65h0c21.8-.11,42.05,11.62,53.01,30.46,3.22,5.62,7.06,10.9,11.45,15.74,16.83,18.63,41.46,29.59,66.63,29.5l-.02-28.7h0Z"/></svg>
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 150 150">
|
||||
<defs>
|
||||
<linearGradient id="d" x1="17.68" y1="116.45" x2="132.14" y2="32.11"
|
||||
gradientTransform="matrix(1, 0, 0, 1, 0, 0)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#2a54ff" />
|
||||
<stop offset=".52" stop-color="#2143cb" />
|
||||
<stop offset="1" stop-color="#2a54ff" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="b">
|
||||
<path id="c" d="M0,0H150V150H0V0Z" fill="none" />
|
||||
</g>
|
||||
<path
|
||||
d="M140.2,22.33c-25.18-.09-49.79,10.83-66.63,29.47-6.06,6.27-10.1,13.95-14.96,21.06-11.64,15.93-29.81,25.14-49.5,25.13h0v28.65h0c25.17,.1,49.78-10.86,66.63-29.5,6.03-6.27,10.13-13.94,14.96-21.06,11.64-15.91,29.81-25.12,49.5-25.11V22.33h0Z"
|
||||
fill="url(#d)" />
|
||||
<path
|
||||
d="M140.2,97.99c-19.68,0-37.86-9.2-49.5-25.11-4.81-7.12-8.92-14.78-14.94-21.06C58.95,33.18,34.3,22.24,9.13,22.35h0v28.65h0c21.8-.11,42.05,11.62,53.01,30.46,3.22,5.62,7.06,10.9,11.45,15.74,16.83,18.63,41.46,29.59,66.63,29.5l-.02-28.7h0Z"
|
||||
fill="#2a54ff" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
1
auth/assets/custom-icons/icons/bitkub.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 245.73 156" xmlns="http://www.w3.org/2000/svg"><g fill="#4cba64"><path d="m167.87 0a23.32 23.32 0 0 0 0 33l44.89 44.9-45 45-22.89-22.9a23.34 23.34 0 0 0 -33 0l55.86 55.87 78-78z"/><circle cx="167.87" cy="78" r="16"/><path d="m77.87 156a23.34 23.34 0 0 0 0-33l-44.87-44.9 45-45 22.87 22.9a23.34 23.34 0 0 0 33 0l-55.87-55.87-78 78z"/><circle cx="77.87" cy="78" r="16"/></g></svg>
|
||||
|
After Width: | Height: | Size: 396 B |
29
auth/assets/custom-icons/icons/bonify.svg
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="logosandtypes_com" data-name="logosandtypes com" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 150 150">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #101010;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: url(#linear-gradient);
|
||||
}
|
||||
</style>
|
||||
<linearGradient id="linear-gradient" x1="186.97" y1="96.04" x2="45.7" y2="96.04" gradientTransform="translate(0 150.11) scale(1 -1)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#165cc3"/>
|
||||
<stop offset="1" stop-color="#3ddabb"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Layer_3" data-name="Layer 3">
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<path id="Layer_3-2" data-name="Layer 3-2" class="cls-2" d="M0,0H150V150H0V0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="cls-1" d="M111.63,75.01c.06,.86,.08,1.72,.08,2.59,0,20.52-16.62,37.16-37.14,37.16-20.52,0-37.16-16.62-37.16-37.14,0-20.52,16.62-37.16,37.14-37.16,0,0,.02,0,.02,0,1.61,0,3.22,.1,4.82,.32l12.7-17.11C62.3,14,30.31,30.3,20.63,60.09c-9.68,29.79,6.62,61.78,36.41,71.47,29.79,9.68,61.78-6.62,71.47-36.41,4.29-13.2,3.59-27.52-1.97-40.24l-14.9,20.11Z"/>
|
||||
<polygon class="cls-3" points="120.26 4.82 74.49 66.53 62.93 53.99 45.67 69.89 76.4 103.32 149.5 4.82 120.26 4.82"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) by Marsupilami -->
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="768" height="768" viewBox="-4.3240767 -4.3240767 152.8084434 152.7840434" id="svg7076">
|
||||
<defs id="defs7078"/>
|
||||
<path d="M 0,72.07202 C 0,32.27318 32.2935,0 72.08013,0 c 39.78662,0 72.08017,32.27318 72.08017,72.07202 0,39.80291 -32.29355,72.06387 -72.08017,72.06387 -17.63317,0 -33.75958,-6.32434 -46.30232,-16.82687 11.769,-19.46163 46.13944,-77.28864 46.13944,-77.28864 l 17.0223,28.5022 c 0,0 -8.95912,0.0448 -17.06303,0 -8.14464,-0.0448 -10.46588,1.7063 -14.00878,7.11027 -2.9321,4.4877 -9.85505,16.21193 -10.01793,16.42776 -0.81448,1.29093 -0.3258,2.54114 1.58818,2.54114 l 55.18001,0 28.01759,0 c 1.66968,0 2.64704,-1.16875 1.58822,-2.6226 L 73.34255,2.43932 c -0.81447,-1.37236 -2.11759,-1.25021 -2.85061,0 L 8.4704,105.97411 C 3.09495,95.87068 0,84.32969 0,72.07202" id="path8406" style="fill:#ec1c23;fill-rule:nonzero;stroke:none"/>
|
||||
</svg>
|
||||
<!-- version: 20110311, original size: 144.16029 144.13589, border: 3% -->
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,130 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#C5C8CA;}
|
||||
.st1{fill:#9DA4A8;}
|
||||
.st2{fill:#B7BBBD;}
|
||||
.st3{fill:#CBCFD1;}
|
||||
.st4{fill:#BBBFC2;}
|
||||
.st5{fill:#CACDCE;}
|
||||
.st6{fill:#BFC3C5;}
|
||||
.st7{fill:#BCC0C2;}
|
||||
.st8{fill:#BDC1C4;}
|
||||
.st9{fill:#C7CACC;}
|
||||
.st10{fill:url(#SVGID_1_);}
|
||||
.st11{fill:#FFFFFF;}
|
||||
.st12{fill:#B8BCBF;}
|
||||
.st13{fill:#C4C7C9;}
|
||||
.st14{fill:#C1C5C7;}
|
||||
.st15{fill:url(#SVGID_00000003093454306001190100000011813141018663887528_);}
|
||||
.st16{fill:url(#SVGID_00000017503418065689336600000007511615486600436881_);}
|
||||
.st17{fill:url(#SVGID_00000057845154053127761930000017803385842445649033_);}
|
||||
.st18{fill:url(#SVGID_00000156571711195124538550000006687723982713171592_);}
|
||||
.st19{fill:#DF3030;}
|
||||
.st20{fill:url(#SVGID_00000001636660173574603980000008731795684331757470_);}
|
||||
.st21{fill:#17181C;}
|
||||
.st22{fill:url(#SVGID_00000180343933242210086490000003762167186865041053_);}
|
||||
.st23{fill:url(#SVGID_00000015338415700440354440000005681408021599925436_);}
|
||||
</style>
|
||||
<g>
|
||||
<path class="st0" d="M14.4,29.5c0.1,0,0.1,0,0.2,0c0.1,0,0.2,0,0.2,0H14.4z"/>
|
||||
<path class="st1" d="M15.3,29.5h0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0L15.3,29.5
|
||||
C15.2,29.5,15.3,29.5,15.3,29.5z"/>
|
||||
<path class="st2" d="M15.3,29.5L15.3,29.5l-0.2,0C15.2,29.5,15.2,29.5,15.3,29.5z"/>
|
||||
<path class="st3" d="M15.5,29.5L15.5,29.5L15.5,29.5L15.5,29.5L15.5,29.5z"/>
|
||||
<path class="st0" d="M14.1,29.5c0.1,0,0.1,0,0.2,0H14.1z"/>
|
||||
<path class="st4" d="M13.9,29.5C13.9,29.5,14,29.5,13.9,29.5c0.1,0,0.1,0,0.2,0H13.9z"/>
|
||||
<path class="st5" d="M13.6,29.5C13.6,29.5,13.6,29.5,13.6,29.5c0.1,0,0.1,0,0.1,0H13.6z"/>
|
||||
<path class="st6" d="M13.7,29.5C13.8,29.5,13.8,29.5,13.7,29.5c0.1,0,0.1,0,0.1,0H13.7z"/>
|
||||
<path class="st7" d="M13.3,29.4C13.3,29.4,13.3,29.4,13.3,29.4C13.4,29.4,13.4,29.4,13.3,29.4L13.3,29.4z"/>
|
||||
<path class="st8" d="M13.4,29.5C13.4,29.4,13.5,29.4,13.4,29.5C13.5,29.4,13.5,29.4,13.4,29.5L13.4,29.5z"/>
|
||||
<path class="st8" d="M13.1,29.4C13.1,29.4,13.1,29.4,13.1,29.4C13.1,29.4,13.1,29.4,13.1,29.4L13.1,29.4z"/>
|
||||
<path class="st9" d="M13.2,29.4C13.2,29.4,13.2,29.4,13.2,29.4C13.2,29.4,13.2,29.4,13.2,29.4C13.2,29.4,13.2,29.4,13.2,29.4
|
||||
C13.3,29.4,13.3,29.4,13.2,29.4L13.2,29.4z"/>
|
||||
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="21.8812" y1="-88.078" x2="8.2545" y2="-104.6955" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
|
||||
<stop offset="0" style="stop-color:#020037"/>
|
||||
<stop offset="1" style="stop-color:#050F62"/>
|
||||
</linearGradient>
|
||||
<path class="st10" d="M15,0.4C11.1,0.4,7.5,2,4.7,4.7C2,7.4,0.5,11.1,0.5,15c0,1.7,0.3,3.4,0.9,5.1c0.3,0,0.5,0,0.8,0
|
||||
c2.9,0,5.8,0.9,8.2,2.6c2.4,1.7,4.2,4.1,5.1,6.9c3.8-0.1,7.4-1.7,10-4.4c2.6-2.7,4.1-6.4,4.1-10.1c0-3.9-1.5-7.6-4.3-10.3
|
||||
C22.6,2,18.9,0.4,15,0.4"/>
|
||||
<path class="st11" d="M20.7,22.5C20.7,22.5,20.7,22.5,20.7,22.5L20.7,22.5c0,0.4,0.1,0.8,0.3,1c0.2,0.2,0.6,0.3,1,0.3c0,0,0,0,0,0
|
||||
c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c-0.4,0-0.7,0.1-1,0.3c-0.2,0.2-0.3,0.6-0.3,1c0,0,0,0,0,0c0,0,0,0,0,0
|
||||
c0,0,0,0,0,0c0,0,0,0,0,0h0l0,0c0-0.4-0.1-0.7-0.3-1c-0.2-0.2-0.6-0.3-1-0.3c0,0,0,0,0,0l0,0c0,0,0,0,0,0s0,0,0,0c0,0,0,0,0,0
|
||||
c0,0,0,0,0,0c0.4,0,0.7-0.1,1-0.3S20.7,22.9,20.7,22.5C20.7,22.5,20.7,22.5,20.7,22.5C20.7,22.5,20.7,22.5,20.7,22.5z"/>
|
||||
<path class="st11" d="M6.9,15.5C6.9,15.5,6.9,15.5,6.9,15.5L6.9,15.5c0,0.4,0.1,0.8,0.3,1c0.2,0.2,0.6,0.3,1,0.3c0,0,0,0,0,0
|
||||
c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0l0,0c-0.4,0-0.7,0.1-1,0.3c-0.2,0.2-0.3,0.6-0.3,1c0,0,0,0,0,0c0,0,0,0,0,0
|
||||
c0,0,0,0,0,0c0,0,0,0,0,0h0l0,0c0-0.4-0.1-0.7-0.3-1c-0.2-0.2-0.6-0.3-1-0.3c0,0,0,0,0,0l0,0c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0,0,0
|
||||
c0,0,0,0,0,0c0.4,0,0.7-0.1,1-0.3C6.8,16.2,6.9,15.9,6.9,15.5C6.9,15.5,6.9,15.5,6.9,15.5C6.9,15.5,6.9,15.5,6.9,15.5z"/>
|
||||
<path class="st11" d="M10.6,4.1L10.6,4.1C10.7,4.1,10.7,4.1,10.6,4.1c0,0.3,0.1,0.5,0.3,0.7c0.2,0.2,0.4,0.3,0.7,0.2h0v0l0,0l0,0
|
||||
l0,0l0,0c-0.3,0-0.5,0.1-0.7,0.2c-0.2,0.2-0.3,0.4-0.2,0.7l0,0l0,0l0,0l0,0h0v0c0-0.3-0.1-0.5-0.2-0.7C10.2,5.1,10,5,9.7,5.1h0v0v0
|
||||
h0C10,5,10.2,5,10.4,4.8C10.6,4.6,10.7,4.3,10.6,4.1C10.6,4.1,10.6,4.1,10.6,4.1C10.6,4.1,10.6,4.1,10.6,4.1z"/>
|
||||
<path class="st12" d="M12.8,29.4C12.8,29.4,12.8,29.4,12.8,29.4C12.8,29.4,12.8,29.4,12.8,29.4C12.8,29.4,12.8,29.4,12.8,29.4
|
||||
C12.8,29.4,12.8,29.4,12.8,29.4L12.8,29.4z"/>
|
||||
<path class="st13" d="M13,29.4C13,29.4,13,29.4,13,29.4C13,29.4,13,29.4,13,29.4L13,29.4z"/>
|
||||
<path class="st14" d="M12.9,29.4C12.9,29.4,12.9,29.4,12.9,29.4C12.9,29.4,12.9,29.4,12.9,29.4L12.9,29.4z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000173122186048074043340000017421439166240502921_" gradientUnits="userSpaceOnUse" x1="19.2457" y1="-89.3156" x2="22.9553" y2="-91.7188" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
|
||||
<stop offset="0" style="stop-color:#E5E5E5"/>
|
||||
<stop offset="1" style="stop-color:#B7B8C1"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000173122186048074043340000017421439166240502921_);" d="M21.8,1.2c-1.4,0.7-3,1.9-4.4,4.2
|
||||
c-2.5,3.9-3.2,7.4-3.2,7.4L16,14l0.3,0.2l1.9,1.2c0,0,2.9-2,5.4-5.9c1.5-2.3,2-4.3,2-5.8c-0.8-0.1-1.5-0.4-2.2-0.8
|
||||
C22.8,2.5,22.2,1.9,21.8,1.2z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000127763695479642710240000017533313096818365313_" gradientUnits="userSpaceOnUse" x1="21.2378" y1="-99.9826" x2="19.0472" y2="-97.8815" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
|
||||
<stop offset="0" style="stop-color:#EC4F4F"/>
|
||||
<stop offset="1" style="stop-color:#A91919"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000127763695479642710240000017533313096818365313_);" d="M20.8,16.8c0.9-1.4,0.3-3.2,0-3.8
|
||||
c-0.7,0.8-1.5,1.5-2.3,2.1c0.1,0.4,0.3,0.8,0.3,1.2c0,0.1,0,0.2-0.1,0.3c-0.4,0.6-0.8,1.3-1.1,2c-0.1,0.1-0.1,0.2-0.1,0.3
|
||||
c-0.1,0.2-0.1,0.3,0,0.5c0,0.3,0.2,0.5,0.3,0.8c0,0,0.1,0.1,0.1,0.1c0.1,0,0.1,0.1,0.2,0.1s0.1,0,0.2-0.1c0.1-0.1,0.3-0.2,0.4-0.4
|
||||
C19.5,19,19.8,18.5,20.8,16.8z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000060717637781723915790000002744012061535479481_" gradientUnits="userSpaceOnUse" x1="11.3158" y1="-99.2586" x2="14.8122" y2="-101.5237" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
|
||||
<stop offset="0" style="stop-color:#F2A518"/>
|
||||
<stop offset="1" style="stop-color:#F4E23E"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000060717637781723915790000002744012061535479481_);" d="M15.1,15.7l-1.7-1.1c-2,3.1-3.3,7-2.4,7.5
|
||||
c0.9,0.6,3.9-2.2,5.9-5.3L15.1,15.7z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000070084874335106853820000008402293642909580433_" gradientUnits="userSpaceOnUse" x1="-4386.2534" y1="747.6443" x2="-4497.9517" y2="769.0099" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
|
||||
<stop offset="0" style="stop-color:#EC4F4F"/>
|
||||
<stop offset="1" style="stop-color:#A91919"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000070084874335106853820000008402293642909580433_);" d="M15.2,9.5c-0.7-0.1-2.5,0.1-3.4,1.5
|
||||
c-1.1,1.6-1.5,2.1-2,3.2c-0.1,0.2-0.1,0.3-0.2,0.5c0,0.1,0,0.1,0,0.2C9.6,15,9.7,15,9.7,15c0,0,0.1,0,0.2,0.1c0.3,0.1,0.6,0,0.8,0
|
||||
c0.2,0,0.3-0.1,0.4-0.2c0.1-0.1,0.2-0.2,0.3-0.3c0.5-0.6,0.9-1.2,1.3-1.8c0.1-0.1,0.2-0.2,0.3-0.2c0.4-0.1,0.8-0.1,1.2-0.2l0,0
|
||||
C14.5,11.4,14.8,10.4,15.2,9.5z"/>
|
||||
<path class="st19" d="M25,0.6c-0.2-0.1-1.5-0.2-3.2,0.7c0.4,0.7,1,1.2,1.6,1.7c0.7,0.4,1.4,0.7,2.2,0.8C25.7,1.9,25.1,0.7,25,0.6z"
|
||||
/>
|
||||
<path class="st19" d="M18.4,15.5L14,12.7c-0.1,0-0.1,0-0.2,0l-0.9,1.4c0,0.1,0,0.1,0,0.2l4.4,2.8c0.1,0,0.1,0,0.2,0l0.9-1.4
|
||||
C18.4,15.6,18.4,15.6,18.4,15.5z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000044894753735506851200000013592864944465274029_" gradientUnits="userSpaceOnUse" x1="14.9436" y1="-95.9217" x2="16.3716" y2="-96.8468" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
|
||||
<stop offset="0" style="stop-color:#B71E1E"/>
|
||||
<stop offset="0.44" style="stop-color:#DF3030"/>
|
||||
<stop offset="1" style="stop-color:#C51D1D"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000044894753735506851200000013592864944465274029_);" d="M17.8,11.6c-0.4-0.2-2.1,1.6-3.2,3.3
|
||||
c-0.8,1.2-1.4,3-1.1,3.2c0.4,0.2,1.7-1,2.5-2.3C17.1,14.2,18.1,11.9,17.8,11.6z"/>
|
||||
<path class="st21" d="M21.2,8.6c1.3,0,2.3-1,2.3-2.3s-1-2.3-2.3-2.3c-1.3,0-2.3,1-2.3,2.3S20,8.6,21.2,8.6z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000090987122570624474440000002432161440392897685_" gradientUnits="userSpaceOnUse" x1="20.068" y1="-87.0655" x2="22.3556" y2="-88.5473" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
|
||||
<stop offset="0" style="stop-color:#CED1EC"/>
|
||||
<stop offset="1" style="stop-color:#FFFFFF"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000090987122570624474440000002432161440392897685_);" d="M21.2,7.7c0.8,0,1.4-0.6,1.4-1.4
|
||||
S22,5,21.2,5c-0.8,0-1.4,0.6-1.4,1.4S20.5,7.7,21.2,7.7z"/>
|
||||
|
||||
<linearGradient id="SVGID_00000044151119195171880090000016489263670362291109_" gradientUnits="userSpaceOnUse" x1="14.4192" y1="-110.4727" x2="2.0973" y2="-101.7197" gradientTransform="matrix(1 0 0 -1 0 -81.48)">
|
||||
<stop offset="0" style="stop-color:#B7B7BD"/>
|
||||
<stop offset="0.68" style="stop-color:#EFEFEF"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_00000044151119195171880090000016489263670362291109_);" d="M2.1,20c-0.3,0-0.5,0-0.8,0
|
||||
c1,2.8,2.9,5.2,5.3,6.9s5.3,2.6,8.3,2.6c0.1,0,0.3,0,0.4,0c-0.9-2.8-2.7-5.2-5.1-6.9C7.9,20.9,5.1,20,2.1,20z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xml:space="preserve" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0" y="0" version="1.1"
|
||||
viewBox="0 0 30 30">
|
||||
<path d="M14.4 29.5h.4z" fill="#c5c8ca" />
|
||||
<path fill="#9da4a8" d="M15.3 29.5h.1zc-.1 0 0 0 0 0" />
|
||||
<path fill="#b7bbbd" d="M15.3 29.5h-.2z" />
|
||||
<path d="M14.1 29.5h.2z" fill="#c5c8ca" />
|
||||
<path fill="#bbbfc2" d="M13.9 29.5s.1 0 0 0h.2z" />
|
||||
<path fill="#cacdce" d="M13.6 29.5h.1z" />
|
||||
<path fill="#bfc3c5" d="M13.7 29.5q.15 0 0 0h.1z" />
|
||||
<path fill="#bcc0c2" d="M13.3 29.4q.15 0 0 0" />
|
||||
<path fill="#bdc1c4" d="M13.4 29.5c0-.1.1-.1 0 0q.15-.15 0 0m-.3-.1" />
|
||||
<path fill="#c7cacc" d="M13.2 29.4q.15 0 0 0" />
|
||||
<linearGradient id="SVGID_1_" x1="21.8812" x2="8.2545" y1="-88.078" y2="-104.6955" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#020037" />
|
||||
<stop offset="1" stop-color="#050f62" />
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_1_)" d="M15 .4C11.1.4 7.5 2 4.7 4.7 2 7.4.5 11.1.5 15q0 2.55.9 5.1h.8c2.9 0 5.8.9 8.2 2.6s4.2 4.1 5.1 6.9c3.8-.1 7.4-1.7 10-4.4s4.1-6.4 4.1-10.1c0-3.9-1.5-7.6-4.3-10.3C22.6 2 18.9.4 15 .4" />
|
||||
<path fill="#fff" d="M20.7 22.5c0 .4.1.8.3 1s.6.3 1 .3c-.4 0-.7.1-1 .3-.2.2-.3.6-.3 1 0-.4-.1-.7-.3-1-.2-.2-.6-.3-1-.3.4 0 .7-.1 1-.3s.3-.6.3-1m-13.8-7c0 .4.1.8.3 1s.6.3 1 .3c-.4 0-.7.1-1 .3-.2.2-.3.6-.3 1 0-.4-.1-.7-.3-1-.2-.2-.6-.3-1-.3.4 0 .7-.1 1-.3.2-.3.3-.6.3-1m3.7-11.4q.15 0 0 0c0 .3.1.5.3.7s.4.3.7.2c-.3 0-.5.1-.7.2-.2.2-.3.4-.2.7 0-.3-.1-.5-.2-.7-.3-.1-.5-.2-.8-.1.3-.1.5-.1.7-.3s.3-.5.2-.7" />
|
||||
<linearGradient id="SVGID_00000173122186048074043340000017421439166240502921_" x1="19.2457" x2="22.9553" y1="-89.3156" y2="-91.7188" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#e5e5e5" />
|
||||
<stop offset="1" stop-color="#b7b8c1" />
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_00000173122186048074043340000017421439166240502921_)" d="M21.8 1.2c-1.4.7-3 1.9-4.4 4.2-2.5 3.9-3.2 7.4-3.2 7.4L16 14l.3.2 1.9 1.2s2.9-2 5.4-5.9c1.5-2.3 2-4.3 2-5.8-.8-.1-1.5-.4-2.2-.8-.6-.4-1.2-1-1.6-1.7" />
|
||||
<linearGradient id="SVGID_00000127763695479642710240000017533313096818365313_" x1="21.2378" x2="19.0472" y1="-99.9826" y2="-97.8815" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#ec4f4f" />
|
||||
<stop offset="1" stop-color="#a91919" />
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_00000127763695479642710240000017533313096818365313_)" d="M20.8 16.8c.9-1.4.3-3.2 0-3.8-.7.8-1.5 1.5-2.3 2.1.1.4.3.8.3 1.2 0 .1 0 .2-.1.3-.4.6-.8 1.3-1.1 2-.1.1-.1.2-.1.3-.1.2-.1.3 0 .5 0 .3.2.5.3.8l.1.1c.1 0 .1.1.2.1s.1 0 .2-.1.3-.2.4-.4c.8-.9 1.1-1.4 2.1-3.1" />
|
||||
<linearGradient id="SVGID_00000060717637781723915790000002744012061535479481_" x1="11.3158" x2="14.8122" y1="-99.2586" y2="-101.5237" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#f2a518" />
|
||||
<stop offset="1" stop-color="#f4e23e" />
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_00000060717637781723915790000002744012061535479481_)" d="m15.1 15.7-1.7-1.1c-2 3.1-3.3 7-2.4 7.5.9.6 3.9-2.2 5.9-5.3z" />
|
||||
<linearGradient id="SVGID_00000070084874335106853820000008402293642909580433_" x1="-4386.2534" x2="-4497.9517" y1="747.6443" y2="769.0099" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#ec4f4f" />
|
||||
<stop offset="1" stop-color="#a91919" />
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_00000070084874335106853820000008402293642909580433_)" d="M15.2 9.5c-.7-.1-2.5.1-3.4 1.5-1.1 1.6-1.5 2.1-2 3.2-.1.2-.1.3-.2.5v.2c0 .1.1.1.1.1s.1 0 .2.1c.3.1.6 0 .8 0s.3-.1.4-.2l.3-.3c.5-.6.9-1.2 1.3-1.8.1-.1.2-.2.3-.2.4-.1.8-.1 1.2-.2.3-1 .6-2 1-2.9" />
|
||||
<path fill="#df3030" d="M25 .6c-.2-.1-1.5-.2-3.2.7.4.7 1 1.2 1.6 1.7.7.4 1.4.7 2.2.8.1-1.9-.5-3.1-.6-3.2m-6.6 14.9L14 12.7h-.2l-.9 1.4v.2l4.4 2.8h.2l.9-1.4z" />
|
||||
<linearGradient id="SVGID_00000044894753735506851200000013592864944465274029_" x1="14.9436" x2="16.3716" y1="-95.9217" y2="-96.8468" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#b71e1e" />
|
||||
<stop offset=".44" stop-color="#df3030" />
|
||||
<stop offset="1" stop-color="#c51d1d" />
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_00000044894753735506851200000013592864944465274029_)" d="M17.8 11.6c-.4-.2-2.1 1.6-3.2 3.3-.8 1.2-1.4 3-1.1 3.2.4.2 1.7-1 2.5-2.3 1.1-1.6 2.1-3.9 1.8-4.2" />
|
||||
<path fill="#17181c" d="M21.2 8.6c1.3 0 2.3-1 2.3-2.3S22.5 4 21.2 4s-2.3 1-2.3 2.3 1.1 2.3 2.3 2.3" />
|
||||
<linearGradient id="SVGID_00000090987122570624474440000002432161440392897685_" x1="20.068" x2="22.3556" y1="-87.0655" y2="-88.5473" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#ced1ec" />
|
||||
<stop offset="1" stop-color="#fff" />
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_00000090987122570624474440000002432161440392897685_)" d="M21.2 7.7c.8 0 1.4-.6 1.4-1.4S22 5 21.2 5s-1.4.6-1.4 1.4.7 1.3 1.4 1.3" />
|
||||
<linearGradient id="SVGID_00000044151119195171880090000016489263670362291109_" x1="14.4192" x2="2.0973" y1="-110.4727" y2="-101.7197" gradientTransform="matrix(1 0 0 -1 0 -81.48)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#b7b7bd" />
|
||||
<stop offset=".68" stop-color="#efefef" />
|
||||
</linearGradient>
|
||||
<path fill="url(#SVGID_00000044151119195171880090000016489263670362291109_)" d="M2.1 20h-.8c1 2.8 2.9 5.2 5.3 6.9s5.3 2.6 8.3 2.6h.4c-.9-2.8-2.7-5.2-5.1-6.9C7.9 20.9 5.1 20 2.1 20" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 5.6 KiB |
44
auth/assets/custom-icons/icons/fanatical.svg
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 399.76401 400"
|
||||
preserveAspectRatio="xMinYMid"
|
||||
aria-labelledby="navbar-fanatical-logo"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
sodipodi:docname="Untitled.svg"
|
||||
width="399.76401"
|
||||
height="400"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="namedview2"
|
||||
pagecolor="#505050"
|
||||
bordercolor="#ffffff"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#505050"
|
||||
inkscape:zoom="0.69295302"
|
||||
inkscape:cx="205.64165"
|
||||
inkscape:cy="207.08475"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="938"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<g
|
||||
fill="none"
|
||||
id="g2">
|
||||
<path
|
||||
fill="#ff9800"
|
||||
d="m 2.8756,166.0056 h 284.671 a 2.9981,2.9981 0 0 0 2.7221,-1.7424 l 25.8632,-56.0452 c 0.6946,-1.504 0.0391,-3.2867 -1.464,-3.9817 a 2.9968,2.9968 0 0 0 -1.258,-0.2767 L 24.4917,103.9952 C 58.4482,42.0187 124.261,0 199.882,0 c 110.3917,0 199.882,89.543 199.882,200 0,110.457 -89.4903,200 -199.882,200 C 89.4902,400 0,310.457 0,200 0,188.412 0.985,177.054 2.8756,166.0056 Z M 125.9256,328 c 0,2.2091 1.7898,4 3.9977,4 h 5.1722 l 62.8312,-79.0111 h 49.4291 a 2.9981,2.9981 0 0 0 2.722,-1.7422 l 25.835,-55.976 a 3.0015,3.0015 0 0 0 0.2761,-1.2577 c 0,-1.6569 -1.3423,-3 -2.9982,-3 H 125.9257 V 328 Z"
|
||||
id="path1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
1
auth/assets/custom-icons/icons/fortrabbit.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><path d="M115 144c0 6-2 12-7 16s-9 7-16 7-11-3-16-7-6-10-6-16 2-12 6-16 10-7 16-7 12 3 16 7c5 5 7 10 7 16zm71-23-8 38-7 34a63 63 0 0 1-36 42c-5 2-11 3-17 3s-10 0-14-2l-7-4c-2-1-4-3-4-5l-1-6c0-4 1-7 3-9s6-4 10-4l9 2c3 1 4 4 6 6l4 8 3 7c3-3 5-7 7-13l7-22 16-75h-18l2-9h18l1-7c1-6 4-11 7-17s7-10 12-14c4-4 10-8 16-10s11-4 17-4l13 1 8 4 4 6 1 6a15 15 0 0 1-3 8l-4 4-7 1-8-2-6-6-4-8-3-7c-3 3-5 7-7 12l-6 23-2 10h22l-2 9h-22z"/></svg>
|
||||
|
After Width: | Height: | Size: 491 B |
6
auth/assets/custom-icons/icons/immo_scout_24.svg
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -1,4 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 724 264">
|
||||
<path
|
||||
d="M38.53 260.65H.43V27.86h38.1zm86.46 2.77c-42.25 0-66.48-22.96-66.48-63V89.33h38.1v108.28c0 23.61 8.7 32.39 32.12 32.39 30.35 0 42.73-14.54 42.73-50.17v-90.5h38.1v171.33h-36.54v-29.91c-4.99 22.98-27.12 32.67-48.03 32.67zm347.2-2.77H434.4V149.87c0-22.5-7.01-30.87-25.88-30.87-24.28 0-37.11 14.45-37.11 41.79v99.86h-37.79V149.87c0-21.93-7.23-30.87-24.94-30.87-31.59 0-38.05 32.96-38.05 41.79v99.86h-38.1V89.33h36.54v29.96c6.49-21.02 27.02-33.71 47.72-33.71 20.69 0 38.09 7.9 45.64 33.71 10.13-26.76 28.35-33.71 50.15-33.71 37.88 0 59.61 18.88 59.61 51.81v123.26h0zm76.65 2.77c-52.62 0-61.55-33.45-61.55-50.52 0-20.1 8.83-38.21 27.93-45.55 8.41-3.11 16.52-5.43 24.84-7.1 7.33-1.47 18.64-3.03 26.91-4.17l2.73-.38c14.38-2 29.67-9.21 29.67-18.62 0-16-20.51-18.39-32.74-18.39-13.87 0-23.64 3.57-27.53 10.05-3.49 6.46-3.73 7.97-4.62 13.6l-.62 4.43h-38.1l.68-5.61c1.35-11.14 3.41-19.03 6.48-24.83 10.54-20.39 31.77-30.75 63.08-30.75 26.11 0 44.63 8.23 53.26 15.94 5.31 4.6 9.1 9.84 11.89 16.46 5.84 12.36 6.32 20.63 6.32 29.4v86.43c0 8.07.78 14.97 2.31 20.5l1.76 6.35h-38.91l-.7-4.19c-.5-2.96-.67-19.75-.88-26.23-8.99 23.61-28.27 33.18-52.21 33.18zm50.53-93.72c-7.97 6.11-20.47 9.6-38.62 13.23-31.27 5.78-36.54 13.06-36.54 27.22 0 12.5 10.63 20.26 27.75 20.26 33.23 0 47.41-15.48 47.41-51.77v-8.94zm124.2-105.51C688.46 64.19 660 35.73 660 .62c0 35.11-28.46 63.57-63.57 63.57h0c35.11 0 63.57 28.46 63.57 63.57h0c0-35.11 28.46-63.57 63.57-63.57z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 724 264">
|
||||
<path
|
||||
d="M38.53 260.65H.43V27.86h38.1zm86.46 2.77c-42.25 0-66.48-22.96-66.48-63V89.33h38.1v108.28c0 23.61 8.7 32.39 32.12 32.39 30.35 0 42.73-14.54 42.73-50.17v-90.5h38.1v171.33h-36.54v-29.91c-4.99 22.98-27.12 32.67-48.03 32.67zm347.2-2.77H434.4V149.87c0-22.5-7.01-30.87-25.88-30.87-24.28 0-37.11 14.45-37.11 41.79v99.86h-37.79V149.87c0-21.93-7.23-30.87-24.94-30.87-31.59 0-38.05 32.96-38.05 41.79v99.86h-38.1V89.33h36.54v29.96c6.49-21.02 27.02-33.71 47.72-33.71 20.69 0 38.09 7.9 45.64 33.71 10.13-26.76 28.35-33.71 50.15-33.71 37.88 0 59.61 18.88 59.61 51.81v123.26h0zm76.65 2.77c-52.62 0-61.55-33.45-61.55-50.52 0-20.1 8.83-38.21 27.93-45.55 8.41-3.11 16.52-5.43 24.84-7.1 7.33-1.47 18.64-3.03 26.91-4.17l2.73-.38c14.38-2 29.67-9.21 29.67-18.62 0-16-20.51-18.39-32.74-18.39-13.87 0-23.64 3.57-27.53 10.05-3.49 6.46-3.73 7.97-4.62 13.6l-.62 4.43h-38.1l.68-5.61c1.35-11.14 3.41-19.03 6.48-24.83 10.54-20.39 31.77-30.75 63.08-30.75 26.11 0 44.63 8.23 53.26 15.94 5.31 4.6 9.1 9.84 11.89 16.46 5.84 12.36 6.32 20.63 6.32 29.4v86.43c0 8.07.78 14.97 2.31 20.5l1.76 6.35h-38.91l-.7-4.19c-.5-2.96-.67-19.75-.88-26.23-8.99 23.61-28.27 33.18-52.21 33.18zm50.53-93.72c-7.97 6.11-20.47 9.6-38.62 13.23-31.27 5.78-36.54 13.06-36.54 27.22 0 12.5 10.63 20.26 27.75 20.26 33.23 0 47.41-15.48 47.41-51.77v-8.94zm124.2-105.51C688.46 64.19 660 35.73 660 .62c0 35.11-28.46 63.57-63.57 63.57h0c35.11 0 63.57 28.46 63.57 63.57h0c0-35.11 28.46-63.57 63.57-63.57z"
|
||||
fill="#ffffff" style="mix-blend-mode: difference;" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
auth/assets/generation-icons/icon-monochrome.png
Normal file
|
After Width: | Height: | Size: 489 KiB |
@@ -90,11 +90,11 @@ PODS:
|
||||
- SDWebImage (5.21.0):
|
||||
- SDWebImage/Core (= 5.21.0)
|
||||
- SDWebImage/Core (5.21.0)
|
||||
- Sentry/HybridSDK (8.36.0)
|
||||
- sentry_flutter (8.9.0):
|
||||
- Sentry/HybridSDK (8.46.0)
|
||||
- sentry_flutter (8.14.2):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 8.36.0)
|
||||
- Sentry/HybridSDK (= 8.46.0)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@@ -232,44 +232,44 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
|
||||
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
|
||||
app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874
|
||||
connectivity_plus: 3f6c9057f4cd64198dc826edfb0542892f825343
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
|
||||
fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
|
||||
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
|
||||
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
|
||||
fk_user_agent: 137145b086229251761678fe034da53753f4ce59
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
flutter_email_sender: 10a22605f92809a11ef52b2f412db806c6082d40
|
||||
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
|
||||
flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
|
||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
|
||||
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
|
||||
flutter_email_sender: 2397f5e84aaacfb61af569637a963e7c687858d8
|
||||
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
|
||||
flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f
|
||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||
flutter_native_splash: 35ddbc7228eafcb3969dcc5f1fbbe27c1145a4f0
|
||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||
fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
move_to_background: 155f7bfbd34d43ad847cb630d2d2d87c17199710
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
objective_c: 77e887b5ba1827970907e10e832eec1683f3431d
|
||||
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
package_info_plus: 580e9a5f1b6ca5594e7c9ed5f92d1dfb2a66b5e1
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4
|
||||
qr_code_scanner: d77f94ecc9abf96d9b9b8fc04ef13f611e5a147a
|
||||
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
|
||||
Sentry: f8374b5415bc38dfb5645941b3ae31230fbeae57
|
||||
sentry_flutter: 0eb93e5279eb41e2392212afe1ccd2fecb4f8cbe
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sodium_libs: 1faae17af662384acbd13e41867a0008cd2e2318
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
|
||||
sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
|
||||
share_plus: 011d6fb4f9d2576b83179a3a5c5e323202cdabcf
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sodium_libs: 6c6d0e83f4ee427c6464caa1f1bdc2abf3ca0b7f
|
||||
sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3
|
||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
|
||||
sqlite3_flutter_libs: 9379996d65aa23dcda7585a5b58766cebe0aa042
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
|
||||
PODFILE CHECKSUM: 78f002751f1a8f65042b8da97902ba4124271c5a
|
||||
|
||||
|
||||
@@ -499,6 +499,7 @@
|
||||
"duplicateCodes": "Doppelte Codes",
|
||||
"noDuplicates": "✨ Keine Duplikate",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Du hast keine doppelten Codes, die bereinigt werden können",
|
||||
"deduplicateCodes": "Codes deduplizieren",
|
||||
"deselectAll": "Alle abwählen",
|
||||
"selectAll": "Alles auswählen",
|
||||
"deleteDuplicates": "Duplikate löschen",
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"contactSupport": "Lépj kapcsolatba az Ügyfélszolgálattal",
|
||||
"rateUsOnStore": "Értékelj minket a következőn: {storeName}",
|
||||
"blog": "Blog",
|
||||
"merchandise": "Áru",
|
||||
"merchandise": "Ajándéktárgyak",
|
||||
"verifyPassword": "Jelszó megerősítése",
|
||||
"pleaseWait": "Kérem várjon...",
|
||||
"generatingEncryptionKeysTitle": "Titkosítási kulcs generálása...",
|
||||
@@ -499,12 +499,15 @@
|
||||
"appLockOfflineModeWarning": "Úgy döntött, hogy biztonsági mentés nélkül folytatja. Ha elfelejti az alkalmazászárat, akkor nem férhet hozzá adataihoz.",
|
||||
"duplicateCodes": "Ismétlődő kódok",
|
||||
"noDuplicates": "✨Nincs ismétlődés",
|
||||
"youveNoDuplicateCodesThatCanBeCleared": "Nincsenek törölhető ismétlődő kódok",
|
||||
"deduplicateCodes": "Ismétlődő kódok",
|
||||
"deselectAll": "Összes kijelölés megszüntetése",
|
||||
"selectAll": "Összes kijelölése",
|
||||
"deleteDuplicates": "Ismétlődések törlése",
|
||||
"plainHTML": "Sima HTML kód",
|
||||
"tellUsWhatYouThink": "Mondja el mit gondol",
|
||||
"dropReviewiOS": "Írj véleményt az App Store-ban",
|
||||
"dropReviewAndroid": "Írj véleményt a Play Store-ban",
|
||||
"supportEnte": "Támogassa <bold-green>ente <bold-green>",
|
||||
"giveUsAStarOnGithub": "Adj nekünk egy csillagot a Githubon",
|
||||
"free5GB": "5GB ingyen <bold-green>ente <bold-green> Photos",
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
{}
|
||||
{
|
||||
"importScanQrCode": ""
|
||||
}
|
||||
@@ -51,7 +51,7 @@
|
||||
"trashCode": "Xóa mã?",
|
||||
"trashCodeMessage": "Bạn có chắc chắn muốn xóa mã cho {account} không?",
|
||||
"trash": "Xóa",
|
||||
"viewLogsAction": "Xem các bản ghi",
|
||||
"viewLogsAction": "Xem nhật ký",
|
||||
"sendLogsDescription": "Thao tác này sẽ gửi nhật ký để giúp chúng tôi gỡ lỗi sự cố của bạn. Mặc dù chúng tôi thực hiện các biện pháp phòng ngừa để đảm bảo rằng thông tin nhạy cảm không được ghi lại, nhưng chúng tôi khuyến khích bạn xem các nhật ký này trước khi chia sẻ chúng.",
|
||||
"preparingLogsTitle": "Đang chuẩn bị nhật ký...",
|
||||
"emailLogsTitle": "Nhật ký email",
|
||||
@@ -506,6 +506,8 @@
|
||||
"deleteDuplicates": "Xóa trùng lặp",
|
||||
"plainHTML": "HTML thuần",
|
||||
"tellUsWhatYouThink": "Hãy cho chúng tôi biết bạn nghĩ gì",
|
||||
"dropReviewiOS": "Đánh giá ngay trên App Store",
|
||||
"dropReviewAndroid": "Đánh giá ngay trên Play Store",
|
||||
"supportEnte": "Hỗ trợ <bold-green>ente</bold-green>",
|
||||
"giveUsAStarOnGithub": "Cho chúng tôi ngôi sao trên Github",
|
||||
"free5GB": "Miễn phí 5GB cho <bold-green>ente</bold-green> Hình ảnh",
|
||||
|
||||
@@ -504,7 +504,7 @@
|
||||
"deselectAll": "取消全選",
|
||||
"selectAll": "全選",
|
||||
"deleteDuplicates": "刪除重複項",
|
||||
"plainHTML": "Plain HTML",
|
||||
"plainHTML": "純HTML",
|
||||
"tellUsWhatYouThink": "告訴我們您的想法",
|
||||
"dropReviewiOS": "在 App Store 上發表意見",
|
||||
"dropReviewAndroid": "在 Play 商店上發表評測",
|
||||
|
||||
@@ -34,11 +34,11 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- screen_retriever (0.0.1):
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (8.36.0)
|
||||
- sentry_flutter (8.9.0):
|
||||
- Sentry/HybridSDK (8.46.0)
|
||||
- sentry_flutter (8.14.2):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 8.36.0)
|
||||
- Sentry/HybridSDK (= 8.46.0)
|
||||
- share_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@@ -157,33 +157,33 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a
|
||||
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
|
||||
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
|
||||
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
|
||||
file_saver: 44e6fbf666677faf097302460e214e977fdd977b
|
||||
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
|
||||
flutter_local_authentication: 85674893931e1c9cfa7c9e4f5973cb8c56b018b0
|
||||
flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
|
||||
flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9
|
||||
app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468
|
||||
connectivity_plus: 3f6c9057f4cd64198dc826edfb0542892f825343
|
||||
cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c
|
||||
device_info_plus: b0fafc687fb901e2af612763340f1b0d4352f8e5
|
||||
file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f
|
||||
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
|
||||
flutter_local_authentication: 2f9a2682f498abcc12d7e9729b5007a947170fdc
|
||||
flutter_local_notifications: 453432cd6399a07d072885bc7828fb2307868856
|
||||
flutter_secure_storage_macos: b2d62a774c23b060f0b99d0173b0b36abb4a8632
|
||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
|
||||
objective_c: e5f8194456e8fc943e034d1af00510a1bc29c067
|
||||
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||
objective_c: ec13431e45ba099cb734eb2829a5c1cd37986cba
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
|
||||
Sentry: f8374b5415bc38dfb5645941b3ae31230fbeae57
|
||||
sentry_flutter: 0eb93e5279eb41e2392212afe1ccd2fecb4f8cbe
|
||||
share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sodium_libs: d39bd76697736cb11ce4a0be73b9b4bc64466d6f
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
package_info_plus: a8a591e70e87ce97ce5d21b2594f69cea9e0312f
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
screen_retriever: 4f97c103641aab8ce183fa5af3b87029df167936
|
||||
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
|
||||
sentry_flutter: 27892878729f42701297c628eb90e7c6529f3684
|
||||
share_plus: 11c7b7fa7020465584eca3ff6392c5bc1e399d6e
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sodium_libs: b9459e5bfc1185349f43472e79fc5d8e526b2bda
|
||||
sqflite: c35dad70033b8862124f8337cc994a809fcd9fa3
|
||||
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
|
||||
sqlite3_flutter_libs: 5ca46c1a04eddfbeeb5b16566164aa7ad1616e7b
|
||||
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
|
||||
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
|
||||
window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8
|
||||
sqlite3_flutter_libs: 03311aede9d32fb2d24e32bebb8cd01c3b2e6239
|
||||
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
|
||||
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673
|
||||
window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c
|
||||
|
||||
PODFILE CHECKSUM: 6ff827273ace187339fc5d3684072a26ad85c298
|
||||
|
||||
|
||||
@@ -5,15 +5,15 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
|
||||
sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "72.0.0"
|
||||
version: "76.0.0"
|
||||
_macros:
|
||||
dependency: transitive
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.2"
|
||||
version: "0.3.3"
|
||||
adaptive_theme:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -26,10 +26,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
|
||||
sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.7.0"
|
||||
version: "6.11.0"
|
||||
ansicolor:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -90,10 +90,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
version: "2.12.0"
|
||||
auto_size_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -130,10 +130,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
build:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -202,10 +202,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.0"
|
||||
checked_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -234,10 +234,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.2"
|
||||
code_builder:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -250,10 +250,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.19.1"
|
||||
confetti:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -435,10 +435,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.2"
|
||||
ffi:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -675,14 +675,13 @@ packages:
|
||||
source: hosted
|
||||
version: "9.2.2"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: "direct overridden"
|
||||
dependency: transitive
|
||||
description:
|
||||
path: flutter_secure_storage_linux
|
||||
ref: develop
|
||||
resolved-ref: "5a5692b609b3886cdd49b2ed06b9c079ecdff996"
|
||||
url: "https://github.com/mogol/flutter_secure_storage.git"
|
||||
source: git
|
||||
version: "1.2.1"
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
flutter_secure_storage_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -945,18 +944,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.5"
|
||||
version: "10.0.8"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.9"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1025,18 +1024,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: macros
|
||||
sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
|
||||
sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2-main.4"
|
||||
version: "0.1.3-main.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16+1"
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1057,10 +1056,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1169,10 +1168,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.9.1"
|
||||
path_drawing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1361,18 +1360,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry
|
||||
sha256: "033287044a6644a93498969449d57c37907e56f5cedb17b88a3ff20a882261dd"
|
||||
sha256: "599701ca0693a74da361bc780b0752e1abc98226cf5095f6b069648116c896bb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.9.0"
|
||||
version: "8.14.2"
|
||||
sentry_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry_flutter
|
||||
sha256: "3780b5a0bb6afd476857cfbc6c7444d969c29a4d9bd1aa5b6960aa76c65b737a"
|
||||
sha256: "5ba2cf40646a77d113b37a07bd69f61bb3ec8a73cbabe5537b05a7c89d2656f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.9.0"
|
||||
version: "8.14.2"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1473,7 +1472,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
version: "0.0.0"
|
||||
sodium:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1510,10 +1509,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
version: "1.10.1"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1567,10 +1566,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
version: "1.12.1"
|
||||
steam_totp:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1591,10 +1590,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.4"
|
||||
stream_transform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1607,10 +1606,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.4.1"
|
||||
styled_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1631,18 +1630,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
version: "0.7.4"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1807,10 +1806,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.2.5"
|
||||
version: "14.3.1"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1900,5 +1899,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
dart: ">=3.7.0-0 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 4.3.4+435
|
||||
version: 4.3.6+437
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -84,8 +84,8 @@ dependencies:
|
||||
protobuf: ^3.0.0
|
||||
qr_code_scanner: ^1.0.1
|
||||
qr_flutter: ^4.1.0
|
||||
sentry: ^8.7.0
|
||||
sentry_flutter: ^8.7.0
|
||||
sentry: ^8.14.2
|
||||
sentry_flutter: ^8.14.2
|
||||
share_plus: ^10.0.2
|
||||
shared_preferences: ^2.0.5
|
||||
sqflite:
|
||||
@@ -107,12 +107,6 @@ dependencies:
|
||||
window_manager: ^0.4.2
|
||||
xdg_directories: ^1.0.4
|
||||
|
||||
dependency_overrides:
|
||||
flutter_secure_storage_linux:
|
||||
git:
|
||||
url: https://github.com/mogol/flutter_secure_storage.git
|
||||
ref: develop
|
||||
path: flutter_secure_storage_linux
|
||||
dev_dependencies:
|
||||
build_runner: ^2.1.11
|
||||
flutter_test:
|
||||
@@ -148,12 +142,17 @@ flutter:
|
||||
fonts:
|
||||
- asset: fonts/Montserrat-Bold.ttf
|
||||
|
||||
flutter_icons:
|
||||
# run "dart run flutter_launcher_icons" to generate icons
|
||||
flutter_launcher_icons:
|
||||
image_path: "assets/generation-icons/icon-light.png"
|
||||
|
||||
android: "launcher_icon"
|
||||
adaptive_icon_foreground: "assets/generation-icons/icon-light-adaptive-fg.png"
|
||||
adaptive_icon_background: "assets/generation-icons/icon-light-adaptive-bg.png"
|
||||
adaptive_icon_monochrome: "assets/generation-icons/icon-monochrome.png"
|
||||
adaptive_icon_foreground_inset: 0
|
||||
|
||||
ios: true
|
||||
image_path: "assets/generation-icons/icon-light.png"
|
||||
remove_alpha_ios: true
|
||||
|
||||
flutter_native_splash:
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <local_auth_windows/local_auth_plugin.h>
|
||||
#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 <sodium_libs/sodium_libs_plugin_c_api.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
@@ -38,6 +39,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
|
||||
ScreenRetrieverPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
|
||||
SentryFlutterPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SentryFlutterPlugin"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
SodiumLibsPluginCApiRegisterWithRegistrar(
|
||||
|
||||
@@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_secure_storage_windows
|
||||
local_auth_windows
|
||||
screen_retriever
|
||||
sentry_flutter
|
||||
share_plus
|
||||
sodium_libs
|
||||
sqlite3_flutter_libs
|
||||
@@ -21,7 +22,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
jni
|
||||
sentry_flutter
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
# CHANGELOG
|
||||
|
||||
## v1.7.12 (Unreleased)
|
||||
## v1.7.13 (Unreleased)
|
||||
|
||||
- .
|
||||
|
||||
## v1.7.12
|
||||
|
||||
- Improved video player with streaming support (for already processed videos).
|
||||
- Support Arabic translations.
|
||||
|
||||
## v1.7.11
|
||||
|
||||
- Improved file viewer.
|
||||
|
||||
@@ -39,6 +39,15 @@ export default ts.config(
|
||||
"error",
|
||||
{ allowTernary: true },
|
||||
],
|
||||
// Allow force unwrapping potentially optional values.
|
||||
//
|
||||
// See: [Note: non-null-assertions have better stack trace]
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
// Allow `while(true)` etc.
|
||||
"@typescript-eslint/no-unnecessary-condition": [
|
||||
"error",
|
||||
{ allowConstantLoopConditions: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ente",
|
||||
"version": "1.7.12-beta",
|
||||
"version": "1.7.13-beta",
|
||||
"private": true,
|
||||
"description": "Desktop client for Ente Photos",
|
||||
"repository": "github:ente-io/photos-desktop",
|
||||
@@ -31,9 +31,9 @@
|
||||
"clip-bpe-js": "^0.0.6",
|
||||
"comlink": "^4.4.2",
|
||||
"compare-versions": "^6.1.1",
|
||||
"electron-log": "^5.3.3",
|
||||
"electron-log": "^5.4.0",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
"electron-updater": "^6.6.3",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"lru-cache": "^11.1.0",
|
||||
"next-electron-server": "^1.0.0",
|
||||
@@ -41,22 +41,22 @@
|
||||
"onnxruntime-node": "^1.20.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@eslint/js": "^9.25.1",
|
||||
"@tsconfig/node22": "^22.0.1",
|
||||
"@types/auto-launch": "^5.0.5",
|
||||
"@types/ffmpeg-static": "^3.0.3",
|
||||
"ajv": "^8.17.1",
|
||||
"concurrently": "^9.1.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^35.1.4",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron": "^36.1.0",
|
||||
"electron-builder": "^26.0.14",
|
||||
"eslint": "^9",
|
||||
"prettier": "3.5.3",
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"prettier-plugin-packagejson": "^2.5.10",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.29.1"
|
||||
"typescript-eslint": "^8.31.1"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"productName": "ente"
|
||||
|
||||
@@ -423,7 +423,14 @@ const createMainWindow = () => {
|
||||
window.on("hide", () => {
|
||||
// On macOS, when hiding the window also hide the app's icon in the dock
|
||||
// unless the user has unchecked the Settings > Hide dock icon checkbox.
|
||||
if (shouldHideDockIcon()) app.dock?.hide();
|
||||
if (shouldHideDockIcon()) {
|
||||
// macOS emits a window "hide" event when going fullscreen, and if
|
||||
// we hide the dock icon there then the window disappears. So ignore
|
||||
// this scenario.
|
||||
if (!window.isFullScreen()) {
|
||||
app.dock?.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.on("show", () => void app.dock?.show());
|
||||
|
||||
@@ -13,8 +13,10 @@ import type { BrowserWindow } from "electron";
|
||||
import { ipcMain } from "electron/main";
|
||||
import type {
|
||||
CollectionMapping,
|
||||
FFmpegCommand,
|
||||
FolderWatch,
|
||||
PendingUploads,
|
||||
UtilityProcessType,
|
||||
ZipItem,
|
||||
} from "../types/ipc";
|
||||
import { logToDisk } from "./log";
|
||||
@@ -40,12 +42,12 @@ import {
|
||||
fsRename,
|
||||
fsRm,
|
||||
fsRmdir,
|
||||
fsStatMtime,
|
||||
fsWriteFile,
|
||||
fsWriteFileViaBackup,
|
||||
} from "./services/fs";
|
||||
import { convertToJPEG, generateImageThumbnail } from "./services/image";
|
||||
import { logout } from "./services/logout";
|
||||
import { createMLWorker } from "./services/ml";
|
||||
import {
|
||||
lastShownChangelogVersion,
|
||||
masterKeyB64,
|
||||
@@ -55,8 +57,8 @@ import {
|
||||
import {
|
||||
clearPendingUploads,
|
||||
listZipItems,
|
||||
markUploadedFiles,
|
||||
markUploadedZipItems,
|
||||
markUploadedFile,
|
||||
markUploadedZipItem,
|
||||
pathOrZipItemSize,
|
||||
pendingUploads,
|
||||
setPendingUploads,
|
||||
@@ -68,6 +70,7 @@ import {
|
||||
watchUpdateIgnoredFiles,
|
||||
watchUpdateSyncedFiles,
|
||||
} from "./services/watch";
|
||||
import { triggerCreateUtilityProcess } from "./services/workers";
|
||||
|
||||
/**
|
||||
* Listen for IPC events sent/invoked by the renderer process, and route them to
|
||||
@@ -163,6 +166,8 @@ export const attachIPCHandlers = () => {
|
||||
|
||||
ipcMain.handle("fsIsDir", (_, dirPath: string) => fsIsDir(dirPath));
|
||||
|
||||
ipcMain.handle("fsStatMtime", (_, path: string) => fsStatMtime(path));
|
||||
|
||||
ipcMain.handle("fsFindFiles", (_, folderPath: string) =>
|
||||
fsFindFiles(folderPath),
|
||||
);
|
||||
@@ -187,7 +192,7 @@ export const attachIPCHandlers = () => {
|
||||
"ffmpegExec",
|
||||
(
|
||||
_,
|
||||
command: string[],
|
||||
command: FFmpegCommand,
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
outputFileExtension: string,
|
||||
) => ffmpegExec(command, dataOrPathOrZipItem, outputFileExtension),
|
||||
@@ -210,13 +215,15 @@ export const attachIPCHandlers = () => {
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"markUploadedFiles",
|
||||
(_, paths: PendingUploads["filePaths"]) => markUploadedFiles(paths),
|
||||
"markUploadedFile",
|
||||
(_, path: string, associatedPath: string | undefined) =>
|
||||
markUploadedFile(path, associatedPath),
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
"markUploadedZipItems",
|
||||
(_, items: PendingUploads["zipItems"]) => markUploadedZipItems(items),
|
||||
"markUploadedZipItem",
|
||||
(_, item: ZipItem, associatedItem: ZipItem | undefined) =>
|
||||
markUploadedZipItem(item, associatedItem),
|
||||
);
|
||||
|
||||
ipcMain.handle("clearPendingUploads", () => clearPendingUploads());
|
||||
@@ -227,9 +234,11 @@ export const attachIPCHandlers = () => {
|
||||
* the main window to do their thing.
|
||||
*/
|
||||
export const attachMainWindowIPCHandlers = (mainWindow: BrowserWindow) => {
|
||||
// - ML
|
||||
// - Utility processes
|
||||
|
||||
ipcMain.on("createMLWorker", () => createMLWorker(mainWindow));
|
||||
ipcMain.on("triggerCreateUtilityProcess", (_, type: UtilityProcessType) =>
|
||||
triggerCreateUtilityProcess(type, mainWindow),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
59
desktop/src/main/log-worker.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* A object that behaves similar to the default export of "./log", except this
|
||||
* can be used from within a utility process.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* We cannot directly do
|
||||
*
|
||||
* import log from "../log";
|
||||
*
|
||||
* because that requires the Electron APIs that are not available to a utility
|
||||
* process (See: [Note: Using Electron APIs in UtilityProcess]).
|
||||
*
|
||||
* But even if that were to work, logging will still be problematic since we'd
|
||||
* try opening the log file from two different Node.js processes (this one, and
|
||||
* the main one), and I didn't find any indication in the electron-log
|
||||
* repository that the log file's integrity would be maintained in such cases.
|
||||
*
|
||||
* So instead we provide this proxy log object that uses the
|
||||
* `process.parentPort` to transport the logs over to the main process, where
|
||||
* the {@link processUtilityProcessLogMessage} function in the main process is
|
||||
* expected to handle these (sending them to the actual log).
|
||||
*/
|
||||
export default {
|
||||
error: (s: string, e?: unknown) =>
|
||||
mainProcess("log.errorString", messageWithError(s, e)),
|
||||
warn: (s: string, e?: unknown) =>
|
||||
mainProcess("log.warnString", messageWithError(s, e)),
|
||||
info: (...ms: unknown[]) => mainProcess("log.info", ms),
|
||||
/**
|
||||
* Unlike the real {@link log.debug}, this is (a) eagerly evaluated, and (b)
|
||||
* accepts only strings.
|
||||
*/
|
||||
debugString: (s: string) => mainProcess("log.debugString", s),
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a message to the main process using a barebones RPC protocol.
|
||||
*/
|
||||
const mainProcess = (method: string, param: unknown) =>
|
||||
process.parentPort.postMessage({ method, p: param });
|
||||
|
||||
// Duplicated verbatim from ./log.ts
|
||||
const messageWithError = (message: string, e?: unknown) => {
|
||||
if (!e) return message;
|
||||
|
||||
let es: string;
|
||||
if (e instanceof Error) {
|
||||
// In practice, we expect ourselves to be called with Error objects, so
|
||||
// this is the happy path so to say.
|
||||
es = [`${e.name}: ${e.message}`, e.stack].filter((x) => x).join("\n");
|
||||
} else {
|
||||
// For the rest rare cases, use the default string serialization of e.
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
es = String(e);
|
||||
}
|
||||
|
||||
return `${message}: ${es}`;
|
||||
};
|
||||
@@ -83,6 +83,56 @@ const logDebug = (param: () => unknown) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle log messages posted from the utility process in the main process.
|
||||
*
|
||||
* See: [Note: Using Electron APIs in UtilityProcess]
|
||||
*
|
||||
* @param message The arbitrary message that was received as an argument to the
|
||||
* "message" event invoked on a {@link UtilityProcess}.
|
||||
*
|
||||
* @returns true if the message was recognized and handled, and false otherwise.
|
||||
*/
|
||||
export const processUtilityProcessLogMessage = (
|
||||
logTag: string,
|
||||
message: unknown,
|
||||
) => {
|
||||
const m = message; /* shorter alias */
|
||||
if (m && typeof m == "object" && "method" in m && "p" in m) {
|
||||
const p = m.p;
|
||||
switch (m.method) {
|
||||
case "log.errorString":
|
||||
if (typeof p == "string") {
|
||||
logError(`${logTag} ${p}`);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case "log.warnString":
|
||||
if (typeof p == "string") {
|
||||
logWarn(`${logTag} ${p}`);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case "log.info":
|
||||
if (Array.isArray(p)) {
|
||||
// Need to cast from any[] to unknown[]
|
||||
logInfo(logTag, ...(p as unknown[]));
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case "log.debugString":
|
||||
if (typeof p == "string") {
|
||||
logDebug(() => `${logTag} ${p}`);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ente's logger.
|
||||
*
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import pathToFfmpeg from "ffmpeg-static";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import type { ZipItem } from "../../types/ipc";
|
||||
import { ensure } from "../utils/common";
|
||||
import path, { basename } from "node:path";
|
||||
import type { FFmpegCommand, ZipItem } from "../../types/ipc";
|
||||
import log from "../log";
|
||||
import { execAsync } from "../utils/electron";
|
||||
import {
|
||||
deleteTempFileIgnoringErrors,
|
||||
makeFileForDataOrPathOrZipItem,
|
||||
makeFileForDataOrStreamOrPathOrZipItem,
|
||||
makeTempFilePath,
|
||||
} from "../utils/temp";
|
||||
|
||||
@@ -42,7 +44,7 @@ const outputPathPlaceholder = "OUTPUT";
|
||||
* But I'm not sure if our code is supposed to be able to use it, and how.
|
||||
*/
|
||||
export const ffmpegExec = async (
|
||||
command: string[],
|
||||
command: FFmpegCommand,
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
outputFileExtension: string,
|
||||
): Promise<Uint8Array> => {
|
||||
@@ -50,14 +52,23 @@ export const ffmpegExec = async (
|
||||
path: inputFilePath,
|
||||
isFileTemporary: isInputFileTemporary,
|
||||
writeToTemporaryFile: writeToTemporaryInputFile,
|
||||
} = await makeFileForDataOrPathOrZipItem(dataOrPathOrZipItem);
|
||||
} = await makeFileForDataOrStreamOrPathOrZipItem(dataOrPathOrZipItem);
|
||||
|
||||
const outputFilePath = await makeTempFilePath(outputFileExtension);
|
||||
try {
|
||||
await writeToTemporaryInputFile();
|
||||
|
||||
let resolvedCommand: string[];
|
||||
if (Array.isArray(command)) {
|
||||
resolvedCommand = command;
|
||||
} else {
|
||||
const isHDR = await isHDRVideo(inputFilePath);
|
||||
log.debug(() => [basename(inputFilePath), { isHDR }]);
|
||||
resolvedCommand = isHDR ? command.hdr : command.default;
|
||||
}
|
||||
|
||||
const cmd = substitutePlaceholders(
|
||||
command,
|
||||
resolvedCommand,
|
||||
inputFilePath,
|
||||
outputFilePath,
|
||||
);
|
||||
@@ -99,18 +110,17 @@ const ffmpegBinaryPath = () => {
|
||||
// This substitution of app.asar by app.asar.unpacked is suggested by the
|
||||
// ffmpeg-static library author themselves:
|
||||
// https://github.com/eugeneware/ffmpeg-static/issues/16
|
||||
return ensure(pathToFfmpeg).replace("app.asar", "app.asar.unpacked");
|
||||
return pathToFfmpeg!.replace("app.asar", "app.asar.unpacked");
|
||||
};
|
||||
|
||||
/**
|
||||
* A variant of {@link ffmpegExec} adapted to work with streams so that it can
|
||||
* handle the MP4 conversion of large video files.
|
||||
*
|
||||
* See: [Note: Convert to MP4]
|
||||
|
||||
* @param inputFilePath The path to a file on the user's local file system. This
|
||||
* is the video we want to convert.
|
||||
* @param inputFilePath The path to a file on the user's local file system where
|
||||
*
|
||||
* @param outputFilePath The path to a file on the user's local file system where
|
||||
* we should write the converted MP4 video.
|
||||
*/
|
||||
export const ffmpegConvertToMP4 = async (
|
||||
@@ -130,3 +140,539 @@ export const ffmpegConvertToMP4 = async (
|
||||
|
||||
await execAsync(cmd);
|
||||
};
|
||||
|
||||
export interface FFmpegGenerateHLSPlaylistAndSegmentsResult {
|
||||
playlistPath: string;
|
||||
videoPath: string;
|
||||
dimensions: { width: number; height: number };
|
||||
videoSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A bespoke variant of {@link ffmpegExec} for generation of HLS playlists for
|
||||
* videos.
|
||||
*
|
||||
* Overview of the cases:
|
||||
*
|
||||
* H.264, <= 10 MB - Skip
|
||||
* H.264, <= 4000 kb/s bitrate - Don't re-encode video stream
|
||||
* BT.709, <= 2000 kb/s bitrate - Don't apply the scale+fps filter
|
||||
* !BT.709 - Apply tonemap (zscale+tonemap+zscale)
|
||||
*
|
||||
* Example invocation:
|
||||
*
|
||||
* ffmpeg -i in.mov -vf 'scale=-2:720,fps=30,zscale=transfer=linear,tonemap=tonemap=hable:desat=0,zscale=primaries=709:transfer=709:matrix=709,format=yuv420p' -c:v libx264 -c:a aac -f hls -hls_key_info_file out.m3u8.info -hls_list_size 0 -hls_flags single_file out.m3u8
|
||||
*
|
||||
* See: [Note: Preview variant of videos]
|
||||
*
|
||||
* @param inputFilePath The path to a file on the user's local file system. This
|
||||
* is the video we want to generate an streamable HLS playlist for.
|
||||
*
|
||||
* @param outputPathPrefix The path to unique, unused and temporary prefix on
|
||||
* the user's local file system. This function will write the generated HLS
|
||||
* playlist and video segments under this prefix.
|
||||
*
|
||||
* @returns The paths to two files on the user's local file system - one
|
||||
* containing the generated HLS playlist, and the other containing the
|
||||
* transcoded and encrypted video segments that the HLS playlist refers to.
|
||||
*
|
||||
* If the video is such that it doesn't require stream generation, then this
|
||||
* function returns `undefined`.
|
||||
*/
|
||||
export const ffmpegGenerateHLSPlaylistAndSegments = async (
|
||||
inputFilePath: string,
|
||||
outputPathPrefix: string,
|
||||
): Promise<FFmpegGenerateHLSPlaylistAndSegmentsResult | undefined> => {
|
||||
const { isH264, isBT709, bitrate } =
|
||||
await detectVideoCharacteristics(inputFilePath);
|
||||
|
||||
log.debug(() => [basename(inputFilePath), { isH264, isBT709, bitrate }]);
|
||||
|
||||
// If the video is smaller than 10 MB, and already H.264 (the codec we are
|
||||
// going to use for the conversion), then a streaming variant is not much
|
||||
// use. Skip such cases.
|
||||
//
|
||||
// ---
|
||||
//
|
||||
// [Note: HEVC/H.265 issues]
|
||||
//
|
||||
// We've observed two issues out in the wild with HEVC videos:
|
||||
//
|
||||
// 1. On Linux, HEVC video streams don't play. However, since the audio
|
||||
// stream plays, the browser tells us that the "video" itself is
|
||||
// playable, but the user sees a blank screen with only audio.
|
||||
//
|
||||
// 2. HEVC + HDR videos taken on an iPhone have a rotation (`Side data:
|
||||
// displaymatrix` in the ffmpeg output) that Chrome (and thus Electron)
|
||||
// doesn't take into account, so these play upside down.
|
||||
//
|
||||
// Not fully related to this case, but mentioning here as to why both the
|
||||
// size and codec need to be checked before skipping stream generation.
|
||||
if (isH264) {
|
||||
const inputVideoSize = await fs
|
||||
.stat(inputFilePath)
|
||||
.then((st) => st.size);
|
||||
if (inputVideoSize <= 10 * 1024 * 1024 /* 10 MB */) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// If the video is already H.264 with a bitrate less than 4000 kbps, then we
|
||||
// do not need to reencode the video stream (by _far_ the costliest part of
|
||||
// the HLS stream generation).
|
||||
const reencodeVideo = !(isH264 && bitrate && bitrate <= 4000 * 1000);
|
||||
|
||||
// If the bitrate is not too high, then we don't need to rescale the video
|
||||
// when generating the video stream. This is not a performance optimization,
|
||||
// but more for avoiding making the video size smaller unnecessarily.
|
||||
const rescaleVideo = !(bitrate && bitrate <= 2000 * 1000);
|
||||
|
||||
// [Note: Tonemapping HDR to HD]
|
||||
//
|
||||
// BT.709 ("HD") is a standard that describes things like how color is
|
||||
// encoded, the range of values, and their "meaning" - i.e. how to map the
|
||||
// values in the video to the pixels on the screen.
|
||||
//
|
||||
// It is not the only such standard, there are three common examples:
|
||||
//
|
||||
// - BT.601 ("Standard-Definition" or SD)
|
||||
// - BT.709 ("High-Definition" or HD)
|
||||
// - BT.2020 ("Ultra-High-Definition" or UHD, aka HDR^).
|
||||
//
|
||||
// ^ HDR ("High-Dynamic-Range") is an addendum to BT.2020, but for our
|
||||
// purpose here we can treat it as as alias.
|
||||
//
|
||||
// BT.709 is the most common amongst these for older files out stored on
|
||||
// computers, and they conform mostly to the standard (one notable exception
|
||||
// is that the BT.709 standard also recommends using the yuv422p pixel
|
||||
// format, but de facto yuv420p is used because many video players only
|
||||
// support yuv420p).
|
||||
//
|
||||
// Since BT.709 is the most widely supported standard, we use it when
|
||||
// generating the HLS playlist so to allow playback across the widest
|
||||
// possible hardware/OS/browser combinations.
|
||||
//
|
||||
// If we convert HDR to HD without naively, then the colors look washed out
|
||||
// compared to the original. To resolve this, we use a ffmpeg filterchain
|
||||
// that uses the tonemap filter.
|
||||
//
|
||||
// However applying this tonemap to videos that are already HD leads to a
|
||||
// brightness drop. So we conditionally apply this filter chain only if the
|
||||
// colorspace is not already BT.709.
|
||||
//
|
||||
// See also: [Note: Alternative FFmpeg command for HDR videos], although
|
||||
// that uses a allow-list based check (while here we use deny-list).
|
||||
//
|
||||
// Reference:
|
||||
// - https://trac.ffmpeg.org/wiki/colorspace
|
||||
const tonemap = !isBT709;
|
||||
|
||||
// We want the generated playlist to refer to the chunks as "output.ts".
|
||||
//
|
||||
// So we arrange things accordingly: We use the `outputPathPrefix` as our
|
||||
// working directory, and then ask ffmpeg to generate a playlist with the
|
||||
// name "output.m3u8".
|
||||
//
|
||||
// ffmpeg will automatically place the segments in a file with the same base
|
||||
// name as the playlist, but with a ".ts" extension. And since we use the
|
||||
// "single_file" option, all the segments will be placed in a file named
|
||||
// "output.ts".
|
||||
|
||||
await fs.mkdir(outputPathPrefix);
|
||||
|
||||
const playlistPath = path.join(outputPathPrefix, "output.m3u8");
|
||||
const videoPath = path.join(outputPathPrefix, "output.ts");
|
||||
|
||||
// Generate a cryptographically secure random key (16 bytes).
|
||||
const keyBytes = randomBytes(16);
|
||||
const keyB64 = keyBytes.toString("base64");
|
||||
|
||||
// Convert it to a data: URI that will be added to the playlist.
|
||||
const keyURI = `data:text/plain;base64,${keyB64}`;
|
||||
|
||||
// Determine two paths - one where we will write the key itself, and where
|
||||
// we will write the "key info" that provides ffmpeg the `keyURI` and the
|
||||
// `keyPath;.
|
||||
const keyPath = playlistPath + ".key";
|
||||
const keyInfoPath = playlistPath + ".key-info";
|
||||
|
||||
// Generate a "key info":
|
||||
//
|
||||
// - the first line specifies the key URI that is written into the playlist.
|
||||
// - the second line specifies the path to the local file system file from
|
||||
// where ffmpeg should read the key.
|
||||
const keyInfo = [keyURI, keyPath].join("\n");
|
||||
|
||||
// Overview:
|
||||
//
|
||||
// - Video H.264 HD 720p 30fps.
|
||||
// - Audio AAC 128kbps.
|
||||
// - Encrypted HLS playlist with a single file containing all the chunks.
|
||||
//
|
||||
// Reference:
|
||||
// - `man ffmpeg-all`
|
||||
// - https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||
//
|
||||
const command = [
|
||||
ffmpegBinaryPath(),
|
||||
// Reduce the amount of output lines we have to parse.
|
||||
["-hide_banner"],
|
||||
// Input file. We don't need any extra options that apply to the input file.
|
||||
"-i",
|
||||
inputFilePath,
|
||||
// The remaining options apply to the next output file (`playlistPath`).
|
||||
reencodeVideo
|
||||
? [
|
||||
// `-vf` creates a filter graph for the video stream. It is a
|
||||
// comma separated list of filters chained together, e.g.
|
||||
// `filter1=key=value:key=value.filter2=key=value`.
|
||||
"-vf",
|
||||
[
|
||||
// Do the rescaling to even number of pixels always if the
|
||||
// tonemapping is going to be applied subsequently,
|
||||
// otherwise the tonemapping will fail with "image
|
||||
// dimensions must be divisible by subsampling factor".
|
||||
//
|
||||
// While we add the extra condition here for completeness,
|
||||
// it won't usually matter since a non-BT.709 video is
|
||||
// likely using a new codec, and as such would've a high
|
||||
// enough bitrate to require rescaling anyways.
|
||||
rescaleVideo || tonemap
|
||||
? [
|
||||
// Scales the video to maximum 720p height,
|
||||
// keeping aspect ratio and the calculated
|
||||
// dimension divisible by 2 (some of the other
|
||||
// operations require an even pixel count).
|
||||
"scale=-2:720",
|
||||
// Convert the video to a constant 30 fps,
|
||||
// duplicating or dropping frames as necessary.
|
||||
"fps=30",
|
||||
]
|
||||
: [],
|
||||
// Convert the colorspace if the video is not in the HD
|
||||
// color space (bt709). Before conversion, tone map colors
|
||||
// so that they work the same across the change in the
|
||||
// dyamic range.
|
||||
//
|
||||
// 1. The tonemap filter only works linear light, so we
|
||||
// first use zscale with transfer=linear to linearize
|
||||
// the input.
|
||||
//
|
||||
// 2. Then we use the tonemap, with the hable option that
|
||||
// is best for preserving details. desat=0 turns off
|
||||
// the default desaturation.
|
||||
//
|
||||
// 3. Use zscale again to "convert to BT.709" by asking it
|
||||
// to set the all three of color primaries, transfer
|
||||
// characteristics and colorspace matrix to 709 (Note:
|
||||
// the constants specified in the tonemap filter help
|
||||
// do not include the "bt" prefix)
|
||||
//
|
||||
// See: https://ffmpeg.org/ffmpeg-filters.html#tonemap-1
|
||||
//
|
||||
// See: [Note: Tonemapping HDR to HD]
|
||||
tonemap
|
||||
? [
|
||||
"zscale=transfer=linear",
|
||||
"tonemap=tonemap=hable:desat=0",
|
||||
"zscale=primaries=709:transfer=709:matrix=709",
|
||||
]
|
||||
: [],
|
||||
// Output using the well supported pixel format: 8-bit YUV
|
||||
// planar color space with 4:2:0 chroma subsampling.
|
||||
"format=yuv420p",
|
||||
]
|
||||
.flat()
|
||||
.join(","),
|
||||
]
|
||||
: [],
|
||||
reencodeVideo
|
||||
? // Video codec H.264
|
||||
//
|
||||
// - `-c:v libx264` converts the video stream to the H.264 codec.
|
||||
//
|
||||
// - We don't supply a bitrate, instead it uses the default CRF
|
||||
// ("23") as recommended in the ffmpeg trac.
|
||||
//
|
||||
// - We don't supply a preset, it'll use the default ("medium").
|
||||
["-c:v", "libx264"]
|
||||
: // Keep the video stream unchanged
|
||||
["-c:v", "copy"],
|
||||
// Audio codec AAC
|
||||
//
|
||||
// - `-c:a aac` converts the audio stream to use the AAC codec
|
||||
//
|
||||
// - We don't supply a bitrate, it'll use the AAC default 128k bps.
|
||||
["-c:a", "aac"],
|
||||
// Generate a HLS playlist.
|
||||
["-f", "hls"],
|
||||
// Tell ffmpeg where to find the key, and the URI for the key to write
|
||||
// into the generated playlist. Implies "-hls_enc 1".
|
||||
["-hls_key_info_file", keyInfoPath],
|
||||
// Generate as many playlist entries as needed (default limit is 5).
|
||||
["-hls_list_size", "0"],
|
||||
// Place all the video segments within the same .ts file (with the same
|
||||
// path as the playlist file but with a ".ts" extension).
|
||||
["-hls_flags", "single_file"],
|
||||
// Output path where the playlist should be generated.
|
||||
playlistPath,
|
||||
].flat();
|
||||
|
||||
let dimensions: ReturnType<typeof detectVideoDimensions>;
|
||||
let videoSize: number;
|
||||
|
||||
try {
|
||||
// Write the key and the keyInfo to their desired paths.
|
||||
await Promise.all([
|
||||
fs.writeFile(keyPath, keyBytes),
|
||||
fs.writeFile(keyInfoPath, keyInfo, { encoding: "utf8" }),
|
||||
]);
|
||||
|
||||
// Run the ffmpeg command to generate the HLS playlist and segments.
|
||||
//
|
||||
// Note: Depending on the size of the input file, this may take long!
|
||||
const { stderr: conversionStderr } = await execAsync(command);
|
||||
|
||||
// Determine the dimensions of the generated video from the stderr
|
||||
// output produced by ffmpeg during the conversion.
|
||||
dimensions = detectVideoDimensions(conversionStderr);
|
||||
|
||||
// Find the size of the generated video segments by reading the size of
|
||||
// the generated .ts file.
|
||||
videoSize = await fs.stat(videoPath).then((st) => st.size);
|
||||
} catch (e) {
|
||||
log.error("HLS generation failed", e);
|
||||
await Promise.all([
|
||||
deleteTempFileIgnoringErrors(playlistPath),
|
||||
deleteTempFileIgnoringErrors(videoPath),
|
||||
]);
|
||||
throw e;
|
||||
} finally {
|
||||
await Promise.all([
|
||||
deleteTempFileIgnoringErrors(keyInfoPath),
|
||||
deleteTempFileIgnoringErrors(keyPath),
|
||||
// ffmpeg writes a /path/output.ts.tmp, clear it out too.
|
||||
deleteTempFileIgnoringErrors(videoPath + ".tmp"),
|
||||
]);
|
||||
}
|
||||
|
||||
return { playlistPath, videoPath, dimensions, videoSize };
|
||||
};
|
||||
|
||||
/**
|
||||
* A regex that matches the first line of the form
|
||||
*
|
||||
* Stream #0:0: Video: h264 (High 10) ([27][0][0][0] / 0x001B), yuv420p10le(tv, bt2020nc/bt2020/arib-std-b67), 1920x1080, 30 fps, 30 tbr, 90k tbn
|
||||
*
|
||||
* The part after Video: is the first capture group.
|
||||
*
|
||||
* Another example:
|
||||
*
|
||||
* Stream #0:1[0x2](und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p(progressive), 480x270 [SAR 1:1 DAR 16:9], 539 kb/s, 29.97 fps, 29.97 tbr, 30k tbn (default)
|
||||
*/
|
||||
const videoStreamLineRegex = /Stream #.+: Video:(.+)\n/;
|
||||
|
||||
/** {@link videoStreamLineRegex}, but global. */
|
||||
const videoStreamLinesRegex = /Stream #.+: Video:(.+)\n/g;
|
||||
|
||||
/**
|
||||
* A regex that matches "<digits> kb/s" preceded by a space. See
|
||||
* {@link videoStreamLineRegex} for the context in which it is used.
|
||||
*/
|
||||
const videoBitrateRegex = / ([1-9]\d*) kb\/s/;
|
||||
|
||||
/**
|
||||
* A regex that matches <digits>x<digits> pair preceded by a space. See
|
||||
* {@link videoStreamLineRegex} for the context in which it is used.
|
||||
*
|
||||
* We constrain the digit sequence not to begin with 0 to exclude hexadecimal
|
||||
* representations of various constants that ffmpeg prints on this line (e.g.
|
||||
* "avc1 / 0x31637661").
|
||||
*/
|
||||
const videoDimensionsRegex = / ([1-9]\d*)x([1-9]\d*)/;
|
||||
|
||||
interface VideoCharacteristics {
|
||||
isH264: boolean;
|
||||
isBT709: boolean;
|
||||
bitrate: number | undefined;
|
||||
}
|
||||
/**
|
||||
* Heuristically determine information about the video at the given
|
||||
* {@link inputFilePath}:
|
||||
*
|
||||
* - If is encoded using H.264 codec.
|
||||
* - If it uses the BT.709 colorspace.
|
||||
* - Its bitrate.
|
||||
*
|
||||
* The defaults are tailored for the cases in which these conditions are used,
|
||||
* so that even if we get the detection wrong we'll only end up encoding videos
|
||||
* that could've possibly been skipped as an optimization.
|
||||
*
|
||||
* [Note: Parsing CLI output might break on ffmpeg updates]
|
||||
*
|
||||
* This function tries to determine the these bits of information about the
|
||||
* given video by scanning the ffmpeg info output for the video stream line, and
|
||||
* doing various string matches and regex extractions.
|
||||
*
|
||||
* Needless to say, while this works currently, this is liable to break in the
|
||||
* future. So if something stops working after updating ffmpeg, look here!
|
||||
*
|
||||
* Ideally, we'd have done this using `ffprobe`, but we don't have the ffprobe
|
||||
* binary at hand, so we make do by grepping the log output of ffmpeg.
|
||||
*
|
||||
* For reference,
|
||||
*
|
||||
* - codec and colorspace are printed by the `avcodec_string` function in the
|
||||
* ffmpeg source:
|
||||
* https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/avcodec.c
|
||||
*
|
||||
* - bitrate is printed by the `dump_stream_format` function in `dump.c`.
|
||||
*/
|
||||
const detectVideoCharacteristics = async (inputFilePath: string) => {
|
||||
const videoInfo = await pseudoFFProbeVideo(inputFilePath);
|
||||
const videoStreamLine = videoStreamLineRegex.exec(videoInfo)?.at(1)?.trim();
|
||||
|
||||
// Since the checks are heuristic, start with defaults that would cause the
|
||||
// codec conversion to happen, even if it is unnecessary.
|
||||
const res: VideoCharacteristics = {
|
||||
isH264: false,
|
||||
isBT709: false,
|
||||
bitrate: undefined,
|
||||
};
|
||||
if (!videoStreamLine) return res;
|
||||
|
||||
res.isH264 = videoStreamLine.startsWith("h264 ");
|
||||
res.isBT709 = videoStreamLine.includes("bt709");
|
||||
// The regex matches "\d kb/s", but there can be other units for the
|
||||
// bitrate. However, (a) "kb/s" is the most common for videos out in the
|
||||
// wild, and (b) even if we guess wrong it we'll just do "-v:c x264" instead
|
||||
// of "-v:c copy", so only unnecessary processing but no change in output.
|
||||
const brs = videoBitrateRegex.exec(videoStreamLine)?.at(0);
|
||||
if (brs) {
|
||||
const br = parseInt(brs, 10);
|
||||
if (br) res.bitrate = br;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Heuristically detect the dimensions of the given video from the log output of
|
||||
* the ffmpeg invocation during the HLS playlist generation.
|
||||
*
|
||||
* This function tries to determine the width and height of the generated video
|
||||
* from the output log written by ffmpeg on its stderr during the generation
|
||||
* process, scanning it for the last video stream line, and trying to match a
|
||||
* "<digits>x<digits>" regex.
|
||||
*
|
||||
* See: [Note: Parsing CLI output might break on ffmpeg updates].
|
||||
*/
|
||||
const detectVideoDimensions = (conversionStderr: string) => {
|
||||
// There is a nicer way to do it - by running `pseudoFFProbeVideo` on the
|
||||
// generated playlist. However, that playlist includes a data URL that
|
||||
// specifies the encryption info, and ffmpeg refuses to read that unless we
|
||||
// specify the "-allowed_extensions ALL" or something to that effect.
|
||||
//
|
||||
// Unfortunately, our current ffmpeg binary (5.x) does not support that
|
||||
// option. So we instead parse the conversion output itself.
|
||||
//
|
||||
// This is also nice, since it saves on an extra ffmpeg invocation. But we
|
||||
// now need to be careful to find the right video stream line, since the
|
||||
// conversion output includes both the input and output video stream lines.
|
||||
//
|
||||
// To match the right (output) video stream line, we use a global regex, and
|
||||
// use the last match since that'd correspond to the single video stream
|
||||
// written in the output.
|
||||
const videoStreamLine = Array.from(
|
||||
conversionStderr.matchAll(videoStreamLinesRegex),
|
||||
)
|
||||
.at(-1) /* Last Stream...: Video: line in the output */
|
||||
?.at(1); /* First capture group */
|
||||
if (videoStreamLine) {
|
||||
const [, ws, hs] = videoDimensionsRegex.exec(videoStreamLine) ?? [];
|
||||
if (ws && hs) {
|
||||
const w = parseInt(ws, 10);
|
||||
const h = parseInt(hs, 10);
|
||||
if (w && h) {
|
||||
return { width: w, height: h };
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`Unable to detect video dimensions from stream line [${videoStreamLine ?? ""}]`,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Heuristically detect if the file at given path is a HDR video.
|
||||
*
|
||||
* This is similar to {@link detectVideoCharacteristics}, and see that
|
||||
* function's documentation for all the caveats. However, this function uses an
|
||||
* allow-list instead, and considers any file with color transfer "smpte2084" or
|
||||
* "arib-std-b67" to be HDR. While this is in some sense a more exact check, it
|
||||
* comes with different caveats:
|
||||
*
|
||||
* - These particular constants are not guaranteed to be correct; these are just
|
||||
* what I saw on the internet as being used / recommended for detecting HDR.
|
||||
*
|
||||
* - Since we don't have ffprobe, we're not checking the color space value
|
||||
* itself but a substring of the stream line in the ffmpeg stderr output.
|
||||
*
|
||||
* In particular, we use this more exact check for places where we have less
|
||||
* leeway. e.g. when generating thumbnails, if we apply the tonemapping to any
|
||||
* non-BT.709 file (as the HLS stream generation does), we start getting the
|
||||
* "code 3074: no path between colorspaces" error during the JPEG conversion
|
||||
* (this is not a problem in the H.264 conversion).
|
||||
*
|
||||
* - See: [Note: Alternative FFmpeg command for HDR videos]
|
||||
* - See: [Note: Tonemapping HDR to HD]
|
||||
*
|
||||
* @param inputFilePath The path to a video file on the user's machine.
|
||||
*
|
||||
* @returns `true` if this file is likely a HDR video. Exceptions are treated as
|
||||
* `false` to make this function safe to invoke without breaking the happy path.
|
||||
*/
|
||||
const isHDRVideo = async (inputFilePath: string) => {
|
||||
try {
|
||||
const videoInfo = await pseudoFFProbeVideo(inputFilePath);
|
||||
const vs = videoStreamLineRegex.exec(videoInfo)?.at(1);
|
||||
if (!vs) return false;
|
||||
return vs.includes("smpte2084") || vs.includes("arib-std-b67");
|
||||
} catch (e) {
|
||||
log.warn(`Could not detect HDR status of ${inputFilePath}`, e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the stderr of ffmpeg in an attempt to gain information about the video
|
||||
* at the given {@link inputFilePath}.
|
||||
*
|
||||
* We don't have the ffprobe binary at hand, which is why we need to use this
|
||||
* alternative. See: [Note: Parsing CLI output might break on ffmpeg updates]
|
||||
*
|
||||
* @returns the stderr of ffmpeg after running it on the input file. The exact
|
||||
* command we run is:
|
||||
*
|
||||
* ffmpeg -i in.mov -an -frames:v 0 -f null - 2>info.txt
|
||||
*
|
||||
* And the returned string is the contents of the `info.txt` thus produced.
|
||||
*/
|
||||
const pseudoFFProbeVideo = async (inputFilePath: string) => {
|
||||
const command = [
|
||||
ffmpegPathPlaceholder,
|
||||
// Reduce the amount of output lines we have to parse.
|
||||
["-hide_banner"],
|
||||
["-i", inputPathPlaceholder],
|
||||
"-an",
|
||||
["-frames:v", "0"],
|
||||
["-f", "null"],
|
||||
"-",
|
||||
].flat();
|
||||
|
||||
const cmd = substitutePlaceholders(command, inputFilePath, /* NA */ "");
|
||||
|
||||
const { stderr } = await execAsync(cmd);
|
||||
|
||||
return stderr;
|
||||
};
|
||||
|
||||
@@ -36,6 +36,17 @@ export const fsIsDir = async (dirPath: string) => {
|
||||
return stat.isDirectory();
|
||||
};
|
||||
|
||||
export const fsStatMtime = (path: string) =>
|
||||
// [Note: Integral last modified time]
|
||||
//
|
||||
// Whenever we need to find the modified time of a file, use the
|
||||
// `mtime.getTime()` instead of `mtimeMs` of the stat; this way, it is
|
||||
// guaranteed that the times are integral (we persist these values to remote
|
||||
// in some cases, and the contract is for them to be integral; mtimeMs is a
|
||||
// float with sub-millisecond precision), and that all places use the same
|
||||
// value so that they're comparable.
|
||||
fs.stat(path).then((st) => st.mtime.getTime());
|
||||
|
||||
export const fsFindFiles = async (dirPath: string) => {
|
||||
const items = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
let paths: string[] = [];
|
||||
|
||||
@@ -6,7 +6,7 @@ import { type ZipItem } from "../../types/ipc";
|
||||
import { execAsync, isDev } from "../utils/electron";
|
||||
import {
|
||||
deleteTempFileIgnoringErrors,
|
||||
makeFileForDataOrPathOrZipItem,
|
||||
makeFileForDataOrStreamOrPathOrZipItem,
|
||||
makeTempFilePath,
|
||||
} from "../utils/temp";
|
||||
|
||||
@@ -69,7 +69,7 @@ export const generateImageThumbnail = async (
|
||||
path: inputFilePath,
|
||||
isFileTemporary: isInputFileTemporary,
|
||||
writeToTemporaryFile: writeToTemporaryInputFile,
|
||||
} = await makeFileForDataOrPathOrZipItem(dataOrPathOrZipItem);
|
||||
} = await makeFileForDataOrStreamOrPathOrZipItem(dataOrPathOrZipItem);
|
||||
|
||||
const outputFilePath = await makeTempFilePath("jpeg");
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FSWatcher } from "chokidar";
|
||||
import log from "../log";
|
||||
import { clearConvertToMP4Results } from "../stream";
|
||||
import { clearPendingVideoResults } from "../stream";
|
||||
import { clearStores } from "./store";
|
||||
import { watchReset } from "./watch";
|
||||
import { clearOpenZipCache } from "./zip";
|
||||
@@ -22,9 +22,9 @@ export const logout = (watcher: FSWatcher) => {
|
||||
ignoreError("FS watch", e);
|
||||
}
|
||||
try {
|
||||
clearConvertToMP4Results();
|
||||
clearPendingVideoResults();
|
||||
} catch (e) {
|
||||
ignoreError("convert-to-mp4", e);
|
||||
ignoreError("video", e);
|
||||
}
|
||||
try {
|
||||
clearStores();
|
||||
|
||||
@@ -15,46 +15,13 @@ import { existsSync } from "fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import * as ort from "onnxruntime-node";
|
||||
import log from "../log-worker";
|
||||
import { messagePortMainEndpoint } from "../utils/comlink";
|
||||
import { ensure, wait } from "../utils/common";
|
||||
import { wait } from "../utils/common";
|
||||
import { writeStream } from "../utils/stream";
|
||||
import { fsStatMtime } from "./fs";
|
||||
|
||||
/**
|
||||
* We cannot do
|
||||
*
|
||||
* import log from "../log";
|
||||
*
|
||||
* because that requires the Electron APIs that are not available to a utility
|
||||
* process (See: [Note: Using Electron APIs in UtilityProcess]). But even if
|
||||
* that were to work, logging will still be problematic since we'd try opening
|
||||
* the log file from two different Node.js processes (this one, and the main
|
||||
* one), and I didn't find any indication in the electron-log repository that
|
||||
* the log file's integrity would be maintained in such cases.
|
||||
*
|
||||
* So instead we create this proxy log object that uses `process.parentPort` to
|
||||
* transport the logs over to the main process.
|
||||
*/
|
||||
const log = {
|
||||
/**
|
||||
* Unlike the real {@link log.error}, this accepts only the first string
|
||||
* argument, not the second optional error one.
|
||||
*/
|
||||
errorString: (s: string) => mainProcess("log.errorString", s),
|
||||
info: (...ms: unknown[]) => mainProcess("log.info", ms),
|
||||
/**
|
||||
* Unlike the real {@link log.debug}, this is (a) eagerly evaluated, and (b)
|
||||
* accepts only strings.
|
||||
*/
|
||||
debugString: (s: string) => mainProcess("log.debugString", s),
|
||||
};
|
||||
|
||||
/**
|
||||
* Send a message to the main process using a barebones RPC protocol.
|
||||
*/
|
||||
const mainProcess = (method: string, param: unknown) =>
|
||||
process.parentPort.postMessage({ method, p: param });
|
||||
|
||||
log.debugString(`Started ML worker process`);
|
||||
log.debugString("Started ML utility process");
|
||||
|
||||
process.parentPort.once("message", (e) => {
|
||||
// Initialize ourselves with the data we got from our parent.
|
||||
@@ -63,12 +30,13 @@ process.parentPort.once("message", (e) => {
|
||||
// parent.
|
||||
expose(
|
||||
{
|
||||
fsStatMtime,
|
||||
computeCLIPImageEmbedding,
|
||||
computeCLIPTextEmbeddingIfAvailable,
|
||||
detectFaces,
|
||||
computeFaceEmbeddings,
|
||||
},
|
||||
messagePortMainEndpoint(ensure(e.ports[0])),
|
||||
messagePortMainEndpoint(e.ports[0]!),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -80,7 +48,7 @@ process.parentPort.once("message", (e) => {
|
||||
let _userDataPath: string | undefined;
|
||||
|
||||
/** Equivalent to app.getPath("userData") */
|
||||
const userDataPath = () => ensure(_userDataPath);
|
||||
const userDataPath = () => _userDataPath!;
|
||||
|
||||
const parseInitData = (data: unknown) => {
|
||||
if (
|
||||
@@ -91,7 +59,7 @@ const parseInitData = (data: unknown) => {
|
||||
) {
|
||||
_userDataPath = data.userDataPath;
|
||||
} else {
|
||||
log.errorString("Unparseable initialization data");
|
||||
log.error("Unparseable initialization data");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -159,7 +127,7 @@ const modelPathDownloadingIfNeeded = async (
|
||||
} else {
|
||||
const size = (await fs.stat(modelPath)).size;
|
||||
if (size !== expectedByteSize) {
|
||||
log.errorString(
|
||||
log.error(
|
||||
`The size ${size} of model ${modelName} does not match the expected size, downloading again`,
|
||||
);
|
||||
await downloadModel(modelPath, modelName);
|
||||
@@ -250,7 +218,7 @@ export const computeCLIPImageEmbedding = async (
|
||||
const results = await session.run(feeds);
|
||||
log.debugString(`ONNX/CLIP image embedding took ${Date.now() - t} ms`);
|
||||
/* Need these model specific casts to type the result */
|
||||
return ensure(results.output).data as Float32Array;
|
||||
return results.output!.data as Float32Array;
|
||||
};
|
||||
|
||||
const cachedCLIPTextSession = makeCachedInferenceSession(
|
||||
@@ -290,7 +258,7 @@ export const computeCLIPTextEmbeddingIfAvailable = async (text: string) => {
|
||||
const t = Date.now();
|
||||
const results = await session.run(feeds);
|
||||
log.debugString(`ONNX/CLIP text embedding took ${Date.now() - t} ms`);
|
||||
return ensure(results.output).data as Float32Array;
|
||||
return results.output!.data as Float32Array;
|
||||
};
|
||||
|
||||
const cachedFaceDetectionSession = makeCachedInferenceSession(
|
||||
@@ -311,7 +279,7 @@ export const detectFaces = async (
|
||||
const t = Date.now();
|
||||
const results = await session.run(feeds);
|
||||
log.debugString(`ONNX/YOLO face detection took ${Date.now() - t} ms`);
|
||||
return ensure(results.output).data;
|
||||
return results.output!.data;
|
||||
};
|
||||
|
||||
const cachedFaceEmbeddingSession = makeCachedInferenceSession(
|
||||
|
||||
@@ -135,22 +135,35 @@ export const setPendingUploads = ({
|
||||
});
|
||||
};
|
||||
|
||||
export const markUploadedFiles = (paths: string[]) => {
|
||||
export const markUploadedFile = (
|
||||
path: string,
|
||||
associatedPath: string | undefined,
|
||||
) => {
|
||||
const existing = uploadStatusStore.get("filePaths") ?? [];
|
||||
const updated = existing.filter((p) => !paths.includes(p));
|
||||
const updated = existing.filter((p) => p != path && p != associatedPath);
|
||||
uploadStatusStore.set("filePaths", updated);
|
||||
// See: [Note: Integral last modified time]
|
||||
return fs.stat(path).then((st) => st.mtime.getTime());
|
||||
};
|
||||
|
||||
export const markUploadedZipItems = (
|
||||
items: [zipPath: string, entryName: string][],
|
||||
export const markUploadedZipItem = (
|
||||
item: ZipItem,
|
||||
associatedItem: ZipItem | undefined,
|
||||
) => {
|
||||
const existing = uploadStatusStore.get("zipItems") ?? [];
|
||||
const updated = existing.filter(
|
||||
(z) => !items.some((e) => z[0] == e[0] && z[1] == e[1]),
|
||||
const updated = exceptZipItem(
|
||||
exceptZipItem(existing, item),
|
||||
associatedItem,
|
||||
);
|
||||
uploadStatusStore.set("zipItems", updated);
|
||||
return fs.stat(item[0]).then((st) => st.mtime.getTime());
|
||||
};
|
||||
|
||||
const exceptZipItem = (items: ZipItem[], item: ZipItem | undefined) =>
|
||||
item
|
||||
? items.filter((zi) => !(zi[0] == item[0] && zi[1] == item[1]))
|
||||
: items;
|
||||
|
||||
export const clearPendingUploads = () => {
|
||||
uploadStatusStore.clear();
|
||||
clearOpenZipCache();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/**
|
||||
* @file ML related functionality. This code runs in the main process.
|
||||
* @file This main process code and interface for dealing with the various
|
||||
* utility processes that we create.
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -9,13 +10,14 @@ import {
|
||||
} from "electron";
|
||||
import { app, utilityProcess } from "electron/main";
|
||||
import path from "node:path";
|
||||
import log from "../log";
|
||||
import type { UtilityProcessType } from "../../types/ipc";
|
||||
import log, { processUtilityProcessLogMessage } from "../log";
|
||||
|
||||
/** The active ML worker (utility) process, if any. */
|
||||
/** The active ML utility process, if any. */
|
||||
let _child: UtilityProcess | undefined;
|
||||
|
||||
/**
|
||||
* Create a new ML worker process, terminating the older ones (if any).
|
||||
* Create a new ML utility process, terminating the older ones (if any).
|
||||
*
|
||||
* [Note: ML IPC]
|
||||
*
|
||||
@@ -36,7 +38,7 @@ let _child: UtilityProcess | undefined;
|
||||
* does not forward events to the renderer, causing the UI to jitter.
|
||||
*
|
||||
* The solution for this is to spawn an Electron UtilityProcess, which we can
|
||||
* think of a regular Node.js child process. This frees up the Node.js main
|
||||
* think of a regular Node.js child process. This frees up the Node.js main
|
||||
* process, and would remove the jitter.
|
||||
* https://www.electronjs.org/docs/latest/tutorial/process-model
|
||||
*
|
||||
@@ -70,9 +72,21 @@ let _child: UtilityProcess | undefined;
|
||||
* The RPC protocol is handled using comlink on both ends. The port itself needs
|
||||
* to be relayed using `postMessage`.
|
||||
*/
|
||||
export const createMLWorker = (window: BrowserWindow) => {
|
||||
export const triggerCreateUtilityProcess = (
|
||||
type: UtilityProcessType,
|
||||
window: BrowserWindow,
|
||||
) => {
|
||||
switch (type) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
case "ml":
|
||||
triggerCreateMLUtilityProcess(window);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
export const triggerCreateMLUtilityProcess = (window: BrowserWindow) => {
|
||||
if (_child) {
|
||||
log.debug(() => "Terminating previous ML worker process");
|
||||
log.debug(() => "Terminating previous ML utility process");
|
||||
_child.kill();
|
||||
_child = undefined;
|
||||
}
|
||||
@@ -83,7 +97,7 @@ export const createMLWorker = (window: BrowserWindow) => {
|
||||
const userDataPath = app.getPath("userData");
|
||||
child.postMessage({ userDataPath }, [port1]);
|
||||
|
||||
window.webContents.postMessage("createMLWorker/port", undefined, [port2]);
|
||||
window.webContents.postMessage("utilityProcessPort/ml", undefined, [port2]);
|
||||
|
||||
handleMessagesFromUtilityProcess(child);
|
||||
|
||||
@@ -114,34 +128,10 @@ export const createMLWorker = (window: BrowserWindow) => {
|
||||
* we use the `parentPort` in the utility process.
|
||||
*/
|
||||
const handleMessagesFromUtilityProcess = (child: UtilityProcess) => {
|
||||
const logTag = "[ml-worker]";
|
||||
child.on("message", (m: unknown) => {
|
||||
if (m && typeof m == "object" && "method" in m && "p" in m) {
|
||||
const p = m.p;
|
||||
switch (m.method) {
|
||||
case "log.errorString":
|
||||
if (typeof p == "string") {
|
||||
log.error(`${logTag} ${p}`);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "log.info":
|
||||
if (Array.isArray(p)) {
|
||||
// Need to cast from any[] to unknown[]
|
||||
log.info(logTag, ...(p as unknown[]));
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "log.debugString":
|
||||
if (typeof p == "string") {
|
||||
log.debug(() => `${logTag} ${p}`);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (processUtilityProcessLogMessage("[ml-worker]", m)) {
|
||||
return;
|
||||
}
|
||||
log.info("Ignoring unknown message from ML worker", m);
|
||||
log.info("Ignoring unknown message from ML utility process", m);
|
||||
});
|
||||
};
|
||||
@@ -3,17 +3,23 @@
|
||||
*/
|
||||
import { net, protocol } from "electron/main";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import fs_ from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import { Writable } from "node:stream";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import log from "./log";
|
||||
import { ffmpegConvertToMP4 } from "./services/ffmpeg";
|
||||
import {
|
||||
ffmpegConvertToMP4,
|
||||
ffmpegGenerateHLSPlaylistAndSegments,
|
||||
type FFmpegGenerateHLSPlaylistAndSegmentsResult,
|
||||
} from "./services/ffmpeg";
|
||||
import { markClosableZip, openZip } from "./services/zip";
|
||||
import { ensure } from "./utils/common";
|
||||
import { wait } from "./utils/common";
|
||||
import { writeStream } from "./utils/stream";
|
||||
import {
|
||||
deleteTempFile,
|
||||
deleteTempFileIgnoringErrors,
|
||||
makeFileForDataOrStreamOrPathOrZipItem,
|
||||
makeTempFilePath,
|
||||
} from "./utils/temp";
|
||||
|
||||
@@ -57,25 +63,39 @@ const handleStreamRequest = async (request: Request): Promise<Response> => {
|
||||
const { host, searchParams } = new URL(url);
|
||||
switch (host) {
|
||||
case "read":
|
||||
return handleRead(ensure(searchParams.get("path")));
|
||||
return handleRead(searchParams.get("path")!);
|
||||
|
||||
case "read-zip":
|
||||
return handleReadZip(
|
||||
ensure(searchParams.get("zipPath")),
|
||||
ensure(searchParams.get("entryName")),
|
||||
searchParams.get("zipPath")!,
|
||||
searchParams.get("entryName")!,
|
||||
);
|
||||
|
||||
case "write":
|
||||
return handleWrite(ensure(searchParams.get("path")), request);
|
||||
return handleWrite(searchParams.get("path")!, request);
|
||||
|
||||
case "video": {
|
||||
const op = searchParams.get("op");
|
||||
if (op) {
|
||||
switch (op) {
|
||||
case "convert-to-mp4":
|
||||
return handleConvertToMP4Write(request);
|
||||
case "generate-hls":
|
||||
return handleGenerateHLSWrite(request, searchParams);
|
||||
default:
|
||||
return new Response(`Unknown op ${op}`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
case "convert-to-mp4": {
|
||||
const token = searchParams.get("token");
|
||||
const done = searchParams.get("done") !== null;
|
||||
return token
|
||||
? done
|
||||
? handleConvertToMP4ReadDone(token)
|
||||
: handleConvertToMP4Read(token)
|
||||
: handleConvertToMP4Write(request);
|
||||
if (!token) {
|
||||
return new Response("Missing token", { status: 404 });
|
||||
}
|
||||
|
||||
return done ? handleVideoDone(token) : handleVideoRead(token);
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -105,6 +125,7 @@ const handleRead = async (path: string) => {
|
||||
res.headers.set("Content-Length", `${fileSize}`);
|
||||
|
||||
// Add the file's last modified time (as epoch milliseconds).
|
||||
// See: [Note: Integral last modified time]
|
||||
const mtimeMs = stat.mtime.getTime();
|
||||
res.headers.set("X-Last-Modified-Ms", `${mtimeMs}`);
|
||||
}
|
||||
@@ -166,21 +187,21 @@ const handleReadZip = async (zipPath: string, entryName: string) => {
|
||||
};
|
||||
|
||||
const handleWrite = async (path: string, request: Request) => {
|
||||
await writeStream(path, ensure(request.body));
|
||||
await writeStream(path, request.body!);
|
||||
return new Response("", { status: 200 });
|
||||
};
|
||||
|
||||
/**
|
||||
* A map from token to file paths for convert-to-mp4 requests that we have
|
||||
* received.
|
||||
* A map from token to file paths generated as a result of stream://video
|
||||
* requests we have received.
|
||||
*/
|
||||
const convertToMP4Results = new Map<string, string>();
|
||||
const pendingVideoResults = new Map<string, string>();
|
||||
|
||||
/**
|
||||
* Clear any in-memory state for in-flight convert-to-mp4 requests. Meant to be
|
||||
* called during logout.
|
||||
* Clear any in-memory state for in-flight streamed video processing requests.
|
||||
* Meant to be called during logout.
|
||||
*/
|
||||
export const clearConvertToMP4Results = () => convertToMP4Results.clear();
|
||||
export const clearPendingVideoResults = () => pendingVideoResults.clear();
|
||||
|
||||
/**
|
||||
* [Note: Convert to MP4]
|
||||
@@ -195,26 +216,26 @@ export const clearConvertToMP4Results = () => convertToMP4Results.clear();
|
||||
* mode for the Web fetch API). So we need to simulate that using two different
|
||||
* streaming requests.
|
||||
*
|
||||
* renderer → main stream://convert-to-mp4
|
||||
* renderer → main stream://video?op=convert-to-mp4
|
||||
* → request.body is the original video
|
||||
* ← response is a token
|
||||
* ← response is [token]
|
||||
*
|
||||
* renderer → main stream://convert-to-mp4?token=<token>
|
||||
* renderer → main stream://video?token=<token>
|
||||
* ← response.body is the converted video
|
||||
*
|
||||
* renderer → main stream://convert-to-mp4?token=<token>&done
|
||||
* renderer → main stream://video?token=<token>&done
|
||||
* ← 200 OK
|
||||
*
|
||||
* Note that the conversion itself is not streaming. The conversion still
|
||||
* happens in a single shot, we are just streaming the data across the IPC
|
||||
* boundary to allow us to pass large amounts of data without running out of
|
||||
* memory.
|
||||
* happens in a single invocation of ffmpeg, we are just streaming the data
|
||||
* across the IPC boundary to allow us to pass large amounts of data without
|
||||
* running out of memory.
|
||||
*
|
||||
* See also: [Note: IPC streams]
|
||||
*/
|
||||
const handleConvertToMP4Write = async (request: Request) => {
|
||||
const inputTempFilePath = await makeTempFilePath();
|
||||
await writeStream(inputTempFilePath, ensure(request.body));
|
||||
await writeStream(inputTempFilePath, request.body!);
|
||||
|
||||
const outputTempFilePath = await makeTempFilePath("mp4");
|
||||
try {
|
||||
@@ -228,25 +249,198 @@ const handleConvertToMP4Write = async (request: Request) => {
|
||||
}
|
||||
|
||||
const token = randomUUID();
|
||||
convertToMP4Results.set(token, outputTempFilePath);
|
||||
pendingVideoResults.set(token, outputTempFilePath);
|
||||
return new Response(token, { status: 200 });
|
||||
};
|
||||
|
||||
const handleConvertToMP4Read = async (token: string) => {
|
||||
const filePath = convertToMP4Results.get(token);
|
||||
const handleVideoRead = async (token: string) => {
|
||||
const filePath = pendingVideoResults.get(token);
|
||||
if (!filePath)
|
||||
return new Response(`Unknown token ${token}`, { status: 404 });
|
||||
|
||||
return net.fetch(pathToFileURL(filePath).toString());
|
||||
};
|
||||
|
||||
const handleConvertToMP4ReadDone = async (token: string) => {
|
||||
const filePath = convertToMP4Results.get(token);
|
||||
const handleVideoDone = async (token: string) => {
|
||||
const filePath = pendingVideoResults.get(token);
|
||||
if (!filePath)
|
||||
return new Response(`Unknown token ${token}`, { status: 404 });
|
||||
|
||||
await deleteTempFile(filePath);
|
||||
|
||||
convertToMP4Results.delete(token);
|
||||
pendingVideoResults.delete(token);
|
||||
return new Response("", { status: 200 });
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a HLS playlist for the given video.
|
||||
*
|
||||
* See: [Note: Convert to MP4] for the general architecture of commands that do
|
||||
* renderer <-> main I/O using streams.
|
||||
*
|
||||
* The difference here is that we the conversion generates two streams^ - one
|
||||
* for the HLS playlist itself, and one for the file containing the encrypted
|
||||
* and transcoded video chunks. The video stream we write to the objectUploadURL
|
||||
* (provided via {@link params}), and then we return a JSON object containing
|
||||
* the token for the playlist, and other metadata for use by the renderer.
|
||||
*
|
||||
* ^ if the video doesn't require a stream to be generated (e.g. it is very
|
||||
* small and already uses a compatible codec) then a HTT 204 is returned and
|
||||
* no stream is generated.
|
||||
*/
|
||||
const handleGenerateHLSWrite = async (
|
||||
request: Request,
|
||||
params: URLSearchParams,
|
||||
) => {
|
||||
const objectUploadURL = params.get("objectUploadURL");
|
||||
if (!objectUploadURL) throw new Error("Missing objectUploadURL");
|
||||
|
||||
let inputItem: Parameters<typeof makeFileForDataOrStreamOrPathOrZipItem>[0];
|
||||
const path = params.get("path");
|
||||
if (path) {
|
||||
inputItem = path;
|
||||
} else {
|
||||
const zipPath = params.get("zipPath");
|
||||
const entryName = params.get("entryName");
|
||||
if (zipPath && entryName) {
|
||||
inputItem = [zipPath, entryName];
|
||||
} else {
|
||||
const body = request.body;
|
||||
if (!body) throw new Error("Missing body");
|
||||
inputItem = body;
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
path: inputFilePath,
|
||||
isFileTemporary: isInputFileTemporary,
|
||||
writeToTemporaryFile: writeToTemporaryInputFile,
|
||||
} = await makeFileForDataOrStreamOrPathOrZipItem(inputItem);
|
||||
|
||||
const outputFilePathPrefix = await makeTempFilePath();
|
||||
let result: FFmpegGenerateHLSPlaylistAndSegmentsResult | undefined;
|
||||
try {
|
||||
await writeToTemporaryInputFile();
|
||||
|
||||
result = await ffmpegGenerateHLSPlaylistAndSegments(
|
||||
inputFilePath,
|
||||
outputFilePathPrefix,
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
// This video doesn't require stream generation.
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
const { playlistPath, videoPath, videoSize, dimensions } = result;
|
||||
try {
|
||||
await uploadVideoSegments(videoPath, videoSize, objectUploadURL);
|
||||
|
||||
const playlistToken = randomUUID();
|
||||
pendingVideoResults.set(playlistToken, playlistPath);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ playlistToken, dimensions, videoSize }),
|
||||
{ status: 200 },
|
||||
);
|
||||
} catch (e) {
|
||||
await deleteTempFileIgnoringErrors(playlistPath);
|
||||
throw e;
|
||||
} finally {
|
||||
await deleteTempFileIgnoringErrors(videoPath);
|
||||
}
|
||||
} finally {
|
||||
if (isInputFileTemporary)
|
||||
await deleteTempFileIgnoringErrors(inputFilePath);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload the file at the given {@link videoFilePath} to the provided presigned
|
||||
* {@link objectUploadURL} using a HTTP PUT request.
|
||||
*
|
||||
* In case on non-HTTP-4xx errors, retry up to 3 times with exponential backoff.
|
||||
*
|
||||
* See: [Note: Upload HLS video segment from node side].
|
||||
*
|
||||
* @param videoFilePath The path to the file on the user's file system to
|
||||
* upload.
|
||||
*
|
||||
* @param videoSize The size in bytes of the file at {@link videoFilePath}.
|
||||
*
|
||||
* @param objectUploadURL A pre-signed URL to upload the file.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* This is an inlined but bespoke reimplementation of `retryEnsuringHTTPOkOr4xx`
|
||||
* from `web/packages/base/http.ts`
|
||||
*
|
||||
* - We don't have the rest of the scaffolding used by that function, which is
|
||||
* why it is intially inlined bespoked.
|
||||
*
|
||||
* - It handles the specific use case of uploading videos since generating the
|
||||
* HLS stream is a fairly expensive operation, so a retry to discount
|
||||
* transient network issues is called for. There are only 2 retries for a
|
||||
* total of 3 attempts, and the retry gaps are more spaced out.
|
||||
*
|
||||
* - Later it was discovered that net.fetch is much slower than node's native
|
||||
* fetch, so this implementation has further diverged.
|
||||
*/
|
||||
export const uploadVideoSegments = async (
|
||||
videoFilePath: string,
|
||||
videoSize: number,
|
||||
objectUploadURL: string,
|
||||
) => {
|
||||
const waitTimeBeforeNextTry = [5000, 20000];
|
||||
|
||||
while (true) {
|
||||
let abort = false;
|
||||
try {
|
||||
const nodeStream = fs_.createReadStream(videoFilePath);
|
||||
const webStream = Readable.toWeb(nodeStream);
|
||||
|
||||
// net.fetch is 40-50x slower than the native fetch for this
|
||||
// particular PUT request. This is easily reproducible (replace
|
||||
// `fetch` with `net.fetch`, then even on localhost the PUT requests
|
||||
// start taking a minute or so; with node's native fetch, it is
|
||||
// second(s)).
|
||||
const res = await fetch(objectUploadURL, {
|
||||
method: "PUT",
|
||||
// net.fetch apparently deduces and inserts a content-length,
|
||||
// because when we use the node native fetch then we need to
|
||||
// provide it explicitly.
|
||||
headers: { "Content-Length": `${videoSize}` },
|
||||
// The duplex option is required since we're passing a stream.
|
||||
//
|
||||
// @ts-expect-error TypeScript's libdom.d.ts does not include
|
||||
// the "duplex" parameter, e.g. see
|
||||
// https://github.com/node-fetch/node-fetch/issues/1769.
|
||||
duplex: "half",
|
||||
body: webStream,
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
// Success.
|
||||
return;
|
||||
}
|
||||
if (res.status >= 400 && res.status < 500) {
|
||||
// HTTP 4xx.
|
||||
abort = true;
|
||||
}
|
||||
throw new Error(
|
||||
`Failed to upload generated HLS video: HTTP ${res.status} ${res.statusText}`,
|
||||
);
|
||||
} catch (e) {
|
||||
if (abort) {
|
||||
throw e;
|
||||
}
|
||||
const t = waitTimeBeforeNextTry.shift();
|
||||
if (!t) {
|
||||
throw e;
|
||||
} else {
|
||||
log.warn("Will retry potentially transient request failure", e);
|
||||
}
|
||||
await wait(t);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,20 +5,13 @@
|
||||
* currently a common package that both of them share.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Throw an exception if the given value is `null` or `undefined`.
|
||||
*/
|
||||
export const ensure = <T>(v: T | null | undefined): T => {
|
||||
if (v === null) throw new Error("Required value was null");
|
||||
if (v === undefined) throw new Error("Required value was not found");
|
||||
return v;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wait for {@link ms} milliseconds
|
||||
*
|
||||
* This function is a promisified `setTimeout`. It returns a promise that
|
||||
* resolves after {@link ms} milliseconds.
|
||||
*
|
||||
* Duplicated from `web/packages/utils/promise.ts`.
|
||||
*/
|
||||
export const wait = (ms: number) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from "node:path";
|
||||
import type { ZipItem } from "../../types/ipc";
|
||||
import log from "../log";
|
||||
import { markClosableZip, openZip } from "../services/zip";
|
||||
import { ensure } from "./common";
|
||||
import { writeStream } from "./stream";
|
||||
|
||||
/**
|
||||
* Our very own directory within the system temp directory. Go crazy, but
|
||||
@@ -20,17 +20,21 @@ const enteTempDirPath = async () => {
|
||||
/** Generate a random string suitable for being used as a file name prefix */
|
||||
const randomPrefix = () => {
|
||||
const ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const randomChar = () => ensure(ch[Math.floor(Math.random() * ch.length)]);
|
||||
const randomChar = () => ch[Math.floor(Math.random() * ch.length)]!;
|
||||
|
||||
return Array(10).fill("").map(randomChar).join("");
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the path to a temporary file with the given {@link suffix}.
|
||||
* Return the path to a temporary file with an optional {@link extension}.
|
||||
*
|
||||
* The function returns the path to a file in the system temp directory (in an
|
||||
* Ente specific folder therin) with a random prefix and an (optional)
|
||||
* {@link extension}.
|
||||
* {@link extension}. The parent directory is guaranteed to exist.
|
||||
*
|
||||
* @param extension A string, if provided, is used as the extension for the
|
||||
* generated path. It will be automatically prefixed by a dot, so don't include
|
||||
* the dot in the provided string.
|
||||
*
|
||||
* It ensures that there is no existing item with the same name already.
|
||||
*
|
||||
@@ -76,7 +80,7 @@ export const deleteTempFileIgnoringErrors = async (tempFilePath: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
/** The result of {@link makeFileForDataOrPathOrZipItem}. */
|
||||
/** The result of {@link makeFileForDataOrStreamOrPathOrZipItem}. */
|
||||
interface FileForDataOrPathOrZipItem {
|
||||
/**
|
||||
* The path to the file (possibly temporary).
|
||||
@@ -101,14 +105,14 @@ interface FileForDataOrPathOrZipItem {
|
||||
/**
|
||||
* Return the path to a file, a boolean indicating if this is a temporary path
|
||||
* that needs to be deleted after processing, and a function to write the given
|
||||
* {@link dataOrPathOrZipItem} into that temporary file if needed.
|
||||
* {@link item} into that temporary file if needed.
|
||||
*
|
||||
* @param dataOrPathOrZipItem The contents of the file, or the path to an
|
||||
* existing file, or a (path to a zip file, name of an entry within that zip
|
||||
* file) tuple.
|
||||
* @param item The contents of the file (bytes), or a {@link ReadableStream}
|
||||
* with the contents of the file, or the path to an existing file, or a (path to
|
||||
* a zip file, name of an entry within that zip file) tuple.
|
||||
*/
|
||||
export const makeFileForDataOrPathOrZipItem = async (
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
export const makeFileForDataOrStreamOrPathOrZipItem = async (
|
||||
item: Uint8Array | ReadableStream | string | ZipItem,
|
||||
): Promise<FileForDataOrPathOrZipItem> => {
|
||||
let path: string;
|
||||
let isFileTemporary: boolean;
|
||||
@@ -116,18 +120,19 @@ export const makeFileForDataOrPathOrZipItem = async (
|
||||
/* no-op */
|
||||
};
|
||||
|
||||
if (typeof dataOrPathOrZipItem == "string") {
|
||||
path = dataOrPathOrZipItem;
|
||||
if (typeof item == "string") {
|
||||
path = item;
|
||||
isFileTemporary = false;
|
||||
} else {
|
||||
path = await makeTempFilePath();
|
||||
isFileTemporary = true;
|
||||
if (dataOrPathOrZipItem instanceof Uint8Array) {
|
||||
writeToTemporaryFile = () =>
|
||||
fs.writeFile(path, dataOrPathOrZipItem);
|
||||
if (item instanceof Uint8Array) {
|
||||
writeToTemporaryFile = () => fs.writeFile(path, item);
|
||||
} else if (item instanceof ReadableStream) {
|
||||
writeToTemporaryFile = () => writeStream(path, item);
|
||||
} else {
|
||||
writeToTemporaryFile = async () => {
|
||||
const [zipPath, entryName] = dataOrPathOrZipItem;
|
||||
const [zipPath, entryName] = item;
|
||||
const zip = openZip(zipPath);
|
||||
try {
|
||||
await zip.extract(entryName, path);
|
||||
|
||||
@@ -66,8 +66,10 @@ import type { IpcRendererEvent } from "electron";
|
||||
import type {
|
||||
AppUpdate,
|
||||
CollectionMapping,
|
||||
FFmpegCommand,
|
||||
FolderWatch,
|
||||
PendingUploads,
|
||||
UtilityProcessType,
|
||||
ZipItem,
|
||||
} from "./types/ipc";
|
||||
|
||||
@@ -183,6 +185,8 @@ const fsWriteFileViaBackup = (path: string, contents: string) =>
|
||||
|
||||
const fsIsDir = (dirPath: string) => ipcRenderer.invoke("fsIsDir", dirPath);
|
||||
|
||||
const fsStatMtime = (path: string) => ipcRenderer.invoke("fsStatMtime", path);
|
||||
|
||||
// - Conversion
|
||||
|
||||
const convertToJPEG = (imageData: Uint8Array) =>
|
||||
@@ -201,7 +205,7 @@ const generateImageThumbnail = (
|
||||
);
|
||||
|
||||
const ffmpegExec = (
|
||||
command: string[],
|
||||
command: FFmpegCommand,
|
||||
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
|
||||
outputFileExtension: string,
|
||||
) =>
|
||||
@@ -212,18 +216,19 @@ const ffmpegExec = (
|
||||
outputFileExtension,
|
||||
);
|
||||
|
||||
// - ML
|
||||
// - Utility processes
|
||||
|
||||
const createMLWorker = () => {
|
||||
const triggerCreateUtilityProcess = (type: UtilityProcessType) => {
|
||||
const portEvent = `utilityProcessPort/${type}`;
|
||||
const l = (event: IpcRendererEvent) => {
|
||||
void windowLoaded.then(() => {
|
||||
// "*"" is the origin to send to.
|
||||
window.postMessage("createMLWorker/port", "*", event.ports);
|
||||
ipcRenderer.off("createMLWorker/port", l);
|
||||
window.postMessage(portEvent, "*", event.ports);
|
||||
ipcRenderer.off(portEvent, l);
|
||||
});
|
||||
};
|
||||
ipcRenderer.on("createMLWorker/port", l);
|
||||
ipcRenderer.send("createMLWorker");
|
||||
ipcRenderer.on(portEvent, l);
|
||||
ipcRenderer.send("triggerCreateUtilityProcess", type);
|
||||
};
|
||||
|
||||
// - Watch
|
||||
@@ -289,11 +294,11 @@ const pendingUploads = () => ipcRenderer.invoke("pendingUploads");
|
||||
const setPendingUploads = (pendingUploads: PendingUploads) =>
|
||||
ipcRenderer.invoke("setPendingUploads", pendingUploads);
|
||||
|
||||
const markUploadedFiles = (paths: PendingUploads["filePaths"]) =>
|
||||
ipcRenderer.invoke("markUploadedFiles", paths);
|
||||
const markUploadedFile = (path: string, associatedPath?: string) =>
|
||||
ipcRenderer.invoke("markUploadedFile", path, associatedPath);
|
||||
|
||||
const markUploadedZipItems = (items: PendingUploads["zipItems"]) =>
|
||||
ipcRenderer.invoke("markUploadedZipItems", items);
|
||||
const markUploadedZipItem = (item: ZipItem, associatedItem?: ZipItem) =>
|
||||
ipcRenderer.invoke("markUploadedZipItem", item, associatedItem);
|
||||
|
||||
const clearPendingUploads = () => ipcRenderer.invoke("clearPendingUploads");
|
||||
|
||||
@@ -378,6 +383,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
writeFile: fsWriteFile,
|
||||
writeFileViaBackup: fsWriteFileViaBackup,
|
||||
isDir: fsIsDir,
|
||||
statMtime: fsStatMtime,
|
||||
findFiles: fsFindFiles,
|
||||
},
|
||||
|
||||
@@ -389,7 +395,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
|
||||
// - ML
|
||||
|
||||
createMLWorker,
|
||||
triggerCreateUtilityProcess,
|
||||
|
||||
// - Watch
|
||||
|
||||
@@ -410,7 +416,7 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
pathOrZipItemSize,
|
||||
pendingUploads,
|
||||
setPendingUploads,
|
||||
markUploadedFiles,
|
||||
markUploadedZipItems,
|
||||
markUploadedFile,
|
||||
markUploadedZipItem,
|
||||
clearPendingUploads,
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
* See [Note: types.ts <-> preload.ts <-> ipc.ts]
|
||||
*/
|
||||
|
||||
export type UtilityProcessType = "ml";
|
||||
|
||||
export interface AppUpdate {
|
||||
autoUpdatable: boolean;
|
||||
version: string;
|
||||
@@ -32,3 +34,5 @@ export interface PendingUploads {
|
||||
filePaths: string[];
|
||||
zipItems: ZipItem[];
|
||||
}
|
||||
|
||||
export type FFmpegCommand = string[] | { default: string[]; hdr: string[] };
|
||||
|
||||
@@ -25,7 +25,16 @@
|
||||
ajv "^6.12.0"
|
||||
ajv-keywords "^3.4.1"
|
||||
|
||||
"@electron/asar@3.2.18", "@electron/asar@^3.2.7":
|
||||
"@electron/asar@3.4.1":
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.4.1.tgz#4e9196a4b54fba18c56cd8d5cac67c5bdc588065"
|
||||
integrity sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==
|
||||
dependencies:
|
||||
commander "^5.0.0"
|
||||
glob "^7.1.6"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
"@electron/asar@^3.2.7":
|
||||
version "3.2.18"
|
||||
resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.18.tgz#fa607f829209bab8b9e0ce6658d3fe81b2cba517"
|
||||
integrity sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==
|
||||
@@ -94,10 +103,10 @@
|
||||
minimist "^1.2.6"
|
||||
plist "^3.0.5"
|
||||
|
||||
"@electron/rebuild@3.7.0":
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.7.0.tgz#82e20c467ddedbb295d7f641592c52e68c141e9f"
|
||||
integrity sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==
|
||||
"@electron/rebuild@3.7.2":
|
||||
version "3.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.7.2.tgz#8d808b29159c50086d27a5dec72b40bf16b4b582"
|
||||
integrity sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg==
|
||||
dependencies:
|
||||
"@electron/node-gyp" "https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2"
|
||||
"@malept/cross-spawn-promise" "^2.0.0"
|
||||
@@ -168,10 +177,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.9.1.tgz#4a97e85e982099d6c7ee8410aacb55adaa576f06"
|
||||
integrity sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==
|
||||
|
||||
"@eslint/js@^9.24.0":
|
||||
version "9.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.24.0.tgz#685277980bb7bf84ecc8e4e133ccdda7545a691e"
|
||||
integrity sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==
|
||||
"@eslint/js@^9.25.1":
|
||||
version "9.25.1"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.25.1.tgz#25f5c930c2b68b5ebe7ac857f754cbd61ef6d117"
|
||||
integrity sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==
|
||||
|
||||
"@eslint/object-schema@^2.1.4":
|
||||
version "2.1.4"
|
||||
@@ -376,62 +385,62 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@typescript-eslint/eslint-plugin@8.29.1":
|
||||
version "8.29.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz#593639d9bb5239b2d877d65757b7e2c9100a2e84"
|
||||
integrity sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg==
|
||||
"@typescript-eslint/eslint-plugin@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz#62f1befe59647524994e89de4516d8dcba7a850a"
|
||||
integrity sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.10.0"
|
||||
"@typescript-eslint/scope-manager" "8.29.1"
|
||||
"@typescript-eslint/type-utils" "8.29.1"
|
||||
"@typescript-eslint/utils" "8.29.1"
|
||||
"@typescript-eslint/visitor-keys" "8.29.1"
|
||||
"@typescript-eslint/scope-manager" "8.31.1"
|
||||
"@typescript-eslint/type-utils" "8.31.1"
|
||||
"@typescript-eslint/utils" "8.31.1"
|
||||
"@typescript-eslint/visitor-keys" "8.31.1"
|
||||
graphemer "^1.4.0"
|
||||
ignore "^5.3.1"
|
||||
natural-compare "^1.4.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@typescript-eslint/parser@8.29.1":
|
||||
version "8.29.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.29.1.tgz#10bf37411be0a199c27b6515726e22fe8d3df8d0"
|
||||
integrity sha512-zczrHVEqEaTwh12gWBIJWj8nx+ayDcCJs06yoNMY0kwjMWDM6+kppljY+BxWI06d2Ja+h4+WdufDcwMnnMEWmg==
|
||||
"@typescript-eslint/parser@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.31.1.tgz#e9b0ccf30d37dde724ee4d15f4dbc195995cce1b"
|
||||
integrity sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "8.29.1"
|
||||
"@typescript-eslint/types" "8.29.1"
|
||||
"@typescript-eslint/typescript-estree" "8.29.1"
|
||||
"@typescript-eslint/visitor-keys" "8.29.1"
|
||||
"@typescript-eslint/scope-manager" "8.31.1"
|
||||
"@typescript-eslint/types" "8.31.1"
|
||||
"@typescript-eslint/typescript-estree" "8.31.1"
|
||||
"@typescript-eslint/visitor-keys" "8.31.1"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@8.29.1":
|
||||
version "8.29.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.29.1.tgz#cfdfd4144f20c38b9d3e430efd6480e297ef52f6"
|
||||
integrity sha512-2nggXGX5F3YrsGN08pw4XpMLO1Rgtnn4AzTegC2MDesv6q3QaTU5yU7IbS1tf1IwCR0Hv/1EFygLn9ms6LIpDA==
|
||||
"@typescript-eslint/scope-manager@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz#1eb52e76878f545e4add142e0d8e3e97e7aa443b"
|
||||
integrity sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.29.1"
|
||||
"@typescript-eslint/visitor-keys" "8.29.1"
|
||||
"@typescript-eslint/types" "8.31.1"
|
||||
"@typescript-eslint/visitor-keys" "8.31.1"
|
||||
|
||||
"@typescript-eslint/type-utils@8.29.1":
|
||||
version "8.29.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.29.1.tgz#653dfff5c1711bc920a6a46a5a2c274899f00179"
|
||||
integrity sha512-DkDUSDwZVCYN71xA4wzySqqcZsHKic53A4BLqmrWFFpOpNSoxX233lwGu/2135ymTCR04PoKiEEEvN1gFYg4Tw==
|
||||
"@typescript-eslint/type-utils@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz#be0f438fb24b03568e282a0aed85f776409f970c"
|
||||
integrity sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "8.29.1"
|
||||
"@typescript-eslint/utils" "8.29.1"
|
||||
"@typescript-eslint/typescript-estree" "8.31.1"
|
||||
"@typescript-eslint/utils" "8.31.1"
|
||||
debug "^4.3.4"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@typescript-eslint/types@8.29.1":
|
||||
version "8.29.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.29.1.tgz#984ed1283fedbfb41d3993a9abdcb7b299971500"
|
||||
integrity sha512-VT7T1PuJF1hpYC3AGm2rCgJBjHL3nc+A/bhOp9sGMKfi5v0WufsX/sHCFBfNTx2F+zA6qBc/PD0/kLRLjdt8mQ==
|
||||
"@typescript-eslint/types@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.31.1.tgz#478ed6f7e8aee1be7b63a60212b6bffe1423b5d4"
|
||||
integrity sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==
|
||||
|
||||
"@typescript-eslint/typescript-estree@8.29.1":
|
||||
version "8.29.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.1.tgz#4ac085665ed5390d11c0e3426427978570e3b747"
|
||||
integrity sha512-l1enRoSaUkQxOQnbi0KPUtqeZkSiFlqrx9/3ns2rEDhGKfTa+88RmXqedC1zmVTOWrLc2e6DEJrTA51C9iLH5g==
|
||||
"@typescript-eslint/typescript-estree@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz#37792fe7ef4d3021c7580067c8f1ae66daabacdf"
|
||||
integrity sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.29.1"
|
||||
"@typescript-eslint/visitor-keys" "8.29.1"
|
||||
"@typescript-eslint/types" "8.31.1"
|
||||
"@typescript-eslint/visitor-keys" "8.31.1"
|
||||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
is-glob "^4.0.3"
|
||||
@@ -439,22 +448,22 @@
|
||||
semver "^7.6.0"
|
||||
ts-api-utils "^2.0.1"
|
||||
|
||||
"@typescript-eslint/utils@8.29.1":
|
||||
version "8.29.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.29.1.tgz#3d206c8c8def3527a8eb0588e94e3e60f7e167c9"
|
||||
integrity sha512-QAkFEbytSaB8wnmB+DflhUPz6CLbFWE2SnSCrRMEa+KnXIzDYbpsn++1HGvnfAsUY44doDXmvRkO5shlM/3UfA==
|
||||
"@typescript-eslint/utils@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.31.1.tgz#5628ea0393598a0b2f143d0fc6d019f0dee9dd14"
|
||||
integrity sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "8.29.1"
|
||||
"@typescript-eslint/types" "8.29.1"
|
||||
"@typescript-eslint/typescript-estree" "8.29.1"
|
||||
"@typescript-eslint/scope-manager" "8.31.1"
|
||||
"@typescript-eslint/types" "8.31.1"
|
||||
"@typescript-eslint/typescript-estree" "8.31.1"
|
||||
|
||||
"@typescript-eslint/visitor-keys@8.29.1":
|
||||
version "8.29.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.1.tgz#9b74e5098c71138d42bbf2178fbe4dfad45d6b9a"
|
||||
integrity sha512-RGLh5CRaUEf02viP5c1Vh1cMGffQscyHe7HPAzGpfmfflFg1wUz2rYxd+OZqwpeypYvZ8UxSxuIpF++fmOzEcg==
|
||||
"@typescript-eslint/visitor-keys@8.31.1":
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz#6742b0e3ba1e0c1e35bdaf78c03e759eb8dd8e75"
|
||||
integrity sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "8.29.1"
|
||||
"@typescript-eslint/types" "8.31.1"
|
||||
eslint-visitor-keys "^4.2.0"
|
||||
|
||||
"@xmldom/xmldom@^0.8.8":
|
||||
@@ -560,30 +569,30 @@ app-builder-bin@5.0.0-alpha.12:
|
||||
resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz#2daf82f8badc698e0adcc95ba36af4ff0650dc80"
|
||||
integrity sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==
|
||||
|
||||
app-builder-lib@26.0.12:
|
||||
version "26.0.12"
|
||||
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-26.0.12.tgz#2e33df936e0f78d4266b058ece90308ea981eefb"
|
||||
integrity sha512-+/CEPH1fVKf6HowBUs6LcAIoRcjeqgvAeoSE+cl7Y7LndyQ9ViGPYibNk7wmhMHzNgHIuIbw4nWADPO+4mjgWw==
|
||||
app-builder-lib@26.0.14:
|
||||
version "26.0.14"
|
||||
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-26.0.14.tgz#a28fbefb600cf052d1259932f32289e043573f61"
|
||||
integrity sha512-nc/A9MUd95MCc7bR4yVW7Lhs9FZTA/l8QdV8PE1vZZOOiogK4dupBfCCJG0UqLU81JS62f078/bwAeuMjt3hfQ==
|
||||
dependencies:
|
||||
"@develar/schema-utils" "~2.6.5"
|
||||
"@electron/asar" "3.2.18"
|
||||
"@electron/asar" "3.4.1"
|
||||
"@electron/fuses" "^1.8.0"
|
||||
"@electron/notarize" "2.5.0"
|
||||
"@electron/osx-sign" "1.3.1"
|
||||
"@electron/rebuild" "3.7.0"
|
||||
"@electron/rebuild" "3.7.2"
|
||||
"@electron/universal" "2.0.1"
|
||||
"@malept/flatpak-bundler" "^0.4.0"
|
||||
"@types/fs-extra" "9.0.13"
|
||||
async-exit-hook "^2.0.1"
|
||||
builder-util "26.0.11"
|
||||
builder-util-runtime "9.3.1"
|
||||
builder-util "26.0.13"
|
||||
builder-util-runtime "9.3.2"
|
||||
chromium-pickle-js "^0.2.0"
|
||||
config-file-ts "0.2.8-rc1"
|
||||
debug "^4.3.4"
|
||||
dotenv "^16.4.5"
|
||||
dotenv-expand "^11.0.6"
|
||||
ejs "^3.1.8"
|
||||
electron-publish "26.0.11"
|
||||
electron-publish "26.0.13"
|
||||
fs-extra "^10.1.0"
|
||||
hosted-git-info "^4.1.0"
|
||||
is-ci "^3.0.0"
|
||||
@@ -719,23 +728,23 @@ buffer@^5.1.0, buffer@^5.5.0:
|
||||
base64-js "^1.3.1"
|
||||
ieee754 "^1.1.13"
|
||||
|
||||
builder-util-runtime@9.3.1:
|
||||
version "9.3.1"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz#0daedde0f6d381f2a00a50a407b166fe7dca1a67"
|
||||
integrity sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==
|
||||
builder-util-runtime@9.3.2:
|
||||
version "9.3.2"
|
||||
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.3.2.tgz#2a69a239b50e26accf4ed4ea1730406a3117213c"
|
||||
integrity sha512-7QDXJ1FwT6d9ZhG4kuObUUPY8/ENBS/Ky26O4hR5vbeoRGavgekS2Jxv+8sCn/v23aPGU2DXRWEeJuijN2ooYA==
|
||||
dependencies:
|
||||
debug "^4.3.4"
|
||||
sax "^1.2.4"
|
||||
|
||||
builder-util@26.0.11:
|
||||
version "26.0.11"
|
||||
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-26.0.11.tgz#ad85b92c93f2b976b973e1d87337e0c6813fcb8f"
|
||||
integrity sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==
|
||||
builder-util@26.0.13:
|
||||
version "26.0.13"
|
||||
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-26.0.13.tgz#a2c11f8e89e5392719e540d610d70d8413943d74"
|
||||
integrity sha512-6b64uHzywaL2KAG+rVcqk/Prta1m3I2Jo1d4d2CrApb6EeSk2V384tmSL0EniH+P8jaNbMp6qhg7cIALw32zRA==
|
||||
dependencies:
|
||||
"7zip-bin" "~5.2.0"
|
||||
"@types/debug" "^4.1.6"
|
||||
app-builder-bin "5.0.0-alpha.12"
|
||||
builder-util-runtime "9.3.1"
|
||||
builder-util-runtime "9.3.2"
|
||||
chalk "^4.1.2"
|
||||
cross-spawn "^7.0.6"
|
||||
debug "^4.3.4"
|
||||
@@ -1096,14 +1105,14 @@ dir-compare@^4.2.0:
|
||||
minimatch "^3.0.5"
|
||||
p-limit "^3.1.0 "
|
||||
|
||||
dmg-builder@26.0.12:
|
||||
version "26.0.12"
|
||||
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-26.0.12.tgz#6996ad0bab80a861c9a7b33ee9734d4f60566b46"
|
||||
integrity sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==
|
||||
dmg-builder@26.0.14:
|
||||
version "26.0.14"
|
||||
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-26.0.14.tgz#ce8180da319cf3ee05d42cd460a7509207ad474b"
|
||||
integrity sha512-0l7oEj175hee7NfnaUpb0zf7fsgh1SyHeLjDA0AtOMnBUfTGxPPwrifbUxfd73qzamrGNcyeqza+m/EJx3QUug==
|
||||
dependencies:
|
||||
app-builder-lib "26.0.12"
|
||||
builder-util "26.0.11"
|
||||
builder-util-runtime "9.3.1"
|
||||
app-builder-lib "26.0.14"
|
||||
builder-util "26.0.13"
|
||||
builder-util-runtime "9.3.2"
|
||||
fs-extra "^10.1.0"
|
||||
iconv-lite "^0.6.2"
|
||||
js-yaml "^4.1.0"
|
||||
@@ -1150,35 +1159,35 @@ ejs@^3.1.8:
|
||||
dependencies:
|
||||
jake "^10.8.5"
|
||||
|
||||
electron-builder@^26.0.12:
|
||||
version "26.0.12"
|
||||
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-26.0.12.tgz#797af2e70efdd96c9ea5d8a8164b8728c90d65ff"
|
||||
integrity sha512-cD1kz5g2sgPTMFHjLxfMjUK5JABq3//J4jPswi93tOPFz6btzXYtK5NrDt717NRbukCUDOrrvmYVOWERlqoiXA==
|
||||
electron-builder@^26.0.14:
|
||||
version "26.0.14"
|
||||
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-26.0.14.tgz#8927c6da42a69425d15e08f351e944ea0e7866da"
|
||||
integrity sha512-YBxpWLMGj0oS7fbS3LVingeZqFunU0F8s+uB9QTd5+wN4qgrf/rSGRkqoImbWg2+F2yHq11wmaA/Xr9xzvgQ0w==
|
||||
dependencies:
|
||||
app-builder-lib "26.0.12"
|
||||
builder-util "26.0.11"
|
||||
builder-util-runtime "9.3.1"
|
||||
app-builder-lib "26.0.14"
|
||||
builder-util "26.0.13"
|
||||
builder-util-runtime "9.3.2"
|
||||
chalk "^4.1.2"
|
||||
dmg-builder "26.0.12"
|
||||
dmg-builder "26.0.14"
|
||||
fs-extra "^10.1.0"
|
||||
is-ci "^3.0.0"
|
||||
lazy-val "^1.0.5"
|
||||
simple-update-notifier "2.0.0"
|
||||
yargs "^17.6.2"
|
||||
|
||||
electron-log@^5.3.3:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.3.3.tgz#323f5e70b3658d683a0f51f26867dc077a823aa3"
|
||||
integrity sha512-ZOnlgCVfhKC0Nef68L0wDhwhg8nh5QkpEOA+udjpBxcPfTHGgbZbfoCBS6hmAgVHTAWByHNPkHKpSbEOPGZcxA==
|
||||
electron-log@^5.4.0:
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.4.0.tgz#3180bf5194b2e2efacb62ec1392f8150faf4de6b"
|
||||
integrity sha512-AXI5OVppskrWxEAmCxuv8ovX+s2Br39CpCAgkGMNHQtjYT3IiVbSQTncEjFVGPgoH35ZygRm/mvUMBDWwhRxgg==
|
||||
|
||||
electron-publish@26.0.11:
|
||||
version "26.0.11"
|
||||
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-26.0.11.tgz#92c9329a101af2836d9d228c82966eca1eee9a7b"
|
||||
integrity sha512-a8QRH0rAPIWH9WyyS5LbNvW9Ark6qe63/LqDB7vu2JXYpi0Gma5Q60Dh4tmTqhOBQt0xsrzD8qE7C+D7j+B24A==
|
||||
electron-publish@26.0.13:
|
||||
version "26.0.13"
|
||||
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-26.0.13.tgz#04340520e6e9de5262fecfa011658cfcc3fc8917"
|
||||
integrity sha512-O5hfHSwli5cegQ4JS3Dp0dZcheex6UCRE/qYyRQvhB6DhSwojiwTnAGEuQCJXc8K8Zxz2lku5Du3VwYHf8d5Lw==
|
||||
dependencies:
|
||||
"@types/fs-extra" "^9.0.11"
|
||||
builder-util "26.0.11"
|
||||
builder-util-runtime "9.3.1"
|
||||
builder-util "26.0.13"
|
||||
builder-util-runtime "9.3.2"
|
||||
chalk "^4.1.2"
|
||||
form-data "^4.0.0"
|
||||
fs-extra "^10.1.0"
|
||||
@@ -1193,12 +1202,12 @@ electron-store@^8.2.0:
|
||||
conf "^10.2.0"
|
||||
type-fest "^2.17.0"
|
||||
|
||||
electron-updater@^6.6.2:
|
||||
version "6.6.2"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.6.2.tgz#3e65e044f1a99b00d61e200e24de8e709c69ce99"
|
||||
integrity sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==
|
||||
electron-updater@^6.6.3:
|
||||
version "6.6.3"
|
||||
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.6.3.tgz#a1f53671ffbb08a475d495d48f0c0d971e665d5d"
|
||||
integrity sha512-i448/SwMtqxy5wqAcXScnWjiFxZp+hmWA2jZCmojcdfodEGhi/DWTdRP01mE3lCILb8hmdE28SBaHf1oQW3+kw==
|
||||
dependencies:
|
||||
builder-util-runtime "9.3.1"
|
||||
builder-util-runtime "9.3.2"
|
||||
fs-extra "^10.1.0"
|
||||
js-yaml "^4.1.0"
|
||||
lazy-val "^1.0.5"
|
||||
@@ -1207,10 +1216,10 @@ electron-updater@^6.6.2:
|
||||
semver "^7.6.3"
|
||||
tiny-typed-emitter "^2.1.0"
|
||||
|
||||
electron@^35.1.4:
|
||||
version "35.1.4"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-35.1.4.tgz#53f51c3488e2c49828ce9453e60d60d14fb441d5"
|
||||
integrity sha512-8HjE2wqxY//T09Of8k1eTpK/NeTG2FkTyRD+fyKXmec4wZVscGgZcmWFC0HYN4ktyHAjtplpxdFXjtqRnvzBMg==
|
||||
electron@^36.1.0:
|
||||
version "36.1.0"
|
||||
resolved "https://registry.yarnpkg.com/electron/-/electron-36.1.0.tgz#9919b77e61cd1400acc6dd24f9db8451fba5f8eb"
|
||||
integrity sha512-gnp3BnbKdGsVc7cm1qlEaZc8pJsR08mIs8H/yTo8gHEtFkGGJbDTVZOYNAfbQlL0aXh+ozv+CnyiNeDNkT1Upg==
|
||||
dependencies:
|
||||
"@electron/get" "^2.0.0"
|
||||
"@types/node" "^22.7.7"
|
||||
@@ -3140,14 +3149,14 @@ typedarray@^0.0.6:
|
||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
||||
|
||||
typescript-eslint@^8.29.1:
|
||||
version "8.29.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.29.1.tgz#c0b205e542ade22f9027caaaa9c4ec31a202010f"
|
||||
integrity sha512-f8cDkvndhbQMPcysk6CUSGBWV+g1utqdn71P5YKwMumVMOG/5k7cHq0KyG4O52nB0oKS4aN2Tp5+wB4APJGC+w==
|
||||
typescript-eslint@^8.31.1:
|
||||
version "8.31.1"
|
||||
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.31.1.tgz#b77ab1e48ced2daab9225ff94bab54391a4af69b"
|
||||
integrity sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA==
|
||||
dependencies:
|
||||
"@typescript-eslint/eslint-plugin" "8.29.1"
|
||||
"@typescript-eslint/parser" "8.29.1"
|
||||
"@typescript-eslint/utils" "8.29.1"
|
||||
"@typescript-eslint/eslint-plugin" "8.31.1"
|
||||
"@typescript-eslint/parser" "8.31.1"
|
||||
"@typescript-eslint/utils" "8.31.1"
|
||||
|
||||
typescript@^5.4.3, typescript@^5.8.3:
|
||||
version "5.8.3"
|
||||
|
||||
@@ -182,6 +182,7 @@ export const sidebar = [
|
||||
text: "Auth",
|
||||
items: [
|
||||
{ text: "Introduction", link: "/auth/" },
|
||||
{ text: "Features", link: "/auth/features/" },
|
||||
{
|
||||
text: "FAQ",
|
||||
collapsed: true,
|
||||
@@ -223,6 +224,7 @@ export const sidebar = [
|
||||
},
|
||||
{
|
||||
text: "Troubleshooting",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: "Windows login",
|
||||
@@ -238,58 +240,91 @@ export const sidebar = [
|
||||
items: [
|
||||
{ text: "Getting started", link: "/self-hosting/" },
|
||||
{
|
||||
text: "System requirements",
|
||||
link: "/self-hosting/guides/system-requirements",
|
||||
text: "Connecting to custom server",
|
||||
link: "/self-hosting/guides/custom-server/",
|
||||
},
|
||||
{
|
||||
text: "Creating accounts",
|
||||
link: "/self-hosting/creating-accounts",
|
||||
},
|
||||
{
|
||||
text: "Configuring your server",
|
||||
link: "/self-hosting/museum",
|
||||
},
|
||||
{
|
||||
text: "Configuring S3",
|
||||
link: "/self-hosting/guides/configuring-s3",
|
||||
},
|
||||
{
|
||||
text: "Reverse proxy",
|
||||
link: "/self-hosting/reverse-proxy",
|
||||
},
|
||||
{
|
||||
text: "Guides",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: "Introduction", link: "/self-hosting/guides/" },
|
||||
{
|
||||
text: "Connect to custom server",
|
||||
link: "/self-hosting/guides/custom-server/",
|
||||
},
|
||||
{
|
||||
text: "Hosting the web app",
|
||||
link: "/self-hosting/guides/web-app",
|
||||
},
|
||||
{
|
||||
text: "Configuring S3",
|
||||
link: "/self-hosting/guides/configuring-s3",
|
||||
},
|
||||
{
|
||||
text: "Hosting Ente with external S3 (Community)",
|
||||
link: "/self-hosting/guides/external-s3",
|
||||
},
|
||||
{
|
||||
text: "DB migration",
|
||||
link: "/self-hosting/guides/db-migration",
|
||||
},
|
||||
{
|
||||
text: "Hosting Ente without Docker",
|
||||
link: "/self-hosting/guides/standalone-ente",
|
||||
},
|
||||
{
|
||||
text: "Ente via Tailscale (Community)",
|
||||
link: "/self-hosting/guides/Tailscale.md",
|
||||
},
|
||||
{
|
||||
text: "Configure CLI for Self Hosted Instance",
|
||||
link: "/self-hosting/guides/selfhost-cli",
|
||||
},
|
||||
{
|
||||
text: "Administering your server",
|
||||
link: "/self-hosting/guides/admin",
|
||||
},
|
||||
|
||||
{
|
||||
text: "Mobile build",
|
||||
link: "/self-hosting/guides/mobile-build",
|
||||
text: "Configuring CLI for your instance",
|
||||
link: "/self-hosting/guides/selfhost-cli",
|
||||
},
|
||||
{
|
||||
text: "Running Ente from source",
|
||||
link: "/self-hosting/guides/from-source",
|
||||
},
|
||||
{
|
||||
text: "Running Ente without Docker",
|
||||
link: "/self-hosting/guides/standalone-ente",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Troubleshooting",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: "General",
|
||||
link: "/self-hosting/troubleshooting/misc",
|
||||
},
|
||||
{
|
||||
text: "Bucket CORS",
|
||||
link: '/self-hosting/troubleshooting/bucket-cors'
|
||||
},
|
||||
{
|
||||
text: "Uploads",
|
||||
link: "/self-hosting/troubleshooting/uploads",
|
||||
},
|
||||
{
|
||||
text: "Docker / quickstart",
|
||||
link: "/self-hosting/troubleshooting/docker",
|
||||
},
|
||||
{
|
||||
text: "Ente CLI secrets",
|
||||
link: "/self-hosting/troubleshooting/keyring",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Community Guides",
|
||||
collapsed: true,
|
||||
items :[
|
||||
{
|
||||
text: "Ente via Tailscale",
|
||||
link: "/self-hosting/guides/Tailscale",
|
||||
},
|
||||
{
|
||||
text: "Ente with External S3",
|
||||
link: "/self-hosting/guides/external-s3",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: "FAQ",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: "General", link: "/self-hosting/faq/" },
|
||||
{
|
||||
@@ -304,30 +339,9 @@ export const sidebar = [
|
||||
text: "Backups",
|
||||
link: "/self-hosting/faq/backup",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Troubleshooting",
|
||||
items: [
|
||||
{
|
||||
text: "General",
|
||||
link: "/self-hosting/troubleshooting/misc",
|
||||
},
|
||||
{
|
||||
text: "Uploads",
|
||||
link: "/self-hosting/troubleshooting/uploads",
|
||||
},
|
||||
{
|
||||
text: "Docker / quickstart",
|
||||
link: "/self-hosting/troubleshooting/docker",
|
||||
},
|
||||
{
|
||||
text: "Yarn",
|
||||
link: "/self-hosting/troubleshooting/yarn",
|
||||
},
|
||||
{
|
||||
text: "Ente CLI Secrets",
|
||||
link: "/self-hosting/troubleshooting/keyring",
|
||||
text: "Environment variables",
|
||||
link: "/self-hosting/faq/environment",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
139
docs/docs/auth/features/index.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
title: Features - Auth
|
||||
description: Features available in Ente Auth
|
||||
---
|
||||
|
||||
# Features
|
||||
|
||||
This page outlines the key features available in Ente Auth.
|
||||
|
||||
### Icons
|
||||
|
||||
Ente Auth supports the icon pack provided by
|
||||
[simple-icons](https://github.com/simple-icons/simple-icons). If an icon you
|
||||
need is missing, please refer to the
|
||||
[docs/adding-icons](https://github.com/ente-io/ente/blob/main/auth/docs/adding-icons.md)
|
||||
guide for instructions on how to contribute.
|
||||
|
||||
### Search
|
||||
|
||||
Quickly find your codes by searching based on issuer or account name. You can
|
||||
also configure the app to focus the search bar automatically on app start by
|
||||
going to **Settings → General → Focus search on app start**.
|
||||
|
||||
### Tags
|
||||
|
||||
Organize and filter your codes with ease using tags.
|
||||
|
||||
- **Creating a Tag:** When adding or editing a code, tap the orange (+) icon to
|
||||
create a new tag.
|
||||
- **Adding an existing Tag:** When adding or editing a code, select the desired
|
||||
tag from the list.
|
||||
|
||||
### Pinning
|
||||
|
||||
Highlight your frequently used services by pinning them to the top of your code
|
||||
list. To pin a code, long-press (mobile) or right-click (desktop) the code and
|
||||
select "Pin".
|
||||
|
||||
### Notes
|
||||
|
||||
Add additional information to your codes using notes. Notes can be added during
|
||||
the process of creating or modifying a code.
|
||||
|
||||
### Sharing
|
||||
|
||||
Securely share codes temporarily with others.
|
||||
|
||||
- Long-press (mobile) or right-click (desktop) on a code and choose "Share".
|
||||
- Select a duration for the shared link: 2 minutes, 5 minutes, or 10 minutes.
|
||||
- This generates a unique, time-limited link. Recipients can view the codes for
|
||||
the specified duration without gaining access to the underlying secret key.
|
||||
After the link expires, the recipients will no longer be able to view new
|
||||
codes.
|
||||
|
||||
### Custom sorting
|
||||
|
||||
Customize the order in which your codes are displayed. Ente Auth provides
|
||||
several sorting options:
|
||||
|
||||
- Issuer name
|
||||
- Account name
|
||||
- Frequently used
|
||||
- Recently used
|
||||
- Manual (custom drag-and-drop order)
|
||||
|
||||
Access the sort menu in the top-right corner (next to the search icon) to change
|
||||
your sorting preference.
|
||||
|
||||
### Offline mode
|
||||
|
||||
Ente Auth can be used entirely offline. Choose "Use without backups" on the
|
||||
login screen. In this mode, your codes are stored locally on your device.
|
||||
|
||||
Unlike when using an account, data is not synced or backed up to the cloud. You
|
||||
are responsible for manually backing up your codes.
|
||||
|
||||
### Display options
|
||||
|
||||
Customize how your codes are displayed for optimal usability.
|
||||
|
||||
- **Show large icons:** Display codes with larger icons for enhanced visibility.
|
||||
- **Compact mode:** Switch to a more compact layout to view more codes on the
|
||||
screen simultaneously.
|
||||
- **Hide codes:** Hide the actual code values for extra privacy. Double-tap a
|
||||
code to reveal it when needed.
|
||||
|
||||
### App lock
|
||||
|
||||
Add an additional layer of protection using the app lock. Choose from the
|
||||
following lock methods:
|
||||
|
||||
- **Device lock:** Use the existing lock configured on your device (e.g., Face
|
||||
ID, Touch ID, system password).
|
||||
- **PIN lock:** Set up a 4-digit PIN code to unlock the app.
|
||||
- **Password lock:** Set up a password to unlock the app.
|
||||
|
||||
Additionally, configure **Auto lock** to automatically lock the app after a
|
||||
specified period of time (options: Immediately, 5s, 15s, 1m, 5m, 30m).
|
||||
|
||||
### Import / Export
|
||||
|
||||
Ente Auth offers various import and export options for your codes.
|
||||
|
||||
- **Export:** Export your codes in plain text, as an encrypted file, or
|
||||
automatically via the CLI.
|
||||
- **Import:** Import codes from various other authentication apps.
|
||||
|
||||
For detailed instructions, refer to the
|
||||
[migration guides](../migration-guides/).
|
||||
|
||||
### Deduplicate codes
|
||||
|
||||
If you import codes and end up with duplicates, you can easily remove them. Go
|
||||
to **Settings → Data → Duplicate codes** to find and remove duplicate codes.
|
||||
|
||||
### Trash
|
||||
|
||||
Manage unwanted codes by moving them to the Trash. The Trash is not cleared
|
||||
automatically, giving you the flexibility to restore or permanently delete codes
|
||||
at any time.
|
||||
|
||||
- **Trashing a code:** Long-press (mobile) or right-click (desktop) on a code
|
||||
and select "Trash" to move it to the Trash.
|
||||
- **Viewing trashed codes:** If you have trashed codes, you can view them by
|
||||
selecting the Trash tag.
|
||||
- **Managing trashed codes:** In the Trash view, you can either permanently
|
||||
delete codes or restore them back to your main list.
|
||||
|
||||
### Scan QR
|
||||
|
||||
Easily add or share entries using QR codes:
|
||||
|
||||
- **Add by scanning (mobile):** On mobile, you can add a new entry by scanning
|
||||
the QR code provided by the service. This quickly adds the entry to Ente Auth.
|
||||
- **Show entry as QR code:** On all apps, you can long-press (mobile) or
|
||||
right-click (desktop) a code and select "QR". This allows you to easily share
|
||||
the complete entry (including the secret) with others by letting them scan the
|
||||
displayed QR code. This can also be used to easily add the same entry to
|
||||
another authenticatior app or service.
|
||||
@@ -6,7 +6,7 @@ description: Deleting items and trash
|
||||
# Trash
|
||||
|
||||
Whenever you delete an item from Ente, it is moved to Trash. These items will be
|
||||
automatically deleted from Trash after 30 days. You can manaully select photos
|
||||
automatically deleted from Trash after 30 days. You can manually select photos
|
||||
to permanently delete or completely empty the trash if you wish.
|
||||
|
||||
Items in trash are included in your used storage calculation.
|
||||
|
||||
@@ -14,9 +14,21 @@ directly stream chunks of Google Takeout zips that are stored on network drives.
|
||||
|
||||
In particular, the folder watch functionality suffers a lot since the app needs
|
||||
access to file system events to detect changes to the users files so that they
|
||||
can be uploaded whenever there are changes.
|
||||
can be uploaded whenever there are changes. Network drives are less reliable in
|
||||
providing these file change events correctly.
|
||||
|
||||
Since are high chances of the user having a subpar experience, we request
|
||||
customers to avoid using the desktop app directly with network attached storage
|
||||
and instead temporarily copy the files to their local storage for uploads, and
|
||||
avoid watching folders that live on a network drive.
|
||||
|
||||
## Exporting to UNC paths
|
||||
|
||||
Generally, exports are likely to work better than imports, since the interaction
|
||||
with the file system is relatively simpler (Note that the app still needs to
|
||||
scan the folder to find existing files, esp. if the continuous export option is
|
||||
enabled).
|
||||
|
||||
A special case is when exporting to a UNC path. In this case, the file
|
||||
separators will not work as expected and the export will not start. As a
|
||||
workaround, you can map your UNC path to a network drive and use that instead.
|
||||
|
||||
BIN
docs/docs/public/cloudflare.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
docs/docs/public/endpoint.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
docs/docs/public/otp.png
Normal file
|
After Width: | Height: | Size: 438 KiB |
BIN
docs/docs/public/quickstart.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
docs/docs/public/web-app.webp
Normal file
|
After Width: | Height: | Size: 75 KiB |
27
docs/docs/self-hosting/creating-accounts.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: Creating accounts
|
||||
description: Creating accounts on your deployment
|
||||
---
|
||||
|
||||
# Creating accounts
|
||||
|
||||
Once Ente is up and running, the Ente Photos web app will be accessible on
|
||||
`http://localhost:3000`. Open this URL in your browser and proceed with creating
|
||||
an account.
|
||||
|
||||
The default API endpoint for museum will be `localhost:8080`.
|
||||
|
||||

|
||||
|
||||
To complete your account registration you will need to enter a 6-digit
|
||||
verification code.
|
||||
|
||||
This code can be found in the server logs, which should already be shown in your
|
||||
quickstart terminal. Alternatively, you can open the server logs with the
|
||||
following command from inside the `my-ente` folder:
|
||||
|
||||
```sh
|
||||
sudo docker compose logs
|
||||
```
|
||||
|
||||

|
||||
49
docs/docs/self-hosting/faq/environment.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: "Environment Variables and Ports"
|
||||
description: "Information about all the Environment Variables needed to run Ente"
|
||||
---
|
||||
|
||||
# Environment variables and ports
|
||||
A self-hosted Ente instance requires specific endpoints in both Museum (the server) and web apps. This document outlines the essential environment variables and port mappings of the web apps.
|
||||
|
||||
Here's the list of important variables that a self hoster should know about:
|
||||
|
||||
### Museum
|
||||
|
||||
1. `NEXT_PUBLIC_ENTE_ENDPOINT`
|
||||
|
||||
The above environment variable is used to configure Museums endpoint. Where Museum is
|
||||
running and which port it is listening on. This endpoint should be configured for
|
||||
all the apps to connect to your self hosted endpoint.
|
||||
|
||||
All the apps (regardless of platform) by default connect to api.ente.io - which is
|
||||
our production instance of Museum.
|
||||
|
||||
### Web Apps
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Web apps don't need to be configured with the below endpoints. Web app environment
|
||||
> variables are being documented here just so that the users know everything in detail.
|
||||
> Checkout [Configuring your Server](/self-hosting/museum) to configure endpoints for
|
||||
> particular app.
|
||||
|
||||
In Ente, all the web apps are separate NextJS applications. Therefore, they are all
|
||||
configured via environment variables. The photos app (Ente Photos) has information
|
||||
about and connects to other web apps like albums, cast, etc.
|
||||
|
||||
|
||||
1. `NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT`
|
||||
|
||||
This environment variable is used to configure and declare the endpoint for the Albums
|
||||
web app.
|
||||
|
||||
## Ports
|
||||
|
||||
The below format is according to how ports are mapped in Docker.
|
||||
Typically,`<host>:<container-port>`
|
||||
|
||||
1. `8080:8080`: Museum (Ente's server)
|
||||
2. `3000:3000`: Ente Photos web app
|
||||
3. `3001:3001`: Ente Accounts web app
|
||||
4. `3003:3003`: [EEnte Auth](https://ente.io/auth/)
|
||||
5. `3004:3004`: [Ente Cast web app](http://ente.io/cast)
|
||||
@@ -12,6 +12,19 @@ verification code by:
|
||||
|
||||
- Reading it from the DB (otts table)
|
||||
|
||||
The easiest option when getting started is to look for it in the server (museum)
|
||||
logs. If you're already running the docker compose cluster using the quickstart
|
||||
script, you should be already seeing the logs in your terminal. Otherwise you
|
||||
can go to the folder (e.g. `my-ente`) where your `compose.yaml` is, then run
|
||||
`docker compose logs museum --follow`. Once you can see the logs, look for a
|
||||
line like:
|
||||
|
||||
```
|
||||
... Skipping sending email to email@example.com: *Verification code: 112089*
|
||||
```
|
||||
|
||||
That is the verification code.
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> You can also configure your instance to send out emails so that you can get
|
||||
|
||||
@@ -5,23 +5,26 @@ description:
|
||||
from outside localhost
|
||||
---
|
||||
|
||||
# Components of the Architecture
|
||||
# Architecture
|
||||
|
||||

|
||||
|
||||
There are three components involved in uploading:
|
||||
There are three components involved in uploading a file:
|
||||
|
||||
1. The client (e.g. the web app or the mobile app)
|
||||
2. Ente's server (museum)
|
||||
3. The S3-compatible object storage (e.g. minio in the default starter)
|
||||
3. The S3-compatible object storage (e.g. MinIO in the default starter)
|
||||
|
||||
For the uploads to work, all three of them need to be able to reach each other.
|
||||
This is because the client uploads directly to the object storage. The
|
||||
interaction goes something like this:
|
||||
This is because the client uploads directly to the object storage.
|
||||
|
||||
1. Client wants to upload, it asks museum where it should upload to.
|
||||
2. Museum creates pre-signed URLs for the S3 bucket that was configured.
|
||||
3. Client directly uploads to the S3 buckets these URLs.
|
||||
A file upload flows as follows:
|
||||
|
||||
1. Client that wants to upload a file asks museum where it should upload the
|
||||
file to
|
||||
2. museum creates pre-signed URLs for the S3 bucket that was configured
|
||||
3. Client directly uploads to the S3 buckets these URLs
|
||||
4. Client finally informs museum that a file has been uploaded to this URL
|
||||
|
||||
The upshot of this is that _both_ the client and museum should be able to reach
|
||||
your S3 bucket.
|
||||
@@ -30,10 +33,10 @@ your S3 bucket.
|
||||
|
||||
The URL for the S3 bucket is configured in
|
||||
[scripts/compose/credentials.yaml](https://github.com/ente-io/ente/blob/main/server/scripts/compose/credentials.yaml#L10).
|
||||
You can edit this file directly when testing, though it is just simpler and more
|
||||
robust to create a `museum.yaml` (in the same folder as the Docker compose file)
|
||||
and put your custom configuration there (in your case, you can put an entire
|
||||
`s3` config object in your `museum.yaml`).
|
||||
|
||||
You can edit this file directly while testing, though it is more robust to
|
||||
create a `museum.yaml` (in the same folder as the Docker compose file) and to
|
||||
setup your custom configuration there.
|
||||
|
||||
> [!TIP]
|
||||
> For more details about these configuration objects, see the documentation for
|
||||
@@ -42,29 +45,32 @@ and put your custom configuration there (in your case, you can put an entire
|
||||
|
||||
By default, you only need to configure the endpoint for the first bucket.
|
||||
|
||||
The docker compose file is shipped with MinIO as the Self Hosted S3 Compatible Storage.
|
||||
By default, MinIO server is served on `localhost:3200` and the MinIO UI on
|
||||
`localhost:3201`.
|
||||
For example, in a localhost network situation, the way this
|
||||
connection works is, Museum (`1`) and MinIO (`2`) run on the same docker network and
|
||||
the web app (`3`) which will also be hosted on the localhost. This enables all the
|
||||
three components of the setup being able to communicate with each other seamlessly.
|
||||
The Docker compose file is shipped with MinIO as the self hosted S3 compatible
|
||||
storage. By default, MinIO server is served on `localhost:3200` and the MinIO UI
|
||||
on `localhost:3201`.
|
||||
|
||||
For example, in a localhost network situation, the way this connection works is,
|
||||
museum (`1`) and MinIO (`2`) run on the same Docker network and the web app
|
||||
(`3`) will also be hosted on your localhost. This enables all the three
|
||||
components of the setup to communicate with each other seamlessly.
|
||||
|
||||
The same principle applies if you're deploying to your custom domain.
|
||||
|
||||
## Replication
|
||||
|
||||

|
||||
<p align="center">Community contributed diagram of Ente's Replication Process</p>
|
||||
<p align="center">Community contributed diagram of Ente's replication process</p>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> As of now, Replication works only if all the 3 storage type
|
||||
> needs are fulfilled (1 Hot, 1 Cold and 1 Glacier Storage).
|
||||
>
|
||||
> As of now, replication works only if all the 3 storage type needs are
|
||||
> fulfilled (1 hot, 1 cold and 1 glacier storage).
|
||||
>
|
||||
> [Reference](https://github.com/ente-io/ente/discussions/3167#discussioncomment-10585970)
|
||||
|
||||
If you're wondering why there are 3 buckets on MinIO UI - that's because our
|
||||
production instance uses these to perform [replication](https://ente.io/reliability/).
|
||||
If you're wondering why there are 3 buckets on the MinIO UI - that's because our
|
||||
production instance uses these to perform
|
||||
[replication](https://ente.io/reliability/).
|
||||
|
||||
If you're also wondering about why the bucket names are specifically what they are,
|
||||
it's because that is exactly what we are using on our production instance.
|
||||
@@ -72,10 +78,10 @@ We use `b2-eu-cen` as hot, `wasabi-eu-central-2-v3` as cold (also the secondary
|
||||
and `scw-eu-fr-v3` as glacier storage. As of now, all of this is hardcoded.
|
||||
Hence, the same hardcoded configuration is applied when you self host Ente.
|
||||
|
||||
In a Self hosted Ente Instance replication is turned off by default.
|
||||
When replication is turned off, only the first bucket (`b2-eu-cen`) is used,
|
||||
and the other two are ignored. Only the names here are specifically fixed, but
|
||||
in the configuration body you can put any other keys. It does not have any relation
|
||||
In a self hosted Ente instance replication is turned off by default. When
|
||||
replication is turned off, only the first bucket (`b2-eu-cen`) is used, and the
|
||||
other two are ignored. Only the names here are specifically fixed, but in the
|
||||
configuration body you can put any other keys. It does not have any relation
|
||||
with `b2`, `wasabi` or even `scaleway`.
|
||||
|
||||
Use the `s3.hot_storage.primary` option if you'd like to set one of the other
|
||||
@@ -85,23 +91,23 @@ predefined buckets as the primary bucket.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> If you need to configure SSL, for example if you're running over the internet,
|
||||
> you'll need to turn off `s3.are_local_buckets` (which disables SSL in the
|
||||
> default starter compose template).
|
||||
> If you need to configure SSL, you'll need to turn off `s3.are_local_buckets`
|
||||
> (which disables SSL in the default starter compose template).
|
||||
>
|
||||
|
||||
Disabling `s3.are_local_buckets` also switches to the subdomain style URLs for
|
||||
the buckets. However, not all S3 providers support these, in particular, minio
|
||||
does not work with these in default configuration. So in such cases you'll
|
||||
also need to then enable `s3.use_path_style_urls`.
|
||||
the buckets. However, not all S3 providers support these. In particular, MinIO
|
||||
does not work with these in default configuration. So in such cases you'll also
|
||||
need to enable `s3.use_path_style_urls`.
|
||||
|
||||
## Summary
|
||||
|
||||
Set the S3 bucket `endpoint` in `credentials.yaml` to a `yourserverip:3200` or
|
||||
some such IP/hostname that accessible from both where you are running the Ente
|
||||
clients (e.g. the mobile app) and also from within the Docker compose cluster.
|
||||
some such IP / hostname 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.
|
||||
|
||||
#### Example
|
||||
### Example
|
||||
|
||||
An example `museum.yaml` when you're trying to connect to museum running on your
|
||||
computer from your phone on the same WiFi network:
|
||||
@@ -115,51 +121,4 @@ s3:
|
||||
endpoint: http://<YOUR-WIFI-IP>:3200
|
||||
region: eu-central-2
|
||||
bucket: b2-eu-cen
|
||||
```
|
||||
|
||||
## FAE (Frequently Answered Errors)
|
||||
|
||||
Here are some Frequently Answered Errors from the Community Chat with the reasoning
|
||||
for a particular error and its potential fix.
|
||||
|
||||
In most situations, the problem turns out to be some minute mistakes or misconfigurations
|
||||
on the users end and that turns out to be the bottleneck of the whole problem.
|
||||
Please make sure to `reverse_proxy` Museum to a domain as well as check your S3
|
||||
Credentials and whole config for any minor mis-configurations.
|
||||
|
||||
It is also suggested that the user setups Bucket CORS on MinIO or any external
|
||||
S3 Bucket they are connecting to. To setup Bucket CORS, help yourself by upload
|
||||
[this](https://help.ente.io/self-hosting/guides/external-s3#_5-fix-potential-cors-issue-with-your-bucket).
|
||||
|
||||
### 403 Forbidden
|
||||
|
||||
If museum (`2`) is able to make a network connection to your S3 bucket (`3`) but
|
||||
uploads are still failing, it could be a credentials or permissions issue. A
|
||||
telltale sign of this is that in the museum logs you can see `403 Forbidden`
|
||||
errors about it not able to find the size of a file even though the
|
||||
corresponding object exists in the S3 bucket.
|
||||
|
||||
To fix these, you should ensure the following:
|
||||
|
||||
1. The bucket CORS rules do not allow museum to access these objects.
|
||||
- For uploading files from the browser, you will need to currently set
|
||||
allowedOrigins to "\*", and allow the "X-Auth-Token", "X-Client-Package"
|
||||
headers configuration too.
|
||||
[Here is an example of a working configuration](https://github.com/ente-io/ente/discussions/1764#discussioncomment-9478204).
|
||||
|
||||
2. The credentials are not being picked up (you might be setting the correct
|
||||
creds, but not in the place where museum picks them from).
|
||||
|
||||
### Mismatch in File Size
|
||||
|
||||
The "Mismatch in File Size" error mostly occurs in a situation where the client (`1`)
|
||||
is re-uploading a file which is already in the bucket with a different File Size. The
|
||||
reason for re-upload could be anything including Network issue, sudden killing of app
|
||||
before the upload is complete and etc.
|
||||
|
||||
This is also one of Museums (`2`) Validation Checks for the size of file being
|
||||
re-uploaded from the client to the size of the file which is already
|
||||
uploaded to the S3 Bucket.
|
||||
|
||||
In most case, it is very unlikely that this error could be a cause of some mistake in
|
||||
the configuration or Browser/Bucket CORS.
|
||||
```
|
||||
@@ -250,64 +250,6 @@ docker compose exec -i postgres psql -U pguser -d ente_db -c "INSERT INTO storag
|
||||
|
||||
After few reloads, you should see 1 To of quota.
|
||||
|
||||
## 5. Fix potential CORS issue with your bucket
|
||||
|
||||
### For AWS S3
|
||||
|
||||
If you cannot upload a photo due to a CORS issue, you need to fix the CORS
|
||||
configuration of your bucket.
|
||||
|
||||
Create a `cors.json` file with the following content:
|
||||
|
||||
```json
|
||||
{
|
||||
"CORSRules": [
|
||||
{
|
||||
"AllowedOrigins": ["*"],
|
||||
"AllowedHeaders": ["*"],
|
||||
"AllowedMethods": ["GET", "HEAD", "POST", "PUT", "DELETE"],
|
||||
"MaxAgeSeconds": 3000,
|
||||
"ExposeHeaders": ["Etag"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You may want to change the `AllowedOrigins` to a more restrictive value.
|
||||
|
||||
If you are using AWS for S3, you can execute the below command to get rid of
|
||||
CORS. Make sure to enter the right path for the `cors.json` file.
|
||||
|
||||
```bash
|
||||
aws s3api put-bucket-cors --bucket YOUR_S3_BUCKET --cors-configuration /path/to/cors.json
|
||||
```
|
||||
|
||||
### For Self-hosted Minio Instance
|
||||
|
||||
> Important: MinIO does not take JSON CORS file as the input, instead you will
|
||||
> have to build a CORS.xml file or just convert the above `cors.json` to XML.
|
||||
|
||||
A minor requirement here is the tool `mc` for managing buckets via command line
|
||||
interface. Checkout the `mc set alias` document to configure alias for your
|
||||
instance and bucket. After this you will be prompted for your AccessKey and
|
||||
Secret, which is your username and password, go ahead and enter that.
|
||||
|
||||
```sh
|
||||
mc cors set <your-minio>/<your-bucket-name /path/to/cors.xml
|
||||
```
|
||||
|
||||
or, if you just want to just set the `AllowedOrigins` Header, you can use the
|
||||
following command to do so.
|
||||
|
||||
```sh
|
||||
mc admin config set <your-minio>/<your-bucket-name> api cors_allow_origin="*"
|
||||
```
|
||||
|
||||
You can create also `.csv` file and dump the list of origins you would like to
|
||||
allow and replace the `*` with `path` to the CSV file.
|
||||
|
||||
Now, uploads should be working fine.
|
||||
|
||||
## Related
|
||||
|
||||
Some other users have also shared their setups.
|
||||
@@ -315,3 +257,5 @@ Some other users have also shared their setups.
|
||||
- [Using Traefik](https://github.com/ente-io/ente/pull/3663)
|
||||
|
||||
- [Building custom images from source (Linux)](https://github.com/ente-io/ente/discussions/3778)
|
||||
|
||||
- [Troubleshooting Bucket CORS](/self-hosting/troubleshooting/bucket-cors)
|
||||
|
||||
228
docs/docs/self-hosting/guides/from-source.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
title: Ente from Source
|
||||
description: Getting started self hosting Ente Photos and/or Ente Auth
|
||||
---
|
||||
|
||||
|
||||
# Ente from Source
|
||||
|
||||
> [!WARNING] NOTE
|
||||
> The below documentation will cover instructions about self-hosting the web app manually. If you
|
||||
> want to deploy Ente hassle free, use the [one line](https://ente.io/blog/self-hosting-quickstart/)
|
||||
> command to setup Ente. This guide might be deprecated in the near future.
|
||||
|
||||
## Installing Docker
|
||||
|
||||
Refer to
|
||||
[How to install Docker from the APT repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository)
|
||||
for detailed instructions.
|
||||
|
||||
## Start the server
|
||||
|
||||
```sh
|
||||
git clone https://github.com/ente-io/ente
|
||||
cd ente/server
|
||||
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))
|
||||
|
||||
Install the necessary dependencies for running the web client
|
||||
|
||||
```sh
|
||||
# installing npm and yarn
|
||||
|
||||
sudo apt update
|
||||
sudo apt install nodejs npm
|
||||
sudo npm install -g yarn // to install yarn globally
|
||||
```
|
||||
|
||||
Then in a separate terminal, you can run (e.g) the web client
|
||||
|
||||
```sh
|
||||
cd ente/web
|
||||
git submodule update --init --recursive
|
||||
yarn install
|
||||
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev
|
||||
```
|
||||
|
||||
That's about it. If you open http://localhost:3000, you will be able to create
|
||||
an account on a Ente Photos web app running on your machine, and this web app
|
||||
will be connecting to the server running on your local machine at
|
||||
`localhost:8080`.
|
||||
|
||||
For the mobile apps, you don't even need to build, and can install normal Ente
|
||||
apps and configure them to use your
|
||||
[custom self-hosted server](/self-hosting/guides/custom-server/).
|
||||
|
||||
> If you want to build the mobile apps from source, see the instructions
|
||||
> [here](/self-hosting/guides/mobile-build).
|
||||
|
||||
## Web app with Docker and Compose
|
||||
|
||||
The instructoins in previous section were just a temporary way to run the web app locally.
|
||||
To run the web apps as services, the user has to build a docker image manually.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> Recurring changes might be made by the team or from community if more
|
||||
> improvements can be made so that we are able to build a full-fledged docker
|
||||
> image.
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-bookworm-slim as builder
|
||||
|
||||
WORKDIR ./ente
|
||||
|
||||
COPY . .
|
||||
COPY apps/ .
|
||||
|
||||
# Will help default to yarn versoin 1.22.22
|
||||
RUN corepack enable
|
||||
|
||||
# Endpoint for Ente Server
|
||||
ENV NEXT_PUBLIC_ENTE_ENDPOINT=https://your-ente-endpoint.com
|
||||
ENV NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=https://your-albums-endpoint.com
|
||||
|
||||
RUN yarn cache clean
|
||||
RUN yarn install --network-timeout 1000000000
|
||||
RUN yarn build:photos && yarn build:accounts && yarn build:auth && yarn build:cast
|
||||
|
||||
FROM node:20-bookworm-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /ente/apps/photos/out /app/photos
|
||||
COPY --from=builder /ente/apps/accounts/out /app/accounts
|
||||
COPY --from=builder /ente/apps/auth/out /app/auth
|
||||
COPY --from=builder /ente/apps/cast/out /app/cast
|
||||
|
||||
RUN npm install -g serve
|
||||
|
||||
ENV PHOTOS=3000
|
||||
EXPOSE ${PHOTOS}
|
||||
|
||||
ENV ACCOUNTS=3001
|
||||
EXPOSE ${ACCOUNTS}
|
||||
|
||||
ENV AUTH=3002
|
||||
EXPOSE ${AUTH}
|
||||
|
||||
ENV CAST=3003
|
||||
EXPOSE ${CAST}
|
||||
|
||||
# The albums app does not have navigable pages on it, but the
|
||||
# port will be exposed in-order to self up the albums endpoint
|
||||
# `apps.public-albums` in museum.yaml configuration file.
|
||||
ENV ALBUMS=3004
|
||||
EXPOSE ${ALBUMS}
|
||||
|
||||
CMD ["sh", "-c", "serve /app/photos -l tcp://0.0.0.0:${PHOTOS} & serve /app/accounts -l tcp://0.0.0.0:${ACCOUNTS} & serve /app/auth -l tcp://0.0.0.0:${AUTH} & serve /app/cast -l tcp://0.0.0.0:${CAST}"]
|
||||
```
|
||||
|
||||
The above is a multi-stage Dockerfile which creates a production ready static
|
||||
output of the 4 apps (Photos, Accounts, Auth and Cast) and serves the static
|
||||
content with Caddy.
|
||||
|
||||
Looking at 2 different node base-images doing different tasks in the same
|
||||
Dockerfile would not make sense, but the Dockerfile is divided into two just to
|
||||
improve the build efficiency as building this Dockerfile will arguably take more
|
||||
time.
|
||||
|
||||
Lets build a Docker image from the above Dockerfile. Copy and paste the above
|
||||
Dockerfile contents in the root of your web directory which is inside
|
||||
`ente/web`. Execute the below command to create an image from this Dockerfile.
|
||||
|
||||
```sh
|
||||
# Build the image
|
||||
docker build -t <image-name>:<tag> --no-cache --progress plain .
|
||||
```
|
||||
|
||||
You can always edit the Dockerfile and remove the steps for apps which you do
|
||||
not intend to install on your system (like auth or cast) and opt out of those.
|
||||
|
||||
Regarding Albums App, take a note that they are not apps with navigable pages,
|
||||
if accessed on the web-browser they will simply redirect to ente.web.io.
|
||||
|
||||
## compose.yaml
|
||||
|
||||
Moving ahead, we need to paste the below contents into the compose.yaml inside
|
||||
`ente/server/compose.yaml` under the services section.
|
||||
|
||||
```yaml
|
||||
ente-web:
|
||||
image: <image-name> # name of the image you used while building
|
||||
ports:
|
||||
- 3000:3000
|
||||
- 3001:3001
|
||||
- 3002:3002
|
||||
- 3003:3003
|
||||
- 3004:3004
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
restart: always
|
||||
```
|
||||
|
||||
Now, we're good to go. All we are left to do now is start the containers.
|
||||
|
||||
```sh
|
||||
docker compose up -d # --build
|
||||
|
||||
# Accessing the logs
|
||||
docker compose logs <container-name>
|
||||
```
|
||||
|
||||
## Configure App Endpoints
|
||||
|
||||
> [!NOTE]
|
||||
> Previously, this was dependent on the env variables `NEXT_ENTE_PUBLIC_ACCOUNTS_ENDPOINT`
|
||||
> and etc. Please check the below documentation to update your setup configurations
|
||||
|
||||
You can configure the web endpoints for the other apps including Accounts, Albums
|
||||
Family and Cast in your `museum.yaml` configuration file. Checkout
|
||||
[`local.yaml`](https://github.com/ente-io/ente/blob/543411254b2bb55bd00a0e515dcafa12d12d3b35/server/configurations/local.yaml#L76-L89)
|
||||
to configure the endpoints. Make sure to setup up your DNS Records accordingly to the
|
||||
similar URL's you set up in `museum.yaml`.
|
||||
|
||||
Next part is to configure the web server.
|
||||
|
||||
# Web server configuration
|
||||
|
||||
The last step ahead is configuring reverse_proxy for the ports on which the apps
|
||||
are being served (you will have to make changes, if you have cusotmized the
|
||||
ports). The web server of choice in this guide is
|
||||
[Caddy](https://caddyserver.com) because with caddy you don't have to manually
|
||||
configure/setup SSL ceritifcates as caddy will take care of that.
|
||||
|
||||
```sh
|
||||
photos.yourdomain.com {
|
||||
reverse_proxy http://localhost:3001
|
||||
# for logging
|
||||
log {
|
||||
level error
|
||||
}
|
||||
}
|
||||
|
||||
auth.yourdomain.com {
|
||||
reverse_proxy http://localhost:3002
|
||||
}
|
||||
# and so on ...
|
||||
```
|
||||
|
||||
Next, start the caddy server :).
|
||||
|
||||
```sh
|
||||
# If caddy service is not enabled
|
||||
sudo systemctl enable caddy
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl start caddy
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Please start a discussion on the Github Repo if you have any suggestions for the
|
||||
Dockerfile, You can also share your setups on Github Discussions.
|
||||
@@ -5,6 +5,12 @@ description:
|
||||
server
|
||||
---
|
||||
|
||||
|
||||
> [!WARNING] NOTE
|
||||
> This page covers documentation around self-hosting the web app manually. If you
|
||||
> want to deploy Ente hassle free, please use the [one line](https://ente.io/blog/self-hosting-quickstart/)
|
||||
> command to setup Ente. This guide might be deprecated in the near future.
|
||||
|
||||
# Web app
|
||||
|
||||
The getting started instructions mention using `yarn dev` (which is an alias of
|
||||
|
||||
@@ -10,104 +10,32 @@ the same code we use for our own cloud service.
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> To get some context, you might find our
|
||||
> [blog post](https://ente.io/blog/open-sourcing-our-server/) announcing the
|
||||
> open sourcing of our server useful.
|
||||
> You might find our [blog post](https://ente.io/blog/open-sourcing-our-server/)
|
||||
> announcing the open sourcing of our server useful.
|
||||
|
||||
## Getting started - Quickstart
|
||||
## System requirements
|
||||
|
||||
Install [Docker](https://www.docker.com). Then, paste the following command in a
|
||||
your terminal:
|
||||
The server has minimal resource requirements, running as a lightweight Go
|
||||
binary. It performs well on small cloud instances, old laptops, and even
|
||||
[low-end embedded devices](https://github.com/ente-io/ente/discussions/594).
|
||||
|
||||
## Getting started
|
||||
|
||||
Run this command on your terminal to setup Ente.
|
||||
|
||||
```sh
|
||||
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ente-io/ente/main/server/quickstart.sh)"
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> For more details about what this does, see [the quickstart
|
||||
> README](https://github.com/ente-io/ente/blob/main/server/docs/quickstart.md).
|
||||
The above `curl` command pulls the Docker image, creates a directory `my-ente`
|
||||
in the current working directory and starts all containers required to run Ente.
|
||||
|
||||
That's about it. If you open http://localhost:3000 from the machine where the
|
||||
server is running, you will be able to create an account on a Ente Photos web
|
||||
app. This web app will be connecting to the server running on your local machine
|
||||
at `localhost:8080`.
|
||||

|
||||
|
||||
To complete your account registration you need to enter a 6-digit verification
|
||||
code. These can be found in the server logs which should already be shown in
|
||||
your quickstart terminal. Otherwise you can open the server logs with the
|
||||
following command from inside the `my-ente` folder:
|
||||

|
||||
|
||||
```sh
|
||||
sudo docker compose logs
|
||||
```
|
||||
## Queries?
|
||||
|
||||
In the logs, find the code at the end of a message that resembles the following:
|
||||
```sh
|
||||
museum | INFO[0102]email.go:130 sendViaTransmail Skipping sending email to email@example.com: *Verification code: 112089*
|
||||
```
|
||||
|
||||
There are [prebuilt apps](https://ente.io/download) for iPad, iPhone, Android,
|
||||
Linux, Mac, and Windows. These can easily be configured to use your [custom
|
||||
self-hosted server](guides/custom-server/).
|
||||
|
||||
## Getting started - From source
|
||||
|
||||
The quickstart method above uses pre-built images. Alternatively, if you want to
|
||||
build the self hosted server images from source, you can use the steps in this
|
||||
section.
|
||||
|
||||
#### Installing Docker
|
||||
|
||||
Refer to
|
||||
[How to install Docker from the APT repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository)
|
||||
for detailed instructions.
|
||||
|
||||
#### Start the server
|
||||
|
||||
```sh
|
||||
git clone https://github.com/ente-io/ente
|
||||
cd ente/server
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Install the necessary dependencies for running the web client
|
||||
|
||||
```sh
|
||||
# installing npm and yarn
|
||||
|
||||
sudo apt update
|
||||
sudo apt install nodejs npm
|
||||
sudo npm install -g yarn // to install yarn globally
|
||||
```
|
||||
|
||||
Then in a separate terminal, you can run (e.g) the web client
|
||||
|
||||
```sh
|
||||
cd ente/web
|
||||
yarn install
|
||||
NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev
|
||||
```
|
||||
|
||||
> If you want to build the mobile apps from source, see the instructions
|
||||
> [here](guides/mobile-build).
|
||||
|
||||
## Next steps
|
||||
|
||||
- More details about the server are in its
|
||||
[README](https://github.com/ente-io/ente/tree/main/server#readme)
|
||||
|
||||
- More details about running the server (with or without Docker) are in
|
||||
[RUNNING](https://github.com/ente-io/ente/blob/main/server/RUNNING.md)
|
||||
|
||||
- If you have questions around self-hosting that are not answered in any of the
|
||||
existing documentation, you can ask in our
|
||||
[GitHub Discussions](https://github.com/ente-io/ente/discussions). **Please
|
||||
remember to search first if the query has been already asked and answered.**
|
||||
|
||||
## Contributing!
|
||||
|
||||
One particular way in which you can help is by adding new [guides](guides/) on
|
||||
this help site. The documentation is written in Markdown and adding new pages is
|
||||
[easy](https://github.com/ente-io/ente/tree/main/docs#readme). Editing existing
|
||||
pages is even easier: at the bottom of each page is an _Edit this page_ link.
|
||||
If you need support, please ask on our community
|
||||
[Discord](https://ente.io/discord) or start a discussion on
|
||||
[GitHub](https://github.com/ente-io/ente/discussions/).
|
||||
|
||||
77
docs/docs/self-hosting/museum.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Configuring your server
|
||||
description: Guide to writing a museum.yaml
|
||||
---
|
||||
|
||||
# Configuring your server
|
||||
|
||||
Ente's monolithic server is called **museum**.
|
||||
|
||||
`museum.yaml` is a YAML configuration file used to configure museum. By default,
|
||||
[`local.yaml`](https://github.com/ente-io/ente/tree/main/server/configurations/local.yaml)
|
||||
is provided, but its settings are overridden with those from `museum.yaml`.
|
||||
|
||||
If you used our quickstart script, your `my-ente` directory will include a
|
||||
`museum.yaml` file with preset configurations for encryption keys, secrets,
|
||||
PostgreSQL and MinIO.
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> Always do `docker compose down` inside your `my-ente` directory. If you've
|
||||
> made changes to `museum.yaml`, restart the containers with `docker compose up
|
||||
> -d ` to see your changes in action.
|
||||
|
||||
## S3 buckets
|
||||
|
||||
The `s3` section within `museum.yaml` is by default configured to use local
|
||||
MinIO buckets.
|
||||
|
||||
If you wish to use an external S3 provider, you can edit the configuration with
|
||||
your provider's credentials, and set `are_local_buckets` to `false`.
|
||||
|
||||
Check out [Configuring S3](/self-hosting/guides/configuring-s3.md) to understand
|
||||
more about configuring S3 buckets.
|
||||
|
||||
MinIO uses the port `3200` for API Endpoints and their web app runs over
|
||||
`:3201`. You can login to MinIO Web Console by opening `localhost:3201` in your browser.
|
||||
|
||||
If you face any issues related to uploads then checkout [Troubleshooting bucket
|
||||
CORS](/self-hosting/troubleshooting/bucket-cors) and [Frequently encountered S3
|
||||
errors](/self-hosting/guides/configuring-s3#frequently-encountered-errors).
|
||||
|
||||
## Web apps
|
||||
|
||||
The web apps for Ente Photos is divided into multiple sub-apps like albums,
|
||||
cast, auth, etc. These endpoints are configurable in the museum.yaml under the
|
||||
`apps.*` section.
|
||||
|
||||
For example,
|
||||
|
||||
```yaml
|
||||
apps:
|
||||
public-albums: albums.myente.xyz
|
||||
cast: cast.myente.xyz
|
||||
accounts: accounts.myente.xyz
|
||||
family: family.myente.xyz
|
||||
```
|
||||
|
||||
>[!IMPORTANT]
|
||||
>By default, all the values redirect to our publicly hosted production services.
|
||||
>For example, if `public-albums` is not configured your shared album will
|
||||
>use the `albums.ente.io` URL.
|
||||
|
||||
After you are done with filling the values, restart museum and the app will
|
||||
start utilizing those endpoints instead of Ente's production instances.
|
||||
|
||||
Once you have configured all the necessary endpoints, `cd` into `my-ente` and
|
||||
stop all the Docker containers with `docker compose down` and restart them with
|
||||
`docker compose up -d`.
|
||||
|
||||
Similarly, you can use the default
|
||||
[`local.yaml`](https://github.com/ente-io/ente/tree/main/server/configurations/local.yaml)
|
||||
as a reference for building a functioning `museum.yaml` for many other
|
||||
functionalities like SMTP, Discord notifications, Hardcoded-OTTs, etc.
|
||||
|
||||
## References
|
||||
|
||||
- [Environment variables and ports](/self-hosting/faq/environment)
|
||||
49
docs/docs/self-hosting/reverse-proxy.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
Title: Configuring Reverse Proxy
|
||||
Description: configuring reverse proxy for Museum and other endpoints
|
||||
---
|
||||
|
||||
# Reverse proxy
|
||||
|
||||
Ente's server (museum) runs on port `:8080`, web app on `:3000` and the other
|
||||
apps from ports `3001-3004`.
|
||||
|
||||
We highly recommend using HTTPS for Museum (`8080`). For security reasons museum
|
||||
will not accept incoming HTTP traffic.
|
||||
|
||||
Head over to your DNS management dashboard and setup the appropriate records for
|
||||
the endpoints. Mostly, `A` or `AAAA` records targeting towards your server's IP
|
||||
address should be sufficient. The rest of the work will be done by the web
|
||||
server on your machine.
|
||||
|
||||

|
||||
|
||||
### Caddy
|
||||
|
||||
Setting up a reverse proxy with Caddy is easy and straightforward.
|
||||
|
||||
Firstly, install Caddy on your server.
|
||||
|
||||
```sh
|
||||
sudo apt install caddy
|
||||
```
|
||||
|
||||
After the installation is complete, a `Caddyfile` is created on the path
|
||||
`/etc/caddy/`. This file is used to configure reverse proxies among other
|
||||
things.
|
||||
|
||||
```yaml
|
||||
# Caddyfile - myente.xyz is just an example.
|
||||
api.myente.xyz {
|
||||
reverse_proxy http://localhost:8080
|
||||
}
|
||||
ente.myente.xyz {
|
||||
reverse_proxy http://localhost:3000
|
||||
}
|
||||
#...and so on for other endpoints
|
||||
```
|
||||
|
||||
After a hard-reload, the Ente Photos web app should be up on https://ente.myente.xyz.
|
||||
|
||||
If you are using a different tool for reverse proxy (like nginx), please check
|
||||
out their documentation.
|
||||
62
docs/docs/self-hosting/troubleshooting/bucket-cors.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Bucket CORS
|
||||
description: Troubleshooting CORS issues with S3 Buckets
|
||||
---
|
||||
|
||||
# Fix potential CORS issues with your Buckets
|
||||
|
||||
## For AWS S3
|
||||
|
||||
If you cannot upload a photo due to a CORS issue, you need to fix the CORS
|
||||
configuration of your bucket.
|
||||
|
||||
Create a `cors.json` file with the following content:
|
||||
|
||||
```json
|
||||
{
|
||||
"CORSRules": [
|
||||
{
|
||||
"AllowedOrigins": ["*"],
|
||||
"AllowedHeaders": ["*"],
|
||||
"AllowedMethods": ["GET", "HEAD", "POST", "PUT", "DELETE"],
|
||||
"MaxAgeSeconds": 3000,
|
||||
"ExposeHeaders": ["Etag"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You may want to change the `AllowedOrigins` to a more restrictive value.
|
||||
|
||||
If you are using AWS for S3, you can execute the below command to get rid of
|
||||
CORS. Make sure to enter the right path for the `cors.json` file.
|
||||
|
||||
```bash
|
||||
aws s3api put-bucket-cors --bucket YOUR_S3_BUCKET --cors-configuration /path/to/cors.json
|
||||
```
|
||||
|
||||
## For Self-hosted Minio Instance
|
||||
|
||||
> Important: MinIO does not take JSON CORS file as the input, instead you will
|
||||
> have to build a CORS.xml file or just convert the above `cors.json` to XML.
|
||||
|
||||
A minor requirement here is the tool `mc` for managing buckets via command line
|
||||
interface. Checkout the `mc set alias` document to configure alias for your
|
||||
instance and bucket. After this you will be prompted for your AccessKey and
|
||||
Secret, which is your username and password, go ahead and enter that.
|
||||
|
||||
```sh
|
||||
mc cors set <your-minio>/<your-bucket-name /path/to/cors.xml
|
||||
```
|
||||
|
||||
or, if you just want to just set the `AllowedOrigins` Header, you can use the
|
||||
following command to do so.
|
||||
|
||||
```sh
|
||||
mc admin config set <your-minio>/<your-bucket-name> api cors_allow_origin="*"
|
||||
```
|
||||
|
||||
You can create also `.csv` file and dump the list of origins you would like to
|
||||
allow and replace the `*` with `path` to the CSV file.
|
||||
|
||||
Now, uploads should be working fine.
|
||||
@@ -1,13 +1,54 @@
|
||||
---
|
||||
title: Uploads failing
|
||||
title: Uploads
|
||||
description: Fixing upload errors when trying to self host Ente
|
||||
---
|
||||
|
||||
# Uploads failing
|
||||
# Troubleshooting upload failures
|
||||
|
||||
If uploads to your minio are failing, you need to ensure that you've configured
|
||||
the S3 bucket `endpoint` in `credentials.yaml` (or `museum.yaml`) to, say,
|
||||
`yourserverip:3200`. This can be any host or port, it just need to be a value
|
||||
that is reachable from both your client and from museum.
|
||||
Here are some errors our community members frequently encountered with the
|
||||
context and potential fixes.
|
||||
|
||||
For more details, see [configuring-s3](/self-hosting/guides/configuring-s3).
|
||||
Fundamentally in most situations, the problem is because of minor mistakes or
|
||||
misconfiguration. Please make sure to reverse proxy museum and MinIO API
|
||||
endpoint to a domain and check your S3 credentials and whole configuration
|
||||
file for any minor misconfigurations.
|
||||
|
||||
It is also suggested that the user setups bucket CORS on MinIO or any external
|
||||
S3 service provider they are connecting to. To setup bucket CORS, please [read
|
||||
this](/self-hosting/troubleshooting/bucket-cors).
|
||||
|
||||
## What is S3 and how is it incorporated in Ente ?
|
||||
|
||||
S3 is an cloud storage protocol made by Amazon (specifically AWS). S3 is designed to store
|
||||
files and data as objects inside Buckets and it is mostly used for Online
|
||||
Backups and storing different types of files.
|
||||
|
||||
Ente's Docker setup is shipped with [MinIO](https://min.io/) as its default S3 provider.
|
||||
MinIO supports the Amazon S3 protocol and leverages your disk storage to
|
||||
dump all the uploaded files as encrypted object blobs.
|
||||
|
||||
## 403 Forbidden
|
||||
|
||||
If museum is able to make a network connection to your S3 bucket but
|
||||
uploads are still failing, it could be a credentials or permissions issue.
|
||||
|
||||
A telltale sign of this is that in the museum logs you can see `403 Forbidden`
|
||||
errors about it not able to find the size of a file even though the
|
||||
corresponding object exists in the S3 bucket.
|
||||
|
||||
This could be because
|
||||
|
||||
1. The bucket CORS rules do not allow museum to access these objects. For
|
||||
uploading files from the browser, you will need to set `allowedOrigins` to
|
||||
`*`, and allow the `X-Auth-Token`, `X-Client-Package` headers configuration
|
||||
too. [Here is an example of a working
|
||||
configuration](https://github.com/ente-io/ente/discussions/1764#discussioncomment-9478204).
|
||||
|
||||
2. The credentials are not being picked up (you might be setting the correct
|
||||
credentials, but not in the place where museum reads them from).
|
||||
|
||||
## Mismatch in file size
|
||||
|
||||
The "Mismatch in file size" error mostly occurs in a situation where the client is re-uploading a file which is already in the bucket with a different
|
||||
file size. The reason for re-upload could be anything including network issue,
|
||||
sudden killing of app before the upload is complete and etc.
|
||||
|
||||
@@ -10,10 +10,10 @@ import "./App.css";
|
||||
import FamilyTableComponent from "./components/FamilyComponentTable";
|
||||
import StorageBonusTableComponent from "./components/StorageBonusTableComponent";
|
||||
import TokensTableComponent from "./components/TokenTableComponent";
|
||||
import type { UserData } from "./components/UserComponent";
|
||||
import UserComponent from "./components/UserComponent";
|
||||
import duckieimage from "./components/duckie.png";
|
||||
import { apiOrigin } from "./services/support";
|
||||
import type { UserData, UserResponse } from "./types";
|
||||
|
||||
export let email = "";
|
||||
export let token = "";
|
||||
@@ -29,38 +29,6 @@ export const setToken = (newToken: string) => {
|
||||
export const getEmail = () => email;
|
||||
export const getToken = () => token;
|
||||
|
||||
interface User {
|
||||
ID: string;
|
||||
email: string;
|
||||
creationTime: number;
|
||||
}
|
||||
|
||||
interface Subscription {
|
||||
productID: string;
|
||||
paymentProvider: string;
|
||||
expiryTime: number;
|
||||
storage: number;
|
||||
}
|
||||
|
||||
interface Security {
|
||||
isEmailMFAEnabled: boolean;
|
||||
isTwoFactorEnabled: boolean;
|
||||
passkeys: string;
|
||||
passkeyCount: number;
|
||||
canDisableEmailMFA: boolean;
|
||||
}
|
||||
|
||||
interface UserResponse {
|
||||
user: User;
|
||||
subscription: Subscription;
|
||||
authCodes?: number;
|
||||
details?: {
|
||||
usage?: number;
|
||||
storageBonus?: number;
|
||||
profileData: Security;
|
||||
};
|
||||
}
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [localEmail, setLocalEmail] = useState<string>("");
|
||||
const [localToken, setLocalToken] = useState<string>("");
|
||||
@@ -139,7 +107,7 @@ const App: React.FC = () => {
|
||||
console.log("API Response:", userDataResponse);
|
||||
|
||||
const extractedUserData: UserData = {
|
||||
User: {
|
||||
user: {
|
||||
"User ID": userDataResponse.user.ID || "None",
|
||||
Email: userDataResponse.user.email || "None",
|
||||
"Creation time":
|
||||
@@ -147,7 +115,7 @@ const App: React.FC = () => {
|
||||
userDataResponse.user.creationTime / 1000,
|
||||
).toLocaleString() || "None",
|
||||
},
|
||||
Storage: {
|
||||
storage: {
|
||||
Total: userDataResponse.subscription.storage
|
||||
? userDataResponse.subscription.storage >= 1024 ** 3
|
||||
? `${(userDataResponse.subscription.storage / 1024 ** 3).toFixed(2)} GB`
|
||||
@@ -166,7 +134,7 @@ const App: React.FC = () => {
|
||||
: `${(userDataResponse.details.storageBonus / 1024 ** 2).toFixed(2)} MB`
|
||||
: "None",
|
||||
},
|
||||
Subscription: {
|
||||
subscription: {
|
||||
"Product ID":
|
||||
userDataResponse.subscription.productID || "None",
|
||||
Provider:
|
||||
@@ -176,7 +144,7 @@ const App: React.FC = () => {
|
||||
userDataResponse.subscription.expiryTime / 1000,
|
||||
).toLocaleString() || "None",
|
||||
},
|
||||
Security: {
|
||||
security: {
|
||||
"Email MFA": userDataResponse.details?.profileData
|
||||
.isEmailMFAEnabled
|
||||
? "Enabled"
|
||||
|
||||
@@ -10,10 +10,10 @@ import {
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getEmail, getToken } from "../App";
|
||||
import { apiOrigin } from "../services/support";
|
||||
interface ErrorResponse {
|
||||
message: string;
|
||||
}
|
||||
import type { ErrorResponse } from "../types";
|
||||
|
||||
// The below interfaces will only be used in this file
|
||||
// hence not including them into a sub-merged types file
|
||||
interface ChangeEmailProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
|
||||
@@ -10,14 +10,7 @@ import {
|
||||
import React, { useState } from "react";
|
||||
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
|
||||
import { apiOrigin } from "../services/support";
|
||||
|
||||
interface UserData {
|
||||
subscription?: {
|
||||
userID: string;
|
||||
// Add other properties as per your API response structure
|
||||
};
|
||||
// Add other properties as per your API response structure
|
||||
}
|
||||
import type { UserData } from "../types";
|
||||
|
||||
interface CloseFamilyProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -10,14 +10,7 @@ import {
|
||||
import React, { useState } from "react";
|
||||
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
|
||||
import { apiOrigin } from "../services/support";
|
||||
|
||||
interface UserData {
|
||||
subscription?: {
|
||||
userID: string;
|
||||
// Add other properties as per your API response structure
|
||||
};
|
||||
// Add other properties as per your API response structure
|
||||
}
|
||||
import type { UserData } from "../types";
|
||||
|
||||
interface Disable2FAProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -10,20 +10,7 @@ import {
|
||||
import React, { useState } from "react";
|
||||
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
|
||||
import { apiOrigin } from "../services/support";
|
||||
|
||||
interface UserData {
|
||||
subscription?: {
|
||||
userID: string;
|
||||
// Add other properties as per your API response structure
|
||||
};
|
||||
// Add other properties as per your API response structure
|
||||
}
|
||||
|
||||
interface DisablePasskeysProps {
|
||||
open: boolean;
|
||||
handleClose: () => void;
|
||||
handleDisablePasskeys: () => void; // Callback to handle disabling passkeys
|
||||
}
|
||||
import type { DisablePasskeysProps, UserData } from "../types";
|
||||
|
||||
const DisablePasskeys: React.FC<DisablePasskeysProps> = ({
|
||||
open,
|
||||
|
||||
@@ -13,23 +13,10 @@ import * as React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getEmail, getToken } from "../App";
|
||||
import { apiOrigin } from "../services/support";
|
||||
import type { FamilyMember, UserData } from "../types";
|
||||
import { formatUsageToGB } from "../utils/";
|
||||
import CloseFamily from "./CloseFamily";
|
||||
|
||||
interface FamilyMember {
|
||||
id: string;
|
||||
email: string;
|
||||
status: string;
|
||||
usage: number;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
details: {
|
||||
familyData: {
|
||||
members: FamilyMember[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const FamilyTableComponent: React.FC = () => {
|
||||
const [familyMembers, setFamilyMembers] = useState<FamilyMember[]>([]);
|
||||
const [closeFamilyOpen, setCloseFamilyOpen] = useState(false);
|
||||
@@ -54,7 +41,7 @@ const FamilyTableComponent: React.FC = () => {
|
||||
}
|
||||
const userData = (await response.json()) as UserData; // Typecast to UserData interface
|
||||
const members: FamilyMember[] =
|
||||
userData.details.familyData.members;
|
||||
userData.details?.familyData.members ?? [];
|
||||
setFamilyMembers(members);
|
||||
} catch (error) {
|
||||
console.error("Error fetching family data:", error);
|
||||
@@ -69,11 +56,6 @@ const FamilyTableComponent: React.FC = () => {
|
||||
);
|
||||
}, []);
|
||||
|
||||
const formatUsageToGB = (usage: number): string => {
|
||||
const usageInGB = (usage / (1024 * 1024 * 1024)).toFixed(2);
|
||||
return `${usageInGB} GB`;
|
||||
};
|
||||
|
||||
const handleOpenCloseFamily = () => {
|
||||
setCloseFamilyOpen(true);
|
||||
};
|
||||
@@ -111,6 +93,9 @@ const FamilyTableComponent: React.FC = () => {
|
||||
<Table aria-label="family-table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<b>ID</b>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<b>User</b>
|
||||
</TableCell>
|
||||
@@ -121,13 +106,14 @@ const FamilyTableComponent: React.FC = () => {
|
||||
<b>Usage</b>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<b>ID</b>
|
||||
<b>Quota</b>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{familyMembers.map((member) => (
|
||||
<TableRow key={member.id}>
|
||||
<TableCell>{member.id}</TableCell>
|
||||
<TableCell>{member.email}</TableCell>
|
||||
<TableCell>
|
||||
<span
|
||||
@@ -152,7 +138,15 @@ const FamilyTableComponent: React.FC = () => {
|
||||
<TableCell>
|
||||
{formatUsageToGB(member.usage)}
|
||||
</TableCell>
|
||||
<TableCell>{member.id}</TableCell>
|
||||
<TableCell>
|
||||
{member.status !== "SELF"
|
||||
? (member.storageLimit &&
|
||||
formatUsageToGB(
|
||||
member.storageLimit,
|
||||
)) ||
|
||||
"NA"
|
||||
: ""}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
@@ -10,14 +10,7 @@ import {
|
||||
import React, { useState } from "react";
|
||||
import { getEmail, getToken } from "../App"; // Import getEmail and getToken functions
|
||||
import { apiOrigin } from "../services/support";
|
||||
|
||||
interface UserData {
|
||||
subscription?: {
|
||||
userID: string;
|
||||
// Add other properties as per your API response structure
|
||||
};
|
||||
// Add other properties as per your API response structure
|
||||
}
|
||||
import type { UserData } from "../types";
|
||||
|
||||
interface ToggleEmailMFAProps {
|
||||
open: boolean;
|
||||
|
||||
@@ -62,8 +62,8 @@ const UpdateSubscription: React.FC<UpdateSubscriptionProps> = ({
|
||||
expiryTime: "",
|
||||
userId: "",
|
||||
attributes: {
|
||||
"customerID": "",
|
||||
"stripeAccountCountry": ""
|
||||
customerID: "",
|
||||
stripeAccountCountry: "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -108,9 +108,13 @@ const UpdateSubscription: React.FC<UpdateSubscriptionProps> = ({
|
||||
expiryTime: expiryTime,
|
||||
userId: userDataResponse.subscription.userID || "",
|
||||
attributes: {
|
||||
customerID: userDataResponse.subscription.attributes.customerID || "",
|
||||
stripeAccountCountry: userDataResponse.subscription.attributes.stripeAccountCountry || ""
|
||||
}
|
||||
customerID:
|
||||
userDataResponse.subscription.attributes
|
||||
.customerID || "",
|
||||
stripeAccountCountry:
|
||||
userDataResponse.subscription.attributes
|
||||
.stripeAccountCountry || "",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
@@ -174,8 +178,9 @@ const UpdateSubscription: React.FC<UpdateSubscriptionProps> = ({
|
||||
transactionId: values.transactionId,
|
||||
attributes: {
|
||||
customerID: values.attributes.customerID,
|
||||
stripeAccountCountry: values.attributes.stripeAccountCountry
|
||||
}
|
||||
stripeAccountCountry:
|
||||
values.attributes.stripeAccountCountry,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -13,6 +13,7 @@ import TableContainer from "@mui/material/TableContainer";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import * as React from "react";
|
||||
import type { UserComponentProps } from "../types";
|
||||
import ChangeEmail from "./ChangeEmail";
|
||||
import DeleteAccount from "./DeleteAccont";
|
||||
import Disable2FA from "./Disable2FA";
|
||||
@@ -20,17 +21,6 @@ import DisablePasskeys from "./DisablePasskeys";
|
||||
import ToggleEmailMFA from "./ToggleEmailMFA";
|
||||
import UpdateSubscription from "./UpdateSubscription";
|
||||
|
||||
export interface UserData {
|
||||
User: Record<string, string>;
|
||||
Storage: Record<string, string>;
|
||||
Subscription: Record<string, string>;
|
||||
Security: Record<string, string>;
|
||||
}
|
||||
|
||||
interface UserComponentProps {
|
||||
userData: UserData | null;
|
||||
}
|
||||
|
||||
const UserComponent: React.FC<UserComponentProps> = ({ userData }) => {
|
||||
const [deleteAccountOpen, setDeleteAccountOpen] = React.useState(false);
|
||||
const [email2FAEnabled, setEmail2FAEnabled] = React.useState(false);
|
||||
@@ -44,10 +34,10 @@ const UserComponent: React.FC<UserComponentProps> = ({ userData }) => {
|
||||
const [disablePasskeysOpen, setDisablePasskeysOpen] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTwoFactorEnabled(userData?.Security["Two factor 2FA"] === "Enabled");
|
||||
setEmail2FAEnabled(userData?.Security["Email MFA"] === "Enabled");
|
||||
setTwoFactorEnabled(userData?.security["Two factor 2FA"] === "Enabled");
|
||||
setEmail2FAEnabled(userData?.security["Email MFA"] === "Enabled");
|
||||
setCanDisableEmailMFA(
|
||||
userData?.Security["Can Disable EmailMFA"] === "Yes",
|
||||
userData?.security["Can Disable EmailMFA"] === "Yes",
|
||||
);
|
||||
}, [userData]);
|
||||
|
||||
@@ -148,14 +138,10 @@ const DataTable: React.FC<DataTableProps> = ({
|
||||
minHeight: 300,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
marginBottom: "20px",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
padding: "13px",
|
||||
padding: "10px",
|
||||
overflowX: "hidden",
|
||||
"&:not(:last-child)": {
|
||||
marginBottom: "40px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@@ -176,9 +162,9 @@ const DataTable: React.FC<DataTableProps> = ({
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
{title.charAt(0).toUpperCase() + title.slice(1)}
|
||||
</Typography>
|
||||
{title === "User" && (
|
||||
{title === "user" && (
|
||||
<IconButton
|
||||
edge="start"
|
||||
aria-label="delete"
|
||||
@@ -187,7 +173,7 @@ const DataTable: React.FC<DataTableProps> = ({
|
||||
<DeleteIcon style={{ color: "" }} />
|
||||
</IconButton>
|
||||
)}
|
||||
{title === "Subscription" && (
|
||||
{title === "subscription" && (
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="edit"
|
||||
|
||||
71
infra/staff/src/types/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
// Type related Users
|
||||
|
||||
export interface User {
|
||||
ID: string;
|
||||
email: string;
|
||||
creationTime: number;
|
||||
}
|
||||
|
||||
export interface UserResponse {
|
||||
user: User;
|
||||
subscription: Subscription;
|
||||
authCodes?: number;
|
||||
details?: {
|
||||
usage?: number;
|
||||
storageBonus?: number;
|
||||
profileData: Security;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserData {
|
||||
user: Record<string, string>;
|
||||
storage: Record<string, string>;
|
||||
subscription?: Record<string, string>;
|
||||
security: Record<string, string>;
|
||||
details?: {
|
||||
familyData: {
|
||||
members: FamilyMember[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface UserComponentProps {
|
||||
userData: UserData | null;
|
||||
}
|
||||
|
||||
// Error Response Interface
|
||||
export interface ErrorResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Types related to Subscriptions
|
||||
export interface Subscription {
|
||||
productID: string;
|
||||
paymentProvider: string;
|
||||
expiryTime: number;
|
||||
storage: number;
|
||||
}
|
||||
|
||||
export interface Security {
|
||||
isEmailMFAEnabled: boolean;
|
||||
isTwoFactorEnabled: boolean;
|
||||
passkeys: string;
|
||||
passkeyCount: number;
|
||||
canDisableEmailMFA: boolean;
|
||||
}
|
||||
|
||||
// Types related Family
|
||||
export interface FamilyMember {
|
||||
id: string;
|
||||
email: string;
|
||||
status: string;
|
||||
usage: number;
|
||||
storageLimit: number;
|
||||
}
|
||||
|
||||
// Types related to passkeys
|
||||
export interface DisablePasskeysProps {
|
||||
open: boolean;
|
||||
handleClose: () => void;
|
||||
handleDisablePasskeys: () => void; // Callback to handle disabling passkeys
|
||||
}
|
||||
5
infra/staff/src/utils/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Common utilities
|
||||
export function formatUsageToGB(usage: number): string {
|
||||
const usageInGB = (usage / (1024 * 1024 * 1024)).toFixed(2);
|
||||
return `${usageInGB} GB`;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* User facing strings in the app.
|
||||
*
|
||||
* By keeping them separate, we make our lives easier if/when we need to
|
||||
* localize the corresponding pages. Right now, these are just the values in the
|
||||
* default language, English.
|
||||
*/
|
||||
const S = {
|
||||
hello: "Hello Ente!",
|
||||
error_generic: "Oops, something went wrong.",
|
||||
};
|
||||
|
||||
export default S;
|
||||
@@ -12,6 +12,9 @@ allprojects {
|
||||
maven {
|
||||
url "${project(':background_fetch').projectDir}/libs"
|
||||
}
|
||||
maven {
|
||||
url "${project(':ffmpeg_kit_flutter').projectDir}/libs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
ente is a simple app to backup and share your photos and videos.
|
||||
Ente هو تطبيق بسيط للنسخ الاحتياطي لصورك ومقاطع الفيديو الخاصة بك ومشاركتها.
|
||||
|
||||
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.
|
||||
إذا كنت تبحث عن بديل لتطبيق صور جوجل (Google Photos) يراعي خصوصيتك، فقد وصلت إلى المكان المناسب. مع Ente، يتم تخزين ذكرياتك بتشفير كامل من طرف إلى طرف (e2ee)، مما يعني أنك وحدك من يستطيع الاطلاع عليها. هذا يعني أنك وحدك من يستطيع رؤيتها.
|
||||
|
||||
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.
|
||||
لدينا تطبيقات مفتوحة المصدر لمنصات أندرويد، وآي أو إس، والويب، وسطح المكتب، وستتم مزامنة صورك بسلاسة بينها جميعًا بطريقة مشفرة بالكامل من طرف إلى طرف (e2ee).
|
||||
|
||||
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.
|
||||
يجعل من السهل أيضا مشاركة الألبومات مع أحبائك، حتى وإن لم يكونوا على ente. يمكنك مشاركة روابط عامة يمكن لأي شخص عرضها، حيث يستطيعون مشاهدة ألبومك والمساهمة بإضافة الصور إليه، حتى بدون الحاجة إلى حساب أو تطبيق.
|
||||
|
||||
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.
|
||||
تُكرّر بياناتك المشفرة في 3 مواقع مختلفة، بما في ذلك ملجأ محصن في باريس. نحن نأخذ مسألة الحفاظ على ذكرياتك للأجيال القادمة بجدية ونسهّل عليك ضمان بقاء ذكرياتك حية بعد رحيلك.
|
||||
|
||||
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!
|
||||
الميزات
|
||||
- نسخ احتياطية بالجودة الأصلية، لأن كل بكسل له قيمة.
|
||||
- خطط عائلية، لتتمكن من مشاركة مساحة التخزين مع عائلتك.
|
||||
- ألبومات تعاونية، لتتمكنوا من جمع صوركم المشتركة بعد رحلة ما.
|
||||
- مجلدات مشتركة، إذا كنت ترغب في أن يستمتع شريكك بصور "الكاميرا" الخاصة بك.
|
||||
- روابط للألبومات، يمكن حمايتها بكلمة مرور.
|
||||
- إمكانية تحرير مساحة على جهازك، عن طريق إزالة الملفات التي تم نسخها احتياطيًا بأمان.
|
||||
- دعم فني يقدمه أفراد حقيقيون، لأنك تستحق الاهتمام.
|
||||
- إضافة أوصاف، لتتمكن من تعليق ذكرياتك والعثور عليها بسهولة.
|
||||
- محرر صور، لإضفاء لمساتك النهائية.
|
||||
- إضافة للمفضلة، إخفاء، واستعادة ذكرياتك، فهي لا تُقدّر بثمن.
|
||||
- استيراد بنقرة واحدة من جوجل، وآبل، والقرص الصلب الخاص بك، وغيرها.
|
||||
- الوضع الداكن، لأن صورك تظهر بشكلٍ أفضل فيه
|
||||
- مصادقة ثنائية (2FA)، مصادقة ثلاثية (3FA)، مصادقة بيومترية
|
||||
- والكثير غيرها!
|
||||
|
||||
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
|
||||
أذونات
|
||||
يطلب Ente أذونات معينة للعمل كمزود لتخزين الصور، يمكن مراجعة تفاصيلها هنا: 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.
|
||||
التسعير
|
||||
نحن لا نوفر خططًا مجانية دائمة، لأنه من المهم بالنسبة لنا أن نضمن استدامة الخدمة وصمودها أمام اختبار الزمن. بدلاً من ذلك، نقدم خططًا بأسعار معقولة يمكنك مشاركتها بحرية مع عائلتك. يمكنك العثور على مزيد من المعلومات على 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.
|
||||
الدعم
|
||||
نحن نفخر بتقديم الدعم البشري. إذا كنت من عملائنا المشتركين، يمكنك التواصل عبر team@ente.io وتوقع ردًا من فريقنا في غضون 24 ساعة.
|
||||
|
||||
36
mobile/fastlane/metadata/android/eu/full_description.txt
Normal file
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
ente is an end-to-end encrypted photo storage app
|
||||
1
mobile/fastlane/metadata/android/eu/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
ente - encrypted photo storage
|
||||
@@ -1 +1 @@
|
||||
ente é uma aplicação de armazenamento de fotos encriptadas de ponta a ponta
|
||||
ente é uma aplicação de armazenamento de fotos encriptadas ponta a ponta
|
||||
@@ -1,33 +1,33 @@
|
||||
Ente is a simple app to automatically backup and organize your photos and videos.
|
||||
Ente هو تطبيق بسيط لإنشاء نسخ احتياطية وتنظيم صورك ومقاطع الفيديو الخاصة بك تلقائيًا.
|
||||
|
||||
If you've been looking for a privacy-friendly alternative to preserve your memories, 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.
|
||||
إذا كنت تبحث عن بديل يحفظ الخصوصية للحفاظ على ذكرياتك، فأنت في المكان الصحيح. مع Ente، يتم تخزينهن بتشفير من طرف إلى طرف (e2ee). هذا يعني أنك وحدك من يستطيع رؤيتها.
|
||||
|
||||
We have apps across all platforms, and your photos will seamlessly sync between all your devices in an end-to-end encrypted (e2ee) manner.
|
||||
لدينا تطبيقات عبر جميع المنصات، وستتم مزامنة صورك بسلاسة بين جميع أجهزتك بطريقة مشفرة من طرف إلى طرف (e2ee).
|
||||
|
||||
Ente also makes it simple to share your albums with your loved ones. You can either share them directly with other Ente users, end-to-end encrypted; or with publicly viewable links.
|
||||
يجعل Ente أيضًا من السهل مشاركة ألبوماتك مع أحبائك. يمكنك إما مشاركتها مباشرة مع مستخدمي Ente الآخرين، بتشفير من طرف إلى طرف؛ أو باستخدام روابط قابلة للعرض بشكل عام.
|
||||
|
||||
Your encrypted data is stored across multiple 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
|
||||
- Shared folders, in case you want your partner to enjoy your "Camera" clicks
|
||||
- Album links, that can be protected with a password and set to expire
|
||||
- Ability to free up space, by removing files that have been safely backed up
|
||||
- Image editor, to add finishing touches
|
||||
- Favorite, hide and relive your memories, for they are precious
|
||||
- One-click import from all major storage providers
|
||||
- Dark theme, because your photos look good in it
|
||||
- 2FA, 3FA, biometric auth
|
||||
- and a LOT more!
|
||||
الميزات
|
||||
- نسخ احتياطية بالجودة الأصلية، لأن كل بكسل مهم
|
||||
- خطط عائلية، حتى تتمكن من مشاركة مساحة التخزين مع عائلتك
|
||||
- مجلدات مشتركة، في حال كنت ترغب في أن يستمتع شريكك بصور "الكاميرا" الخاصة بك
|
||||
- روابط الألبوم، التي يمكن حمايتها بكلمة مرور وتعيينها لتنتهي صلاحيتها
|
||||
- القدرة على تحرير المساحة، عن طريق إزالة الملفات التي تم نسخها احتياطيًا بأمان
|
||||
- محرر الصور، لإضافة اللمسات النهائية
|
||||
- إضافة إلى المفضلة، إخفاء، وإعادة إحياء ذكرياتك، فهي ثمينة
|
||||
- استيراد بنقرة واحدة من جميع مزودي التخزين الرئيسيين
|
||||
- الوضع الداكن، لأن صورك تظهر بشكلٍ أفضل فيه
|
||||
- مصادقة ثنائية (2FA)، مصادقة ثلاثية (3FA)، مصادقة بيومترية
|
||||
- وأكثر من ذلك بكثير!
|
||||
|
||||
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.
|
||||
التسعير
|
||||
نحن لا نقدم خططًا مجانية إلى الأبد، لأنه من المهم بالنسبة لنا أن نبقى مستدامين ونتجاوز اختبار الزمن. بدلاً من ذلك، نقدم خططًا بأسعار معقولة يمكنك مشاركتها بحرية مع عائلتك. يمكنك العثور على مزيد من المعلومات على 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.
|
||||
الدعم
|
||||
نحن نفخر بتقديم الدعم البشري. إذا كنت عميلاً مدفوعًا لدينا، يمكنك التواصل عبر team@ente.io وتوقع ردًا من فريقنا في غضون 24 ساعة.
|
||||
|
||||
TERMS
|
||||
الشروط
|
||||
https://ente.io/terms
|
||||
|
||||
@@ -1 +1 @@
|
||||
صور، تصوير، عائلة، خصوصية، سحابة، نسخ احتياطي، مقاطع الفيديو، صورة، تشفير، تخزين، ألبوم، بديل
|
||||
صور، تصوير، عائلة، خصوصية، سحابة، نسخ احتياطي، مقاطع الفيديو، صورة، تشفير، تخزين، مجموعة الصور، بديل
|
||||
|
||||
33
mobile/fastlane/metadata/ios/eu/description.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
Ente is a simple app to automatically backup and organize your photos and videos.
|
||||
|
||||
If you've been looking for a privacy-friendly alternative to preserve your memories, 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 apps across all platforms, and your photos will seamlessly sync between all your devices in an end-to-end encrypted (e2ee) manner.
|
||||
|
||||
Ente also makes it simple to share your albums with your loved ones. You can either share them directly with other Ente users, end-to-end encrypted; or with publicly viewable links.
|
||||
|
||||
Your encrypted data is stored across multiple 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
|
||||
- Shared folders, in case you want your partner to enjoy your "Camera" clicks
|
||||
- Album links, that can be protected with a password and set to expire
|
||||
- Ability to free up space, by removing files that have been safely backed up
|
||||
- Image editor, to add finishing touches
|
||||
- Favorite, hide and relive your memories, for they are precious
|
||||
- One-click import from all major storage providers
|
||||
- Dark theme, because your photos look good in it
|
||||
- 2FA, 3FA, biometric auth
|
||||
- and a LOT more!
|
||||
|
||||
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.
|
||||
|
||||
TERMS
|
||||
https://ente.io/terms
|
||||
1
mobile/fastlane/metadata/ios/eu/keywords.txt
Normal file
@@ -0,0 +1 @@
|
||||
photos,photography,family,privacy,cloud,backup,videos,photo,encryption,storage,album,alternative
|
||||
1
mobile/fastlane/metadata/ios/eu/name.txt
Normal file
@@ -0,0 +1 @@
|
||||
Ente Photos
|
||||
1
mobile/fastlane/metadata/ios/eu/subtitle.txt
Normal file
@@ -0,0 +1 @@
|
||||
Encrypted photo storage
|
||||
@@ -12,7 +12,7 @@ Esame čia tam, kad sukurtume saugiausią nuotraukų programą, prisijunkite pri
|
||||
|
||||
FUNKCIJOS
|
||||
– Originalios kokybės atsarginės kopijos, nes kiekvienas taškelis yra svarbus
|
||||
– Šeimos planai, kad galėtumėte dalytis saugykla su šeima
|
||||
– Šeimos planai, tad galite dalytis saugykla su šeima
|
||||
– Bendrinami aplankai, jei norite, kad partneris galėtų mėgautis jūsų „fotoaparato“ paspaudimais
|
||||
– Albumo nuorodos, kurias galima apsaugoti slaptažodžiu ir nustatyti jų galiojimo laiką
|
||||
– Galimybė atlaisvinti vietą, pašalinant saugiai atsargines kopijas sukūrusius failus
|
||||
@@ -24,9 +24,9 @@ FUNKCIJOS
|
||||
– ir DAR daugiau!
|
||||
|
||||
KAINODARA
|
||||
Nesiūlome amžinai nemokamų planų, nes mums svarbu, kad išliktume tvarūs ir atlaikytume laiko išbandymą. Vietoj to siūlome nebrangius planus, kuriais galite laisvai dalytis su savo šeima. Daugiau informacijos galima rasti svetainėje ente.io.
|
||||
Nesiūlome visam laikui nemokamų planų, nes mums svarbu, kad išliktume tvarūs ir atlaikytume laiko išbandymą. Vietoj to siūlome nebrangius planus, kuriais galite laisvai dalytis su savo šeima. Daugiau informacijos galima rasti svetainėje ente.io.
|
||||
|
||||
PALAIKYMAS
|
||||
PAGALBA
|
||||
Didžiuojamės galėdami pasiūlyti žmogiškąją pagalbą. Jei esate mūsų mokamas klientas, galite susisiekti adresu team@ente.io ir tikėtis mūsų komandos atsakymo per 24 valandas.
|
||||
|
||||
SĄLYGOS
|
||||
|
||||
@@ -6,28 +6,28 @@ Já temos aplicações em todas as plataformas, e as suas fotos sincronizam perf
|
||||
|
||||
O Ente também simplifica a partilha de álbuns com os seus entes queridos. Também pode partilhá-los diretamente com outros utilizadores do Ente, encriptado de ponta a ponta; ou com ligações visíveis publicamente.
|
||||
|
||||
Os seus dados encriptados são armazenados em vários locais, incluindo um abrigo de emergência em Paris. Levamos a posteridade a sério e facilitamos a tarefa de garantir que as suas memórias perdurem para além de si.
|
||||
Os seus dados encriptados são armazenados em várias localizações, incluindo um abrigo avançado em Paris. Levamos a nossa postura seriamente e facilitamos que as suas memórias vivenciem.
|
||||
|
||||
Estamos aqui para criar a aplicação de fotos mais segura de sempre, junte-se à nossa viagem!
|
||||
Aqui estamos, fazendo a aplicação de fotos MAIS segura, vêm e adere a nossa jornada!
|
||||
|
||||
RECURSOS
|
||||
- Cópias de segurança de qualidade original, porque cada pixel é importante
|
||||
- Planos familiares, para que possa partilhar o armazenamento com a sua família
|
||||
- Pastas partilhadas, caso queira que o seu parceiro desfrute dos seus cliques na “Câmara”
|
||||
- Links para álbuns, que podem ser protegidas com uma palavra-passe e definidas para expirar
|
||||
- Capacidade de libertar espaço, removendo ficheiros dos quais foi feita uma cópia de segurança segura
|
||||
- Editor de imagens, para adicionar últimos toques
|
||||
- Favoritar, ocultar e reviver suas memórias, pois elas são preciosas
|
||||
- Importação com um clique de todos os principais fornecedores de armazenamento
|
||||
- Tema escuro, porque as suas fotografias ficam bem com ele
|
||||
FUNCIONALIDADES
|
||||
- Backups de qualidade original, por cada píxel ser importante
|
||||
- Planos familiares, para partilhar o armazenamento com a sua família
|
||||
- Pastas partilhadas, se deseja que o seu parceiro desfrute dos cliques da "Câmara"
|
||||
- Links de álbuns, para poder ser protegido por uma palavra-passe e definido para expiração
|
||||
- Capacidade de liberar espaço, eliminando ficheiros com backups já feitos
|
||||
- Editor de imagens, para dar toques finais
|
||||
- Adicione aos favoritos, oculte e reanime as suas memórias, já que elas são preciosas
|
||||
- Importação num clique de todos os principais fornecedores de armazenamento
|
||||
- Tema escuro, porque as suas fotos ficam bonitas nele
|
||||
- 2FA, 3FA, autenticação biométrica
|
||||
- e MUITO mais!
|
||||
|
||||
PREÇOS
|
||||
Não oferecemos planos gratuitos para sempre, porque é importante para nós mantermo-nos sustentáveis e resistirmos ao teste do tempo. Em vez disso, oferecemos planos acessíveis que pode partilhar livremente com a sua família You can find more information at ente.io.
|
||||
Não é oferecido pacotes gratuitos para sempre, porque é importante que nos mantenha sustentável e resistentes ao teste do tempo. Ao invés, oferecemos planos acessíveis para poder partilhar livremente com a sua família. Pode achar mais informações em ente.io.
|
||||
|
||||
SUPPORT
|
||||
Orgulhamo-nos de oferecer um support humano. Se for nosso cliente pago, pode contactar team@ente.io e esperar uma resposta da nossa equipa no prazo de 24 horas.
|
||||
SUPORTE
|
||||
Orgulhamo-nos de oferecer suporte humano. Se é um cliente pago, pode contactar ao team@ente.io e esperar uma resposta da nossa equipa dentre 1 dia.
|
||||
|
||||
TERMOS
|
||||
https://ente.io/terms
|
||||
|
||||